From 533df8ae883d46797f9c1951441c53cc0e2413e9 Mon Sep 17 00:00:00 2001 From: Mark Mandel Date: Thu, 15 Dec 2022 15:45:24 -0800 Subject: [PATCH] PodHostname: GameServer Pod Stable Network ID (#2826) Under the `PodHostname` feature gate, this change sets the hostName (if not already set) on the Pod of the GameServer, if an end user would like a DNS entry to communicate directly to the GameServer Pod in the cluster. Closes #2704 --- build/Makefile | 2 +- cloudbuild.yaml | 2 +- install/helm/agones/defaultfeaturegates.yaml | 1 + pkg/apis/agones/v1/gameserver.go | 6 +++ pkg/apis/agones/v1/gameserver_test.go | 21 ++++++++ pkg/util/runtime/features.go | 4 ++ site/content/en/docs/Guides/feature-stages.md | 1 + site/content/en/docs/Reference/gameserver.md | 19 +++++++ test/e2e/gameserver_test.go | 51 +++++++++++++++++++ 9 files changed, 105 insertions(+), 2 deletions(-) diff --git a/build/Makefile b/build/Makefile index 717ff728c4..1558edd521 100644 --- a/build/Makefile +++ b/build/Makefile @@ -68,7 +68,7 @@ KIND_CONTAINER_NAME=$(KIND_PROFILE)-control-plane GS_TEST_IMAGE ?= us-docker.pkg.dev/agones-images/examples/simple-game-server:0.14 # Enable all alpha feature gates. Keep in sync with `false` (alpha) entries in pkg/util/runtime/features.go:featureDefaults -ALPHA_FEATURE_GATES ?= "PlayerAllocationFilter=true&PlayerTracking=true&ResetMetricsOnDelete=true&SafeToEvict=true&Example=true" +ALPHA_FEATURE_GATES ?= "PlayerAllocationFilter=true&PlayerTracking=true&ResetMetricsOnDelete=true&SafeToEvict=true&PodHostname=true&Example=true" # Build with Windows support WITH_WINDOWS=1 diff --git a/cloudbuild.yaml b/cloudbuild.yaml index b695561a75..3c57ef1b02 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -245,7 +245,7 @@ steps: - name: 'e2e-runner' args: - - 'CustomFasSyncInterval=false&SDKGracefulTermination=false&StateAllocationFilter=false&PlayerAllocationFilter=true&PlayerTracking=true&ResetMetricsOnDelete=true&SafeToEvict=true&Example=true' + - 'CustomFasSyncInterval=false&SDKGracefulTermination=false&StateAllocationFilter=false&PlayerAllocationFilter=true&PlayerTracking=true&ResetMetricsOnDelete=true&SafeToEvict=true&PodHostname=true&Example=true' - 'e2e-test-cluster' - "${_REGISTRY}" id: e2e-feature-gates diff --git a/install/helm/agones/defaultfeaturegates.yaml b/install/helm/agones/defaultfeaturegates.yaml index 64eeee7449..7180b06fd0 100644 --- a/install/helm/agones/defaultfeaturegates.yaml +++ b/install/helm/agones/defaultfeaturegates.yaml @@ -24,6 +24,7 @@ PlayerAllocationFilter: false PlayerTracking: false ResetMetricsOnDelete: false SafeToEvict: false +PodHostname: false # Pre-Alpha features SplitControllerAndExtensions: false diff --git a/pkg/apis/agones/v1/gameserver.go b/pkg/apis/agones/v1/gameserver.go index 9ba83ede71..80d1d1ca74 100644 --- a/pkg/apis/agones/v1/gameserver.go +++ b/pkg/apis/agones/v1/gameserver.go @@ -18,6 +18,7 @@ import ( "encoding/json" "fmt" "net" + "strings" "agones.dev/agones/pkg" "agones.dev/agones/pkg/apis" @@ -598,6 +599,11 @@ func (gs *GameServer) Pod(sidecars ...corev1.Container) (*corev1.Pod, error) { Spec: *gs.Spec.Template.Spec.DeepCopy(), } + if len(pod.Spec.Hostname) == 0 { + // replace . with - since it must match RFC 1123 + pod.Spec.Hostname = strings.ReplaceAll(gs.ObjectMeta.Name, ".", "-") + } + gs.podObjectMeta(pod) for _, p := range gs.Spec.Ports { cp := corev1.ContainerPort{ diff --git a/pkg/apis/agones/v1/gameserver_test.go b/pkg/apis/agones/v1/gameserver_test.go index cd48cc0a93..01cd27973c 100644 --- a/pkg/apis/agones/v1/gameserver_test.go +++ b/pkg/apis/agones/v1/gameserver_test.go @@ -25,6 +25,7 @@ import ( "agones.dev/agones/pkg/apis/agones" "agones.dev/agones/pkg/util/runtime" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -1154,6 +1155,7 @@ func TestGameServerPodNoErrors(t *testing.T) { pod, err := fixture.Pod() assert.Nil(t, err, "Pod should not return an error") assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name) + assert.Equal(t, fixture.ObjectMeta.Name, pod.Spec.Hostname) assert.Equal(t, fixture.ObjectMeta.Namespace, pod.ObjectMeta.Namespace) assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"]) assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Labels[GameServerPodLabel]) @@ -1165,6 +1167,25 @@ func TestGameServerPodNoErrors(t *testing.T) { assert.True(t, metav1.IsControlledBy(pod, fixture)) } +func TestGameServerPodHostName(t *testing.T) { + t.Parallel() + + fixture := defaultGameServer() + fixture.ObjectMeta.Name = "test-1.0" + fixture.ApplyDefaults() + pod, err := fixture.Pod() + require.NoError(t, err) + assert.Equal(t, "test-1-0", pod.Spec.Hostname) + + fixture = defaultGameServer() + fixture.ApplyDefaults() + expected := "ORANGE" + fixture.Spec.Template.Spec.Hostname = expected + pod, err = fixture.Pod() + require.NoError(t, err) + assert.Equal(t, expected, pod.Spec.Hostname) +} + func TestGameServerPodContainerNotFoundErrReturned(t *testing.T) { t.Parallel() diff --git a/pkg/util/runtime/features.go b/pkg/util/runtime/features.go index 6e1c94f247..72c3a5b404 100644 --- a/pkg/util/runtime/features.go +++ b/pkg/util/runtime/features.go @@ -57,6 +57,9 @@ const ( // FeatureSafeToEvict enables the `SafeToEvict` API to specify disruption tolerance. FeatureSafeToEvict Feature = "SafeToEvict" + // FeaturePodHostname enables the Pod Hostname being assigned the name of the GameServer + FeaturePodHostname = "PodHostname" + //////////////// // "Pre"-Alpha features @@ -107,6 +110,7 @@ var ( FeaturePlayerTracking: false, FeatureResetMetricsOnDelete: false, FeatureSafeToEvict: false, + FeaturePodHostname: false, // Pre-Alpha features FeatureSplitControllerAndExtensions: false, diff --git a/site/content/en/docs/Guides/feature-stages.md b/site/content/en/docs/Guides/feature-stages.md index e8f077b6d8..230fa35288 100644 --- a/site/content/en/docs/Guides/feature-stages.md +++ b/site/content/en/docs/Guides/feature-stages.md @@ -33,6 +33,7 @@ The current set of `alpha` and `beta` feature gates: | [GameServer player capacity filtering on GameServerAllocations](https://github.com/googleforgames/agones/issues/1239) | `PlayerAllocationFilter` | Disabled | `Alpha` | 1.14.0 | | [Player Tracking]({{< ref "/docs/Guides/player-tracking.md" >}}) | `PlayerTracking` | Disabled | `Alpha` | 1.6.0 | | [Reset Metric Export on Fleet / Autoscaler deletion]({{% relref "./metrics.md#dropping-metric-labels" %}}) | `ResetMetricsOnDelete` | Disabled | `Alpha` | 1.26.0 | +| [GameServer Stable Network ID]({{% ref "/docs/Reference/gameserver.md#stable-network-id" %}}) | `PodHostname` | Disabled | `Alpha` | 1.29.0 | | [GameServer `SafeToEvict` API](https://github.com/googleforgames/agones/issues/2794) | `SafeToEvict` | Disabled | `Alpha` | 1.29.0 | | Example Gate (not in use) | `Example` | Disabled | None | 0.13.0 | {{% /feature %}} diff --git a/site/content/en/docs/Reference/gameserver.md b/site/content/en/docs/Reference/gameserver.md index a54cb92970..cbacf9eedc 100644 --- a/site/content/en/docs/Reference/gameserver.md +++ b/site/content/en/docs/Reference/gameserver.md @@ -131,6 +131,25 @@ The `spec` field is the actual GameServer specification and it is composed as fo The GameServer resource does not support updates. If you need to make regular updates to the GameServer spec, consider using a [Fleet]({{< ref "/docs/Reference/fleet.md" >}}). {{< /alert >}} +## Stable Network ID + +{{< alpha title="Stable Network ID" gate="PodHostname" >}} + +Each Pod attached to a `GameServer` derives its hostname from the name of the `GameServer`. +A group of `Pods` attached to `GameServers` can use a +[Headless Service](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services) to control +the domain of the Pods, along with providing +a [`subdomain` value to the `GameServer` `PodTemplateSpec`](https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-hostname-and-subdomain-fields) +to provide all the required details such that Kubernetes will create a DNS record for each Pod behind the Service. + +You are also responsible for setting the labels on the `GameServer.Spec.Template.Metadata` to set the labels on the +created Pods and creating the Headless Service responsible for the network identity of the pods, Agones will not do +this for you, as a stable DNS record is not required for all use cases. + +To ensure that the `hostName` value matches +[RFC 1123](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names), any `.` values +in the `GameServer` name are replaced by `-` when setting the `hostName` value. + ## GameServer State Diagram The following diagram shows the lifecycle of a `GameServer`. diff --git a/test/e2e/gameserver_test.go b/test/e2e/gameserver_test.go index 7837fa041c..64a3475c74 100644 --- a/test/e2e/gameserver_test.go +++ b/test/e2e/gameserver_test.go @@ -68,6 +68,57 @@ func TestCreateConnect(t *testing.T) { assert.Equal(t, "ACK: Hello World !\n", reply) } +func TestHostName(t *testing.T) { + t.Parallel() + + pods := framework.KubeClient.CoreV1().Pods(framework.Namespace) + + fixtures := map[string]struct { + setup func(gs *agonesv1.GameServer) + test func(gs *agonesv1.GameServer, pod *corev1.Pod) + }{ + "standard hostname": { + setup: func(_ *agonesv1.GameServer) {}, + test: func(gs *agonesv1.GameServer, pod *corev1.Pod) { + assert.Equal(t, gs.ObjectMeta.Name, pod.Spec.Hostname) + }, + }, + "a . in the name": { + setup: func(gs *agonesv1.GameServer) { + gs.ObjectMeta.GenerateName = "game-server-1.0-" + }, + test: func(gs *agonesv1.GameServer, pod *corev1.Pod) { + expected := "game-server-1-0-" + // since it's a generated name, we just check the beginning. + assert.Equal(t, expected, pod.Spec.Hostname[:len(expected)]) + }, + }, + // generated name will automatically truncate to 63 chars. + "generated with > 63 chars": { + setup: func(gs *agonesv1.GameServer) { + gs.ObjectMeta.GenerateName = "game-server-" + strings.Repeat("n", 100) + }, + test: func(gs *agonesv1.GameServer, pod *corev1.Pod) { + assert.Equal(t, gs.ObjectMeta.Name, pod.Spec.Hostname) + }, + }, + // Note: no need to test for a gs.ObjectMeta.Name > 63 chars, as it will be rejected as invalid + } + + for k, v := range fixtures { + t.Run(k, func(t *testing.T) { + gs := framework.DefaultGameServer(framework.Namespace) + gs.Spec.Template.Spec.Subdomain = "default" + v.setup(gs) + readyGs, err := framework.CreateGameServerAndWaitUntilReady(t, framework.Namespace, gs) + require.NoError(t, err) + pod, err := pods.Get(context.Background(), readyGs.ObjectMeta.Name, metav1.GetOptions{}) + require.NoError(t, err) + v.test(readyGs, pod) + }) + } +} + // nolint:dupl func TestSDKSetLabel(t *testing.T) { t.Parallel()