Skip to content

Commit

Permalink
Add TCPUDP protocol (googleforgames#1764)
Browse files Browse the repository at this point in the history
* Implementation of TCPUDP protocol

* Added TCPUDP to the GameServerSpec
* Added unit test for tcpudp protocol
* Added simple tcpudp server
* Added unit and e2e tests
* Add documentation
* Update install/helm/agones/templates/crds/_gameserverspecvalidation.yaml
* Added TCP firewall rule to e2e test cluster

Co-authored-by: Alexander Apalikov <[email protected]>
Co-authored-by: Mark Mandel <[email protected]>
  • Loading branch information
3 people authored and ilkercelikyilmaz committed Oct 23, 2020
1 parent d1e665c commit a3a7d2e
Show file tree
Hide file tree
Showing 12 changed files with 197 additions and 18 deletions.
13 changes: 13 additions & 0 deletions build/terraform/e2e/module.tf
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,16 @@ resource "helm_release" "consul" {
value = "ClusterIP"
}
}

resource "google_compute_firewall" "tcp" {
name = "game-server-firewall-tcp"
project = var.project
network = "default"

allow {
protocol = "tcp"
ports = ["7000-8000"]
}

target_tags = ["game-server"]
}
2 changes: 1 addition & 1 deletion examples/gameserver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ spec:
containerPort: 7654
# the port exposed on the host, only required when `portPolicy` is "Static". Overwritten when portPolicy is "Dynamic".
hostPort: 7777
# protocol being used. Defaults to UDP. TCP is the only other option
# protocol being used. Defaults to UDP. TCP and TCPUDP are other options
protocol: UDP
# Health checking for the running game server
health:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,12 @@ properties:
- Static
- Passthrough
protocol:
title: Protocol being used. Defaults to UDP. TCP is the only other option
title: Protocol being used. Defaults to UDP. TCP and TCPUDP are other options.
type: string
enum:
- UDP
- TCP
- TCPUDP
containerPort:
title: The port that is being opened on the game server process
type: integer
Expand Down Expand Up @@ -160,4 +161,4 @@ properties:
type: integer
title: The initial player capacity of this Game Server
minimum: 0
{{- end }}
{{- end }}
1 change: 1 addition & 0 deletions install/terraform/modules/gke/cluster.tf
Original file line number Diff line number Diff line change
Expand Up @@ -157,5 +157,6 @@ resource "google_compute_firewall" "default" {
protocol = "udp"
ports = [var.ports]
}

