diff --git a/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml b/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml index 3b01bd1f1b..539bff3341 100644 --- a/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml +++ b/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml @@ -64,17 +64,20 @@ properties: portPolicy: title: the port policy that will be applied to the game server description: | - portPolicy has three options: + portPolicy has four 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. + - "Internal" means that the system won't allocate a hostPort for this port. The onus is on the user to find a way + to connect players to that port, whether it be through a custom proxy or otherwise. type: string enum: - Dynamic - Static - Passthrough + - Internal protocol: title: Protocol being used. Defaults to UDP. TCP is the only other option type: string diff --git a/pkg/apis/agones/v1/common.go b/pkg/apis/agones/v1/common.go index 318d025d6f..3b81ee75e3 100644 --- a/pkg/apis/agones/v1/common.go +++ b/pkg/apis/agones/v1/common.go @@ -28,7 +28,7 @@ import ( // Block of const Error messages const ( ErrContainerRequired = "Container is required when using multiple containers in the pod template" - ErrHostPort = "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy" + ErrHostPort = "HostPort cannot be specified with a Dynamic, Internal or Passthrough PortPolicy" ErrPortPolicyStatic = "PortPolicy must be Static" ErrContainerPortRequired = "ContainerPort must be defined for Dynamic and Static PortPolicies" ErrContainerPortPassthrough = "ContainerPort cannot be specified with Passthrough PortPolicy" diff --git a/pkg/apis/agones/v1/gameserver.go b/pkg/apis/agones/v1/gameserver.go index af70e1282a..21ecde64f5 100644 --- a/pkg/apis/agones/v1/gameserver.go +++ b/pkg/apis/agones/v1/gameserver.go @@ -70,6 +70,9 @@ const ( // 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" + // Internal PortPolicy means that the system will never allocate a hostPort to the container, expecting users + // to use their own internal load balancer/proxy solution to direct players to the container + Internal PortPolicy = "Internal" // SdkServerLogLevelInfo will cause the SDK server to output all messages except for debug messages. SdkServerLogLevelInfo SdkServerLogLevel = "Info" @@ -424,7 +427,7 @@ func (gss *GameServerSpec) Validate(devAddress string) ([]metav1.StatusCause, bo // no host port when using dynamic PortPolicy for _, p := range gss.Ports { - if p.PortPolicy == Dynamic || p.PortPolicy == Static { + if p.PortPolicy == Dynamic || p.PortPolicy == Static || p.PortPolicy == Internal { //nolint: gocyclo if p.ContainerPort <= 0 { causes = append(causes, metav1.StatusCause{ Type: metav1.CauseTypeFieldValueInvalid, @@ -442,7 +445,7 @@ func (gss *GameServerSpec) Validate(devAddress string) ([]metav1.StatusCause, bo }) } - if p.HostPort > 0 && (p.PortPolicy == Dynamic || p.PortPolicy == Passthrough) { + if p.HostPort > 0 && (p.PortPolicy == Dynamic || p.PortPolicy == Passthrough || p.PortPolicy == Internal) { //nolint: gocyclo causes = append(causes, metav1.StatusCause{ Type: metav1.CauseTypeFieldValueInvalid, Field: fmt.Sprintf("%s.hostPort", p.Name), diff --git a/pkg/gameservers/portallocator.go b/pkg/gameservers/portallocator.go index 648dc0953e..979803ff38 100644 --- a/pkg/gameservers/portallocator.go +++ b/pkg/gameservers/portallocator.go @@ -121,6 +121,9 @@ func (pa *PortAllocator) Allocate(gs *agonesv1.GameServer) *agonesv1.GameServer // Also the return gives an escape from the double loop findOpenPorts := func(amount int) []pn { var ports []pn + if amount == 0 { + return ports + } for _, n := range pa.portAllocations { for p, taken := range n { if !taken { diff --git a/pkg/gameservers/portallocator_test.go b/pkg/gameservers/portallocator_test.go index 0d69832afb..45dc611e87 100644 --- a/pkg/gameservers/portallocator_test.go +++ b/pkg/gameservers/portallocator_test.go @@ -106,6 +106,15 @@ func TestPortAllocatorAllocate(t *testing.T) { assert.Equal(t, gsCopy.Spec.Ports[0].HostPort, gsCopy.Spec.Ports[0].ContainerPort) assert.Nil(t, err) assert.Equal(t, 11, countTotalAllocatedPorts(pa)) + + // single port, internal + gsCopy = fixture.DeepCopy() + gsCopy.Spec.Ports[0] = agonesv1.GameServerPort{Name: "internal", PortPolicy: agonesv1.Internal} + assert.Len(t, gsCopy.Spec.Ports, 1) + pa.Allocate(gsCopy) + assert.Empty(t, gsCopy.Spec.Ports[0].HostPort) + assert.Nil(t, err) + assert.Equal(t, 11, countTotalAllocatedPorts(pa)) }) t.Run("ports are all allocated", func(t *testing.T) { diff --git a/site/content/en/docs/Reference/gameserver.md b/site/content/en/docs/Reference/gameserver.md index 4c2f13dbb7..ffadfb4050 100644 --- a/site/content/en/docs/Reference/gameserver.md +++ b/site/content/en/docs/Reference/gameserver.md @@ -98,12 +98,14 @@ spec: ports: # name is a descriptive name for the port - name: default - # portPolicy has three options: + # portPolicy has four options: # - "Dynamic" (default) the system allocates a free hostPort for the gameserver, for game clients to connect to # - "Static", user defines the hostPort that the game client will connect to. Then onus is on the user to ensure that the # port is available. When static is the policy specified, `hostPort` is required to be populated # - "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. + # - "Internal" means that the system won't allocate a hostPort for this port. The onus is on the user to find a way + # to connect players to that port, whether it be through a custom proxy or otherwise. portPolicy: Static # (Alpha) the name of the container to open the port on. Defaults to the game server container if omitted or empty. container: simple-udp @@ -201,6 +203,7 @@ The `spec` field is the actual GameServer specification and it is composed as fo - `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 to open through the server side SDK before starting communications. + - `Internal` means that the system wont allocate a hostPort for this port. The onus is on the user to find a way to connect players to that port, whether it be through a custom proxy or otherwise. - `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 Passthrough configuration. - `protocol` the protocol being used. Defaults to UDP. TCP is the only other option. diff --git a/test/e2e/gameserver_test.go b/test/e2e/gameserver_test.go index 01f8e84f8f..d81bfd229e 100644 --- a/test/e2e/gameserver_test.go +++ b/test/e2e/gameserver_test.go @@ -681,6 +681,27 @@ func TestGameServerPassthroughPort(t *testing.T) { assert.Equal(t, "ACK: Hello World !\n", reply) } +func TestGameServerInternalPort(t *testing.T) { + t.Parallel() + gs := framework.DefaultGameServer(defaultNs) + gs.Spec.Ports[0] = agonesv1.GameServerPort{PortPolicy: agonesv1.Internal, ContainerPort: 999} + gs.Spec.Template.Spec.Containers[0].Env = []corev1.EnvVar{{Name: "INTERNAL", Value: "TRUE"}} + + _, 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, agonesv1.Internal, port.PortPolicy) + assert.NotEmpty(t, port.HostPort) + + // TODO How to test this? Try pinging server externally and check for fail? +} + // 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) {