diff --git a/pkg/apis/agones/v1/gameserver.go b/pkg/apis/agones/v1/gameserver.go index fe399ea23a..7b629fcfac 100644 --- a/pkg/apis/agones/v1/gameserver.go +++ b/pkg/apis/agones/v1/gameserver.go @@ -143,6 +143,9 @@ const ( // error state. The timestamp is encoded in RFC3339 format. GameServerErroredAtAnnotation = agones.GroupName + "/errored-at" + // NodePodIP identifies an IP address from a pod. + NodePodIP corev1.NodeAddressType = "PodIP" + // True is the string "true" to appease the goconst lint. True = "true" // False is the string "false" to appease the goconst lint. diff --git a/pkg/gameservers/controller.go b/pkg/gameservers/controller.go index 79d78d1174..75afb42e0c 100644 --- a/pkg/gameservers/controller.go +++ b/pkg/gameservers/controller.go @@ -819,6 +819,12 @@ func (c *Controller) syncGameServerStartingState(ctx context.Context, gs *agones if pod.Spec.NodeName == "" { return gs, workerqueue.NewDebugError(errors.Errorf("node not yet populated for Pod %s", pod.ObjectMeta.Name)) } + + // Ensure the pod IPs are populated + if pod.Status.PodIPs == nil || len(pod.Status.PodIPs) == 0 { + return gs, workerqueue.NewDebugError(errors.Errorf("pod IPs not yet populated for Pod %s", pod.ObjectMeta.Name)) + } + node, err := c.nodeLister.Get(pod.Spec.NodeName) if err != nil { return gs, errors.Wrapf(err, "error retrieving node %s for Pod %s", pod.Spec.NodeName, pod.ObjectMeta.Name) diff --git a/pkg/gameservers/controller_test.go b/pkg/gameservers/controller_test.go index e05bef7d33..acdc85fe6e 100644 --- a/pkg/gameservers/controller_test.go +++ b/pkg/gameservers/controller_test.go @@ -51,6 +51,7 @@ import ( const ( ipFixture = "12.12.12.12" + ipv6Fixture = "2001:0db8:85a3:0000:0000:8a2e:0370:7334" nodeFixtureName = "node1" ) @@ -88,6 +89,7 @@ func TestControllerSyncGameServer(t *testing.T) { ca := action.(k8stesting.CreateAction) pod := ca.GetObject().(*corev1.Pod) pod.Spec.NodeName = node.ObjectMeta.Name + pod.Status.PodIPs = []corev1.PodIP{{IP: ipv6Fixture}} podCreated = true assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name) watchPods.Add(pod) @@ -120,7 +122,10 @@ func TestControllerSyncGameServer(t *testing.T) { assert.Equal(t, expectedState, gs.Status.State) if expectedState == agonesv1.GameServerStateScheduled { assert.Equal(t, ipFixture, gs.Status.Address) - assert.Equal(t, []corev1.NodeAddress{{Address: ipFixture, Type: "ExternalIP"}}, gs.Status.Addresses) + assert.Equal(t, []corev1.NodeAddress{ + {Address: ipFixture, Type: "ExternalIP"}, + {Address: ipv6Fixture, Type: "PodIP"}, + }, gs.Status.Addresses) assert.NotEmpty(t, gs.Status.Ports[0].Port) } @@ -1093,6 +1098,7 @@ func TestControllerSyncGameServerStartingState(t *testing.T) { pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) assert.Nil(t, err) pod.Spec.NodeName = nodeFixtureName + pod.Status.PodIPs = []corev1.PodIP{{IP: ipv6Fixture}} gsUpdated := false m.KubeClient.AddReactor("list", "nodes", func(action k8stesting.Action) (bool, runtime.Object, error) { @@ -1118,12 +1124,42 @@ func TestControllerSyncGameServerStartingState(t *testing.T) { assert.True(t, gsUpdated) assert.Equal(t, gs.Status.NodeName, node.ObjectMeta.Name) assert.Equal(t, gs.Status.Address, ipFixture) - assert.Equal(t, []corev1.NodeAddress{{Address: ipFixture, Type: "ExternalIP"}}, gs.Status.Addresses) + assert.Equal(t, []corev1.NodeAddress{ + {Address: ipFixture, Type: "ExternalIP"}, + {Address: ipv6Fixture, Type: "PodIP"}, + }, gs.Status.Addresses) agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Address and port populated") assert.NotEmpty(t, gs.Status.Ports) }) + t.Run("Error on podIPs not populated", func(t *testing.T) { + c, m := newFakeController() + gsFixture := newFixture() + gsFixture.ApplyDefaults() + pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) + require.NoError(t, err) + pod.Spec.NodeName = nodeFixtureName + + m.KubeClient.AddReactor("list", "nodes", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &corev1.NodeList{Items: []corev1.Node{node}}, nil + }) + m.KubeClient.AddReactor("list", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil + }) + m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + ua := action.(k8stesting.UpdateAction) + gs := ua.GetObject().(*agonesv1.GameServer) + assert.Equal(t, agonesv1.GameServerStateScheduled, gs.Status.State) + return true, gs, errors.New("update-err") + }) + ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced, c.podSynced, c.nodeSynced) + defer cancel() + + _, err = c.syncGameServerStartingState(ctx, gsFixture) + assert.Error(t, err) + }) + t.Run("Error on update", func(t *testing.T) { c, m := newFakeController() gsFixture := newFixture() @@ -1131,6 +1167,7 @@ func TestControllerSyncGameServerStartingState(t *testing.T) { pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) require.NoError(t, err) pod.Spec.NodeName = nodeFixtureName + pod.Status.PodIPs = []corev1.PodIP{{IP: ipv6Fixture}} m.KubeClient.AddReactor("list", "nodes", func(action k8stesting.Action) (bool, runtime.Object, error) { return true, &corev1.NodeList{Items: []corev1.Node{node}}, nil diff --git a/pkg/gameservers/gameservers.go b/pkg/gameservers/gameservers.go index 1d2ae7431e..9ea99a365b 100644 --- a/pkg/gameservers/gameservers.go +++ b/pkg/gameservers/gameservers.go @@ -88,6 +88,13 @@ func applyGameServerAddressAndPort(gs *agonesv1.GameServer, node *corev1.Node, p gs.Status.Addresses = addrs gs.Status.NodeName = pod.Spec.NodeName + for _, ip := range pod.Status.PodIPs { + gs.Status.Addresses = append(gs.Status.Addresses, corev1.NodeAddress{ + Type: agonesv1.NodePodIP, + Address: ip.IP, + }) + } + if err := syncPodPortsToGameServer(gs, pod); err != nil { return gs, errors.Wrapf(err, "cloud product error syncing ports on GameServer %s", gs.ObjectMeta.Name) } diff --git a/pkg/gameservers/gameservers_test.go b/pkg/gameservers/gameservers_test.go index 58d38b2e64..51aa30b337 100644 --- a/pkg/gameservers/gameservers_test.go +++ b/pkg/gameservers/gameservers_test.go @@ -136,6 +136,7 @@ func TestApplyGameServerAddressAndPort(t *testing.T) { pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) require.NoError(t, err) pod.Spec.NodeName = node.ObjectMeta.Name + pod.Status.PodIPs = []corev1.PodIP{{IP: ipFixture}} tc.podMod(pod) gs, err := applyGameServerAddressAndPort(gsFixture, node, pod, tc.podSyncer) @@ -146,6 +147,10 @@ func TestApplyGameServerAddressAndPort(t *testing.T) { } assert.Equal(t, ipFixture, gs.Status.Address) assert.Equal(t, node.ObjectMeta.Name, gs.Status.NodeName) + assert.Equal(t, []corev1.NodeAddress{ + {Address: ipFixture, Type: "ExternalIP"}, + {Address: ipFixture, Type: "PodIP"}, + }, gs.Status.Addresses) }) } diff --git a/site/content/en/docs/Reference/gameserver.md b/site/content/en/docs/Reference/gameserver.md index 635e58b318..0f02866a5f 100644 --- a/site/content/en/docs/Reference/gameserver.md +++ b/site/content/en/docs/Reference/gameserver.md @@ -190,7 +190,7 @@ Game Servers are created through Kubernetes API (either directly or through a [F [`GameServer.Status`][gss] has two fields which reflect the network address of the `GameServer`: `address` and `addresses`. The `address` field is a policy-based choice of "primary address" that will work for many use cases, -and will always be one of the `addresses`. The `addresses` field contains every address in the [`Node.Status.addresses`][addresses], +and will always be one of the `addresses`. The `addresses` field contains every address in the [`Node.Status.addresses`][addresses] and [`Pod.Status.podIPs`][podIPs] (to allow a direct pod access), representing all known ways to reach the `GameServer` over the network. To choose `address` from `addresses`, [Agones looks for the following address types][addressFunc], in highest to lowest priorty: @@ -199,11 +199,13 @@ To choose `address` from `addresses`, [Agones looks for the following address ty * `InternalDNS` * `InternalIP` -e.g. if any `ExternalDNS` address is found in the respective `Node`, it is used as the `address`. +e.g. if any `ExternalDNS` address is found in the respective `Node`, it is used as the `address`. (`PodIP` is not considered +for `address`.) The policy for `address` will work for many use-cases, but for some advanced cases, such as IPv6 enablement, you may need to evaluate all `addresses` and pick the addresses that best suits your needs. [addresses]: https://v1-26.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#nodeaddress-v1-core +[podIPs]: https://v1-26.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#podip-v1-core [addressFunc]: https://github.com/googleforgames/agones/blob/a59c5394c7f5bac66e530d21446302581c10c225/pkg/gameservers/gameservers.go#L37-L71 [gss]: {{% ref "/docs/Reference/agones_crd_api_reference.html#agones.dev/v1.GameServerStatus" %}} diff --git a/test/e2e/gameserver_test.go b/test/e2e/gameserver_test.go index a18f438461..a1e723e75a 100644 --- a/test/e2e/gameserver_test.go +++ b/test/e2e/gameserver_test.go @@ -60,6 +60,16 @@ func TestCreateConnect(t *testing.T) { assert.NotEmpty(t, readyGs.Status.Ports[0].Port) assert.NotEmpty(t, readyGs.Status.Address) assert.NotEmpty(t, readyGs.Status.Addresses) + + var hasPodIPAddress bool + for i, addr := range readyGs.Status.Addresses { + if addr.Type == agonesv1.NodePodIP { + assert.NotEmpty(t, readyGs.Status.Addresses[i].Address) + hasPodIPAddress = true + } + } + assert.True(t, hasPodIPAddress) + assert.NotEmpty(t, readyGs.Status.NodeName) assert.Equal(t, readyGs.Status.State, agonesv1.GameServerStateReady)