From 1cf5c69006d68b8266c553484f838b33a24f5064 Mon Sep 17 00:00:00 2001 From: Mark Mandel Date: Thu, 6 Jun 2019 17:48:45 -0700 Subject: [PATCH] PortPolicy of Passthrough - Same Port for Container and Host This implements a new PortPolicy, such that the containerPort that is specified for the game server is dynamically set to the same random value that the hostPort is. This is useful for game servers that have already been configured to broadcast the port that they originally started on as their connection port, with no overrides. Closes #294 --- build/Makefile | 2 +- examples/allocator-service/main.go | 2 +- examples/fleet.yaml | 2 +- examples/gameserver.yaml | 2 + examples/simple-udp/Dockerfile | 4 +- examples/simple-udp/Makefile | 2 +- examples/simple-udp/fleet-distributed.yaml | 2 +- examples/simple-udp/fleet.yaml | 2 +- .../simple-udp/gameserver-passthrough.yaml | 37 ++++++++++++++ examples/simple-udp/gameserver.yaml | 2 +- examples/simple-udp/gameserverset.yaml | 2 +- examples/simple-udp/main.go | 28 +++++++--- examples/xonotic/main.go | 2 +- install/helm/agones/templates/NOTES.txt | 2 +- .../crds/_gameserverspecvalidation.yaml | 11 ++-- install/yaml/install.yaml | 33 ++++++------ pkg/apis/stable/v1alpha1/common.go | 8 +-- pkg/apis/stable/v1alpha1/gameserver.go | 33 +++++++++--- pkg/apis/stable/v1alpha1/gameserver_test.go | 51 +++++++++++++++++-- pkg/gameservers/portallocator.go | 12 +++-- pkg/gameservers/portallocator_test.go | 34 ++++++++++--- .../en/docs/Advanced/limiting-resources.md | 2 +- .../Advanced/scheduling-and-autoscaling.md | 4 +- .../en/docs/Advanced/service-accounts.md | 2 +- .../en/docs/Getting Started/create-fleet.md | 4 +- .../docs/Getting Started/create-gameserver.md | 2 +- site/content/en/docs/Guides/access-api.md | 8 +-- site/content/en/docs/Reference/gameserver.md | 8 +-- test/e2e/gameserver_test.go | 27 ++++++++++ test/e2e/main_test.go | 4 +- 30 files changed, 259 insertions(+), 75 deletions(-) create mode 100644 examples/simple-udp/gameserver-passthrough.yaml diff --git a/build/Makefile b/build/Makefile index d1b500b5e4..ab8434d6a6 100644 --- a/build/Makefile +++ b/build/Makefile @@ -57,7 +57,7 @@ KIND_PROFILE ?= agones KIND_CONTAINER_NAME=kind-$(KIND_PROFILE)-control-plane # Game Server image to use while doing end-to-end tests -GS_TEST_IMAGE ?= gcr.io/agones-images/udp-server:0.9 +GS_TEST_IMAGE ?= gcr.io/agones-images/udp-server:0.10 # Directory that this Makefile is in. mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) diff --git a/examples/allocator-service/main.go b/examples/allocator-service/main.go index 2a53a00772..a1fa844cbf 100644 --- a/examples/allocator-service/main.go +++ b/examples/allocator-service/main.go @@ -9,7 +9,7 @@ import ( "agones.dev/agones/pkg/apis/stable/v1alpha1" "agones.dev/agones/pkg/client/clientset/versioned" "agones.dev/agones/pkg/util/runtime" // for the logger - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/rest" ) diff --git a/examples/fleet.yaml b/examples/fleet.yaml index 9a2c508c8d..7dd3efc164 100644 --- a/examples/fleet.yaml +++ b/examples/fleet.yaml @@ -67,4 +67,4 @@ spec: spec: containers: - name: simple-udp - image: gcr.io/agones-images/udp-server:0.9 \ No newline at end of file + image: gcr.io/agones-images/udp-server:0.10 \ No newline at end of file diff --git a/examples/gameserver.yaml b/examples/gameserver.yaml index 97689f755e..9e71aab563 100644 --- a/examples/gameserver.yaml +++ b/examples/gameserver.yaml @@ -38,6 +38,8 @@ spec: # portPolicy has two options: # - "Dynamic" (default) the system allocates a free hostPort for the gameserver, for game clients to connect to # - "Static", user defines the hostPort that the game client will connect to. Then onus is on the user to ensure that the + # - "Passthrough" dynamically sets the `containerPort` to the same value as the dynamically selected hostPort. + # This will mean that users will need to lookup what port has been opened through the server side SDK. # port is available. When static is the policy specified, `hostPort` is required to be populated portPolicy: Dynamic # the port that is being opened on the game server process diff --git a/examples/simple-udp/Dockerfile b/examples/simple-udp/Dockerfile index 355856bd55..f676b9698a 100644 --- a/examples/simple-udp/Dockerfile +++ b/examples/simple-udp/Dockerfile @@ -21,7 +21,7 @@ COPY . /go/src/agones.dev/agones RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server . # final image -FROM alpine:3.8 +FROM alpine:3.9 RUN adduser -D server COPY --from=builder /go/src/simple-udp/server /home/server/server @@ -29,4 +29,4 @@ RUN chown -R server /home/server && \ chmod o+x /home/server/server USER server -ENTRYPOINT /home/server/server \ No newline at end of file +ENTRYPOINT ["/home/server/server"] \ No newline at end of file diff --git a/examples/simple-udp/Makefile b/examples/simple-udp/Makefile index 411710c370..8c3385a57f 100644 --- a/examples/simple-udp/Makefile +++ b/examples/simple-udp/Makefile @@ -27,7 +27,7 @@ REPOSITORY = gcr.io/agones-images mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) project_path := $(dir $(mkfile_path)) -server_tag = $(REPOSITORY)/udp-server:0.9 +server_tag = $(REPOSITORY)/udp-server:0.10 root_path = $(realpath $(project_path)/../..) # _____ _ diff --git a/examples/simple-udp/fleet-distributed.yaml b/examples/simple-udp/fleet-distributed.yaml index ff53f58689..7b54a2a190 100644 --- a/examples/simple-udp/fleet-distributed.yaml +++ b/examples/simple-udp/fleet-distributed.yaml @@ -32,7 +32,7 @@ spec: spec: containers: - name: simple-udp - image: gcr.io/agones-images/udp-server:0.9 + image: gcr.io/agones-images/udp-server:0.10 resources: requests: memory: "32Mi" diff --git a/examples/simple-udp/fleet.yaml b/examples/simple-udp/fleet.yaml index e2d91c8de4..1ff2db32bf 100644 --- a/examples/simple-udp/fleet.yaml +++ b/examples/simple-udp/fleet.yaml @@ -27,7 +27,7 @@ spec: spec: containers: - name: simple-udp - image: gcr.io/agones-images/udp-server:0.9 + image: gcr.io/agones-images/udp-server:0.10 resources: requests: memory: "64Mi" diff --git a/examples/simple-udp/gameserver-passthrough.yaml b/examples/simple-udp/gameserver-passthrough.yaml new file mode 100644 index 0000000000..e5b4d02224 --- /dev/null +++ b/examples/simple-udp/gameserver-passthrough.yaml @@ -0,0 +1,37 @@ +# Copyright 2017 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: "stable.agones.dev/v1alpha1" +kind: GameServer +metadata: + generateName: "simple-udp-" +spec: + ports: + - name: default + portPolicy: Passthrough + template: + spec: + containers: + - name: simple-udp + image: gcr.io/agones-images/udp-server:0.10 + env: + - name: "PASSTHROUGH" + value: "TRUE" + resources: + requests: + memory: "32Mi" + cpu: "20m" + limits: + memory: "32Mi" + cpu: "20m" diff --git a/examples/simple-udp/gameserver.yaml b/examples/simple-udp/gameserver.yaml index 306f0ec902..3c8b1321b8 100644 --- a/examples/simple-udp/gameserver.yaml +++ b/examples/simple-udp/gameserver.yaml @@ -25,7 +25,7 @@ spec: spec: containers: - name: simple-udp - image: gcr.io/agones-images/udp-server:0.9 + image: gcr.io/agones-images/udp-server:0.10 resources: requests: memory: "32Mi" diff --git a/examples/simple-udp/gameserverset.yaml b/examples/simple-udp/gameserverset.yaml index 6d534e3c18..338f522dcd 100644 --- a/examples/simple-udp/gameserverset.yaml +++ b/examples/simple-udp/gameserverset.yaml @@ -31,4 +31,4 @@ spec: spec: containers: - name: simple-udp - image: gcr.io/agones-images/udp-server:0.9 \ No newline at end of file + image: gcr.io/agones-images/udp-server:0.10 \ No newline at end of file diff --git a/examples/simple-udp/main.go b/examples/simple-udp/main.go index d2c6b87665..1c19d9e2d0 100644 --- a/examples/simple-udp/main.go +++ b/examples/simple-udp/main.go @@ -36,17 +36,15 @@ func main() { go doSignal() port := flag.String("port", "7654", "The port to listen to udp traffic on") + passthrough := flag.Bool("passthrough", false, "Get listening port from the SDK, rather than use the 'port' value") flag.Parse() if ep := os.Getenv("PORT"); ep != "" { port = &ep } - - log.Printf("Starting UDP server, listening on port %s", *port) - conn, err := net.ListenPacket("udp", ":"+*port) - if err != nil { - log.Fatalf("Could not start udp server: %v", err) + if epass := os.Getenv("PASSTHROUGH"); epass != "" { + p := strings.ToUpper(epass) == "TRUE" + passthrough = &p } - defer conn.Close() // nolint: errcheck log.Print("Creating SDK instance") s, err := sdk.NewSDK() @@ -58,6 +56,24 @@ func main() { stop := make(chan struct{}) go doHealth(s, stop) + if *passthrough { + var gs *coresdk.GameServer + gs, err = s.GameServer() + if err != nil { + log.Fatalf("Could not get gameserver port details: %s", err) + } + + p := strconv.FormatInt(int64(gs.Status.Ports[0].Port), 10) + port = &p + } + + log.Printf("Starting UDP server, listening on port %s", *port) + conn, err := net.ListenPacket("udp", ":"+*port) + if err != nil { + log.Fatalf("Could not start udp server: %v", err) + } + defer conn.Close() // nolint: errcheck + log.Print("Marking this server as ready") // This tells Agones that the server is ready to receive connections. err = s.Ready() diff --git a/examples/xonotic/main.go b/examples/xonotic/main.go index 51aeefbfbe..7cc3860fe2 100644 --- a/examples/xonotic/main.go +++ b/examples/xonotic/main.go @@ -24,7 +24,7 @@ import ( "strings" "time" - "agones.dev/agones/sdks/go" + sdk "agones.dev/agones/sdks/go" ) type interceptor struct { diff --git a/install/helm/agones/templates/NOTES.txt b/install/helm/agones/templates/NOTES.txt index 7a534bcdb3..3f6967c237 100644 --- a/install/helm/agones/templates/NOTES.txt +++ b/install/helm/agones/templates/NOTES.txt @@ -19,7 +19,7 @@ spec: spec: containers: - name: simple-udp - image: gcr.io/agones-images/udp-server:0.9 + image: gcr.io/agones-images/udp-server:0.10 Finally don't forget to explore our documentation and usage guides on how to develop and host dedicated game servers on top of Agones. : diff --git a/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml b/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml index d9ffb4d74b..696a61f26b 100644 --- a/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml +++ b/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml @@ -58,22 +58,23 @@ properties: title: array of ports to expose on the game server container type: array minItems: 1 - required: - - containerPort items: type: object properties: portPolicy: title: the port policy that will be applied to the game server description: | - portPolicy has two options: - - "Dynamic" (default) the system allocates a free hostPort for the gameserver, for game clients to connect to + portPolicy has three options: + - "Dynamic" (default) the system allocates a random free hostPort for the gameserver, for game clients to connect to - "Static", user defines the hostPort that the game client will connect to. Then onus is on the user to ensure that the port is available. When static is the policy specified, `hostPort` is required to be populated + - "Passthrough" dynamically sets the `containerPort` to the same value as the dynamically selected hostPort. + This will mean that users will need to lookup what port has been opened through the server side SDK. type: string enum: - Dynamic - Static + - Passthrough protocol: title: Protocol being used. Defaults to UDP. TCP is the only other option type: string @@ -87,7 +88,7 @@ properties: maximum: 65535 hostPort: title: The port exposed on the host - description: Only required when `portPolicy` is "Static". Overwritten when portPolicy is "dynamic". + description: Only required when `portPolicy` is "Static". Overwritten when portPolicy is "Dynamic" or "Passthrough". type: integer minimum: 1 maximum: 65535 diff --git a/install/yaml/install.yaml b/install/yaml/install.yaml index f098845093..bd58c21534 100644 --- a/install/yaml/install.yaml +++ b/install/yaml/install.yaml @@ -301,22 +301,23 @@ spec: title: array of ports to expose on the game server container type: array minItems: 1 - required: - - containerPort items: type: object properties: portPolicy: title: the port policy that will be applied to the game server description: | - portPolicy has two options: - - "Dynamic" (default) the system allocates a free hostPort for the gameserver, for game clients to connect to + portPolicy has three options: + - "Dynamic" (default) the system allocates a random free hostPort for the gameserver, for game clients to connect to - "Static", user defines the hostPort that the game client will connect to. Then onus is on the user to ensure that the port is available. When static is the policy specified, `hostPort` is required to be populated + - "Passthrough" dynamically sets the `containerPort` to the same value as the dynamically selected hostPort. + This will mean that users will need to lookup what port has been opened through the server side SDK. type: string enum: - Dynamic - Static + - Passthrough protocol: title: Protocol being used. Defaults to UDP. TCP is the only other option type: string @@ -330,7 +331,7 @@ spec: maximum: 65535 hostPort: title: The port exposed on the host - description: Only required when `portPolicy` is "Static". Overwritten when portPolicy is "dynamic". + description: Only required when `portPolicy` is "Static". Overwritten when portPolicy is "Dynamic" or "Passthrough". type: integer minimum: 1 maximum: 65535 @@ -620,22 +621,23 @@ spec: title: array of ports to expose on the game server container type: array minItems: 1 - required: - - containerPort items: type: object properties: portPolicy: title: the port policy that will be applied to the game server description: | - portPolicy has two options: - - "Dynamic" (default) the system allocates a free hostPort for the gameserver, for game clients to connect to + portPolicy has three options: + - "Dynamic" (default) the system allocates a random free hostPort for the gameserver, for game clients to connect to - "Static", user defines the hostPort that the game client will connect to. Then onus is on the user to ensure that the port is available. When static is the policy specified, `hostPort` is required to be populated + - "Passthrough" dynamically sets the `containerPort` to the same value as the dynamically selected hostPort. + This will mean that users will need to lookup what port has been opened through the server side SDK. type: string enum: - Dynamic - Static + - Passthrough protocol: title: Protocol being used. Defaults to UDP. TCP is the only other option type: string @@ -649,7 +651,7 @@ spec: maximum: 65535 hostPort: title: The port exposed on the host - description: Only required when `portPolicy` is "Static". Overwritten when portPolicy is "dynamic". + description: Only required when `portPolicy` is "Static". Overwritten when portPolicy is "Dynamic" or "Passthrough". type: integer minimum: 1 maximum: 65535 @@ -882,22 +884,23 @@ spec: title: array of ports to expose on the game server container type: array minItems: 1 - required: - - containerPort items: type: object properties: portPolicy: title: the port policy that will be applied to the game server description: | - portPolicy has two options: - - "Dynamic" (default) the system allocates a free hostPort for the gameserver, for game clients to connect to + portPolicy has three options: + - "Dynamic" (default) the system allocates a random free hostPort for the gameserver, for game clients to connect to - "Static", user defines the hostPort that the game client will connect to. Then onus is on the user to ensure that the port is available. When static is the policy specified, `hostPort` is required to be populated + - "Passthrough" dynamically sets the `containerPort` to the same value as the dynamically selected hostPort. + This will mean that users will need to lookup what port has been opened through the server side SDK. type: string enum: - Dynamic - Static + - Passthrough protocol: title: Protocol being used. Defaults to UDP. TCP is the only other option type: string @@ -911,7 +914,7 @@ spec: maximum: 65535 hostPort: title: The port exposed on the host - description: Only required when `portPolicy` is "Static". Overwritten when portPolicy is "dynamic". + description: Only required when `portPolicy` is "Static". Overwritten when portPolicy is "Dynamic" or "Passthrough". type: integer minimum: 1 maximum: 65535 diff --git a/pkg/apis/stable/v1alpha1/common.go b/pkg/apis/stable/v1alpha1/common.go index 65f31f7cbf..6345f90330 100644 --- a/pkg/apis/stable/v1alpha1/common.go +++ b/pkg/apis/stable/v1alpha1/common.go @@ -24,9 +24,11 @@ import ( // Block of const Error messages const ( - ErrContainerRequired = "Container is required when using multiple containers in the pod template" - ErrHostPortDynamic = "HostPort cannot be specified with a Dynamic PortPolicy" - ErrPortPolicyStatic = "PortPolicy must be Static" + ErrContainerRequired = "Container is required when using multiple containers in the pod template" + ErrHostPortDynamic = "HostPort cannot be specified with a Dynamic PortPolicy" + ErrPortPolicyStatic = "PortPolicy must be Static" + ErrContainerPortRequired = "ContainerPort must be defined for Dynamic and Static PortPolicies" + ErrContainerPortPassthrough = "ContainerPort cannot be specified with Passthrough PortPolicy" ) // crd is an interface to get Name and Kind of CRD diff --git a/pkg/apis/stable/v1alpha1/gameserver.go b/pkg/apis/stable/v1alpha1/gameserver.go index 9ab52f7090..8222e254ab 100644 --- a/pkg/apis/stable/v1alpha1/gameserver.go +++ b/pkg/apis/stable/v1alpha1/gameserver.go @@ -66,6 +66,9 @@ const ( // Dynamic PortPolicy means that the system will choose an open // port for the GameServer in question Dynamic PortPolicy = "Dynamic" + // Passthrough dynamically sets the `containerPort` to the same value as the dynamically selected hostPort. + // This will mean that users will need to lookup what port has been opened through the server side SDK. + Passthrough PortPolicy = "Passthrough" // RoleLabel is the label in which the Agones role is specified. // Pods from a GameServer will have the value "gameserver" @@ -161,7 +164,7 @@ type GameServerPort struct { // connect to PortPolicy PortPolicy `json:"portPolicy,omitempty"` // ContainerPort is the port that is being opened on the game server process - ContainerPort int32 `json:"containerPort"` + ContainerPort int32 `json:"containerPort,omitempty"` // HostPort the port exposed on the host for clients to connect to HostPort int32 `json:"hostPort,omitempty"` // Protocol is the network protocol being used. Defaults to UDP. TCP is the only other option @@ -227,7 +230,7 @@ func (gs *GameServer) applyStateDefaults() { if gs.Status.State == "" { gs.Status.State = GameServerStateCreating // applyStateDefaults() should be called after applyPortDefaults() - if gs.HasPortPolicy(Dynamic) { + if gs.HasPortPolicy(Dynamic) || gs.HasPortPolicy(Passthrough) { gs.Status.State = GameServerStatePortAllocation } } @@ -297,7 +300,25 @@ func (gss GameServerSpec) Validate(devAddress string) ([]metav1.StatusCause, boo // no host port when using dynamic PortPolicy for _, p := range gss.Ports { - if p.HostPort > 0 && p.PortPolicy == Dynamic { + if p.PortPolicy == Dynamic || p.PortPolicy == Static { + if p.ContainerPort <= 0 { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: fmt.Sprintf("%s.containerPort", p.Name), + Message: ErrContainerPortRequired, + }) + } + } + + if p.PortPolicy == Passthrough && p.ContainerPort > 0 { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: fmt.Sprintf("%s.containerPort", p.Name), + Message: ErrContainerPortPassthrough, + }) + } + + if p.HostPort > 0 && (p.PortPolicy == Dynamic || p.PortPolicy == Passthrough) { causes = append(causes, metav1.StatusCause{ Type: metav1.CauseTypeFieldValueInvalid, Field: fmt.Sprintf("%s.hostPort", p.Name), @@ -517,11 +538,11 @@ func (p GameServerPort) Status() GameServerStatusPort { } // CountPorts returns the number of -// ports that have this type of PortPolicy -func (gs *GameServer) CountPorts(policy PortPolicy) int { +// ports that match condition function +func (gs *GameServer) CountPorts(f func(policy PortPolicy) bool) int { count := 0 for _, p := range gs.Spec.Ports { - if p.PortPolicy == policy { + if f(p.PortPolicy) { count++ } } diff --git a/pkg/apis/stable/v1alpha1/gameserver_test.go b/pkg/apis/stable/v1alpha1/gameserver_test.go index f78aa61828..843f9326e6 100644 --- a/pkg/apis/stable/v1alpha1/gameserver_test.go +++ b/pkg/apis/stable/v1alpha1/gameserver_test.go @@ -94,6 +94,29 @@ func TestGameServerApplyDefaults(t *testing.T) { }, }, }, + "defaults on passthrough": { + gameServer: GameServer{ + Spec: GameServerSpec{ + Ports: []GameServerPort{{PortPolicy: Passthrough}}, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{Containers: []corev1.Container{ + {Name: "testing", Image: "testing/image"}, + }}}}, + }, + container: "testing", + expected: expected{ + protocol: "UDP", + state: GameServerStatePortAllocation, + policy: Passthrough, + scheduling: apis.Packed, + health: Health{ + Disabled: false, + FailureThreshold: 3, + InitialDelaySeconds: 5, + PeriodSeconds: 5, + }, + }, + }, "defaults are already set": { gameServer: GameServer{ Spec: GameServerSpec{ @@ -242,9 +265,10 @@ func TestGameServerValidate(t *testing.T) { fields = append(fields, f.Field) } assert.False(t, ok) - assert.Len(t, causes, 3) + assert.Len(t, causes, 4) assert.Contains(t, fields, "container") assert.Contains(t, fields, "main.hostPort") + assert.Contains(t, fields, "main.containerPort") assert.Equal(t, causes[0].Type, metav1.CauseTypeFieldValueInvalid) gs = GameServer{ @@ -296,6 +320,23 @@ func TestGameServerValidate(t *testing.T) { causes, ok = gs.Validate() assert.True(t, ok) assert.Len(t, causes, 0) + + gs = GameServer{ + Spec: GameServerSpec{ + Ports: []GameServerPort{{Name: "one", PortPolicy: Passthrough, ContainerPort: 1294}, {PortPolicy: Passthrough, Name: "two", HostPort: 7890}}, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}}, + }, + } + gs.ApplyDefaults() + causes, ok = gs.Validate() + for _, f := range causes { + fields = append(fields, f.Field) + } + assert.False(t, ok) + assert.Len(t, causes, 2) + assert.Contains(t, fields, "one.containerPort") + assert.Contains(t, fields, "two.hostPort") } func TestGameServerPod(t *testing.T) { @@ -417,8 +458,12 @@ func TestGameServerCountPorts(t *testing.T) { {PortPolicy: Static}, }}} - assert.Equal(t, 3, fixture.CountPorts(Dynamic)) - assert.Equal(t, 1, fixture.CountPorts(Static)) + assert.Equal(t, 3, fixture.CountPorts(func(policy PortPolicy) bool { + return policy == Dynamic + })) + assert.Equal(t, 1, fixture.CountPorts(func(policy PortPolicy) bool { + return policy == Static + })) } func TestGameServerPatch(t *testing.T) { diff --git a/pkg/gameservers/portallocator.go b/pkg/gameservers/portallocator.go index 7092bc5679..1fd5dc2ef9 100644 --- a/pkg/gameservers/portallocator.go +++ b/pkg/gameservers/portallocator.go @@ -138,19 +138,25 @@ func (pa *PortAllocator) Allocate(gs *v1alpha1.GameServer) *v1alpha1.GameServer // this allows us to do recursion, within the mutex lock var allocate func(gs *v1alpha1.GameServer) *v1alpha1.GameServer allocate = func(gs *v1alpha1.GameServer) *v1alpha1.GameServer { - amount := gs.CountPorts(v1alpha1.Dynamic) + amount := gs.CountPorts(func(policy v1alpha1.PortPolicy) bool { + return policy == v1alpha1.Dynamic || policy == v1alpha1.Passthrough + }) allocations := findOpenPorts(amount) if len(allocations) == amount { pa.gameServerRegistry[gs.ObjectMeta.UID] = true for i, p := range gs.Spec.Ports { - if p.PortPolicy == v1alpha1.Dynamic { + if p.PortPolicy == v1alpha1.Dynamic || p.PortPolicy == v1alpha1.Passthrough { // pop off allocation var a pn a, allocations = allocations[0], allocations[1:] a.pa[a.port] = true gs.Spec.Ports[i].HostPort = a.port + + if p.PortPolicy == v1alpha1.Passthrough { + gs.Spec.Ports[i].ContainerPort = a.port + } } } @@ -265,7 +271,7 @@ func (pa *PortAllocator) registerExistingGameServerPorts(gameservers []*v1alpha1 for _, gs := range gameservers { for _, p := range gs.Spec.Ports { - if p.PortPolicy == v1alpha1.Dynamic { + if p.PortPolicy == v1alpha1.Dynamic || p.PortPolicy == v1alpha1.Passthrough { gsRegistry[gs.ObjectMeta.UID] = true // if the node doesn't exist, it's likely unscheduled diff --git a/pkg/gameservers/portallocator_test.go b/pkg/gameservers/portallocator_test.go index 7198338c4e..5a00e97c7c 100644 --- a/pkg/gameservers/portallocator_test.go +++ b/pkg/gameservers/portallocator_test.go @@ -96,6 +96,16 @@ func TestPortAllocatorAllocate(t *testing.T) { assert.Equal(t, 10, countTotalAllocatedPorts(pa)) assert.Equal(t, v1alpha1.Static, copy.Spec.Ports[3].PortPolicy) assert.Equal(t, expected, copy.Spec.Ports[3].HostPort) + + // single port, passthrough + copy = fixture.DeepCopy() + copy.Spec.Ports[0] = v1alpha1.GameServerPort{Name: "passthrough", PortPolicy: v1alpha1.Passthrough} + assert.Len(t, copy.Spec.Ports, 1) + pa.Allocate(copy) + assert.NotEmpty(t, copy.Spec.Ports[0].HostPort) + assert.Equal(t, copy.Spec.Ports[0].HostPort, copy.Spec.Ports[0].ContainerPort) + assert.Nil(t, err) + assert.Equal(t, 11, countTotalAllocatedPorts(pa)) }) t.Run("ports are all allocated", func(t *testing.T) { @@ -145,6 +155,9 @@ func TestPortAllocatorAllocate(t *testing.T) { morePortFixture := fixture.DeepCopy() morePortFixture.Spec.Ports = append(morePortFixture.Spec.Ports, v1alpha1.GameServerPort{Name: "another", ContainerPort: 6666, PortPolicy: v1alpha1.Dynamic}) morePortFixture.Spec.Ports = append(morePortFixture.Spec.Ports, v1alpha1.GameServerPort{Name: "static", ContainerPort: 6666, PortPolicy: v1alpha1.Static, HostPort: 9999}) + morePortFixture.Spec.Ports = append(morePortFixture.Spec.Ports, v1alpha1.GameServerPort{Name: "passthrough", PortPolicy: v1alpha1.Passthrough}) + + assert.Len(t, morePortFixture.Spec.Ports, 4) // Make sure the add's don't corrupt the sync nodeWatch.Add(&n1) @@ -161,11 +174,20 @@ func TestPortAllocatorAllocate(t *testing.T) { copy := morePortFixture.DeepCopy() copy.ObjectMeta.UID = types.UID(strconv.Itoa(x) + ":" + strconv.Itoa(i)) gs := pa.Allocate(copy) + + // Dynamic assert.NotEmpty(t, gs.Spec.Ports[0].HostPort) + + // Passthrough + passThrough := gs.Spec.Ports[3] + assert.Equal(t, v1alpha1.Passthrough, passThrough.PortPolicy) + assert.NotEmpty(t, passThrough.HostPort) + assert.Equal(t, passThrough.HostPort, passThrough.ContainerPort) + logrus.WithField("uid", copy.ObjectMeta.UID).WithField("ports", gs.Spec.Ports).WithError(err).Info("Allocated Port") assert.Nil(t, err) for _, p := range gs.Spec.Ports { - if p.PortPolicy == v1alpha1.Dynamic { + if p.PortPolicy == v1alpha1.Dynamic || p.PortPolicy == v1alpha1.Passthrough { assert.True(t, 10 <= p.HostPort && p.HostPort <= maxPort, "%v is not between 10 and 20", p) } } @@ -173,10 +195,10 @@ func TestPortAllocatorAllocate(t *testing.T) { } logrus.WithField("allocated", countTotalAllocatedPorts(pa)).WithField("count", len(pa.portAllocations[0])+len(pa.portAllocations[1])).Info("How many allocated") - assert.Len(t, pa.portAllocations, 2) + assert.Len(t, pa.portAllocations, 3) gs := pa.Allocate(fixture.DeepCopy()) assert.NotEmpty(t, gs.Spec.Ports[0].HostPort) - assert.Len(t, pa.portAllocations, 3) + assert.Len(t, pa.portAllocations, 4) }) t.Run("ports are unique in a node", func(t *testing.T) { @@ -307,7 +329,7 @@ func TestPortAllocatorSyncPortAllocations(t *testing.T) { Status: v1alpha1.GameServerStatus{State: v1alpha1.GameServerStateReady, Ports: []v1alpha1.GameServerStatusPort{{Port: 11}}, NodeName: n3.ObjectMeta.Name}} gs4 := v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gs4", UID: "4"}, Spec: v1alpha1.GameServerSpec{ - Ports: []v1alpha1.GameServerPort{{PortPolicy: v1alpha1.Dynamic, HostPort: 12}}, + Ports: []v1alpha1.GameServerPort{{PortPolicy: v1alpha1.Passthrough, HostPort: 12}}, }, Status: v1alpha1.GameServerStatus{State: v1alpha1.GameServerStateCreating}} gs5 := v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gs5", UID: "5"}, @@ -364,7 +386,7 @@ func TestPortAllocatorSyncDeleteGameServer(t *testing.T) { Status: v1alpha1.GameServerStatus{State: v1alpha1.GameServerStateReady, Ports: []v1alpha1.GameServerStatusPort{{Port: 11}}, NodeName: n1.ObjectMeta.Name}} gs3 := &v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gs3", UID: "3"}, Spec: v1alpha1.GameServerSpec{ - Ports: []v1alpha1.GameServerPort{{PortPolicy: v1alpha1.Dynamic, HostPort: 10}}, + Ports: []v1alpha1.GameServerPort{{PortPolicy: v1alpha1.Passthrough, HostPort: 10}}, }, Status: v1alpha1.GameServerStatus{State: v1alpha1.GameServerStateReady, Ports: []v1alpha1.GameServerStatusPort{{Port: 10}}, NodeName: n2.ObjectMeta.Name}} gs4 := &v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gs4", UID: "4"}, @@ -474,7 +496,7 @@ func TestPortAllocatorRegisterExistingGameServerPorts(t *testing.T) { gs3 := &v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gs3", UID: "3"}, Spec: v1alpha1.GameServerSpec{ - Ports: []v1alpha1.GameServerPort{{PortPolicy: v1alpha1.Dynamic, HostPort: 12}}, + Ports: []v1alpha1.GameServerPort{{PortPolicy: v1alpha1.Passthrough, HostPort: 12}}, }, Status: v1alpha1.GameServerStatus{State: v1alpha1.GameServerStateReady, Ports: []v1alpha1.GameServerStatusPort{{Port: 12}}, NodeName: n1.ObjectMeta.Name}} diff --git a/site/content/en/docs/Advanced/limiting-resources.md b/site/content/en/docs/Advanced/limiting-resources.md index 3c04be0cc0..f3fd99fd7a 100644 --- a/site/content/en/docs/Advanced/limiting-resources.md +++ b/site/content/en/docs/Advanced/limiting-resources.md @@ -38,7 +38,7 @@ spec: spec: containers: - name: simple-udp - image: gcr.io/agones-images/udp-server:0.9 + image: gcr.io/agones-images/udp-server:0.10 resources: limit: cpu: "250m" #this is our limit here diff --git a/site/content/en/docs/Advanced/scheduling-and-autoscaling.md b/site/content/en/docs/Advanced/scheduling-and-autoscaling.md index 7f1790dda1..ce754d78f7 100644 --- a/site/content/en/docs/Advanced/scheduling-and-autoscaling.md +++ b/site/content/en/docs/Advanced/scheduling-and-autoscaling.md @@ -80,7 +80,7 @@ spec: spec: containers: - name: simple-udp - image: gcr.io/agones-images/udp-server:0.9 + image: gcr.io/agones-images/udp-server:0.10 ``` This is the *default* Fleet scheduling strategy. It is designed for dynamic Kubernetes environments, wherein you wish @@ -135,7 +135,7 @@ spec: spec: containers: - name: simple-udp - image: gcr.io/agones-images/udp-server:0.9 + image: gcr.io/agones-images/udp-server:0.10 ``` This Fleet scheduling strategy is designed for static Kubernetes environments, such as when you are running Kubernetes diff --git a/site/content/en/docs/Advanced/service-accounts.md b/site/content/en/docs/Advanced/service-accounts.md index bb9bca7b7b..93de124ff8 100644 --- a/site/content/en/docs/Advanced/service-accounts.md +++ b/site/content/en/docs/Advanced/service-accounts.md @@ -39,7 +39,7 @@ spec: serviceAccountName: my-special-service-account # a custom service account containers: - name: simple-udp - image: gcr.io/agones-images/udp-server:0.9 + image: gcr.io/agones-images/udp-server:0.10 ``` If a service account is configured, the mounted key is not overwritten, as it assumed that you want to have full control diff --git a/site/content/en/docs/Getting Started/create-fleet.md b/site/content/en/docs/Getting Started/create-fleet.md index b6003de09a..fd885b138f 100644 --- a/site/content/en/docs/Getting Started/create-fleet.md +++ b/site/content/en/docs/Getting Started/create-fleet.md @@ -111,7 +111,7 @@ Spec: Creation Timestamp: Spec: Containers: - Image: gcr.io/agones-images/udp-server:0.9 + Image: gcr.io/agones-images/udp-server:0.10 Name: simple-udp Resources: Status: @@ -308,7 +308,7 @@ status: creationTimestamp: null spec: containers: - - image: gcr.io/agones-images/udp-server:0.9 + - image: gcr.io/agones-images/udp-server:0.10 name: simple-udp resources: {} status: diff --git a/site/content/en/docs/Getting Started/create-gameserver.md b/site/content/en/docs/Getting Started/create-gameserver.md index c5a1c4f773..614c3ebbb9 100644 --- a/site/content/en/docs/Getting Started/create-gameserver.md +++ b/site/content/en/docs/Getting Started/create-gameserver.md @@ -107,7 +107,7 @@ Spec: Creation Timestamp: Spec: Containers: - Image: gcr.io/agones-images/udp-server:0.9 + Image: gcr.io/agones-images/udp-server:0.10 Name: simple-udp Resources: Limits: diff --git a/site/content/en/docs/Guides/access-api.md b/site/content/en/docs/Guides/access-api.md index 1cf8476e82..da716e0ca4 100644 --- a/site/content/en/docs/Guides/access-api.md +++ b/site/content/en/docs/Guides/access-api.md @@ -90,7 +90,7 @@ func main() { Spec: v1alpha1.GameServerSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ - Containers: []corev1.Container{{Name: "udp-server", Image: "gcr.io/agones-images/udp-server:0.9"}}, + Containers: []corev1.Container{{Name: "udp-server", Image: "gcr.io/agones-images/udp-server:0.10"}}, }, }, }, @@ -178,7 +178,7 @@ $ curl http://localhost:8001/apis/stable.agones.dev/v1alpha1/namespaces/default/ "kind": "GameServer", "metadata": { "annotations": { - "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"stable.agones.dev/v1alpha1\",\"kind\":\"GameServer\",\"metadata\":{\"annotations\":{},\"name\":\"simple-udp\",\"namespace\":\"default\"},\"spec\":{\"containerPort\":7654,\"hostPort\":7777,\"portPolicy\":\"static\",\"template\":{\"spec\":{\"containers\":[{\"image\":\"gcr.io/agones-images/udp-server:0.9\",\"name\":\"simple-udp\"}]}}}}\n" + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"stable.agones.dev/v1alpha1\",\"kind\":\"GameServer\",\"metadata\":{\"annotations\":{},\"name\":\"simple-udp\",\"namespace\":\"default\"},\"spec\":{\"containerPort\":7654,\"hostPort\":7777,\"portPolicy\":\"static\",\"template\":{\"spec\":{\"containers\":[{\"image\":\"gcr.io/agones-images/udp-server:0.10\",\"name\":\"simple-udp\"}]}}}}\n" }, "clusterName": "", "creationTimestamp": "2018-03-02T21:41:05Z", @@ -210,7 +210,7 @@ $ curl http://localhost:8001/apis/stable.agones.dev/v1alpha1/namespaces/default/ "spec": { "containers": [ { - "image": "gcr.io/agones-images/udp-server:0.9", + "image": "gcr.io/agones-images/udp-server:0.10", "name": "simple-udp", "resources": {} } @@ -317,7 +317,7 @@ $ curl -d '{"apiVersion":"stable.agones.dev/v1alpha1","kind":"FleetAllocation"," "spec": { "containers": [ { - "image": "gcr.io/agones-images/udp-server:0.9", + "image": "gcr.io/agones-images/udp-server:0.10", "name": "simple-udp", "resources": {} } diff --git a/site/content/en/docs/Reference/gameserver.md b/site/content/en/docs/Reference/gameserver.md index 3b0bacbb80..d219a3a57f 100644 --- a/site/content/en/docs/Reference/gameserver.md +++ b/site/content/en/docs/Reference/gameserver.md @@ -49,9 +49,11 @@ The `spec` field is the actual GameServer specification and it is composed as fo - `container` is the name of container running the GameServer in case you have more than one container defined in the [pod](https://kubernetes.io/docs/concepts/workloads/pods/pod-overview/). If you do, this is a mandatory field. For instance this is useful if you want to run a sidecar to ship logs. - `ports` are an array of ports that can be exposed as direct connections to the game server container - `name` is an optional descriptive name for a port - - `portPolicy` has two options `Dynamic` (default) the system allocates a free hostPort for the gameserver, for game clients to connect to. - And `Static`, user defines the hostPort that the game client will connect to. Then onus is on the user to ensure that the port is available. When static is the policy specified, `hostPort` is required to be populated. - - `containerPort` the port that is being opened on the game server process, this is a required field. + - `portPolicy` has {{< feature expiryVersion="0.11.0" >}}two{{< /feature >}}{{< feature publishVersion="0.11.0" >}}three{{< /feature >}} options: + - `Dynamic` (default) the system allocates a random free hostPort for the gameserver, for game clients to connect to. + - `Static`, user defines the hostPort that the game client will connect to. Then onus is on the user to ensure that the port is available. When static is the policy specified, `hostPort` is required to be populated. +{{% feature publishVersion="0.11.0" %}} - `Passthrough` dynamically sets the `containerPort` to the same value a randomly selected hostPort. This will mean that users will need to lookup what port to open through the server side SDK before starting communications.{{% /feature %}} + - `containerPort` the port that is being opened on the game server process, this is a required field{{< feature publishVersion="0.11.0" >}} for Dynamic and Static port policies, and should not be included in Passthrough configuration.{{< /feature >}}. - `protocol` the protocol being used. Defaults to UDP. TCP is the only other option. - `health` to track the overall healthy state of the GameServer, more information available in the [health check documentation]({{< relref "../Guides/health-checking.md" >}}). - `template` the [pod spec template](https://v1-10.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.10/#podtemplatespec-v1-core) to run your GameServer containers, [see](https://kubernetes.io/docs/concepts/workloads/pods/pod-overview/#pod-templates) for more information. diff --git a/test/e2e/gameserver_test.go b/test/e2e/gameserver_test.go index 0ce47bbb72..baeceb9fb7 100644 --- a/test/e2e/gameserver_test.go +++ b/test/e2e/gameserver_test.go @@ -319,6 +319,33 @@ func TestGameServerShutdown(t *testing.T) { assert.NoError(t, err) } +func TestGameServerPassthroughPort(t *testing.T) { + t.Parallel() + gs := defaultGameServer() + gs.Spec.Ports[0] = v1alpha1.GameServerPort{PortPolicy: v1alpha1.Passthrough} + gs.Spec.Template.Spec.Containers[0].Env = []corev1.EnvVar{{Name: "PASSTHROUGH", Value: "TRUE"}} + // gate + _, valid := gs.Validate() + assert.True(t, valid) + + readyGs, err := framework.CreateGameServerAndWaitUntilReady(defaultNs, gs) + if !assert.NoError(t, err) { + assert.FailNow(t, "Could not get a GameServer ready") + } + + port := readyGs.Spec.Ports[0] + assert.Equal(t, v1alpha1.Passthrough, port.PortPolicy) + assert.NotEmpty(t, port.HostPort) + assert.Equal(t, port.HostPort, port.ContainerPort) + + reply, err := e2eframework.SendGameServerUDP(readyGs, "Hello World !") + if err != nil { + t.Fatalf("Could ping GameServer: %v", err) + } + + assert.Equal(t, "ACK: Hello World !\n", reply) +} + func defaultGameServer() *v1alpha1.GameServer { gs := &v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{GenerateName: "udp-server", Namespace: defaultNs}, Spec: v1alpha1.GameServerSpec{ diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index 76a7cea91a..75e72b523d 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -34,8 +34,8 @@ func TestMain(m *testing.M) { usr, _ := user.Current() kubeconfig := flag.String("kubeconfig", filepath.Join(usr.HomeDir, "/.kube/config"), "kube config path, e.g. $HOME/.kube/config") - gsimage := flag.String("gameserver-image", "gcr.io/agones-images/udp-server:0.9", - "gameserver image to use for those tests, gcr.io/agones-images/udp-server:0.9") + gsimage := flag.String("gameserver-image", "gcr.io/agones-images/udp-server:0.10", + "gameserver image to use for those tests, gcr.io/agones-images/udp-server:0.10") pullSecret := flag.String("pullsecret", "", "optional secret to be used for pulling the gameserver and/or Agones SDK sidecar images") stressTestLevel := flag.Int("stress", 0, "enable stress test at given level 0-100")