From 28f1c55da6d18b7c92f04c3f5ba313a90d55470e Mon Sep 17 00:00:00 2001 From: Victor Prodan Date: Tue, 4 Sep 2018 19:40:14 +0300 Subject: [PATCH] FleetAutoscaler --- README.md | 1 + cmd/controller/main.go | 5 +- docs/create_fleet.md | 5 +- docs/create_fleetautoscaler.md | 254 ++++++++++ examples/fleetautoscaler.yaml | 46 ++ examples/simple-udp/fleetautoscaler.yaml | 31 ++ examples/xonotic/fleetautoscaler.yaml | 31 ++ .../templates/admissionregistration.yaml | 3 + .../templates/crds/fleetautoscaler.yaml | 64 +++ .../templates/serviceaccounts/controller.yaml | 2 +- install/yaml/install.yaml | 72 ++- pkg/apis/stable/v1alpha1/fleetautoscaler.go | 184 +++++++ .../stable/v1alpha1/fleetautoscaler_test.go | 233 +++++++++ pkg/apis/stable/v1alpha1/register.go | 2 + .../stable/v1alpha1/zz_generated.deepcopy.go | 144 ++++++ .../v1alpha1/fake/fake_fleetautoscaler.go | 128 +++++ .../v1alpha1/fake/fake_stable_client.go | 4 + .../typed/stable/v1alpha1/fleetautoscaler.go | 157 ++++++ .../stable/v1alpha1/generated_expansion.go | 2 + .../typed/stable/v1alpha1/stable_client.go | 5 + .../informers/externalversions/generic.go | 2 + .../stable/v1alpha1/fleetautoscaler.go | 89 ++++ .../stable/v1alpha1/interface.go | 7 + .../stable/v1alpha1/expansion_generated.go | 8 + .../stable/v1alpha1/fleetautoscaler.go | 94 ++++ pkg/fleetautoscalers/controller.go | 364 ++++++++++++++ pkg/fleetautoscalers/controller_test.go | 467 ++++++++++++++++++ pkg/fleetautoscalers/doc.go | 17 + pkg/fleetautoscalers/fleetautoscalers.go | 70 +++ pkg/fleetautoscalers/fleetautoscalers_test.go | 114 +++++ test/e2e/fleetautoscaler_test.go | 223 +++++++++ test/e2e/framework/framework.go | 3 + 32 files changed, 2827 insertions(+), 4 deletions(-) create mode 100644 docs/create_fleetautoscaler.md create mode 100644 examples/fleetautoscaler.yaml create mode 100644 examples/simple-udp/fleetautoscaler.yaml create mode 100644 examples/xonotic/fleetautoscaler.yaml create mode 100644 install/helm/agones/templates/crds/fleetautoscaler.yaml create mode 100644 pkg/apis/stable/v1alpha1/fleetautoscaler.go create mode 100644 pkg/apis/stable/v1alpha1/fleetautoscaler_test.go create mode 100644 pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_fleetautoscaler.go create mode 100644 pkg/client/clientset/versioned/typed/stable/v1alpha1/fleetautoscaler.go create mode 100644 pkg/client/informers/externalversions/stable/v1alpha1/fleetautoscaler.go create mode 100644 pkg/client/listers/stable/v1alpha1/fleetautoscaler.go create mode 100644 pkg/fleetautoscalers/controller.go create mode 100644 pkg/fleetautoscalers/controller_test.go create mode 100644 pkg/fleetautoscalers/doc.go create mode 100644 pkg/fleetautoscalers/fleetautoscalers.go create mode 100644 pkg/fleetautoscalers/fleetautoscalers_test.go create mode 100644 test/e2e/fleetautoscaler_test.go diff --git a/README.md b/README.md index a5b22dfefe..84c0cdc711 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Documentation and usage guides on how to develop and host dedicated game servers ### Quickstarts: - [Create a Game Server](./docs/create_gameserver.md) - [Create a Game Server Fleet](./docs/create_fleet.md) + - [Create a Fleet Autoscaler](./docs/create_fleetautoscaler.md) - [Edit Your First Game Server (Go)](./docs/edit_first_game_server.md) ### Guides diff --git a/cmd/controller/main.go b/cmd/controller/main.go index c0291160ca..dbb73378e5 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -28,6 +28,7 @@ import ( "agones.dev/agones/pkg/client/clientset/versioned" "agones.dev/agones/pkg/client/informers/externalversions" "agones.dev/agones/pkg/fleetallocation" + "agones.dev/agones/pkg/fleetautoscalers" "agones.dev/agones/pkg/fleets" "agones.dev/agones/pkg/gameservers" "agones.dev/agones/pkg/gameserversets" @@ -101,6 +102,8 @@ func main() { fleetController := fleets.NewController(wh, health, kubeClient, extClient, agonesClient, agonesInformerFactory) faController := fleetallocation.NewController(wh, allocationMutex, kubeClient, extClient, agonesClient, agonesInformerFactory) + fasController := fleetautoscalers.NewController(wh, health, + kubeClient, extClient, agonesClient, agonesInformerFactory) stop := signals.NewStopChannel() @@ -108,7 +111,7 @@ func main() { agonesInformerFactory.Start(stop) rs := []runner{ - wh, gsController, gsSetController, fleetController, faController, healthServer{handler: health}, + wh, gsController, gsSetController, fleetController, faController, fasController, healthServer{handler: health}, } for _, r := range rs { go func(rr runner) { diff --git a/docs/create_fleet.md b/docs/create_fleet.md index fd455f8778..615d096ad5 100644 --- a/docs/create_fleet.md +++ b/docs/create_fleet.md @@ -358,4 +358,7 @@ You have now deployed a new version of your game! ## Next Steps -If you want to use your own GameServer container make sure you have properly integrated the [Agones SDK](../sdks/). +You can now create a fleet autoscaler to automatically resize your fleet based on the actual usage. +See [Create a Fleet Autoscaler](./create_fleetautoscaler.md). + +Or if you want to try to use your own GameServer container make sure you have properly integrated the [Agones SDK](../sdks/). diff --git a/docs/create_fleetautoscaler.md b/docs/create_fleetautoscaler.md new file mode 100644 index 0000000000..5472446b7e --- /dev/null +++ b/docs/create_fleetautoscaler.md @@ -0,0 +1,254 @@ +# Quickstart Create a Fleet Autoscaler + +This guide covers how you can quickly get started using Agones to create a Fleet +Autoscaler to manage your fleet size automatically, based on actual load. + +## Prerequisites + +It is assumed that you have followed the instructions to [Create a Game Server Fleet](./create_fleet.md) +and you have a running fleet of game servers. + +## Objectives + +- Create a Fleet Autoscaler in Kubernetes using Agones custom resource. +- Watch the Fleet scale up when allocating GameServers +- Watch the Fleet scale down when shutting down allocated GameServers +- Edit the autoscaler specification to apply live changes + +### 1. Create a Fleet Autoscaler + +Let's create a Fleet Autoscaler using the following command : + +``` +kubectl apply -f https://raw.githubusercontent.com/GoogleCloudPlatform/agones/master/examples/simple-udp/fleetautoscaler.yaml +``` + +You should see a successful ouput similar to this : + +``` +fleetautoscaler.stable.agones.sev "simple-udp-autoscaler" created +``` + +This has created a FleetAutoscaler record inside Kubernetes. + +### 2. See the autoscaler status. + +``` +kubectl describe fleetautoscaler simple-udp-autoscaler +``` + +It should look something like this: + +``` +Name: simple-udp-autoscaler +Namespace: default +Labels: +Annotations: kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"st +able.agones.dev/v1alpha1","kind":"FleetAutoscaler","metadata":{"annotations":{}, +"name":"simple-udp-autoscaler","namespace":"default"},... +API Version: stable.agones.dev/v1alpha1 +Kind: FleetAutoscaler +Metadata: + Cluster Name: + Creation Timestamp: 2018-10-02T15:19:58Z + Generation: 1 + Owner References: + API Version: stable.agones.dev/v1alpha1 + Block Owner Deletion: true + Controller: true + Kind: Fleet + Name: simple-udp + UID: 9960762e-c656-11e8-933e-fa163e07a1d4 + Resource Version: 6123197 + Self Link: /apis/stable.agones.dev/v1alpha1/namespaces/default/f +leetautoscalers/simple-udp-autoscaler + UID: 9fd0efa1-c656-11e8-933e-fa163e07a1d4 +Spec: + Fleet Name: simple-udp + Policy: + Buffer: + Buffer Size: 2 + Max Replicas: 10 + Min Replicas: 2 + Type: Buffer +Status: + Able To Scale: true + Current Replicas: 2 + Desired Replicas: 2 + Last Scale Time: + Scaling Limited: false +Events: +``` + +You can see the status (able to scale, not limited), the last time the fleet was scaled (nil for never) +and the current and desired fleet size. + +The autoscaler works by changing the desired size, and the fleet creates/deletes game server instances +to achieve to that number. The convergence is achieved in time, which is usually measured in seconds. + +### 3. Allocate a Game Server from the Fleet + +If you're interested in more details for game server allocation, you should consult the [Create a Game Server Fleet](./create_fleet.md) page. +In here we are only interested in triggering allocations to see the autoscaler in action. + +``` +kubectl create -f https://raw.githubusercontent.com/GoogleCloudPlatform/agones/master/examples/simple-udp/fleetallocation.yaml -o yaml +``` + +You should get in return the allocated game server details, which should end with something like: +``` + status: + address: 10.30.64.99 + nodeName: universal3 + ports: + - name: default + port: 7131 + state: Allocated +``` + +Note the address and port, you might need them later to connect to the server. + +### 4. See the autoscaler in action + +Now let's wait a few seconds to allow the autoscaler to detect the change in the fleet and check again its status + +``` +kubectl describe fleetautoscaler simple-udp-autoscaler +``` + +The last part should look something like this: + +``` +Spec: + Fleet Name: simple-udp + Policy: + Buffer: + Buffer Size: 2 + Max Replicas: 10 + Min Replicas: 2 + Type: Buffer +Status: + Able To Scale: true + Current Replicas: 3 + Desired Replicas: 3 + Last Scale Time: 2018-10-02T16:00:02Z + Scaling Limited: false +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal AutoScalingFleet 2m fleetautoscaler-controller Scaling fleet simple-udp from 2 to 3 +``` + +You can see that the fleet size has increased, the autoscaler having compensated for the allocated instance. +Last Scale Time has been updated, and a scaling event has been logged. + +Double-check the actual number of game server instances and status by running + +``` +kubectl get gs -o=custom-columns=NAME:.metadata.name,STATUS:.status.state,IP:.status.address,PORT:.status.ports +``` + +This will get you a list of all the current `GameSevers` and their `Status > State`. + +``` +NAME STATUS IP PORT +simple-udp-mzhrl-hz8wk Allocated 10.30.64.99 [map[name:default port:7131]] +simple-udp-mzhrl-k6jg5 Ready 10.30.64.100 [map[name:default port:7243]] +simple-udp-mzhrl-n2sk2 Ready 10.30.64.168 [map[name:default port:7658]] +``` + +### 5. Shut the allocated instance down + +Since we've only got one allocation, we'll just grab the details of the IP and port of the +only allocated `GameServer`: + +``` +kubectl get $(kubectl get fleetallocation -o name) -o jsonpath='{.status.GameServer.status.GameServer.status.ports[0].port}' +``` + +This should output your Game Server IP address and port. (eg `10.130.65.208:7936`) + +You can now communicate with the `GameServer`: + +``` +nc -u {IP} {PORT} +Hello World ! +ACK: Hello World ! +EXIT +``` + +You can finally type `EXIT` which tells the SDK to run the [Shutdown command](../sdks/README.md#shutdown), and therefore shuts down the `GameServer`. + +### 6. See the fleet scaling down + +Now let's wait a few seconds to allow the autoscaler to detect the change in the fleet and check again its status + +``` +kubectl describe fleetautoscaler simple-udp-autoscaler +``` + +It should look something like this: + +``` +Spec: + Fleet Name: simple-udp + Policy: + Buffer: + Buffer Size: 2 + Max Replicas: 10 + Min Replicas: 2 + Type: Buffer +Status: + Able To Scale: true + Current Replicas: 3 + Desired Replicas: 2 + Last Scale Time: 2018-10-02T16:09:02Z + Scaling Limited: false +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal AutoScalingFleet 9m fleetautoscaler-controller Scaling fleet simple-udp from 2 to 3 + Normal AutoScalingFleet 45s fleetautoscaler-controller Scaling fleet simple-udp from 3 to 2 +``` + +You can see that the fleet size has decreased, the autoscaler adjusting to game server instance being de-allocated, +the Last Scale Time and the events have been updated. Note that simple-udp game server instance you just closed earlier +might stay a bit in 'Unhealthy' state (and its pod in 'Terminating' until it gets removed. + +Double-check the actual number of game server instances and status by running + +``` +kubectl get gs -o=custom-columns=NAME:.metadata.name,STATUS:.status.state,IP:.status.address,PORT:.status.ports +``` + +This will get you a list of all the current `GameSevers` and their `Status > State`. + +``` +NAME STATUS IP PORT +simple-udp-mzhrl-k6jg5 Ready 10.30.64.100 [map[name:default port:7243]] +simple-udp-mzhrl-t7944 Ready 10.30.64.168 [map[port:7561 name:default]] +``` + +### 7. Change autoscaling parameters + +We can also change the configuration of the `FleetAutoscaler` of the running `Fleet`, and have the changes +applied live, without interruptions of service. + +Run `kubectl edit fleetautoscaler simple-udp-autoscaler` and set the `bufferSize` field to `5`. + +Let's look at the list of game servers again. Run `watch kubectl get gs -o=custom-columns=NAME:.metadata.name,STATUS:.status.state,IP:.status.address,PORT:.status.ports` +until you can see that are 5 ready server instances: + +``` +NAME STATUS IP PORT +simple-udp-mzhrl-7jpkp Ready 10.30.64.100 [map[name:default port:7019]] +simple-udp-mzhrl-czt8v Ready 10.30.64.168 [map[name:default port:7556]] +simple-udp-mzhrl-k6jg5 Ready 10.30.64.100 [map[name:default port:7243]] +simple-udp-mzhrl-nb8h2 Ready 10.30.64.168 [map[name:default port:7357]] +simple-udp-mzhrl-qspb6 Ready 10.30.64.99 [map[name:default port:7859]] +simple-udp-mzhrl-zg9rq Ready 10.30.64.99 [map[name:default port:7745]] +``` + +## Next Steps + +If you want to use your own GameServer container make sure you have properly integrated the [Agones SDK](../sdks/). \ No newline at end of file diff --git a/examples/fleetautoscaler.yaml b/examples/fleetautoscaler.yaml new file mode 100644 index 0000000000..62a0204bff --- /dev/null +++ b/examples/fleetautoscaler.yaml @@ -0,0 +1,46 @@ +# 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. + +# +# Full example of a FleetAutoscaler - this is used to scale a Fleet +# automatically depending on load +# + +apiVersion: "stable.agones.dev/v1alpha1" +kind: FleetAutoscaler +metadata: + # FleetAutoscaler Metadata + # https://v1-9.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.9/#objectmeta-v1-meta + name: fleet-autoscaler-example +spec: + # The name of the fleet to attach to and control. Must be an existing Fleet in the same namespace + # as this FleetAutoscaler + fleetName: fleet-example + # The autoscaling policy + policy: + # type of the policy. for now, only Buffer is available + type: Buffer + # parameters of the buffer policy + buffer: + # Size of a buffer of "ready" game server instances + # The FleetAutoscaler will scale the fleet up and down trying to maintain this buffer, + # as instances are being allocated or terminated + # it can be specified either in absolute (i.e. 5) or percentage format (i.e. 5%) + bufferSize: 5 + # minimum fleet size to be set by this FleetAutoscaler. + # if not specified, the actual minimum fleet size will be bufferSize + minReplicas: 10 + # maximum fleet size that can be set by this FleetAutoscaler + # required + maxReplicas: 20 diff --git a/examples/simple-udp/fleetautoscaler.yaml b/examples/simple-udp/fleetautoscaler.yaml new file mode 100644 index 0000000000..e8860e83a3 --- /dev/null +++ b/examples/simple-udp/fleetautoscaler.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. + +# +# A FleetAutoscaler is used to scale the fleet automatically +# up and down depending on usage +# + +apiVersion: "stable.agones.dev/v1alpha1" +kind: FleetAutoscaler +metadata: + name: simple-udp-autoscaler +spec: + fleetName: simple-udp + policy: + type: Buffer + buffer: + bufferSize: 2 + minReplicas: 0 + maxReplicas: 10 diff --git a/examples/xonotic/fleetautoscaler.yaml b/examples/xonotic/fleetautoscaler.yaml new file mode 100644 index 0000000000..96f465f2eb --- /dev/null +++ b/examples/xonotic/fleetautoscaler.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. + +# +# A FleetAutoscaler is used to scale the fleet automatically +# up and down depending on usage +# + +apiVersion: "stable.agones.dev/v1alpha1" +kind: FleetAutoscaler +metadata: + name: xonotic-autoscaler +spec: + fleetName: xonotic + policy: + type: Buffer + buffer: + bufferSize: 2 + minReplicas: 0 + maxReplicas: 10 diff --git a/install/helm/agones/templates/admissionregistration.yaml b/install/helm/agones/templates/admissionregistration.yaml index 6f72ac3927..5b2cfa12b7 100644 --- a/install/helm/agones/templates/admissionregistration.yaml +++ b/install/helm/agones/templates/admissionregistration.yaml @@ -40,6 +40,7 @@ webhooks: resources: - "gameservers" - "fleetallocations" + - "fleetautoscalers" apiVersions: - "v1alpha1" operations: @@ -49,6 +50,7 @@ webhooks: resources: - "gameserversets" - "fleetallocations" + - "fleetautoscalers" apiVersions: - "v1alpha1" operations: @@ -84,6 +86,7 @@ webhooks: - "gameservers" - "fleets" - "fleetallocations" + - "fleetautoscalers" apiVersions: - "v1alpha1" operations: diff --git a/install/helm/agones/templates/crds/fleetautoscaler.yaml b/install/helm/agones/templates/crds/fleetautoscaler.yaml new file mode 100644 index 0000000000..435d1a3b84 --- /dev/null +++ b/install/helm/agones/templates/crds/fleetautoscaler.yaml @@ -0,0 +1,64 @@ +# 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: fleetautoscalers.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: FleetAutoscaler + plural: fleetautoscalers + shortNames: + - fas + singular: fleetautoscaler + validation: + openAPIV3Schema: + properties: + spec: + required: + - fleetName + - policy + properties: + fleetName: + type: string + minLength: 1 + maxLength: 63 + pattern: "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + policy: + required: + - type + properties: + type: + enum: + - Buffer + buffer: + required: + - maxReplicas + properties: + minReplicas: + type: integer + minimum: 0 + maxReplicas: + type: integer + minimum: 1 diff --git a/install/helm/agones/templates/serviceaccounts/controller.yaml b/install/helm/agones/templates/serviceaccounts/controller.yaml index d3fa47b417..461a249a11 100644 --- a/install/helm/agones/templates/serviceaccounts/controller.yaml +++ b/install/helm/agones/templates/serviceaccounts/controller.yaml @@ -51,7 +51,7 @@ rules: resources: ["gameservers", "gameserversets"] verbs: ["create", "delete", "get", "list", "update", "watch"] - apiGroups: ["stable.agones.dev"] - resources: ["fleets", "fleetallocations"] + resources: ["fleets", "fleetallocations", "fleetautoscalers"] verbs: ["get", "list", "update", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/install/yaml/install.yaml b/install/yaml/install.yaml index cfee708041..d6fb245972 100644 --- a/install/yaml/install.yaml +++ b/install/yaml/install.yaml @@ -52,7 +52,7 @@ rules: resources: ["gameservers", "gameserversets"] verbs: ["create", "delete", "get", "list", "update", "watch"] - apiGroups: ["stable.agones.dev"] - resources: ["fleets", "fleetallocations"] + resources: ["fleets", "fleetallocations", "fleetautoscalers"] verbs: ["get", "list", "update", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 @@ -367,6 +367,73 @@ spec: maxLength: 63 pattern: "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" +--- +# Source: agones/templates/crds/fleetautoscaler.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: fleetautoscalers.stable.agones.dev + labels: + component: crd + app: agones + chart: agones-0.5.0.rc + release: agones-manual + heritage: Tiller +spec: + group: stable.agones.dev + version: v1alpha1 + scope: Namespaced + names: + kind: FleetAutoscaler + plural: fleetautoscalers + shortNames: + - fas + singular: fleetautoscaler + validation: + openAPIV3Schema: + properties: + spec: + required: + - fleetName + - policy + properties: + fleetName: + type: string + minLength: 1 + maxLength: 63 + pattern: "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + policy: + required: + - type + properties: + type: + enum: + - Buffer + buffer: + required: + - maxReplicas + properties: + minReplicas: + type: integer + minimum: 0 + maxReplicas: + type: integer + minimum: 1 + --- # Source: agones/templates/crds/gameserver.yaml # Copyright 2018 Google Inc. All Rights Reserved. @@ -854,6 +921,7 @@ webhooks: resources: - "gameservers" - "fleetallocations" + - "fleetautoscalers" apiVersions: - "v1alpha1" operations: @@ -863,6 +931,7 @@ webhooks: resources: - "gameserversets" - "fleetallocations" + - "fleetautoscalers" apiVersions: - "v1alpha1" operations: @@ -894,6 +963,7 @@ webhooks: - "gameservers" - "fleets" - "fleetallocations" + - "fleetautoscalers" apiVersions: - "v1alpha1" operations: diff --git a/pkg/apis/stable/v1alpha1/fleetautoscaler.go b/pkg/apis/stable/v1alpha1/fleetautoscaler.go new file mode 100644 index 0000000000..716db8c22b --- /dev/null +++ b/pkg/apis/stable/v1alpha1/fleetautoscaler.go @@ -0,0 +1,184 @@ +// 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 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import "k8s.io/apimachinery/pkg/util/intstr" + +// +genclient +// +genclient:noStatus +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// FleetAutoscaler is the data structure for a FleetAutoscaler resource +type FleetAutoscaler struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FleetAutoscalerSpec `json:"spec"` + Status FleetAutoscalerStatus `json:"status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// FleetAutoscalerList is a list of Fleet Scaler resources +type FleetAutoscalerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []FleetAutoscaler `json:"items"` +} + +// FleetAutoscalerSpec is the spec for a Fleet Scaler +type FleetAutoscalerSpec struct { + FleetName string `json:"fleetName"` + + // Autoscaling policy + Policy FleetAutoscalerPolicy `json:"policy"` +} + +// FleetAutoscalerPolicy describes how to scale a fleet +type FleetAutoscalerPolicy struct { + // Type of autoscaling policy. + Type FleetAutoscalerPolicyType `json:"type"` + + // Buffer policy config params. Present only if FleetAutoscalerPolicyType = Buffer. + // +optional + Buffer *BufferPolicy `json:"buffer,omitempty"` +} + +type FleetAutoscalerPolicyType string + +const ( + // Kill all existing pods before creating new ones. + BufferPolicyType FleetAutoscalerPolicyType = "Buffer" +) + +// BufferPolicy controls the desired behavior of the buffer policy. +type BufferPolicy struct { + // MaxReplicas is the maximum amount of replicas that the fleet may have. + // It must be bigger than both MinReplicas and BufferSize + MaxReplicas int32 `json:"maxReplicas"` + + // MinReplicas is the minimum amount of replicas that the fleet must have + // If zero, it is ignored. + // If non zero, it must be smaller than MaxReplicas and bigger than BufferSize + MinReplicas int32 `json:"minReplicas"` + + // BufferSize defines how many replicas the autoscaler tries to have ready all the time + // Value can be an absolute number (ex: 5) or a percentage of desired gs instances (ex: 15%) + // Absolute number is calculated from percentage by rounding up. + // Example: when this is set to 20%, the autoscaler will make sure that 20% + // of the fleet's game server replicas are ready. When this is set to 20, + // the autoscaler will make sure that there are 20 available game servers + // Must be bigger than 0 + // Note: by "ready" we understand in this case "non-allocated"; this is done to ensure robustness + // and computation stability in different edge case (fleet just created, not enough + // capacity in the cluster etc) + BufferSize intstr.IntOrString `json:"bufferSize"` +} + +// FleetAutoscalerStatus defines the current status of a FleetAutoscaler +type FleetAutoscalerStatus struct { + // CurrentReplicas is the current number of gameserver replicas + // of the fleet managed by this autoscaler, as last seen by the autoscaler + CurrentReplicas int32 `json:"currentReplicas"` + + // DesiredReplicas is the desired number of gameserver replicas + // of the fleet managed by this autoscaler, as last calculated by the autoscaler + DesiredReplicas int32 `json:"desiredReplicas"` + + // lastScaleTime is the last time the FleetAutoscaler scaled the attached fleet, + // +optional + LastScaleTime *metav1.Time `json:"lastScaleTime"` + + // AbleToScale indicates that we can access the target fleet + AbleToScale bool `json:"ableToScale"` + + // ScalingLimited indicates that the calculated scale would be above or below the range + // defined by MinReplicas and MaxReplicas, and has thus been capped. + ScalingLimited bool `json:"scalingLimited"` +} + +// ValidateUpdate validates when an update occurs +func (fas *FleetAutoscaler) ValidateUpdate(new *FleetAutoscaler, causes []metav1.StatusCause) []metav1.StatusCause { + if fas.Spec.FleetName != new.Spec.FleetName { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "fleetName", + Message: "fleetName cannot be updated", + }) + } + + return new.ValidateAutoScalingSettings(causes) +} + +//ValidateAutoScalingSettings validates the FleetAutoscaler scaling settings +func (fas *FleetAutoscaler) ValidateAutoScalingSettings(causes []metav1.StatusCause) []metav1.StatusCause { + if fas.Spec.Policy.Type == BufferPolicyType { + causes = fas.Spec.Policy.Buffer.ValidateAutoScalingBufferPolicy(causes) + } + return causes +} + +//ValidateAutoScalingSettings validates the FleetAutoscaler Buffer policy settings +func (b *BufferPolicy) ValidateAutoScalingBufferPolicy(causes []metav1.StatusCause) []metav1.StatusCause { + if b == nil { + return append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "buffer", + Message: "Buffer policy config params are missing", + }) + } + if b.MinReplicas > b.MaxReplicas { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "minReplicas", + Message: "minReplicas is bigger than maxReplicas", + }) + } + if b.BufferSize.Type == intstr.Int { + if b.BufferSize.IntValue() <= 0 { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "bufferSize", + Message: "bufferSize must be bigger than 0", + }) + } + if b.MaxReplicas < int32(b.BufferSize.IntValue()) { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "maxReplicas", + Message: "maxReplicas must be bigger than bufferSize", + }) + } + if b.MinReplicas != 0 && b.MinReplicas < int32(b.BufferSize.IntValue()) { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "minReplicas", + Message: "minReplicas is smaller than bufferSize", + }) + } + } else { + r, err := intstr.GetValueFromIntOrPercent(&b.BufferSize, 100, true) + if err != nil || r < 1 || r > 99 { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "bufferSize", + Message: "bufferSize does not have a valid percentage value (1%-99%)", + }) + } + } + return causes +} diff --git a/pkg/apis/stable/v1alpha1/fleetautoscaler_test.go b/pkg/apis/stable/v1alpha1/fleetautoscaler_test.go new file mode 100644 index 0000000000..57d65bc68b --- /dev/null +++ b/pkg/apis/stable/v1alpha1/fleetautoscaler_test.go @@ -0,0 +1,233 @@ +// 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" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestFleetAutoscalerValidateUpdate(t *testing.T) { + t.Parallel() + + t.Run("same fleet name", func(t *testing.T) { + + fas := &FleetAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: FleetAutoscalerSpec{ + FleetName: "testing", + Policy: FleetAutoscalerPolicy{ + Type: BufferPolicyType, + Buffer: &BufferPolicy{ + BufferSize: intstr.FromInt(1), + MaxReplicas: 10, + }, + }, + }, + } + + causes := fas.ValidateUpdate(fas.DeepCopy(), nil) + assert.Len(t, causes, 0) + }) + + t.Run("different fleet name", func(t *testing.T) { + + fas := &FleetAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: FleetAutoscalerSpec{ + FleetName: "testing", + Policy: FleetAutoscalerPolicy{ + Type: BufferPolicyType, + Buffer: &BufferPolicy{ + BufferSize: intstr.FromInt(1), + MaxReplicas: 10, + }, + }, + }, + } + fasCopy := fas.DeepCopy() + fasCopy.Spec.FleetName = "notthesame" + + causes := fas.ValidateUpdate(fasCopy, nil) + + assert.Len(t, causes, 1) + assert.Equal(t, "fleetName", causes[0].Field) + }) + + t.Run("bad buffer size", func(t *testing.T) { + + fas := &FleetAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: FleetAutoscalerSpec{ + FleetName: "testing", + Policy: FleetAutoscalerPolicy{ + Type: BufferPolicyType, + Buffer: &BufferPolicy{ + BufferSize: intstr.FromInt(1), + MaxReplicas: 10, + }, + }, + }, + } + + fasCopy := fas.DeepCopy() + fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromInt(0) + + causes := fas.ValidateUpdate(fasCopy, nil) + + assert.Len(t, causes, 1) + assert.Equal(t, "bufferSize", causes[0].Field) + }) + + t.Run("bad min replicas", func(t *testing.T) { + + fas := &FleetAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: FleetAutoscalerSpec{ + FleetName: "testing", + Policy: FleetAutoscalerPolicy{ + Type: BufferPolicyType, + Buffer: &BufferPolicy{ + BufferSize: intstr.FromInt(5), + MaxReplicas: 10, + }, + }, + }, + } + + fasCopy := fas.DeepCopy() + fasCopy.Spec.Policy.Buffer.MinReplicas = 2 + + causes := fas.ValidateUpdate(fasCopy, nil) + + assert.Len(t, causes, 1) + assert.Equal(t, "minReplicas", causes[0].Field) + }) + + t.Run("bad max replicas", func(t *testing.T) { + + fas := &FleetAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: FleetAutoscalerSpec{ + FleetName: "testing", + Policy: FleetAutoscalerPolicy{ + Type: BufferPolicyType, + Buffer: &BufferPolicy{ + BufferSize: intstr.FromInt(5), + MaxReplicas: 10, + }, + }, + }, + } + + fasCopy := fas.DeepCopy() + fasCopy.Spec.Policy.Buffer.MaxReplicas = 2 + causes := fas.ValidateUpdate(fasCopy, nil) + + assert.Len(t, causes, 1) + assert.Equal(t, "maxReplicas", causes[0].Field) + }) + + t.Run("minReplicas > maxReplicas", func(t *testing.T) { + + fas := &FleetAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: FleetAutoscalerSpec{ + FleetName: "testing", + Policy: FleetAutoscalerPolicy{ + Type: BufferPolicyType, + Buffer: &BufferPolicy{ + BufferSize: intstr.FromInt(5), + MaxReplicas: 10, + }, + }, + }, + } + + fasCopy := fas.DeepCopy() + fasCopy.Spec.Policy.Buffer.MinReplicas = 20 + causes := fas.ValidateUpdate(fasCopy, nil) + + assert.Len(t, causes, 1) + assert.Equal(t, "minReplicas", causes[0].Field) + }) + + t.Run("bufferSize good percent", func(t *testing.T) { + + fas := &FleetAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: FleetAutoscalerSpec{ + FleetName: "testing", + Policy: FleetAutoscalerPolicy{ + Type: BufferPolicyType, + Buffer: &BufferPolicy{ + BufferSize: intstr.FromInt(5), + MaxReplicas: 10, + }, + }, + }, + } + + fasCopy := fas.DeepCopy() + fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromString("20%") + causes := fas.ValidateUpdate(fasCopy, nil) + + assert.Len(t, causes, 0) + }) + + t.Run("bufferSize bad percent", func(t *testing.T) { + + fas := &FleetAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: FleetAutoscalerSpec{ + FleetName: "testing", + Policy: FleetAutoscalerPolicy{ + Type: BufferPolicyType, + Buffer: &BufferPolicy{ + BufferSize: intstr.FromInt(5), + MaxReplicas: 10, + }, + }, + }, + } + + fasCopy := fas.DeepCopy() + fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromString("120%") + causes := fas.ValidateUpdate(fasCopy, nil) + assert.Len(t, causes, 1) + assert.Equal(t, "bufferSize", causes[0].Field) + + fasCopy = fas.DeepCopy() + fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromString("0%") + causes = fas.ValidateUpdate(fasCopy, nil) + assert.Len(t, causes, 1) + assert.Equal(t, "bufferSize", causes[0].Field) + + fasCopy = fas.DeepCopy() + fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromString("-10%") + causes = fas.ValidateUpdate(fasCopy, nil) + assert.Len(t, causes, 1) + assert.Equal(t, "bufferSize", causes[0].Field) + fasCopy = fas.DeepCopy() + + fasCopy.Spec.Policy.Buffer.BufferSize = intstr.FromString("notgood") + causes = fas.ValidateUpdate(fasCopy, nil) + assert.Len(t, causes, 1) + assert.Equal(t, "bufferSize", causes[0].Field) + }) +} diff --git a/pkg/apis/stable/v1alpha1/register.go b/pkg/apis/stable/v1alpha1/register.go index 760c23e1a2..d7a8413586 100644 --- a/pkg/apis/stable/v1alpha1/register.go +++ b/pkg/apis/stable/v1alpha1/register.go @@ -58,6 +58,8 @@ func addKnownTypes(scheme *k8sruntime.Scheme) error { &FleetList{}, &FleetAllocation{}, &FleetAllocationList{}, + &FleetAutoscaler{}, + &FleetAutoscalerList{}, ) 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 3d4d665278..d85b58df42 100644 --- a/pkg/apis/stable/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/stable/v1alpha1/zz_generated.deepcopy.go @@ -24,6 +24,23 @@ 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 *BufferPolicy) DeepCopyInto(out *BufferPolicy) { + *out = *in + out.BufferSize = in.BufferSize + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BufferPolicy. +func (in *BufferPolicy) DeepCopy() *BufferPolicy { + if in == nil { + return nil + } + out := new(BufferPolicy) + in.DeepCopyInto(out) + return out +} + // 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 @@ -185,6 +202,133 @@ func (in *FleetAllocationStatus) DeepCopy() *FleetAllocationStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FleetAutoscaler) DeepCopyInto(out *FleetAutoscaler) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FleetAutoscaler. +func (in *FleetAutoscaler) DeepCopy() *FleetAutoscaler { + if in == nil { + return nil + } + out := new(FleetAutoscaler) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FleetAutoscaler) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FleetAutoscalerList) DeepCopyInto(out *FleetAutoscalerList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]FleetAutoscaler, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FleetAutoscalerList. +func (in *FleetAutoscalerList) DeepCopy() *FleetAutoscalerList { + if in == nil { + return nil + } + out := new(FleetAutoscalerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FleetAutoscalerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FleetAutoscalerPolicy) DeepCopyInto(out *FleetAutoscalerPolicy) { + *out = *in + if in.Buffer != nil { + in, out := &in.Buffer, &out.Buffer + if *in == nil { + *out = nil + } else { + *out = new(BufferPolicy) + **out = **in + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FleetAutoscalerPolicy. +func (in *FleetAutoscalerPolicy) DeepCopy() *FleetAutoscalerPolicy { + if in == nil { + return nil + } + out := new(FleetAutoscalerPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FleetAutoscalerSpec) DeepCopyInto(out *FleetAutoscalerSpec) { + *out = *in + in.Policy.DeepCopyInto(&out.Policy) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FleetAutoscalerSpec. +func (in *FleetAutoscalerSpec) DeepCopy() *FleetAutoscalerSpec { + if in == nil { + return nil + } + out := new(FleetAutoscalerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FleetAutoscalerStatus) DeepCopyInto(out *FleetAutoscalerStatus) { + *out = *in + if in.LastScaleTime != nil { + in, out := &in.LastScaleTime, &out.LastScaleTime + if *in == nil { + *out = nil + } else { + *out = (*in).DeepCopy() + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FleetAutoscalerStatus. +func (in *FleetAutoscalerStatus) DeepCopy() *FleetAutoscalerStatus { + if in == nil { + return nil + } + out := new(FleetAutoscalerStatus) + in.DeepCopyInto(out) + return out +} + // 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 diff --git a/pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_fleetautoscaler.go b/pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_fleetautoscaler.go new file mode 100644 index 0000000000..e3b24309bc --- /dev/null +++ b/pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_fleetautoscaler.go @@ -0,0 +1,128 @@ +// 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. + +// Code generated by client-gen. DO NOT EDIT. + +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" +) + +// FakeFleetAutoscalers implements FleetAutoscalerInterface +type FakeFleetAutoscalers struct { + Fake *FakeStableV1alpha1 + ns string +} + +var fleetautoscalersResource = schema.GroupVersionResource{Group: "stable.agones.dev", Version: "v1alpha1", Resource: "fleetautoscalers"} + +var fleetautoscalersKind = schema.GroupVersionKind{Group: "stable.agones.dev", Version: "v1alpha1", Kind: "FleetAutoscaler"} + +// Get takes name of the fleetAutoscaler, and returns the corresponding fleetAutoscaler object, and an error if there is any. +func (c *FakeFleetAutoscalers) Get(name string, options v1.GetOptions) (result *v1alpha1.FleetAutoscaler, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(fleetautoscalersResource, c.ns, name), &v1alpha1.FleetAutoscaler{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.FleetAutoscaler), err +} + +// List takes label and field selectors, and returns the list of FleetAutoscalers that match those selectors. +func (c *FakeFleetAutoscalers) List(opts v1.ListOptions) (result *v1alpha1.FleetAutoscalerList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(fleetautoscalersResource, fleetautoscalersKind, c.ns, opts), &v1alpha1.FleetAutoscalerList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.FleetAutoscalerList{} + for _, item := range obj.(*v1alpha1.FleetAutoscalerList).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 fleetAutoscalers. +func (c *FakeFleetAutoscalers) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(fleetautoscalersResource, c.ns, opts)) + +} + +// Create takes the representation of a fleetAutoscaler and creates it. Returns the server's representation of the fleetAutoscaler, and an error, if there is any. +func (c *FakeFleetAutoscalers) Create(fleetAutoscaler *v1alpha1.FleetAutoscaler) (result *v1alpha1.FleetAutoscaler, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(fleetautoscalersResource, c.ns, fleetAutoscaler), &v1alpha1.FleetAutoscaler{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.FleetAutoscaler), err +} + +// Update takes the representation of a fleetAutoscaler and updates it. Returns the server's representation of the fleetAutoscaler, and an error, if there is any. +func (c *FakeFleetAutoscalers) Update(fleetAutoscaler *v1alpha1.FleetAutoscaler) (result *v1alpha1.FleetAutoscaler, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(fleetautoscalersResource, c.ns, fleetAutoscaler), &v1alpha1.FleetAutoscaler{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.FleetAutoscaler), err +} + +// Delete takes name of the fleetAutoscaler and deletes it. Returns an error if one occurs. +func (c *FakeFleetAutoscalers) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(fleetautoscalersResource, c.ns, name), &v1alpha1.FleetAutoscaler{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeFleetAutoscalers) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(fleetautoscalersResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha1.FleetAutoscalerList{}) + return err +} + +// Patch applies the patch and returns the patched fleetAutoscaler. +func (c *FakeFleetAutoscalers) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.FleetAutoscaler, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(fleetautoscalersResource, c.ns, name, data, subresources...), &v1alpha1.FleetAutoscaler{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.FleetAutoscaler), 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 0481af0e6e..bb29000b2e 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 @@ -36,6 +36,10 @@ func (c *FakeStableV1alpha1) FleetAllocations(namespace string) v1alpha1.FleetAl return &FakeFleetAllocations{c, namespace} } +func (c *FakeStableV1alpha1) FleetAutoscalers(namespace string) v1alpha1.FleetAutoscalerInterface { + return &FakeFleetAutoscalers{c, namespace} +} + func (c *FakeStableV1alpha1) GameServers(namespace string) v1alpha1.GameServerInterface { return &FakeGameServers{c, namespace} } diff --git a/pkg/client/clientset/versioned/typed/stable/v1alpha1/fleetautoscaler.go b/pkg/client/clientset/versioned/typed/stable/v1alpha1/fleetautoscaler.go new file mode 100644 index 0000000000..02e63e505a --- /dev/null +++ b/pkg/client/clientset/versioned/typed/stable/v1alpha1/fleetautoscaler.go @@ -0,0 +1,157 @@ +// 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. + +// Code generated by client-gen. DO NOT EDIT. + +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" +) + +// FleetAutoscalersGetter has a method to return a FleetAutoscalerInterface. +// A group's client should implement this interface. +type FleetAutoscalersGetter interface { + FleetAutoscalers(namespace string) FleetAutoscalerInterface +} + +// FleetAutoscalerInterface has methods to work with FleetAutoscaler resources. +type FleetAutoscalerInterface interface { + Create(*v1alpha1.FleetAutoscaler) (*v1alpha1.FleetAutoscaler, error) + Update(*v1alpha1.FleetAutoscaler) (*v1alpha1.FleetAutoscaler, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha1.FleetAutoscaler, error) + List(opts v1.ListOptions) (*v1alpha1.FleetAutoscalerList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.FleetAutoscaler, err error) + FleetAutoscalerExpansion +} + +// fleetAutoscalers implements FleetAutoscalerInterface +type fleetAutoscalers struct { + client rest.Interface + ns string +} + +// newFleetAutoscalers returns a FleetAutoscalers +func newFleetAutoscalers(c *StableV1alpha1Client, namespace string) *fleetAutoscalers { + return &fleetAutoscalers{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the fleetAutoscaler, and returns the corresponding fleetAutoscaler object, and an error if there is any. +func (c *fleetAutoscalers) Get(name string, options v1.GetOptions) (result *v1alpha1.FleetAutoscaler, err error) { + result = &v1alpha1.FleetAutoscaler{} + err = c.client.Get(). + Namespace(c.ns). + Resource("fleetautoscalers"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of FleetAutoscalers that match those selectors. +func (c *fleetAutoscalers) List(opts v1.ListOptions) (result *v1alpha1.FleetAutoscalerList, err error) { + result = &v1alpha1.FleetAutoscalerList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("fleetautoscalers"). + VersionedParams(&opts, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested fleetAutoscalers. +func (c *fleetAutoscalers) Watch(opts v1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("fleetautoscalers"). + VersionedParams(&opts, scheme.ParameterCodec). + Watch() +} + +// Create takes the representation of a fleetAutoscaler and creates it. Returns the server's representation of the fleetAutoscaler, and an error, if there is any. +func (c *fleetAutoscalers) Create(fleetAutoscaler *v1alpha1.FleetAutoscaler) (result *v1alpha1.FleetAutoscaler, err error) { + result = &v1alpha1.FleetAutoscaler{} + err = c.client.Post(). + Namespace(c.ns). + Resource("fleetautoscalers"). + Body(fleetAutoscaler). + Do(). + Into(result) + return +} + +// Update takes the representation of a fleetAutoscaler and updates it. Returns the server's representation of the fleetAutoscaler, and an error, if there is any. +func (c *fleetAutoscalers) Update(fleetAutoscaler *v1alpha1.FleetAutoscaler) (result *v1alpha1.FleetAutoscaler, err error) { + result = &v1alpha1.FleetAutoscaler{} + err = c.client.Put(). + Namespace(c.ns). + Resource("fleetautoscalers"). + Name(fleetAutoscaler.Name). + Body(fleetAutoscaler). + Do(). + Into(result) + return +} + +// Delete takes name of the fleetAutoscaler and deletes it. Returns an error if one occurs. +func (c *fleetAutoscalers) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("fleetautoscalers"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *fleetAutoscalers) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("fleetautoscalers"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched fleetAutoscaler. +func (c *fleetAutoscalers) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.FleetAutoscaler, err error) { + result = &v1alpha1.FleetAutoscaler{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("fleetautoscalers"). + 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 20900b99fd..7ff564b1fb 100644 --- a/pkg/client/clientset/versioned/typed/stable/v1alpha1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/stable/v1alpha1/generated_expansion.go @@ -22,6 +22,8 @@ type FleetExpansion interface{} type FleetAllocationExpansion interface{} +type FleetAutoscalerExpansion 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 49c299364f..c932ad05aa 100644 --- a/pkg/client/clientset/versioned/typed/stable/v1alpha1/stable_client.go +++ b/pkg/client/clientset/versioned/typed/stable/v1alpha1/stable_client.go @@ -29,6 +29,7 @@ type StableV1alpha1Interface interface { RESTClient() rest.Interface FleetsGetter FleetAllocationsGetter + FleetAutoscalersGetter GameServersGetter GameServerSetsGetter } @@ -46,6 +47,10 @@ func (c *StableV1alpha1Client) FleetAllocations(namespace string) FleetAllocatio return newFleetAllocations(c, namespace) } +func (c *StableV1alpha1Client) FleetAutoscalers(namespace string) FleetAutoscalerInterface { + return newFleetAutoscalers(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 58fc4c4128..156c689c3f 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -57,6 +57,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Stable().V1alpha1().Fleets().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("fleetallocations"): return &genericInformer{resource: resource.GroupResource(), informer: f.Stable().V1alpha1().FleetAllocations().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("fleetautoscalers"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Stable().V1alpha1().FleetAutoscalers().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/fleetautoscaler.go b/pkg/client/informers/externalversions/stable/v1alpha1/fleetautoscaler.go new file mode 100644 index 0000000000..4ea101e3fb --- /dev/null +++ b/pkg/client/informers/externalversions/stable/v1alpha1/fleetautoscaler.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. + +// Code generated by informer-gen. DO NOT EDIT. + +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" +) + +// FleetAutoscalerInformer provides access to a shared informer and lister for +// FleetAutoscalers. +type FleetAutoscalerInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.FleetAutoscalerLister +} + +type fleetAutoscalerInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewFleetAutoscalerInformer constructs a new informer for FleetAutoscaler 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 NewFleetAutoscalerInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredFleetAutoscalerInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredFleetAutoscalerInformer constructs a new informer for FleetAutoscaler 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 NewFilteredFleetAutoscalerInformer(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().FleetAutoscalers(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.StableV1alpha1().FleetAutoscalers(namespace).Watch(options) + }, + }, + &stable_v1alpha1.FleetAutoscaler{}, + resyncPeriod, + indexers, + ) +} + +func (f *fleetAutoscalerInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredFleetAutoscalerInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *fleetAutoscalerInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&stable_v1alpha1.FleetAutoscaler{}, f.defaultInformer) +} + +func (f *fleetAutoscalerInformer) Lister() v1alpha1.FleetAutoscalerLister { + return v1alpha1.NewFleetAutoscalerLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/stable/v1alpha1/interface.go b/pkg/client/informers/externalversions/stable/v1alpha1/interface.go index ef8f4e45d5..a964b2d168 100644 --- a/pkg/client/informers/externalversions/stable/v1alpha1/interface.go +++ b/pkg/client/informers/externalversions/stable/v1alpha1/interface.go @@ -28,6 +28,8 @@ type Interface interface { Fleets() FleetInformer // FleetAllocations returns a FleetAllocationInformer. FleetAllocations() FleetAllocationInformer + // FleetAutoscalers returns a FleetAutoscalerInformer. + FleetAutoscalers() FleetAutoscalerInformer // GameServers returns a GameServerInformer. GameServers() GameServerInformer // GameServerSets returns a GameServerSetInformer. @@ -55,6 +57,11 @@ func (v *version) FleetAllocations() FleetAllocationInformer { return &fleetAllocationInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// FleetAutoscalers returns a FleetAutoscalerInformer. +func (v *version) FleetAutoscalers() FleetAutoscalerInformer { + return &fleetAutoscalerInformer{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 c3950f6ad0..2d5bfb8800 100644 --- a/pkg/client/listers/stable/v1alpha1/expansion_generated.go +++ b/pkg/client/listers/stable/v1alpha1/expansion_generated.go @@ -34,6 +34,14 @@ type FleetAllocationListerExpansion interface{} // FleetAllocationNamespaceLister. type FleetAllocationNamespaceListerExpansion interface{} +// FleetAutoscalerListerExpansion allows custom methods to be added to +// FleetAutoscalerLister. +type FleetAutoscalerListerExpansion interface{} + +// FleetAutoscalerNamespaceListerExpansion allows custom methods to be added to +// FleetAutoscalerNamespaceLister. +type FleetAutoscalerNamespaceListerExpansion interface{} + // GameServerListerExpansion allows custom methods to be added to // GameServerLister. type GameServerListerExpansion interface{} diff --git a/pkg/client/listers/stable/v1alpha1/fleetautoscaler.go b/pkg/client/listers/stable/v1alpha1/fleetautoscaler.go new file mode 100644 index 0000000000..752f82af9a --- /dev/null +++ b/pkg/client/listers/stable/v1alpha1/fleetautoscaler.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. + +// Code generated by lister-gen. DO NOT EDIT. + +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" +) + +// FleetAutoscalerLister helps list FleetAutoscalers. +type FleetAutoscalerLister interface { + // List lists all FleetAutoscalers in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.FleetAutoscaler, err error) + // FleetAutoscalers returns an object that can list and get FleetAutoscalers. + FleetAutoscalers(namespace string) FleetAutoscalerNamespaceLister + FleetAutoscalerListerExpansion +} + +// fleetAutoscalerLister implements the FleetAutoscalerLister interface. +type fleetAutoscalerLister struct { + indexer cache.Indexer +} + +// NewFleetAutoscalerLister returns a new FleetAutoscalerLister. +func NewFleetAutoscalerLister(indexer cache.Indexer) FleetAutoscalerLister { + return &fleetAutoscalerLister{indexer: indexer} +} + +// List lists all FleetAutoscalers in the indexer. +func (s *fleetAutoscalerLister) List(selector labels.Selector) (ret []*v1alpha1.FleetAutoscaler, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.FleetAutoscaler)) + }) + return ret, err +} + +// FleetAutoscalers returns an object that can list and get FleetAutoscalers. +func (s *fleetAutoscalerLister) FleetAutoscalers(namespace string) FleetAutoscalerNamespaceLister { + return fleetAutoscalerNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// FleetAutoscalerNamespaceLister helps list and get FleetAutoscalers. +type FleetAutoscalerNamespaceLister interface { + // List lists all FleetAutoscalers in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.FleetAutoscaler, err error) + // Get retrieves the FleetAutoscaler from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.FleetAutoscaler, error) + FleetAutoscalerNamespaceListerExpansion +} + +// fleetAutoscalerNamespaceLister implements the FleetAutoscalerNamespaceLister +// interface. +type fleetAutoscalerNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all FleetAutoscalers in the indexer for a given namespace. +func (s fleetAutoscalerNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.FleetAutoscaler, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.FleetAutoscaler)) + }) + return ret, err +} + +// Get retrieves the FleetAutoscaler from the indexer for a given namespace and name. +func (s fleetAutoscalerNamespaceLister) Get(name string) (*v1alpha1.FleetAutoscaler, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("fleetautoscaler"), name) + } + return obj.(*v1alpha1.FleetAutoscaler), nil +} diff --git a/pkg/fleetautoscalers/controller.go b/pkg/fleetautoscalers/controller.go new file mode 100644 index 0000000000..1f29523767 --- /dev/null +++ b/pkg/fleetautoscalers/controller.go @@ -0,0 +1,364 @@ +// 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 fleetautoscalers + +import ( + "encoding/json" + "fmt" + "time" + + "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/heptiolabs/healthcheck" + "github.com/mattbaird/jsonpatch" + "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" + apiequality "k8s.io/apimachinery/pkg/api/equality" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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 FleetAutoscaler controller +type Controller struct { + logger *logrus.Entry + crdGetter v1beta1.CustomResourceDefinitionInterface + fleetGetter getterv1alpha1.FleetsGetter + fleetLister listerv1alpha1.FleetLister + fleetAutoscalerGetter getterv1alpha1.FleetAutoscalersGetter + fleetAutoscalerLister listerv1alpha1.FleetAutoscalerLister + fleetAutoscalerSynced cache.InformerSynced + workerqueue *workerqueue.WorkerQueue + recorder record.EventRecorder +} + +// NewController returns a controller for a FleetAutoscaler +func NewController( + wh *webhooks.WebHook, + health healthcheck.Handler, + kubeClient kubernetes.Interface, + extClient extclientset.Interface, + agonesClient versioned.Interface, + agonesInformerFactory externalversions.SharedInformerFactory) *Controller { + + agonesInformer := agonesInformerFactory.Stable().V1alpha1() + fasInformer := agonesInformer.FleetAutoscalers().Informer() + + c := &Controller{ + crdGetter: extClient.ApiextensionsV1beta1().CustomResourceDefinitions(), + fleetGetter: agonesClient.StableV1alpha1(), + fleetLister: agonesInformer.Fleets().Lister(), + fleetAutoscalerGetter: agonesClient.StableV1alpha1(), + fleetAutoscalerLister: agonesInformer.FleetAutoscalers().Lister(), + fleetAutoscalerSynced: fasInformer.HasSynced, + } + c.logger = runtime.NewLoggerWithType(c) + c.workerqueue = workerqueue.NewWorkerQueue(c.syncFleetAutoscaler, c.logger, stable.GroupName+".FleetAutoscalerController") + health.AddLivenessCheck("fleetautoscaler-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: "fleetautoscaler-controller"}) + + kind := stablev1alpha1.Kind("FleetAutoscaler") + wh.AddHandler("/mutate", kind, admv1beta1.Create, c.creationMutationHandler) + wh.AddHandler("/validate", kind, admv1beta1.Update, c.mutationValidationHandler) + + fasInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: c.workerqueue.Enqueue, + UpdateFunc: func(_, newObj interface{}) { + c.workerqueue.Enqueue(newObj) + }, + }) + + return c +} + +// Run the FleetAutoscaler controller. Will block until stop is closed. +// Runs threadiness number workers to process the rate limited queue +func (c *Controller) Run(workers int, stop <-chan struct{}) error { + err := crd.WaitForEstablishedCRD(c.crdGetter, "fleetautoscalers."+stable.GroupName, c.logger) + if err != nil { + return err + } + + c.logger.Info("Wait for cache sync") + if !cache.WaitForCacheSync(stop, c.fleetAutoscalerSynced) { + return errors.New("failed to wait for caches to sync") + } + + c.workerqueue.Run(workers, stop) + return nil +} + +// creationMutationHandler will intercept when a FleetAutoscaler is created, and attach it to the requested Fleet +// assuming that it exists. If not, it will reject the AdmissionReview. +func (c *Controller) creationMutationHandler(review admv1beta1.AdmissionReview) (admv1beta1.AdmissionReview, error) { + c.logger.WithField("review", review).Info("creationMutationHandler") + obj := review.Request.Object + fas := &stablev1alpha1.FleetAutoscaler{} + err := json.Unmarshal(obj.Raw, fas) + if err != nil { + return review, errors.Wrapf(err, "error unmarshalling original FleetAutoscaler json: %s", obj.Raw) + } + + var causes []metav1.StatusCause + causes = fas.ValidateAutoScalingSettings(causes) + if len(causes) != 0 { + 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: "FleetAutoscaler creation is invalid", + Reason: metav1.StatusReasonInvalid, + Details: &details, + } + return review, nil + } + + // When being called from the API the fas.ObjectMeta.Namespace isn't populated + // (whereas it is from kubectl). So make sure to pull the namespace from the review + fleet, err := c.fleetLister.Fleets(review.Request.Namespace).Get(fas.Spec.FleetName) + if err != nil { + if k8serrors.IsNotFound(err) { + review.Response.Allowed = false + review.Response.Result = &metav1.Status{ + Status: metav1.StatusFailure, + Reason: metav1.StatusReasonInvalid, + Message: "Invalid FleetAutoscaler", + Details: &metav1.StatusDetails{ + Name: review.Request.Name, + Group: review.Request.Kind.Group, + Kind: review.Request.Kind.Kind, + Causes: []metav1.StatusCause{ + {Type: metav1.CauseTypeFieldValueNotFound, + Message: fmt.Sprintf("Could not find fleet %s in namespace %s", fas.Spec.FleetName, review.Request.Namespace), + Field: "fleetName"}}, + }, + } + return review, nil + } + return review, errors.Wrapf(err, "error retrieving fleet %s", fas.Name) + } + + // Attach to the fleet + // When a Fleet is deleted, the FleetAutoscaler should go with it + ref := metav1.NewControllerRef(fleet, stablev1alpha1.SchemeGroupVersion.WithKind("Fleet")) + fas.ObjectMeta.OwnerReferences = append(fas.ObjectMeta.OwnerReferences, *ref) + + // This event is relevant for both fleet and autoscaler, so we log it to both queues + c.recorder.Eventf(fleet, corev1.EventTypeNormal, "CreatingFleetAutoscaler", "Attached FleetAutoscaler %s to fleet %s", fas.ObjectMeta.Name, fas.Spec.FleetName) + c.recorder.Eventf(fas, corev1.EventTypeNormal, "CreatingFleetAutoscaler", "Attached FleetAutoscaler %s to fleet %s", fas.ObjectMeta.Name, fas.Spec.FleetName) + + newFS, err := json.Marshal(fas) + if err != nil { + return review, errors.Wrapf(err, "error marshalling FleetAutoscaler %s to json", fas.ObjectMeta.Name) + } + + patch, err := jsonpatch.CreatePatch(obj.Raw, newFS) + if err != nil { + return review, errors.Wrapf(err, "error creating patch for FleetAutoscaler %s", fas.ObjectMeta.Name) + } + + json, err := json.Marshal(patch) + if err != nil { + return review, errors.Wrapf(err, "error creating json for patch for FleetAutoscaler %s", fas.ObjectMeta.Name) + } + + c.logger.WithField("fas", fas.ObjectMeta.Name).WithField("patch", string(json)).Infof("patch created!") + + pt := admv1beta1.PatchTypeJSONPatch + review.Response.PatchType = &pt + review.Response.Patch = json + + return review, nil +} + +// mutationValidationHandler stops edits from happening to a +// FleetAutoscaler fleetName value +func (c *Controller) mutationValidationHandler(review admv1beta1.AdmissionReview) (admv1beta1.AdmissionReview, error) { + c.logger.WithField("review", review).Info("mutationValidationHandler") + + newFS := &stablev1alpha1.FleetAutoscaler{} + oldFS := &stablev1alpha1.FleetAutoscaler{} + + if err := json.Unmarshal(review.Request.Object.Raw, newFS); err != nil { + return review, errors.Wrapf(err, "error unmarshalling new FleetAutoscaler json: %s", review.Request.Object.Raw) + } + + if err := json.Unmarshal(review.Request.OldObject.Raw, oldFS); err != nil { + return review, errors.Wrapf(err, "error unmarshalling old FleetAutoscaler json: %s", review.Request.Object.Raw) + } + + causes := oldFS.ValidateUpdate(newFS, nil) + if len(causes) != 0 { + 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: "FleetAutoscaler update is invalid", + Reason: metav1.StatusReasonInvalid, + Details: &details, + } + } + + return review, nil +} + +// syncFleetAutoscaler scales the attached fleet and +// synchronizes the FleetAutoscaler CRD +func (c *Controller) syncFleetAutoscaler(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 + } + + fas, err := c.fleetAutoscalerLister.FleetAutoscalers(namespace).Get(name) + if err != nil { + if k8serrors.IsNotFound(err) { + c.logger.WithField("key", key).Info(fmt.Sprintf("FleetAutoscaler %s from namespace %s is no longer available for syncing", name, namespace)) + return nil + } + return errors.Wrapf(err, "error retrieving FleetAutoscaler %s from namespace %s", name, namespace) + } + + // Retrieve the fleet by spec name + fleet, err := c.fleetLister.Fleets(namespace).Get(fas.Spec.FleetName) + if err != nil { + if k8serrors.IsNotFound(err) { + logrus.WithError(err).WithField("fleetAutoscalerName", fas.Name). + WithField("fleetName", fas.Spec.FleetName). + WithField("namespace", namespace). + Warn("Could not find fleet for autoscaler. Skipping.") + return errors.Wrapf(err, "fleet %s not found in namespace %s", fas.Spec.FleetName, namespace) + } + result := errors.Wrapf(err, "error retrieving fleet %s from namespace %s", fas.Spec.FleetName, namespace) + err := c.updateStatusUnableToScale(fas) + if err != nil { + c.logger.WithError(err).WithField("fleetAutoscalerName", fas.Name). + Error("Failed to update FleetAutoscaler status") + } + return result + } + + currentReplicas := fleet.Status.Replicas + desiredReplicas, scalingLimited, err := computeDesiredFleetSize(fas, fleet) + if err != nil { + result := errors.Wrapf(err, "error calculating autoscaling fleet: %s", fleet.ObjectMeta.Name) + err := c.updateStatusUnableToScale(fas) + if err != nil { + c.logger.WithError(err).WithField("fleetAutoscalerName", fas.Name). + Error("Failed to update FleetAutoscaler status") + } + return result + } + + // Scale the fleet to the new size + err = c.scaleFleet(fas, fleet, desiredReplicas) + if err != nil { + return errors.Wrapf(err, "error autoscaling fleet %s to %d replicas", fas.Spec.FleetName, desiredReplicas) + } + + return c.updateStatus(fas, currentReplicas, desiredReplicas, desiredReplicas != fleet.Spec.Replicas, scalingLimited) +} + +// scaleFleet scales the fleet of the autoscaler to a new number of replicas +func (c *Controller) scaleFleet(fas *stablev1alpha1.FleetAutoscaler, f *stablev1alpha1.Fleet, replicas int32) error { + if replicas != f.Spec.Replicas { + fCopy := f.DeepCopy() + fCopy.Spec.Replicas = replicas + fCopy, err := c.fleetGetter.Fleets(f.ObjectMeta.Namespace).Update(fCopy) + if err != nil { + return errors.Wrapf(err, "error updating replicas for fleet %s", f.ObjectMeta.Name) + } + + c.recorder.Eventf(fas, corev1.EventTypeNormal, "AutoScalingFleet", + "Scaling fleet %s from %d to %d", fCopy.ObjectMeta.Name, f.Spec.Replicas, fCopy.Spec.Replicas) + } + + return nil +} + +// updateStatus updates the status of the given FleetAutoscaler +func (c *Controller) updateStatus(fas *stablev1alpha1.FleetAutoscaler, currentReplicas int32, desiredReplicas int32, scaled bool, scalingLimited bool) error { + fasCopy := fas.DeepCopy() + fasCopy.Status.AbleToScale = true + fasCopy.Status.ScalingLimited = scalingLimited + fasCopy.Status.CurrentReplicas = currentReplicas + fasCopy.Status.DesiredReplicas = desiredReplicas + if scaled { + now := metav1.NewTime(time.Now()) + fasCopy.Status.LastScaleTime = &now + } + + if !apiequality.Semantic.DeepEqual(fas.Status, fasCopy.Status) { + _, err := c.fleetAutoscalerGetter.FleetAutoscalers(fas.ObjectMeta.Namespace).Update(fasCopy) + if err != nil { + return errors.Wrapf(err, "error updating status for fleetautoscaler %s", fas.ObjectMeta.Name) + } + } + + return nil +} + +// updateStatus updates the status of the given FleetAutoscaler in the case we're not able to scale +func (c *Controller) updateStatusUnableToScale(fas *stablev1alpha1.FleetAutoscaler) error { + fasCopy := fas.DeepCopy() + fasCopy.Status.AbleToScale = false + fasCopy.Status.ScalingLimited = false + fasCopy.Status.CurrentReplicas = 0 + fasCopy.Status.DesiredReplicas = 0 + + if !apiequality.Semantic.DeepEqual(fas.Status, fasCopy.Status) { + _, err := c.fleetAutoscalerGetter.FleetAutoscalers(fas.ObjectMeta.Namespace).Update(fasCopy) + if err != nil { + return errors.Wrapf(err, "error updating status for fleetautoscaler %s", fas.ObjectMeta.Name) + } + } + + return nil +} diff --git a/pkg/fleetautoscalers/controller_test.go b/pkg/fleetautoscalers/controller_test.go new file mode 100644 index 0000000000..b6fc3e9a5c --- /dev/null +++ b/pkg/fleetautoscalers/controller_test.go @@ -0,0 +1,467 @@ +// 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 fleetautoscalers + +import ( + "encoding/json" + "fmt" + "testing" + + "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/stretchr/testify/assert" + admv1beta1 "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + k8stesting "k8s.io/client-go/testing" +) + +var ( + gvk = metav1.GroupVersionKind(v1alpha1.SchemeGroupVersion.WithKind("FleetAutoscaler")) +) + +func TestControllerCreationMutationHandler(t *testing.T) { + t.Parallel() + + t.Run("fleet scaler has a fleet", func(t *testing.T) { + c, m := newFakeController() + fas, f := defaultFixtures() + m.AgonesClient.AddReactor("list", "fleets", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.FleetList{Items: []v1alpha1.Fleet{*f}}, nil + }) + + _, cancel := agtesting.StartInformers(m) + defer cancel() + + review, err := newAdmissionReview(*fas) + assert.Nil(t, err) + + result, err := c.creationMutationHandler(review) + assert.Nil(t, err) + assert.True(t, result.Response.Allowed, fmt.Sprintf("%#v", result.Response)) + assert.Equal(t, admv1beta1.PatchTypeJSONPatch, *result.Response.PatchType) + assert.Contains(t, string(result.Response.Patch), "/metadata/ownerReferences") + }) + + t.Run("fleet does not exist", func(t *testing.T) { + c, _ := newFakeController() + fas, _ := defaultFixtures() + + review, err := newAdmissionReview(*fas) + assert.Nil(t, err) + + result, err := c.creationMutationHandler(review) + assert.Nil(t, err) + assert.False(t, result.Response.Allowed) + assert.Equal(t, "fleetName", result.Response.Result.Details.Causes[0].Field) + }) +} + +func TestControllerMutationValidationHandler(t *testing.T) { + t.Parallel() + c, _ := newFakeController() + + t.Run("same fleetName", func(t *testing.T) { + fas, _ := defaultFixtures() + + review, err := newAdmissionReview(*fas) + assert.Nil(t, err) + review.Request.OldObject = *review.Request.Object.DeepCopy() + + result, err := c.mutationValidationHandler(review) + assert.Nil(t, err) + assert.True(t, result.Response.Allowed, fmt.Sprintf("%#v", result.Response)) + }) + + t.Run("different fleetname", func(t *testing.T) { + fas, _ := defaultFixtures() + + review, err := newAdmissionReview(*fas) + assert.Nil(t, err) + oldObject := fas.DeepCopy() + oldObject.Spec.FleetName = "changed" + + json, err := json.Marshal(oldObject) + assert.Nil(t, err) + review.Request.OldObject = runtime.RawExtension{Raw: json} + + result, err := c.mutationValidationHandler(review) + assert.Nil(t, err) + assert.False(t, result.Response.Allowed) + assert.Equal(t, metav1.StatusReasonInvalid, result.Response.Result.Reason) + assert.NotNil(t, result.Response.Result.Details) + }) +} + +// nolint:dupl +func TestControllerSyncFleetAutoscaler(t *testing.T) { + t.Parallel() + + t.Run("scaling up", func(t *testing.T) { + t.Parallel() + c, m := newFakeController() + fas, f := defaultFixtures() + fas.Spec.Policy.Buffer.BufferSize = intstr.FromInt(7) + + f.Spec.Replicas = 5 + f.Status.Replicas = 5 + f.Status.AllocatedReplicas = 5 + f.Status.ReadyReplicas = 0 + + fUpdated := false + fasUpdated := false + + m.AgonesClient.AddReactor("list", "fleetautoscalers", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.FleetAutoscalerList{Items: []v1alpha1.FleetAutoscaler{*fas}}, nil + }) + + m.AgonesClient.AddReactor("update", "fleetautoscalers", func(action k8stesting.Action) (bool, runtime.Object, error) { + fasUpdated = true + ca := action.(k8stesting.UpdateAction) + fas := ca.GetObject().(*v1alpha1.FleetAutoscaler) + assert.Equal(t, fas.Status.AbleToScale, true) + assert.Equal(t, fas.Status.ScalingLimited, false) + assert.Equal(t, fas.Status.CurrentReplicas, int32(5)) + assert.Equal(t, fas.Status.DesiredReplicas, int32(12)) + assert.NotNil(t, fas.Status.LastScaleTime) + return true, fas, nil + }) + + 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("update", "fleets", func(action k8stesting.Action) (bool, runtime.Object, error) { + fUpdated = true + ca := action.(k8stesting.UpdateAction) + f := ca.GetObject().(*v1alpha1.Fleet) + assert.Equal(t, f.Spec.Replicas, int32(12)) + return true, f, nil + }) + + _, cancel := agtesting.StartInformers(m, c.fleetAutoscalerSynced) + defer cancel() + + err := c.syncFleetAutoscaler("default/fas-1") + assert.Nil(t, err) + assert.True(t, fUpdated, "fleet should have been updated") + assert.True(t, fasUpdated, "fleetautoscaler should have been updated") + agtesting.AssertEventContains(t, m.FakeRecorder.Events, "AutoScalingFleet") + }) + + t.Run("scaling down", func(t *testing.T) { + t.Parallel() + c, m := newFakeController() + fas, f := defaultFixtures() + fas.Spec.Policy.Buffer.BufferSize = intstr.FromInt(8) + + f.Spec.Replicas = 20 + f.Status.Replicas = 20 + f.Status.AllocatedReplicas = 5 + f.Status.ReadyReplicas = 15 + + fUpdated := false + fasUpdated := false + + m.AgonesClient.AddReactor("list", "fleetautoscalers", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.FleetAutoscalerList{Items: []v1alpha1.FleetAutoscaler{*fas}}, nil + }) + + m.AgonesClient.AddReactor("update", "fleetautoscalers", func(action k8stesting.Action) (bool, runtime.Object, error) { + fasUpdated = true + ca := action.(k8stesting.UpdateAction) + fas := ca.GetObject().(*v1alpha1.FleetAutoscaler) + assert.Equal(t, fas.Status.AbleToScale, true) + assert.Equal(t, fas.Status.ScalingLimited, false) + assert.Equal(t, fas.Status.CurrentReplicas, int32(20)) + assert.Equal(t, fas.Status.DesiredReplicas, int32(13)) + assert.NotNil(t, fas.Status.LastScaleTime) + return true, fas, nil + }) + + 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("update", "fleets", func(action k8stesting.Action) (bool, runtime.Object, error) { + fUpdated = true + ca := action.(k8stesting.UpdateAction) + f := ca.GetObject().(*v1alpha1.Fleet) + assert.Equal(t, f.Spec.Replicas, int32(13)) + + return true, f, nil + }) + + _, cancel := agtesting.StartInformers(m, c.fleetAutoscalerSynced) + defer cancel() + + err := c.syncFleetAutoscaler("default/fas-1") + assert.Nil(t, err) + assert.True(t, fUpdated, "fleet should have been updated") + assert.True(t, fasUpdated, "fleetautoscaler should have been updated") + agtesting.AssertEventContains(t, m.FakeRecorder.Events, "AutoScalingFleet") + }) + + t.Run("no scaling no update", func(t *testing.T) { + t.Parallel() + c, m := newFakeController() + fas, f := defaultFixtures() + + f.Spec.Replicas = 10 + f.Status.Replicas = 10 + f.Status.ReadyReplicas = 5 + fas.Spec.Policy.Buffer.BufferSize = intstr.FromInt(5) + fas.Status.CurrentReplicas = 10 + fas.Status.DesiredReplicas = 10 + + m.AgonesClient.AddReactor("list", "fleetautoscalers", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.FleetAutoscalerList{Items: []v1alpha1.FleetAutoscaler{*fas}}, nil + }) + + m.AgonesClient.AddReactor("update", "fleetautoscalers", func(action k8stesting.Action) (bool, runtime.Object, error) { + assert.FailNow(t, "fleetautoscaler should not update") + return false, nil, nil + }) + + m.AgonesClient.AddReactor("update", "fleets", func(action k8stesting.Action) (bool, runtime.Object, error) { + assert.FailNow(t, "fleet should not update") + return false, nil, nil + }) + + _, cancel := agtesting.StartInformers(m, c.fleetAutoscalerSynced) + defer cancel() + + err := c.syncFleetAutoscaler(fas.ObjectMeta.Name) + assert.Nil(t, err) + agtesting.AssertNoEvent(t, m.FakeRecorder.Events) + }) +} + +func TestControllerScaleFleet(t *testing.T) { + t.Parallel() + + t.Run("fleet that must be scaled", func(t *testing.T) { + c, m := newFakeController() + fas, f := defaultFixtures() + replicas := f.Spec.Replicas + 5 + + update := false + + m.AgonesClient.AddReactor("update", "fleets", func(action k8stesting.Action) (bool, runtime.Object, error) { + update = true + ca := action.(k8stesting.UpdateAction) + f := ca.GetObject().(*v1alpha1.Fleet) + assert.Equal(t, replicas, f.Spec.Replicas) + + return true, f, nil + }) + + err := c.scaleFleet(fas, f, replicas) + assert.Nil(t, err) + assert.True(t, update, "Fleet should be updated") + agtesting.AssertEventContains(t, m.FakeRecorder.Events, "ScalingFleet") + }) + + t.Run("noop", func(t *testing.T) { + c, m := newFakeController() + fas, f := defaultFixtures() + replicas := f.Spec.Replicas + + m.AgonesClient.AddReactor("update", "fleets", func(action k8stesting.Action) (bool, runtime.Object, error) { + assert.FailNow(t, "fleet should not update") + return false, nil, nil + }) + + err := c.scaleFleet(fas, f, replicas) + assert.Nil(t, err) + agtesting.AssertNoEvent(t, m.FakeRecorder.Events) + }) +} + +func TestControllerUpdateStatus(t *testing.T) { + t.Parallel() + + t.Run("must update", func(t *testing.T) { + c, m := newFakeController() + fas, _ := defaultFixtures() + + fasUpdated := false + + m.AgonesClient.AddReactor("update", "fleetautoscalers", func(action k8stesting.Action) (bool, runtime.Object, error) { + fasUpdated = true + ca := action.(k8stesting.UpdateAction) + fas := ca.GetObject().(*v1alpha1.FleetAutoscaler) + assert.Equal(t, fas.Status.AbleToScale, true) + assert.Equal(t, fas.Status.ScalingLimited, false) + assert.Equal(t, fas.Status.CurrentReplicas, int32(10)) + assert.Equal(t, fas.Status.DesiredReplicas, int32(20)) + assert.NotNil(t, fas.Status.LastScaleTime) + return true, fas, nil + }) + + _, cancel := agtesting.StartInformers(m, c.fleetAutoscalerSynced) + defer cancel() + + err := c.updateStatus(fas, 10, 20, true, false) + assert.Nil(t, err) + assert.True(t, fasUpdated) + agtesting.AssertNoEvent(t, m.FakeRecorder.Events) + }) + + t.Run("must not update", func(t *testing.T) { + c, m := newFakeController() + fas, _ := defaultFixtures() + + fas.Status.AbleToScale = true + fas.Status.ScalingLimited = false + fas.Status.CurrentReplicas = 10 + fas.Status.DesiredReplicas = 20 + fas.Status.LastScaleTime = nil + + m.AgonesClient.AddReactor("update", "fleetautoscalers", func(action k8stesting.Action) (bool, runtime.Object, error) { + assert.FailNow(t, "should not update") + return false, nil, nil + }) + + _, cancel := agtesting.StartInformers(m, c.fleetAutoscalerSynced) + defer cancel() + + err := c.updateStatus(fas, fas.Status.CurrentReplicas, fas.Status.DesiredReplicas, false, fas.Status.ScalingLimited) + assert.Nil(t, err) + agtesting.AssertNoEvent(t, m.FakeRecorder.Events) + }) +} + +func TestControllerUpdateStatusUnableToScale(t *testing.T) { + t.Parallel() + + t.Run("must update", func(t *testing.T) { + c, m := newFakeController() + fas, _ := defaultFixtures() + fas.Status.AbleToScale = true + + fasUpdated := false + + m.AgonesClient.AddReactor("update", "fleetautoscalers", func(action k8stesting.Action) (bool, runtime.Object, error) { + fasUpdated = true + ca := action.(k8stesting.UpdateAction) + fas := ca.GetObject().(*v1alpha1.FleetAutoscaler) + assert.Equal(t, fas.Status.AbleToScale, false) + assert.Equal(t, fas.Status.ScalingLimited, false) + assert.Equal(t, fas.Status.CurrentReplicas, int32(0)) + assert.Equal(t, fas.Status.DesiredReplicas, int32(0)) + assert.Nil(t, fas.Status.LastScaleTime) + return true, fas, nil + }) + + _, cancel := agtesting.StartInformers(m, c.fleetAutoscalerSynced) + defer cancel() + + err := c.updateStatusUnableToScale(fas) + assert.Nil(t, err) + assert.True(t, fasUpdated) + agtesting.AssertNoEvent(t, m.FakeRecorder.Events) + }) + + t.Run("must not update", func(t *testing.T) { + c, m := newFakeController() + fas, _ := defaultFixtures() + fas.Status.AbleToScale = false + fas.Status.ScalingLimited = false + fas.Status.CurrentReplicas = 0 + fas.Status.DesiredReplicas = 0 + + m.AgonesClient.AddReactor("update", "fleetautoscalers", func(action k8stesting.Action) (bool, runtime.Object, error) { + assert.FailNow(t, "fleetautoscaler should not update") + return false, nil, nil + }) + + _, cancel := agtesting.StartInformers(m, c.fleetAutoscalerSynced) + defer cancel() + + err := c.updateStatusUnableToScale(fas) + assert.Nil(t, err) + agtesting.AssertNoEvent(t, m.FakeRecorder.Events) + }) +} + +func defaultFixtures() (*v1alpha1.FleetAutoscaler, *v1alpha1.Fleet) { + f := &v1alpha1.Fleet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fleet-1", + Namespace: "default", + UID: "1234", + }, + Spec: v1alpha1.FleetSpec{ + Replicas: 5, + Template: v1alpha1.GameServerTemplateSpec{}, + }, + Status: v1alpha1.FleetStatus{ + Replicas: 5, + ReadyReplicas: 3, + AllocatedReplicas: 2, + }, + } + + fas := &v1alpha1.FleetAutoscaler{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fas-1", + Namespace: "default", + }, + Spec: v1alpha1.FleetAutoscalerSpec{ + FleetName: f.ObjectMeta.Name, + Policy: v1alpha1.FleetAutoscalerPolicy{ + Type: v1alpha1.BufferPolicyType, + Buffer: &v1alpha1.BufferPolicy{ + BufferSize: intstr.FromInt(5), + MaxReplicas: 100, + }, + }, + }, + } + + return fas, f +} + +// newFakeController returns a controller, backed by the fake Clientset +func newFakeController() (*Controller, agtesting.Mocks) { + m := agtesting.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 newAdmissionReview(fas v1alpha1.FleetAutoscaler) (admv1beta1.AdmissionReview, error) { + raw, err := json.Marshal(fas) + if err != nil { + return admv1beta1.AdmissionReview{}, err + } + review := admv1beta1.AdmissionReview{ + Request: &admv1beta1.AdmissionRequest{ + Kind: gvk, + Operation: admv1beta1.Create, + Object: runtime.RawExtension{ + Raw: raw, + }, + Namespace: "default", + }, + Response: &admv1beta1.AdmissionResponse{Allowed: true}, + } + return review, err +} diff --git a/pkg/fleetautoscalers/doc.go b/pkg/fleetautoscalers/doc.go new file mode 100644 index 0000000000..72b8d74f47 --- /dev/null +++ b/pkg/fleetautoscalers/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 fleetautoscalers handles management of the +// Fleet Custom Resource Definition +package fleetautoscalers diff --git a/pkg/fleetautoscalers/fleetautoscalers.go b/pkg/fleetautoscalers/fleetautoscalers.go new file mode 100644 index 0000000000..5c1f510775 --- /dev/null +++ b/pkg/fleetautoscalers/fleetautoscalers.go @@ -0,0 +1,70 @@ +/* + * 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 fleetautoscalers + +import ( + "math" + + stablev1alpha1 "agones.dev/agones/pkg/apis/stable/v1alpha1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// computeDesiredFleetSize computes the new desired size of the given fleet +func computeDesiredFleetSize(fas *stablev1alpha1.FleetAutoscaler, f *stablev1alpha1.Fleet) (int32, bool, error) { + + switch fas.Spec.Policy.Type { + case stablev1alpha1.BufferPolicyType: + return applyBufferPolicy(fas.Spec.Policy.Buffer, f) + } + + return f.Status.Replicas, false, nil +} + +func applyBufferPolicy(b *stablev1alpha1.BufferPolicy, f *stablev1alpha1.Fleet) (int32, bool, error) { + var replicas int32 + + if b.BufferSize.Type == intstr.Int { + replicas = f.Status.AllocatedReplicas + int32(b.BufferSize.IntValue()) + } else { + // the percentage value is a little more complex, as we can't apply + // the desired percentage to any current value, but to the future one + // Example: we have 8 allocated replicas, 10 total replicas and bufferSize set to 30% + // 30% means that we must have 30% ready instances in the fleet + // Right now there are 20%, so we must increase the fleet until we reach 30% + // To compute the new size, we start from the other end: if ready must be 30% + // it means that allocated must be 70% and adjust the fleet size to make that true. + bufferPercent, err := intstr.GetValueFromIntOrPercent(&b.BufferSize, 100, true) + if err != nil { + return f.Status.Replicas, false, err + } + // use Math.Ceil to round the result up + replicas = int32(math.Ceil(float64(f.Status.AllocatedReplicas*100) / float64(100-bufferPercent))) + } + + limited := false + + if replicas < b.MinReplicas { + replicas = b.MinReplicas + limited = true + } + if replicas > b.MaxReplicas { + replicas = b.MaxReplicas + limited = true + } + + return replicas, limited, nil +} diff --git a/pkg/fleetautoscalers/fleetautoscalers_test.go b/pkg/fleetautoscalers/fleetautoscalers_test.go new file mode 100644 index 0000000000..c68093dc55 --- /dev/null +++ b/pkg/fleetautoscalers/fleetautoscalers_test.go @@ -0,0 +1,114 @@ +/* + * 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 fleetautoscalers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestComputeDesiredFleetSize(t *testing.T) { + t.Parallel() + + fas, f := defaultFixtures() + + fas.Spec.Policy.Buffer.BufferSize = intstr.FromInt(20) + fas.Spec.Policy.Buffer.MaxReplicas = 100 + f.Spec.Replicas = 50 + f.Status.Replicas = f.Spec.Replicas + f.Status.AllocatedReplicas = 40 + f.Status.ReadyReplicas = 10 + + replicas, limited, err := computeDesiredFleetSize(fas, f) + assert.Nil(t, err) + assert.Equal(t, replicas, int32(60)) + assert.Equal(t, limited, false) + + // test empty Policy Type + f.Status.Replicas = 61 + fas.Spec.Policy.Type = "" + replicas, limited, err = computeDesiredFleetSize(fas, f) + assert.Nil(t, err) + assert.Equal(t, replicas, int32(61)) + assert.Equal(t, limited, false) +} + +func TestApplyBufferPolicy(t *testing.T) { + t.Parallel() + + fas, f := defaultFixtures() + b := fas.Spec.Policy.Buffer + + b.BufferSize = intstr.FromInt(20) + b.MaxReplicas = 100 + f.Spec.Replicas = 50 + f.Status.Replicas = f.Spec.Replicas + f.Status.AllocatedReplicas = 40 + f.Status.ReadyReplicas = 10 + + replicas, limited, err := applyBufferPolicy(b, f) + assert.Nil(t, err) + assert.Equal(t, replicas, int32(60)) + assert.Equal(t, limited, false) + + b.MinReplicas = 65 + f.Spec.Replicas = 50 + f.Status.Replicas = f.Spec.Replicas + f.Status.AllocatedReplicas = 40 + f.Status.ReadyReplicas = 10 + replicas, limited, err = applyBufferPolicy(b, f) + assert.Nil(t, err) + assert.Equal(t, replicas, int32(65)) + assert.Equal(t, limited, true) + + b.MinReplicas = 0 + b.MaxReplicas = 55 + f.Spec.Replicas = 50 + f.Status.Replicas = f.Spec.Replicas + f.Status.AllocatedReplicas = 40 + f.Status.ReadyReplicas = 10 + replicas, limited, err = applyBufferPolicy(b, f) + assert.Nil(t, err) + assert.Equal(t, replicas, int32(55)) + assert.Equal(t, limited, true) + + b.BufferSize = intstr.FromString("20%") + b.MinReplicas = 0 + b.MaxReplicas = 100 + f.Spec.Replicas = 50 + f.Status.Replicas = f.Spec.Replicas + f.Status.AllocatedReplicas = 50 + f.Status.ReadyReplicas = 0 + replicas, limited, err = applyBufferPolicy(b, f) + assert.Nil(t, err) + assert.Equal(t, replicas, int32(63)) + assert.Equal(t, limited, false) + + b.BufferSize = intstr.FromString("10%") + b.MinReplicas = 0 + b.MaxReplicas = 10 + f.Spec.Replicas = 1 + f.Status.Replicas = f.Spec.Replicas + f.Status.AllocatedReplicas = 1 + f.Status.ReadyReplicas = 0 + replicas, limited, err = applyBufferPolicy(b, f) + assert.Nil(t, err) + assert.Equal(t, replicas, int32(2)) + assert.Equal(t, limited, false) +} diff --git a/test/e2e/fleetautoscaler_test.go b/test/e2e/fleetautoscaler_test.go new file mode 100644 index 0000000000..fe5dac958b --- /dev/null +++ b/test/e2e/fleetautoscaler_test.go @@ -0,0 +1,223 @@ +// 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 e2e + +import ( + "fmt" + "math/rand" + "testing" + + "agones.dev/agones/pkg/apis/stable/v1alpha1" + e2e "agones.dev/agones/test/e2e/framework" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestAutoscalerBasicFunctions(t *testing.T) { + t.Parallel() + + alpha1 := framework.AgonesClient.StableV1alpha1() + fleets := alpha1.Fleets(defaultNs) + flt, err := fleets.Create(defaultFleet()) + if assert.Nil(t, err) { + defer fleets.Delete(flt.ObjectMeta.Name, nil) // nolint:errcheck + } + + err = framework.WaitForFleetCondition(flt, e2e.FleetReadyCount(flt.Spec.Replicas)) + assert.Nil(t, err, "fleet not ready") + + fleetautoscalers := alpha1.FleetAutoscalers(defaultNs) + fas, err := fleetautoscalers.Create(defaultFleetAutoscaler(flt)) + if assert.Nil(t, err) { + defer fleetautoscalers.Delete(fas.ObjectMeta.Name, nil) // nolint:errcheck + } else { + // if we could not create the autoscaler, their is no point going further + logrus.Error("Failed creating autoscaler, aborting TestAutoscalerBasicFunctions") + return + } + + // the fleet autoscaler should scale the fleet up now up to BufferSize + bufferSize := int32(fas.Spec.Policy.Buffer.BufferSize.IntValue()) + err = framework.WaitForFleetCondition(flt, e2e.FleetReadyCount(bufferSize)) + assert.Nil(t, err, "fleet did not sync with autoscaler") + + // patch the autoscaler to increase MinReplicas and watch the fleet scale up + fas, err = patchFleetAutoscaler(fas, intstr.FromInt(int(bufferSize)), bufferSize+2, fas.Spec.Policy.Buffer.MaxReplicas) + assert.Nil(t, err, "could not patch fleetautoscaler") + + bufferSize = int32(fas.Spec.Policy.Buffer.BufferSize.IntValue()) + err = framework.WaitForFleetCondition(flt, e2e.FleetReadyCount(bufferSize)) + assert.Nil(t, err, "fleet did not sync with autoscaler") + + // patch the autoscaler to remove MinReplicas and watch the fleet scale down + fas, err = patchFleetAutoscaler(fas, intstr.FromInt(int(bufferSize)), 0, fas.Spec.Policy.Buffer.MaxReplicas) + assert.Nil(t, err, "could not patch fleetautoscaler") + + bufferSize = int32(fas.Spec.Policy.Buffer.BufferSize.IntValue()) + err = framework.WaitForFleetCondition(flt, e2e.FleetReadyCount(bufferSize)) + assert.Nil(t, err, "fleet did not sync with autoscaler") + + // do an allocation and watch the fleet scale up + fa := getAllocation(flt) + fa, err = alpha1.FleetAllocations(defaultNs).Create(fa) + assert.Nil(t, err) + assert.Equal(t, v1alpha1.Allocated, fa.Status.GameServer.Status.State) + err = framework.WaitForFleetCondition(flt, func(fleet *v1alpha1.Fleet) bool { + return fleet.Status.AllocatedReplicas == 1 + }) + assert.Nil(t, err) + + err = framework.WaitForFleetCondition(flt, e2e.FleetReadyCount(bufferSize)) + assert.Nil(t, err, "fleet did not sync with autoscaler") + + // patch autoscaler to switch to relative buffer size and check if the fleet adjusts + _, err = patchFleetAutoscaler(fas, intstr.FromString("10%"), 1, fas.Spec.Policy.Buffer.MaxReplicas) + assert.Nil(t, err, "could not patch fleetautoscaler") + + //10% with only one allocated GS means only one ready server + err = framework.WaitForFleetCondition(flt, e2e.FleetReadyCount(1)) + assert.Nil(t, err, "fleet did not sync with autoscaler") + + // delete the allocated GameServer and watch the fleet scale down + gp := int64(1) + err = alpha1.GameServers(defaultNs).Delete(fa.Status.GameServer.ObjectMeta.Name, &metav1.DeleteOptions{GracePeriodSeconds: &gp}) + assert.Nil(t, err) + err = framework.WaitForFleetCondition(flt, func(fleet *v1alpha1.Fleet) bool { + return fleet.Status.AllocatedReplicas == 0 && + fleet.Status.ReadyReplicas == 1 && + fleet.Status.Replicas == 1 + }) + assert.Nil(t, err) +} + +// TestAutoscalerStressCreate creates many fleetautoscalers with random values +// to check if the creation validation works as expected and if the fleet scales +// to the expected number of replicas (when the creation is valid) +func TestAutoscalerStressCreate(t *testing.T) { + t.Parallel() + + alpha1 := framework.AgonesClient.StableV1alpha1() + fleets := alpha1.Fleets(defaultNs) + flt, err := fleets.Create(defaultFleet()) + if assert.Nil(t, err) { + defer fleets.Delete(flt.ObjectMeta.Name, nil) // nolint:errcheck + } + + err = framework.WaitForFleetCondition(flt, e2e.FleetReadyCount(flt.Spec.Replicas)) + assert.Nil(t, err, "fleet not ready") + + r := rand.New(rand.NewSource(1783)) + + fleetautoscalers := alpha1.FleetAutoscalers(defaultNs) + for i := 0; i < 30; i++ { + fas := defaultFleetAutoscaler(flt) + bufferSize := r.Int31n(5) + minReplicas := r.Int31n(5) + maxReplicas := r.Int31n(8) + fas.Spec.Policy.Buffer.BufferSize = intstr.FromInt(int(bufferSize)) + fas.Spec.Policy.Buffer.MinReplicas = minReplicas + fas.Spec.Policy.Buffer.MaxReplicas = maxReplicas + + valid := bufferSize > 0 && + fas.Spec.Policy.Buffer.MaxReplicas > 0 && + fas.Spec.Policy.Buffer.MaxReplicas >= bufferSize && + fas.Spec.Policy.Buffer.MinReplicas <= fas.Spec.Policy.Buffer.MaxReplicas && + (fas.Spec.Policy.Buffer.MinReplicas == 0 || fas.Spec.Policy.Buffer.MinReplicas >= bufferSize) + + fas, err := fleetautoscalers.Create(fas) + if err == nil { + assert.True(t, valid, + fmt.Sprintf("FleetAutoscaler created even if the parameters are NOT valid: %d %d %d", + bufferSize, + fas.Spec.Policy.Buffer.MinReplicas, + fas.Spec.Policy.Buffer.MaxReplicas)) + + expectedReplicas := bufferSize + if expectedReplicas < fas.Spec.Policy.Buffer.MinReplicas { + expectedReplicas = fas.Spec.Policy.Buffer.MinReplicas + } + if expectedReplicas > fas.Spec.Policy.Buffer.MaxReplicas { + expectedReplicas = fas.Spec.Policy.Buffer.MaxReplicas + } + // the fleet autoscaler should scale the fleet now to expectedReplicas + err = framework.WaitForFleetCondition(flt, e2e.FleetReadyCount(expectedReplicas)) + assert.Nil(t, err, fmt.Sprintf("fleet did not sync with autoscaler, expected %d ready replicas", expectedReplicas)) + + fleetautoscalers.Delete(fas.ObjectMeta.Name, nil) // nolint:errcheck + } else { + assert.False(t, valid, + fmt.Sprintf("FleetAutoscaler NOT created even if the parameters are valid: %d %d %d", + bufferSize, + minReplicas, + maxReplicas)) + } + } +} + +// scaleFleet creates a patch to apply to a Fleet. +// easier for testing, as it removes object generational issues. +func patchFleetAutoscaler(fas *v1alpha1.FleetAutoscaler, bufferSize intstr.IntOrString, minReplicas int32, maxReplicas int32) (*v1alpha1.FleetAutoscaler, error) { + var bufferSizeFmt string + if bufferSize.Type == intstr.Int { + bufferSizeFmt = fmt.Sprintf("%d", bufferSize.IntValue()) + } else { + bufferSizeFmt = fmt.Sprintf(`"%s"`, bufferSize.String()) + } + + patch := fmt.Sprintf( + `[{ "op": "replace", "path": "/spec/policy/buffer/bufferSize", "value": %s },`+ + `{ "op": "replace", "path": "/spec/policy/buffer/minReplicas", "value": %d },`+ + `{ "op": "replace", "path": "/spec/policy/buffer/maxReplicas", "value": %d }]`, + bufferSizeFmt, minReplicas, maxReplicas) + logrus. + WithField("fleetautoscaler", fas.ObjectMeta.Name). + WithField("bufferSize", bufferSize.String()). + WithField("minReplicas", minReplicas). + WithField("maxReplicas", maxReplicas). + WithField("patch", patch). + Info("Patching fleetautoscaler") + + return framework.AgonesClient.StableV1alpha1().FleetAutoscalers(defaultNs).Patch(fas.ObjectMeta.Name, types.JSONPatchType, []byte(patch)) +} + +// defaultFleetAutoscaler returns a default fleet autoscaler configuration for a given fleet +func defaultFleetAutoscaler(f *v1alpha1.Fleet) *v1alpha1.FleetAutoscaler { + return &v1alpha1.FleetAutoscaler{ + ObjectMeta: metav1.ObjectMeta{Name: f.ObjectMeta.Name + "-autoscaler", Namespace: defaultNs}, + Spec: v1alpha1.FleetAutoscalerSpec{ + FleetName: f.ObjectMeta.Name, + Policy: v1alpha1.FleetAutoscalerPolicy{ + Type: v1alpha1.BufferPolicyType, + Buffer: &v1alpha1.BufferPolicy{ + BufferSize: intstr.FromInt(3), + MaxReplicas: 10, + }, + }, + }, + } +} + +func getAllocation(f *v1alpha1.Fleet) *v1alpha1.FleetAllocation { + // get an allocation + return &v1alpha1.FleetAllocation{ + ObjectMeta: metav1.ObjectMeta{GenerateName: "allocation-", Namespace: defaultNs}, + Spec: v1alpha1.FleetAllocationSpec{ + FleetName: f.ObjectMeta.Name, + }, + } +} diff --git a/test/e2e/framework/framework.go b/test/e2e/framework/framework.go index 425ece7b57..94233f1eb2 100644 --- a/test/e2e/framework/framework.go +++ b/test/e2e/framework/framework.go @@ -84,6 +84,9 @@ func (f *Framework) CreateGameServerAndWaitUntilReady(ns string, gs *v1alpha1.Ga return nil, fmt.Errorf("waiting for %v GameServer instance readiness timed out (%v): %v", gs.Spec, gs.Name, err) } + if len(readyGs.Status.Ports) == 0 { + return nil, fmt.Errorf("Ready GameServer instance has no port: %v", readyGs.Status) + } return readyGs, nil }