target_tags = ["game-server"]
}
9 changes: 6 additions & 3 deletions install/yaml/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -264,11 +264,12 @@ spec:
- Static
- Passthrough
protocol:
title: Protocol being used. Defaults to UDP. TCP is the only other option
title: Protocol being used. Defaults to UDP. TCP and TCPUDP are other options.
type: string
enum:
- UDP
- TCP
- TCPUDP
containerPort:
title: The port that is being opened on the game server process
type: integer
Expand Down Expand Up @@ -560,11 +561,12 @@ spec:
- Static
- Passthrough
protocol:
title: Protocol being used. Defaults to UDP. TCP is the only other option
title: Protocol being used. Defaults to UDP. TCP and TCPUDP are other options.
type: string
enum:
- UDP
- TCP
- TCPUDP
containerPort:
title: The port that is being opened on the game server process
type: integer
Expand Down Expand Up @@ -875,11 +877,12 @@ spec:
- Static
- Passthrough
protocol:
title: Protocol being used. Defaults to UDP. TCP is the only other option
title: Protocol being used. Defaults to UDP. TCP and TCPUDP are other options.
type: string
enum:
- UDP
- TCP
- TCPUDP
containerPort:
title: The port that is being opened on the game server process
type: integer
Expand Down
5 changes: 4 additions & 1 deletion pkg/apis/agones/v1/gameserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ const (
// This will mean that users will need to lookup what port has been opened through the server side SDK.
Passthrough PortPolicy = "Passthrough"

// ProtocolTCPUDP Protocol exposes the hostPort allocated for this container for both TCP and UDP.
ProtocolTCPUDP corev1.Protocol = "TCPUDP"

// SdkServerLogLevelInfo will cause the SDK server to output all messages except for debug messages.
SdkServerLogLevelInfo SdkServerLogLevel = "Info"
// SdkServerLogLevelDebug will cause the SDK server to output all messages including debug messages.
Expand Down Expand Up @@ -198,7 +201,7 @@ type GameServerPort struct {
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
// Protocol is the network protocol being used. Defaults to UDP. TCP and TCPUDP are other options.
Protocol corev1.Protocol `json:"protocol,omitempty"`
}

Expand Down
24 changes: 24 additions & 0 deletions pkg/gameservers/portallocator.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ func (pa *PortAllocator) Allocate(gs *agonesv1.GameServer) *agonesv1.GameServer
if len(allocations) == amount {
pa.gameServerRegistry[gs.ObjectMeta.UID] = true

var extraPorts []agonesv1.GameServerPort

for i, p := range gs.Spec.Ports {
if p.PortPolicy != agonesv1.Dynamic && p.PortPolicy != agonesv1.Passthrough {
continue
Expand All @@ -161,6 +163,28 @@ func (pa *PortAllocator) Allocate(gs *agonesv1.GameServer) *agonesv1.GameServer
if p.PortPolicy == agonesv1.Passthrough {
gs.Spec.Ports[i].ContainerPort = a.port
}

// create a port for TCP when using TCPUDP protocol
if p.Protocol == agonesv1.ProtocolTCPUDP {
var duplicate = p
duplicate.HostPort = a.port

if duplicate.PortPolicy == agonesv1.Passthrough {
duplicate.ContainerPort = a.port
}

extraPorts = append(extraPorts, duplicate)

gs.Spec.Ports[i].Name = p.Name + "-tcp"
gs.Spec.Ports[i].Protocol = corev1.ProtocolTCP
}
}

// create the UDP port when using TCPUDP protocol
for _, p := range extraPorts {
p.Name += "-udp"
p.Protocol = corev1.ProtocolUDP
gs.Spec.Ports = append(gs.Spec.Ports, p)
}

return gs
Expand Down
14 changes: 14 additions & 0 deletions pkg/gameservers/portallocator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ func TestPortAllocatorAllocate(t *testing.T) {
assert.NotEmpty(t, gsCopy.Spec.Ports[0].HostPort)
assert.Equal(t, gsCopy.Spec.Ports[0].HostPort, gsCopy.Spec.Ports[0].ContainerPort)
assert.Equal(t, 11, countTotalAllocatedPorts(pa))

// single port to two ports, tcpudp
gsCopy = fixture.DeepCopy()
gsCopy.Spec.Ports[0] = agonesv1.GameServerPort{Name: "gameport", PortPolicy: agonesv1.Dynamic, Protocol: agonesv1.ProtocolTCPUDP}

gs = pa.Allocate(gsCopy)
require.NotNil(t, gs)
assert.Equal(t, gsCopy.Spec.Ports[0].HostPort, gsCopy.Spec.Ports[1].HostPort)

assert.Equal(t, corev1.ProtocolTCP, gsCopy.Spec.Ports[0].Protocol)
assert.Equal(t, corev1.ProtocolUDP, gsCopy.Spec.Ports[1].Protocol)
assert.Equal(t, "gameport-tcp", gsCopy.Spec.Ports[0].Name)
assert.Equal(t, "gameport-udp", gsCopy.Spec.Ports[1].Name)
assert.Equal(t, 12, countTotalAllocatedPorts(pa))
})

t.Run("ports are all allocated", func(t *testing.T) {
Expand Down
5 changes: 2 additions & 3 deletions site/content/en/docs/Reference/agones_crd_api_reference.html
Original file line number Diff line number Diff line change
Expand Up @@ -803,8 +803,7 @@ <h3 id="autoscaling.agones.dev/v1.FleetAutoscalerPolicy">FleetAutoscalerPolicy
</em>
</td>
<td>
<em>(Optional)</em>
<p>Webhook policy config params. Present only if FleetAutoscalerPolicyType = Webhook.</p>
<p>Protocol is the network protocol being used. Defaults to UDP. TCP and TCPUDP are other options</p>
</td>
</tr>
</tbody>
Expand Down Expand Up @@ -3466,7 +3465,7 @@ <h3 id="agones.dev/v1.GameServerPort">GameServerPort
</em>
</td>
<td>
<p>Protocol is the network protocol being used. Defaults to UDP. TCP is the only other option</p>
<p>Protocol is the network protocol being used. Defaults to UDP. TCP and TCPUDP are other options.</p>
</td>
</tr>
</tbody>
Expand Down
8 changes: 6 additions & 2 deletions site/content/en/docs/Reference/gameserver.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ spec:
containerPort: 7654
# the port exposed on the host, only required when `portPolicy` is "Static". Overwritten when portPolicy is "Dynamic".
hostPort: 7777
# protocol being used. Defaults to UDP. TCP is the only other option
# protocol being used. Defaults to UDP. TCP and TCPUDP are other options
# - "UDP" (default) use the UDP protocol
# - "TCP", use the TCP protocol
# - "TCPUDP", uses both TCP and UDP, and exposes the same hostPort for both protocols.
# This will mean that it adds an extra port, and the first port is set to TCP, and second port set to UDP
protocol: UDP
# Health checking for the running game server
health:
Expand Down Expand Up @@ -190,7 +194,7 @@ The `spec` field is the actual GameServer specification and it is composed as fo
- `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 to open through the server side SDK before starting communications.
- `container` (Alpha) the name of the container to open the port on. Defaults to the game server container if omitted or empty.
- `containerPort` the port that is being opened on the game server process, this is a required field for `Dynamic` and `Static` port policies, and should not be included in <code>Passthrough</code> configuration.
- `protocol` the protocol being used. Defaults to UDP. TCP is the only other option.
- `protocol` the protocol being used. Defaults to UDP. TCP and TCPUDP are other options.
- `health` to track the overall healthy state of the GameServer, more information available in the [health check documentation]({{< relref "../Guides/health-checking.md" >}}).
- `sdkServer` defines parameters for the game server sidecar
- `logging` field defines log level for SDK server. Defaults to "Info". It has three options:
Expand Down
82 changes: 76 additions & 6 deletions test/e2e/framework/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package framework

import (
"bufio"
"encoding/json"
"flag"
"fmt"
Expand Down Expand Up @@ -441,13 +442,20 @@ func (f *Framework) CreateAndApplyAllocation(t *testing.T, flt *agonesv1.Fleet)
}

// SendGameServerUDP sends a message to a gameserver and returns its reply
// assumes the first port is the port to send the message to,
// finds the first udp port from the spec to send the message to,
// returns error if no Ports were allocated
func SendGameServerUDP(gs *agonesv1.GameServer, msg string) (string, error) {
if len(gs.Status.Ports) == 0 {
return "", errors.New("Empty Ports array")
}
return SendGameServerUDPToPort(gs, gs.Status.Ports[0].Name, msg)

// use first udp port
for _, p := range gs.Spec.Ports {
if p.Protocol == corev1.ProtocolUDP {
return SendGameServerUDPToPort(gs, p.Name, msg)
}
}
return "", errors.New("No UDP ports")
}

// SendGameServerUDPToPort sends a message to a gameserver at the named port and returns its reply
Expand Down Expand Up @@ -493,6 +501,68 @@ func SendUDP(address, msg string) (string, error) {
return string(b[:n]), nil
}

// SendGameServerTCP sends a message to a gameserver and returns its reply
// finds the first tcp port from the spec to send the message to,
// returns error if no Ports were allocated
func SendGameServerTCP(gs *agonesv1.GameServer, msg string) (string, error) {
if len(gs.Status.Ports) == 0 {
return "", errors.New("Empty Ports array")
}

// use first tcp port
for _, p := range gs.Spec.Ports {
if p.Protocol == corev1.ProtocolTCP {
return SendGameServerTCPToPort(gs, p.Name, msg)
}
}
return "", errors.New("No UDP ports")
}

// SendGameServerTCPToPort sends a message to a gameserver at the named port and returns its reply
// returns error if no Ports were allocated or a port of the specified name doesn't exist
func SendGameServerTCPToPort(gs *agonesv1.GameServer, portName string, msg string) (string, error) {
if len(gs.Status.Ports) == 0 {
return "", errors.New("Empty Ports array")
}
var port agonesv1.GameServerStatusPort
for _, p := range gs.Status.Ports {
if p.Name == portName {
port = p
}
}
address := fmt.Sprintf("%s:%d", gs.Status.Address, port.Port)
return SendTCP(address, msg)
}

// SendTCP sends a message to an address, and returns its reply if
// it returns one in 30 seconds
func SendTCP(address, msg string) (string, error) {
conn, err := net.Dial("tcp", address)
if err != nil {
return "", err
}

if err := conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
return "", err
}

defer func() {
if err := conn.Close(); err != nil {
logrus.Warn("Could not close TCP connection")
}
}()

// writes to the tcp connection
fmt.Fprintf(conn, msg+"\n")

response, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
return "", err
}

return response, nil
}

// GetAllocation returns a GameServerAllocation that is looking for a Ready
// GameServer from this fleet.
func GetAllocation(f *agonesv1.Fleet) *allocationv1.GameServerAllocation {
Expand Down Expand Up @@ -606,19 +676,19 @@ type patchRemoveNoValue struct {
// DefaultGameServer provides a default GameServer fixture, based on parameters
// passed to the Test Framework.
func (f *Framework) DefaultGameServer(namespace string) *agonesv1.GameServer {
gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{GenerateName: "udp-server", Namespace: namespace},
gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{GenerateName: "game-server", Namespace: namespace},
Spec: agonesv1.GameServerSpec{
Container: "udp-server",
Container: "game-server",
Ports: []agonesv1.GameServerPort{{
ContainerPort: 7654,
Name: "gameport",
Name: "udp-port",
PortPolicy: agonesv1.Dynamic,
Protocol: corev1.ProtocolUDP,
}},
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "udp-server",
Name: "game-server",
Image: f.GameServerImage,
ImagePullPolicy: corev1.PullIfNotPresent,
Resources: corev1.ResourceRequirements{
Expand Down
47 changes: 47 additions & 0 deletions test/e2e/gameserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,53 @@ func TestGameServerPassthroughPort(t *testing.T) {
assert.Equal(t, "ACK: Hello World !\n", reply)
}

func TestGameServerTcpUdpProtocol(t *testing.T) {
t.Parallel()
gs := framework.DefaultGameServer(framework.Namespace)

gs.Spec.Ports[0].Protocol = agonesv1.ProtocolTCPUDP
gs.Spec.Ports[0].Name = "gameserver"
gs.Spec.Template.Spec.Containers[0].Env = []corev1.EnvVar{{Name: "TCP", Value: "TRUE"}}

_, valid := gs.Validate()
assert.True(t, valid)

readyGs, err := framework.CreateGameServerAndWaitUntilReady(framework.Namespace, gs)
if err != nil {
assert.FailNow(t, "Could not get a GameServer ready", err.Error())
}

tcpPort := readyGs.Spec.Ports[0]
assert.Equal(t, corev1.ProtocolTCP, tcpPort.Protocol)
assert.NotEmpty(t, tcpPort.HostPort)
assert.Equal(t, "gameserver-tcp", tcpPort.Name)

udpPort := readyGs.Spec.Ports[1]
assert.Equal(t, corev1.ProtocolUDP, udpPort.Protocol)
assert.NotEmpty(t, udpPort.HostPort)
assert.Equal(t, "gameserver-udp", udpPort.Name)

assert.Equal(t, tcpPort.HostPort, udpPort.HostPort)

logrus.WithField("name", readyGs.ObjectMeta.Name).Info("GameServer created, sending UDP ping")

replyUDP, err := e2eframework.SendGameServerUDPToPort(readyGs, udpPort.Name, "Hello World !")
if err != nil {
t.Fatalf("Could not ping UDP GameServer: %v", err)
}

assert.Equal(t, "ACK: Hello World !\n", replyUDP)

logrus.WithField("name", readyGs.ObjectMeta.Name).Info("UDP ping passed, sending TCP ping")

replyTCP, err := e2eframework.SendGameServerTCPToPort(readyGs, tcpPort.Name, "Hello World !")
if err != nil {
t.Fatalf("Could not ping TCP GameServer: %v", err)
}

assert.Equal(t, "ACK TCP: Hello World !\n", replyTCP)
}

// TestGameServerResourceValidation - check that we are not able to use
// invalid PodTemplate for GameServer Spec with wrong Resource Requests and Limits
func TestGameServerResourceValidation(t *testing.T) {
Expand Down

0 comments on commit a3a7d2e

Please sign in to comment.