Repository: googleforgames/agones
Pull request #4146: Adoption of Sidecar Containers
Author: login: markmandel
Labels: name: kind/breaking
description: Breaking change
Created: 2025-04-07T01:46:54Z | Merged: 2025-04-30T21:50:16Z

<!--  Thanks for sending a pull request!  Here are some tips for you:

1. If this is your first time, please read our contributor guidelines: https://github.com/googleforgames/agones/blob/main/CONTRIBUTING.md and developer guide https://github.com/googleforgames/agones/blob/main/build/README.md
2. Please label this pull request according to what type of issue you are addressing.
3. Ensure you have added or ran the appropriate tests for your PR: https://github.com/googleforgames/agones/blob/main/build/README.md#testing-and-building
-->

**What type of PR is this?**
> Uncomment only one ` /kind <>` line, press enter to put that in a new line, and remove leading whitespace from that line:
>

/kind breaking

> /kind bug
> /kind cleanup
> /kind documentation
> /kind feature
> /kind hotfix
> /kind release

**What this PR does / Why we need it**:

This is a breaking change for both how health checking works and to the SDK lifecycles, but behind the `SidecarContainer` feature flag.

1. This moves the sdkserver into a initContainer with a `RestartPolicy` of always. This means that it will be alive for the entirety of the main Pod's containers.
2. There is no waiting for the Shutdown signal any more for the sidecar container before shutting down since it now natively waits for the main Pod containers.
3. The RestartPolicy of the `gameserver` container defaults to `Never` (although it can be changed), and no longer restarts before the `Ready` state to simplify lifecycle health checking.
4. Various hacky health checks are now simplified to "is the Pod in a `Failed` state.", and therefore we no longer need to do as much text searching for edge cases.

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Closes #<issue number>`, or `Closes (paste link of issue)`.
-->
Work on #3642

**Special notes for your reviewer**:

Next: Write the docs for this work.



Diff:
diff --git a/build/Makefile b/build/Makefile
index 9637642dad..a4d0989a42 100644
--- a/build/Makefile
+++ b/build/Makefile
@@ -73,7 +73,7 @@ BETA_FEATURE_GATES ?= "AutopilotPassthroughPort=true&CountsAndLists=true&GKEAuto
 
 
 # Enable all alpha feature gates. Keep in sync with `false` (alpha) entries in pkg/util/runtime/features.go:featureDefaults
-ALPHA_FEATURE_GATES ?= "PlayerAllocationFilter=true&FleetAutoscaleRequestMetaData=true&PlayerTracking=true&RollingUpdateFix=true&ScheduledAutoscaler=true&Example=true"
+ALPHA_FEATURE_GATES ?= "PlayerAllocationFilter=true&FleetAutoscaleRequestMetaData=true&PlayerTracking=true&RollingUpdateFix=true&ScheduledAutoscaler=true&SidecarContainers=true&Example=true"
 
 # Build with Windows support
 WITH_WINDOWS=1
@@ -334,7 +334,7 @@ test-e2e-integration: GO_E2E_TEST_INTEGRATION_ARGS ?=\
 test-e2e-integration: $(ensure-build-image)
 	echo "Starting e2e integration test!"
 ifdef E2E_USE_GOTESTSUM
-	$(GOTESTSUM) --packages=$(agones_package)/test/e2e -- $(go_test_args) -timeout=30m $(ARGS) -args \
+	$(GOTESTSUM) --packages=$(agones_package)/test/e2e -- $(go_test_args) -timeout=45m $(ARGS) -args \
 		$(GO_E2E_TEST_ARGS) $(GO_E2E_TEST_INTEGRATION_ARGS)
 else
 	$(GO_TEST) -timeout=30m $(ARGS) $(agones_package)/test/e2e -args \
diff --git a/cloudbuild.yaml b/cloudbuild.yaml
index 90305f4697..bb299d9a28 100644
--- a/cloudbuild.yaml
+++ b/cloudbuild.yaml
@@ -441,7 +441,7 @@ steps:
 
         # Keep in sync with the inverse of 'alpha' and 'beta' features in
         # pkg/util/runtime/features.go:featureDefaults
-        featureWithGate="PlayerAllocationFilter=true&FleetAutoscaleRequestMetaData=true&PlayerTracking=true&CountsAndLists=false&RollingUpdateFix=true&PortRanges=false&PortPolicyNone=false&ScheduledAutoscaler=true&AutopilotPassthroughPort=false&GKEAutopilotExtendedDurationPods=false&Example=true"
+        featureWithGate="PlayerAllocationFilter=true&FleetAutoscaleRequestMetaData=true&PlayerTracking=true&CountsAndLists=false&RollingUpdateFix=true&PortRanges=false&PortPolicyNone=false&ScheduledAutoscaler=true&AutopilotPassthroughPort=false&GKEAutopilotExtendedDurationPods=false&SidecarContainers=true&Example=true"
         featureWithoutGate=""
 
         # Use this if specific feature gates can only be supported on specific Kubernetes versions.
diff --git a/cmd/sdk-server/main.go b/cmd/sdk-server/main.go
index 237de42852..d02f036d1d 100644
--- a/cmd/sdk-server/main.go
+++ b/cmd/sdk-server/main.go
@@ -158,7 +158,10 @@ func main() {
 		if err := s.WaitForConnection(ctx); err != nil {
 			logger.WithError(err).Fatalf("Sidecar networking failure")
 		}
-		if ctlConf.GracefulTermination {
+
+		// if sidecar init container, we no longer need to wait for Shutdown as the sdkserver has an independent lifecycle
+		// from the main pod containers.
+		if ctlConf.GracefulTermination && !runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
 			ctx = s.NewSDKServerContext(ctx)
 		}
 		go func() {
@@ -309,7 +312,7 @@ func parseEnvFlags() config {
 	pflag.String(kubeconfigFlag, viper.GetString(kubeconfigFlag),
 		"Optional. kubeconfig to run the SDK server out of the cluster.")
 	pflag.Bool(gracefulTerminationFlag, viper.GetBool(gracefulTerminationFlag),
-		"Immediately quits when receiving interrupt instead of waiting for GameServer state to progress to \"Shutdown\".")
+		"When false, immediately quits when receiving interrupt instead of waiting for GameServer state to progress to \"Shutdown\".")
 	runtime.FeaturesBindFlags()
 	pflag.Parse()
 
diff --git a/install/helm/agones/defaultfeaturegates.yaml b/install/helm/agones/defaultfeaturegates.yaml
index 385f3aa43e..f46e90fd71 100644
--- a/install/helm/agones/defaultfeaturegates.yaml
+++ b/install/helm/agones/defaultfeaturegates.yaml
@@ -33,6 +33,7 @@ PlayerAllocationFilter: false
 PlayerTracking: false
 RollingUpdateFix: false
 ScheduledAutoscaler: false
+SidecarContainers: false
 
 # Dev features
 
diff --git a/pkg/apis/agones/v1/gameserver.go b/pkg/apis/agones/v1/gameserver.go
index 0f34a814d6..b8596dc957 100644
--- a/pkg/apis/agones/v1/gameserver.go
+++ b/pkg/apis/agones/v1/gameserver.go
@@ -788,11 +788,25 @@ func (gs *GameServer) Pod(apiHooks APIHooks, sidecars ...corev1.Container) (*cor
 		pod.ObjectMeta.Annotations[PassthroughPortAssignmentAnnotation] = string(containerToPassthroughMapJSON)
 	}
 
-	// Put the sidecars at the start of the list of containers so that the kubelet starts them first.
-	containers := make([]corev1.Container, 0, len(sidecars)+len(pod.Spec.Containers))
-	containers = append(containers, sidecars...)
-	containers = append(containers, pod.Spec.Containers...)
-	pod.Spec.Containers = containers
+	if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
+		// make sure all sidecars have a restart policy of Always, so they are valid sidecar containers.
+		// https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/#sidecar-containers-and-pod-lifecycle
+		always := corev1.ContainerRestartPolicyAlways
+		for i := range sidecars {
+			sidecars[i].RestartPolicy = &always
+		}
+
+		// addSidecarsAsInitContainers puts the sidecars in the initContainers list so that they can have their own independent
+		// container restart policies.
+		pod.Spec.InitContainers = append(pod.Spec.InitContainers, sidecars...)
+
+		// default GameServer container should also be Restart: Never
+		if len(pod.Spec.RestartPolicy) == 0 {
+			pod.Spec.RestartPolicy = corev1.RestartPolicyNever
+		}
+	} else {
+		gs.addSidecarsAsContainers(sidecars, pod)
+	}
 
 	gs.podScheduling(pod)
 
@@ -806,6 +820,15 @@ func (gs *GameServer) Pod(apiHooks APIHooks, sidecars ...corev1.Container) (*cor
 	return pod, nil
 }
 
+// addSidecarsAsContainers puts the sidecars at the start of the general list of containers so that the kubelet starts
+// them first
+func (gs *GameServer) addSidecarsAsContainers(sidecars []corev1.Container, pod *corev1.Pod) {
+	containers := make([]corev1.Container, 0, len(sidecars)+len(pod.Spec.Containers))
+	containers = append(containers, sidecars...)
+	containers = append(containers, pod.Spec.Containers...)
+	pod.Spec.Containers = containers
+}
+
 // podObjectMeta configures the pod ObjectMeta details
 func (gs *GameServer) podObjectMeta(pod *corev1.Pod) {
 	pod.ObjectMeta.GenerateName = ""
diff --git a/pkg/apis/agones/v1/gameserver_test.go b/pkg/apis/agones/v1/gameserver_test.go
index ac125cd14b..786aa80191 100644
--- a/pkg/apis/agones/v1/gameserver_test.go
+++ b/pkg/apis/agones/v1/gameserver_test.go
@@ -1386,6 +1386,10 @@ func TestGameServerPodContainerNotFoundErrReturned(t *testing.T) {
 
 func TestGameServerPodWithSidecarNoErrors(t *testing.T) {
 	t.Parallel()
+	runtime.FeatureTestMutex.Lock()
+	defer runtime.FeatureTestMutex.Unlock()
+	require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=false"))
+
 	fixture := defaultGameServer()
 	fixture.ApplyDefaults()
 
@@ -1401,6 +1405,29 @@ func TestGameServerPodWithSidecarNoErrors(t *testing.T) {
 	assert.True(t, metav1.IsControlledBy(pod, fixture))
 }
 
+func TestGameServerPodWithInitSidecarNoErrors(t *testing.T) {
+	t.Parallel()
+	runtime.FeatureTestMutex.Lock()
+	defer runtime.FeatureTestMutex.Unlock()
+	require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=true"))
+
+	fixture := defaultGameServer()
+	fixture.ApplyDefaults()
+
+	sidecar := corev1.Container{Name: "sidecar", Image: "container/sidecar"}
+	fixture.Spec.Template.Spec.ServiceAccountName = "other-agones-sdk"
+	pod, err := fixture.Pod(fakeAPIHooks{}, sidecar)
+	assert.Nil(t, err, "Pod should not return an error")
+	assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name)
+	assert.Len(t, pod.Spec.Containers, 1, "Should have one containers")
+	assert.Equal(t, "other-agones-sdk", pod.Spec.ServiceAccountName)
+	assert.Equal(t, "sidecar", pod.Spec.InitContainers[0].Name)
+	assert.Equal(t, corev1.ContainerRestartPolicyAlways, *pod.Spec.InitContainers[0].RestartPolicy)
+	assert.Equal(t, "container", pod.Spec.Containers[0].Name)
+	assert.True(t, metav1.IsControlledBy(pod, fixture))
+	assert.Equal(t, pod.Spec.RestartPolicy, corev1.RestartPolicyNever)
+}
+
 func TestGameServerPodWithMultiplePortAllocations(t *testing.T) {
 	fixture := defaultGameServer()
 	containerName := "authContainer"
diff --git a/pkg/gameservers/controller.go b/pkg/gameservers/controller.go
index 67cd9f28f4..edce3ca365 100644
--- a/pkg/gameservers/controller.go
+++ b/pkg/gameservers/controller.go
@@ -955,11 +955,37 @@ func (c *Controller) syncGameServerRequestReadyState(ctx context.Context, gs *ag
 		}
 	}
 
+	gsCopy, err = c.applyGameServerReadyContainerIDAnnotation(ctx, gsCopy, pod)
+	if err != nil {
+		return gs, err
+	}
+
+	gsCopy.Status.State = agonesv1.GameServerStateReady
+	gs, err = c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
+	if err != nil {
+		return gs, errors.Wrapf(err, "error setting Ready, Port and address on GameServer %s Status", gs.ObjectMeta.Name)
+	}
+
+	if addressPopulated {
+		c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Address and port populated")
+	}
+	c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "SDK.Ready() complete")
+	return gs, nil
+}
+
+// applyGameServerReadyContainerIDAnnotation updates the GameServer and its corresponding Pod with an annotation
+// indicating the ID of the container that is running the game server, once it's in a Running state.
+func (c *Controller) applyGameServerReadyContainerIDAnnotation(ctx context.Context, gsCopy *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, error) {
+	// if there is a sidecar container, we escape right away. On move to stable, this method can be deleted.
+	if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
+		return gsCopy, nil
+	}
+
 	// track the ready gameserver container, so we can determine that after this point, we should move to Unhealthy
 	// if there is a container crash/restart after we move to Ready
 	for _, cs := range pod.Status.ContainerStatuses {
-		if cs.Name == gs.Spec.Container {
-			if _, ok := gs.ObjectMeta.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]; !ok {
+		if cs.Name == gsCopy.Spec.Container {
+			if _, ok := gsCopy.ObjectMeta.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]; !ok {
 				// check to make sure this container is actually running. If there was a recent crash, the cache may
 				// not yet have the newer, running container.
 				if cs.State.Running == nil {
@@ -984,22 +1010,11 @@ func (c *Controller) syncGameServerRequestReadyState(ctx context.Context, gs *ag
 		}
 
 		podCopy.ObjectMeta.Annotations[agonesv1.GameServerReadyContainerIDAnnotation] = gsCopy.ObjectMeta.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]
-		if _, err = c.podGetter.Pods(pod.ObjectMeta.Namespace).Update(ctx, podCopy, metav1.UpdateOptions{}); err != nil {
-			return gs, errors.Wrapf(err, "error updating ready annotation on Pod: %s", pod.ObjectMeta.Name)
+		if _, err := c.podGetter.Pods(pod.ObjectMeta.Namespace).Update(ctx, podCopy, metav1.UpdateOptions{}); err != nil {
+			return nil, errors.Wrapf(err, "error updating ready annotation on Pod: %s", pod.ObjectMeta.Name)
 		}
 	}
-
-	gsCopy.Status.State = agonesv1.GameServerStateReady
-	gs, err = c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
-	if err != nil {
-		return gs, errors.Wrapf(err, "error setting Ready, Port and address on GameServer %s Status", gs.ObjectMeta.Name)
-	}
-
-	if addressPopulated {
-		c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Address and port populated")
-	}
-	c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "SDK.Ready() complete")
-	return gs, nil
+	return gsCopy, nil
 }
 
 // syncGameServerShutdownState deletes the GameServer (and therefore the backing Pod) if it is in shutdown state
diff --git a/pkg/gameservers/controller_test.go b/pkg/gameservers/controller_test.go
index 79f8ccd054..1e35fe8a32 100644
--- a/pkg/gameservers/controller_test.go
+++ b/pkg/gameservers/controller_test.go
@@ -29,6 +29,7 @@ import (
 	"agones.dev/agones/pkg/cloudproduct/generic"
 	"agones.dev/agones/pkg/portallocator"
 	agtesting "agones.dev/agones/pkg/testing"
+	agruntime "agones.dev/agones/pkg/util/runtime"
 	"agones.dev/agones/pkg/util/webhooks"
 	"github.com/heptiolabs/healthcheck"
 	"github.com/sirupsen/logrus"
@@ -1261,6 +1262,10 @@ func TestControllerSyncGameServerStartingState(t *testing.T) {
 func TestControllerCreateGameServerPod(t *testing.T) {
 	t.Parallel()
 
+	// TODO: remove mutex when "SidecarContainers" moves to stable.
+	agruntime.FeatureTestMutex.Lock()
+	defer agruntime.FeatureTestMutex.Unlock()
+
 	newFixture := func() *agonesv1.GameServer {
 		fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
 			Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateCreating}}
@@ -1292,9 +1297,20 @@ func TestControllerCreateGameServerPod(t *testing.T) {
 			assert.Empty(t, pod.Spec.NodeSelector)
 			assert.Empty(t, pod.Spec.Tolerations)
 
-			assert.Len(t, pod.Spec.Containers, 2, "Should have a sidecar container")
+			// if sidecar feature enabled, should be 1 container and 1 initContainer, otherwise 2 containers
+			var sidecarContainer corev1.Container
+			var gsContainer corev1.Container
+			if agruntime.FeatureEnabled(agruntime.FeatureSidecarContainers) {
+				assert.Len(t, pod.Spec.Containers, 1, "Should have 1 container")
+				assert.Len(t, pod.Spec.InitContainers, 1, "Should have init container")
+				gsContainer = pod.Spec.Containers[0]
+				sidecarContainer = pod.Spec.InitContainers[0]
+			} else {
+				assert.Len(t, pod.Spec.Containers, 2, "Should have a sidecar container")
+				sidecarContainer = pod.Spec.Containers[0]
+				gsContainer = pod.Spec.Containers[1]
+			}
 
-			sidecarContainer := pod.Spec.Containers[0]
 			assert.Equal(t, sidecarContainer.Image, c.sidecarImage)
 			assert.Equal(t, sidecarContainer.Resources.Limits.Cpu(), &c.sidecarCPULimit)
 			assert.Equal(t, sidecarContainer.Resources.Requests.Cpu(), &c.sidecarCPURequest)
@@ -1311,7 +1327,6 @@ func TestControllerCreateGameServerPod(t *testing.T) {
 			assert.Equal(t, *sidecarContainer.SecurityContext.RunAsNonRoot, true)
 			assert.Equal(t, *sidecarContainer.SecurityContext.RunAsUser, int64(sidecarRunAsUser))
 
-			gsContainer := pod.Spec.Containers[1]
 			assert.Equal(t, fixture.Spec.Ports[0].HostPort, gsContainer.Ports[0].HostPort)
 			assert.Equal(t, fixture.Spec.Ports[0].ContainerPort, gsContainer.Ports[0].ContainerPort)
 			assert.Equal(t, corev1.Protocol("UDP"), gsContainer.Ports[0].Protocol)
@@ -1345,7 +1360,11 @@ func TestControllerCreateGameServerPod(t *testing.T) {
 			created = true
 			ca := action.(k8stesting.CreateAction)
 			pod := ca.GetObject().(*corev1.Pod)
-			assert.Len(t, pod.Spec.Containers, 2, "Should have a sidecar container")
+			if agruntime.FeatureEnabled(agruntime.FeatureSidecarContainers) {
+				assert.Len(t, pod.Spec.InitContainers, 1, "Should have init container")
+			} else {
+				assert.Len(t, pod.Spec.Containers, 2, "Should have a sidecar container")
+			}
 			assert.Empty(t, pod.Spec.Containers[0].VolumeMounts)
 
 			return true, pod, nil
@@ -1424,6 +1443,9 @@ func TestControllerSyncGameServerRequestReadyState(t *testing.T) {
 	nodeName := "node"
 	containerID := "1234"
 
+	agruntime.FeatureTestMutex.Lock()
+	defer agruntime.FeatureTestMutex.Unlock()
+
 	t.Run("GameServer with ReadyRequest State", func(t *testing.T) {
 		c, m := newFakeController()
 
@@ -1452,7 +1474,11 @@ func TestControllerSyncGameServerRequestReadyState(t *testing.T) {
 			ua := action.(k8stesting.UpdateAction)
 			gs := ua.GetObject().(*agonesv1.GameServer)
 			assert.Equal(t, agonesv1.GameServerStateReady, gs.Status.State)
-			assert.Equal(t, containerID, gs.Annotations[agonesv1.GameServerReadyContainerIDAnnotation])
+
+			// only set if not using sidecars.
+			if !agruntime.FeatureEnabled(agruntime.FeatureSidecarContainers) {
+				assert.Equal(t, containerID, gs.Annotations[agonesv1.GameServerReadyContainerIDAnnotation])
+			}
 			return true, gs, nil
 		})
 		m.KubeClient.AddReactor("update", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) {
@@ -1469,12 +1495,18 @@ func TestControllerSyncGameServerRequestReadyState(t *testing.T) {
 		gs, err := c.syncGameServerRequestReadyState(ctx, gsFixture)
 		assert.NoError(t, err, "should not error")
 		assert.True(t, gsUpdated, "GameServer wasn't updated")
-		assert.True(t, podUpdated, "Pod wasn't updated")
+		if agruntime.FeatureEnabled(agruntime.FeatureSidecarContainers) {
+			assert.False(t, podUpdated, "Pod was updated")
+		} else {
+			assert.True(t, podUpdated, "Pod was not updated")
+		}
 		assert.Equal(t, agonesv1.GameServerStateReady, gs.Status.State)
 		agtesting.AssertEventContains(t, m.FakeRecorder.Events, "SDK.Ready() complete")
 	})
 
 	t.Run("Error on GameServer update", func(t *testing.T) {
+		require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=false"))
+
 		c, m := newFakeController()
 
 		gsFixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
@@ -1926,6 +1958,10 @@ func TestControllerSyncGameServerShutdownState(t *testing.T) {
 func TestControllerGameServerPod(t *testing.T) {
 	t.Parallel()
 
+	agruntime.FeatureTestMutex.Lock()
+	defer agruntime.FeatureTestMutex.Unlock()
+	require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=false"))
+
 	setup := func() (*Controller, *agonesv1.GameServer, *watch.FakeWatcher, context.Context, context.CancelFunc) {
 		c, mocks := newFakeController()
 		fakeWatch := watch.NewFake()
diff --git a/pkg/gameservers/health.go b/pkg/gameservers/health.go
index 59c0c41380..925e435098 100644
--- a/pkg/gameservers/health.go
+++ b/pkg/gameservers/health.go
@@ -107,12 +107,17 @@ func NewHealthController(
 // isUnhealthy returns if the Pod event is going
 // to cause the GameServer to become Unhealthy
 func (hc *HealthController) isUnhealthy(pod *corev1.Pod) bool {
-	return hc.evictedPod(pod) || hc.unschedulableWithNoFreePorts(pod) || hc.failedContainer(pod)
+	return hc.evictedPod(pod) || hc.unschedulableWithNoFreePorts(pod) || hc.failedContainer(pod) || hc.failedPod(pod)
 }
 
 // unschedulableWithNoFreePorts checks if the reason the Pod couldn't be scheduled
 // was because there weren't any free ports in the range specified
 func (hc *HealthController) unschedulableWithNoFreePorts(pod *corev1.Pod) bool {
+	// return false, when sidecars are enabled, since we are just going to rely mostly on Pod Status.
+	if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
+		return false
+	}
+
 	// On some cloud products (GKE Autopilot), wait on the Autoscaler to schedule a pod with conflicting ports.
 	if hc.waitOnFreePorts {
 		return false
@@ -128,6 +133,16 @@ func (hc *HealthController) unschedulableWithNoFreePorts(pod *corev1.Pod) bool {
 	return false
 }
 
+// failedPod checks if the Pod's phase is "Failed"
+func (hc *HealthController) failedPod(pod *corev1.Pod) bool {
+	// return false, since a failed pod only happens when sidecars are enabled.
+	if !runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
+		return false
+	}
+
+	return pod.Status.Phase == corev1.PodFailed
+}
+
 // evictedPod checks if the Pod was Evicted
 // could be caused by reaching limit on Ephemeral storage
 func (hc *HealthController) evictedPod(pod *corev1.Pod) bool {
@@ -141,6 +156,12 @@ func (hc *HealthController) evictedPod(pod *corev1.Pod) bool {
 // failedContainer checks each container, and determines if the main gameserver
 // container has failed
 func (hc *HealthController) failedContainer(pod *corev1.Pod) bool {
+	// return false, since there is no gameserver restart before ready. When this graduates, you can delete this
+	// whole function.
+	if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
+		return false
+	}
+
 	container := pod.Annotations[agonesv1.GameServerContainerAnnotation]
 	for _, cs := range pod.Status.ContainerStatuses {
 		if cs.Name == container {
@@ -255,6 +276,12 @@ func (hc *HealthController) skipUnhealthyGameContainer(gs *agonesv1.GameServer,
 		return false, nil
 	}
 
+	// if Sidecar is enabled, always return false, since there is no skip - it's just whatever K8s/Agones wants tso do.
+	// on move to stable, this function can be deleted.
+	if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
+		return false, nil
+	}
+
 	// If the GameServer is before Ready, both annotation values should be ""
 	// If the GameServer is past Ready, both the annotations should be exactly the same.
 	// If they are annotations are different, then the data between the GameServer and the Pod is out of sync,
diff --git a/pkg/gameservers/health_test.go b/pkg/gameservers/health_test.go
index 3e14e3a7b7..5f81f02521 100644
--- a/pkg/gameservers/health_test.go
+++ b/pkg/gameservers/health_test.go
@@ -22,6 +22,7 @@ import (
 
 	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
 	agtesting "agones.dev/agones/pkg/testing"
+	agruntime "agones.dev/agones/pkg/util/runtime"
 	"github.com/heptiolabs/healthcheck"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -36,6 +37,10 @@ import (
 func TestHealthControllerFailedContainer(t *testing.T) {
 	t.Parallel()
 
+	agruntime.FeatureTestMutex.Lock()
+	defer agruntime.FeatureTestMutex.Unlock()
+	require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=false"))
+
 	m := agtesting.NewMocks()
 	hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
 
@@ -60,9 +65,37 @@ func TestHealthControllerFailedContainer(t *testing.T) {
 	assert.False(t, hc.failedContainer(pod2))
 }
 
+func TestHealthControllerFailedPod(t *testing.T) {
+	t.Parallel()
+
+	agruntime.FeatureTestMutex.Lock()
+	defer agruntime.FeatureTestMutex.Unlock()
+
+	// set sidecar feature flag
+	require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=true"))
+
+	m := agtesting.NewMocks()
+	hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
+
+	gs := agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test"}, Spec: newSingleContainerSpec()}
+	gs.ApplyDefaults()
+
+	pod, err := gs.Pod(agtesting.FakeAPIHooks{})
+	require.NoError(t, err)
+	assert.False(t, hc.failedPod(pod))
+
+	// set the pod to failed status
+	pod.Status.Phase = corev1.PodFailed
+	assert.True(t, hc.failedPod(pod))
+}
+
 func TestHealthUnschedulableWithNoFreePorts(t *testing.T) {
 	t.Parallel()
 
+	agruntime.FeatureTestMutex.Lock()
+	defer agruntime.FeatureTestMutex.Unlock()
+	require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=false"))
+
 	m := agtesting.NewMocks()
 	hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
 
@@ -103,6 +136,10 @@ func TestHealthUnschedulableWithNoFreePorts(t *testing.T) {
 func TestHealthControllerSkipUnhealthyGameContainer(t *testing.T) {
 	t.Parallel()
 
+	agruntime.FeatureTestMutex.Lock()
+	defer agruntime.FeatureTestMutex.Unlock()
+	require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=false"))
+
 	type expected struct {
 		result bool
 		err    string
@@ -231,6 +268,7 @@ func TestHealthControllerSyncGameServer(t *testing.T) {
 	fixtures := map[string]struct {
 		state     agonesv1.GameServerState
 		podStatus *corev1.PodStatus
+		feature   string
 		expected  expected
 	}{
 		"started": {
@@ -270,7 +308,8 @@ func TestHealthControllerSyncGameServer(t *testing.T) {
 			expected: expected{updated: false},
 		},
 		"container failed after ready": {
-			state: agonesv1.GameServerStateAllocated,
+			state:   agonesv1.GameServerStateAllocated,
+			feature: string(agruntime.FeatureSidecarContainers) + "=false",
 			podStatus: &corev1.PodStatus{ContainerStatuses: []corev1.ContainerStatus{
 				{Name: "container", State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{}}}}},
 			expected: expected{updated: true},
@@ -293,10 +332,22 @@ func TestHealthControllerSyncGameServer(t *testing.T) {
 				{Name: "container", State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}}}}},
 			expected: expected{updated: false},
 		},
+		"pod failed": {
+			feature:   string(agruntime.FeatureSidecarContainers) + "=true",
+			state:     agonesv1.GameServerStateReady,
+			podStatus: &corev1.PodStatus{Phase: corev1.PodFailed},
+			expected:  expected{updated: true},
+		},
 	}
 
 	for name, test := range fixtures {
 		t.Run(name, func(t *testing.T) {
+			agruntime.FeatureTestMutex.Lock()
+			defer agruntime.FeatureTestMutex.Unlock()
+			if len(test.feature) > 0 {
+				require.NoError(t, agruntime.ParseFeatures(test.feature))
+			}
+
 			m := agtesting.NewMocks()
 			hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
 			hc.recorder = m.FakeRecorder
@@ -376,9 +427,13 @@ func TestHealthControllerSyncGameServerUpdateFailed(t *testing.T) {
 	}
 }
 
-func TestHealthControllerRun(t *testing.T) {
+func TestHealthControllerRunNoSideCar(t *testing.T) {
 	t.Parallel()
 
+	agruntime.FeatureTestMutex.Lock()
+	defer agruntime.FeatureTestMutex.Unlock()
+	require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=false"))
+
 	m := agtesting.NewMocks()
 	hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
 	hc.recorder = m.FakeRecorder
@@ -462,3 +517,75 @@ func TestHealthControllerRun(t *testing.T) {
 
 	agtesting.AssertEventContains(t, m.FakeRecorder.Events, string(agonesv1.GameServerStateUnhealthy))
 }
+
+func TestHealthControllerRunWithSideCar(t *testing.T) {
+	t.Parallel()
+
+	agruntime.FeatureTestMutex.Lock()
+	defer agruntime.FeatureTestMutex.Unlock()
+	require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=true"))
+
+	m := agtesting.NewMocks()
+	hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
+	hc.recorder = m.FakeRecorder
+
+	gsWatch := watch.NewFake()
+	m.AgonesClient.AddWatchReactor("gameservers", k8stesting.DefaultWatchReactor(gsWatch, nil))
+
+	podWatch := watch.NewFake()
+	m.KubeClient.AddWatchReactor("pods", k8stesting.DefaultWatchReactor(podWatch, nil))
+
+	updated := make(chan bool)
+	defer close(updated)
+	m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) {
+		defer func() {
+			updated <- true
+		}()
+		ua := action.(k8stesting.UpdateAction)
+		gsObj := ua.GetObject().(*agonesv1.GameServer)
+		assert.Equal(t, agonesv1.GameServerStateUnhealthy, gsObj.Status.State)
+		return true, gsObj, nil
+	})
+
+	gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "test"}, Spec: newSingleContainerSpec(),
+		Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateReady}}
+	gs.ApplyDefaults()
+	pod, err := gs.Pod(agtesting.FakeAPIHooks{})
+	require.NoError(t, err)
+
+	stop, cancel := agtesting.StartInformers(m)
+	defer cancel()
+
+	gsWatch.Add(gs.DeepCopy())
+	podWatch.Add(pod.DeepCopy())
+
+	go hc.Run(stop, 1) // nolint: errcheck
+	err = wait.PollUntilContextTimeout(context.Background(), time.Second, 10*time.Second, true, func(_ context.Context) (bool, error) {
+		return hc.workerqueue.RunCount() == 1, nil
+	})
+	assert.NoError(t, err)
+
+	pod.Status = corev1.PodStatus{Phase: corev1.PodFailed}
+	// gate
+	require.True(t, hc.failedPod(pod))
+	require.False(t, hc.evictedPod(pod))
+
+	podWatch.Modify(pod.DeepCopy())
+
+	select {
+	case <-updated:
+	case <-time.After(10 * time.Second):
+		assert.FailNow(t, "timeout on GameServer update")
+	}
+
+	agtesting.AssertEventContains(t, m.FakeRecorder.Events, string(agonesv1.GameServerStateUnhealthy))
+
+	podWatch.Delete(pod.DeepCopy())
+	select {
+	case <-updated:
+	case <-time.After(10 * time.Second):
+		assert.FailNow(t, "timeout on GameServer update")
+	}
+
+	agtesting.AssertEventContains(t, m.FakeRecorder.Events, string(agonesv1.GameServerStateUnhealthy))
+}
diff --git a/pkg/util/runtime/features.go b/pkg/util/runtime/features.go
index 553d5babf0..1bf9ce6f08 100644
--- a/pkg/util/runtime/features.go
+++ b/pkg/util/runtime/features.go
@@ -73,6 +73,9 @@ const (
 	// FeatureScheduledAutoscaler is a feature flag to enable/disable scheduled fleet autoscaling.
 	FeatureScheduledAutoscaler Feature = "ScheduledAutoscaler"
 
+	// FeatureSidecarContainers is a feature flag to enable/disable k8s sidecar containers for the sdkserver
+	FeatureSidecarContainers = "SidecarContainers"
+
 	////////////////
 	// Dev features
 
@@ -154,6 +157,7 @@ var (
 		FeaturePlayerTracking:                false,
 		FeatureRollingUpdateFix:              false,
 		FeatureScheduledAutoscaler:           false,
+		FeatureSidecarContainers:             false,
 
 		// Dev features
 
diff --git a/test/e2e/gameserver_test.go b/test/e2e/gameserver_test.go
index 6e5086e599..3af3ef1480 100644
--- a/test/e2e/gameserver_test.go
+++ b/test/e2e/gameserver_test.go
@@ -258,6 +258,10 @@ func TestUnhealthyGameServerAfterHealthCheckFail(t *testing.T) {
 
 func TestUnhealthyGameServersWithoutFreePorts(t *testing.T) {
 	framework.SkipOnCloudProduct(t, "gke-autopilot", "does not support Static PortPolicy")
+	if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
+		t.SkipNow()
+	}
+
 	t.Parallel()
 	ctx := context.Background()
 	nodes, err := framework.KubeClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
@@ -322,6 +326,10 @@ func TestGameServerUnhealthyAfterDeletingPod(t *testing.T) {
 }
 
 func TestGameServerRestartBeforeReadyCrash(t *testing.T) {
+	if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
+		t.SkipNow()
+	}
+
 	t.Parallel()
 	ctx := context.Background()
 	logger := e2eframework.TestLogger(t)
@@ -1698,6 +1706,11 @@ func TestSideCarCommunicatesWhileTerminating(t *testing.T) {
 }
 
 func TestGracefulShutdown(t *testing.T) {
+	// with the new sidecar pattern, there's no need for waiting on the Shutdown state.
+	if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
+		t.SkipNow()
+	}
+
 	t.Parallel()
 
 	log := e2eframework.TestLogger(t)


Reviews:
user: login: markmandel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-17T18:30:42Z
user: login: peterzhongyi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-18T00:37:03Z
user: login: markmandel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T01:46:15Z
user: login: peterzhongyi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-24T20:39:38Z
user: login: peterzhongyi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-24T20:44:23Z
user: login: peterzhongyi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-24T20:55:44Z
user: login: markmandel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-29T03:52:18Z
user: login: markmandel
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-30T17:38:58Z
user: login: peterzhongyi
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T18:21:40Z
user: login: peterzhongyi
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-30T18:21:59Z

Review comments:
diff_hunk: @@ -334,7 +334,7 @@ test-e2e-integration: GO_E2E_TEST_INTEGRATION_ARGS ?=\
 test-e2e-integration: $(ensure-build-image)
 	echo "Starting e2e integration test!"
 ifdef E2E_USE_GOTESTSUM
-	$(GOTESTSUM) --packages=$(agones_package)/test/e2e -- $(go_test_args) -timeout=30m $(ARGS) -args \
+	$(GOTESTSUM) --packages=$(agones_package)/test/e2e -- $(go_test_args) -timeout=45m $(ARGS) -args \
path: build/Makefile
user: login: markmandel
body: Revert back down to 30m?
created_at: 2025-04-17T18:30:42Z
author_association: MEMBER
diff_hunk: @@ -788,11 +788,25 @@ func (gs *GameServer) Pod(apiHooks APIHooks, sidecars ...corev1.Container) (*cor
 		pod.ObjectMeta.Annotations[PassthroughPortAssignmentAnnotation] = string(containerToPassthroughMapJSON)
 	}
 
-	// Put the sidecars at the start of the list of containers so that the kubelet starts them first.
-	containers := make([]corev1.Container, 0, len(sidecars)+len(pod.Spec.Containers))
-	containers = append(containers, sidecars...)
-	containers = append(containers, pod.Spec.Containers...)
-	pod.Spec.Containers = containers
+	if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
+		// make sure all sidecars have a restart policy of Always, so they are valid sidecar containers.
+		// https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/#sidecar-containers-and-pod-lifecycle
+		always := corev1.ContainerRestartPolicyAlways
+		for i := range sidecars {
path: pkg/apis/agones/v1/gameserver.go
user: login: peterzhongyi
body: Why is it a list? Or more specifically, why does this Pod() method takes in a list of sidecars? I see it getting used at https://github.com/googleforgames/agones/blob/a8a59509dc2a25781f116bdd822a4289c53a0f63/pkg/gameservers/controller.go#L650

but it is just using 1 sidecar.
created_at: 2025-04-17T23:21:55Z
author_association: COLLABORATOR
diff_hunk: @@ -955,11 +955,37 @@ func (c *Controller) syncGameServerRequestReadyState(ctx context.Context, gs *ag
 		}
 	}
 
+	gsCopy, err = c.applyGameServerReadyContainerIDAnnotation(ctx, gsCopy, pod)
+	if err != nil {
+		return gs, err
+	}
+
+	gsCopy.Status.State = agonesv1.GameServerStateReady
+	gs, err = c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
+	if err != nil {
+		return gs, errors.Wrapf(err, "error setting Ready, Port and address on GameServer %s Status", gs.ObjectMeta.Name)
+	}
+
+	if addressPopulated {
+		c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Address and port populated")
+	}
+	c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "SDK.Ready() complete")
+	return gs, nil
+}
+
+// applyGameServerReadyContainerIDAnnotation updates the GameServer and its corresponding Pod with an annotation
+// indicating the ID of the container that is running the game server, once it's in a Running state.
+func (c *Controller) applyGameServerReadyContainerIDAnnotation(ctx context.Context, gsCopy *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, error) {
path: pkg/gameservers/controller.go
user: login: peterzhongyi
body: Is this function modifying gsCopy at all? I seem to only see various kinds of reads on it.
created_at: 2025-04-18T00:21:34Z
author_association: COLLABORATOR
diff_hunk: @@ -107,12 +107,17 @@ func NewHealthController(
 // isUnhealthy returns if the Pod event is going
 // to cause the GameServer to become Unhealthy
 func (hc *HealthController) isUnhealthy(pod *corev1.Pod) bool {
-	return hc.evictedPod(pod) || hc.unschedulableWithNoFreePorts(pod) || hc.failedContainer(pod)
+	return hc.evictedPod(pod) || hc.unschedulableWithNoFreePorts(pod) || hc.failedContainer(pod) || hc.failedPod(pod)
path: pkg/gameservers/health.go
user: login: peterzhongyi
body: Can you explain more detailedly why unschedulableWithNoFreePorts and failedContainer checks can be omitted, and evictedPod can't be omitted with the addition of the failedPod? I know it's probably related to "not needing to know the GS container is ready or not", but it is not crystal clear to me why these specific checks can be omitted.
created_at: 2025-04-18T00:36:24Z
author_association: COLLABORATOR
diff_hunk: @@ -788,11 +788,25 @@ func (gs *GameServer) Pod(apiHooks APIHooks, sidecars ...corev1.Container) (*cor
 		pod.ObjectMeta.Annotations[PassthroughPortAssignmentAnnotation] = string(containerToPassthroughMapJSON)
 	}
 
-	// Put the sidecars at the start of the list of containers so that the kubelet starts them first.
-	containers := make([]corev1.Container, 0, len(sidecars)+len(pod.Spec.Containers))
-	containers = append(containers, sidecars...)
-	containers = append(containers, pod.Spec.Containers...)
-	pod.Spec.Containers = containers
+	if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
+		// make sure all sidecars have a restart policy of Always, so they are valid sidecar containers.
+		// https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/#sidecar-containers-and-pod-lifecycle
+		always := corev1.ContainerRestartPolicyAlways
+		for i := range sidecars {
path: pkg/apis/agones/v1/gameserver.go
user: login: markmandel
body: I wrote this signature at the beginning, to set things up if we ever decided to add more than one sidecar. It was just a nice way to keep things open if we ever needed it.
created_at: 2025-04-22T01:40:22Z
author_association: MEMBER
diff_hunk: @@ -955,11 +955,37 @@ func (c *Controller) syncGameServerRequestReadyState(ctx context.Context, gs *ag
 		}
 	}
 
+	gsCopy, err = c.applyGameServerReadyContainerIDAnnotation(ctx, gsCopy, pod)
+	if err != nil {
+		return gs, err
+	}
+
+	gsCopy.Status.State = agonesv1.GameServerStateReady
+	gs, err = c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
+	if err != nil {
+		return gs, errors.Wrapf(err, "error setting Ready, Port and address on GameServer %s Status", gs.ObjectMeta.Name)
+	}
+
+	if addressPopulated {
+		c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Address and port populated")
+	}
+	c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "SDK.Ready() complete")
+	return gs, nil
+}
+
+// applyGameServerReadyContainerIDAnnotation updates the GameServer and its corresponding Pod with an annotation
+// indicating the ID of the container that is running the game server, once it's in a Running state.
+func (c *Controller) applyGameServerReadyContainerIDAnnotation(ctx context.Context, gsCopy *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, error) {
path: pkg/gameservers/controller.go
user: login: markmandel
body: Doesn't look like it - would you prefer the signature to be `gs` rather than `gsCopy`. I don't mind either way.
created_at: 2025-04-22T01:42:55Z
author_association: MEMBER
diff_hunk: @@ -107,12 +107,17 @@ func NewHealthController(
 // isUnhealthy returns if the Pod event is going
 // to cause the GameServer to become Unhealthy
 func (hc *HealthController) isUnhealthy(pod *corev1.Pod) bool {
-	return hc.evictedPod(pod) || hc.unschedulableWithNoFreePorts(pod) || hc.failedContainer(pod)
+	return hc.evictedPod(pod) || hc.unschedulableWithNoFreePorts(pod) || hc.failedContainer(pod) || hc.failedPod(pod)
path: pkg/gameservers/health.go
user: login: markmandel
body: Since Kubernetes is not restarting the container anymore, we don't have to look at error message and the like to see if the game server containers crashed, or any other kind of edge condition (which is what `unschedulableWithNoFreePorts` and `failedContainer` did). Now we just need to check if the Pod is failed.

I left in the check for the Evicted reason, mainly because I wasn't 100% sure if an Evicted pod would end up with a failed pod or not, and the check is so simple, if it was a double check, there would be no harm.
created_at: 2025-04-22T01:45:52Z
author_association: MEMBER
diff_hunk: @@ -788,11 +788,25 @@ func (gs *GameServer) Pod(apiHooks APIHooks, sidecars ...corev1.Container) (*cor
 		pod.ObjectMeta.Annotations[PassthroughPortAssignmentAnnotation] = string(containerToPassthroughMapJSON)
 	}
 
-	// Put the sidecars at the start of the list of containers so that the kubelet starts them first.
-	containers := make([]corev1.Container, 0, len(sidecars)+len(pod.Spec.Containers))
-	containers = append(containers, sidecars...)
-	containers = append(containers, pod.Spec.Containers...)
-	pod.Spec.Containers = containers
+	if runtime.FeatureEnabled(runtime.FeatureSidecarContainers) {
+		// make sure all sidecars have a restart policy of Always, so they are valid sidecar containers.
+		// https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/#sidecar-containers-and-pod-lifecycle
+		always := corev1.ContainerRestartPolicyAlways
+		for i := range sidecars {
path: pkg/apis/agones/v1/gameserver.go
user: login: peterzhongyi
body: Sounds good
created_at: 2025-04-24T20:39:38Z
author_association: COLLABORATOR
diff_hunk: @@ -107,12 +107,17 @@ func NewHealthController(
 // isUnhealthy returns if the Pod event is going
 // to cause the GameServer to become Unhealthy
 func (hc *HealthController) isUnhealthy(pod *corev1.Pod) bool {
-	return hc.evictedPod(pod) || hc.unschedulableWithNoFreePorts(pod) || hc.failedContainer(pod)
+	return hc.evictedPod(pod) || hc.unschedulableWithNoFreePorts(pod) || hc.failedContainer(pod) || hc.failedPod(pod)
path: pkg/gameservers/health.go
user: login: peterzhongyi
body: Thanks for the explanation!
created_at: 2025-04-24T20:44:23Z
author_association: COLLABORATOR
diff_hunk: @@ -955,11 +955,37 @@ func (c *Controller) syncGameServerRequestReadyState(ctx context.Context, gs *ag
 		}
 	}
 
+	gsCopy, err = c.applyGameServerReadyContainerIDAnnotation(ctx, gsCopy, pod)
+	if err != nil {
+		return gs, err
+	}
+
+	gsCopy.Status.State = agonesv1.GameServerStateReady
+	gs, err = c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
+	if err != nil {
+		return gs, errors.Wrapf(err, "error setting Ready, Port and address on GameServer %s Status", gs.ObjectMeta.Name)
+	}
+
+	if addressPopulated {
+		c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Address and port populated")
+	}
+	c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "SDK.Ready() complete")
+	return gs, nil
+}
+
+// applyGameServerReadyContainerIDAnnotation updates the GameServer and its corresponding Pod with an annotation
+// indicating the ID of the container that is running the game server, once it's in a Running state.
+func (c *Controller) applyGameServerReadyContainerIDAnnotation(ctx context.Context, gsCopy *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, error) {
path: pkg/gameservers/controller.go
user: login: peterzhongyi
body: I was asking more about why the gsCopy was even returned by the function if it is just being read? In this case, maybe the function should just return error and exclude *agonesv1.GameServer?

Also, in the function description you mentioned the "applyGameServerReadyContainerIDAnnotation updates the GameServer" - maybe adjust the description if no game server is being updated?
created_at: 2025-04-24T20:55:44Z
author_association: COLLABORATOR
diff_hunk: @@ -955,11 +955,37 @@ func (c *Controller) syncGameServerRequestReadyState(ctx context.Context, gs *ag
 		}
 	}
 
+	gsCopy, err = c.applyGameServerReadyContainerIDAnnotation(ctx, gsCopy, pod)
+	if err != nil {
+		return gs, err
+	}
+
+	gsCopy.Status.State = agonesv1.GameServerStateReady
+	gs, err = c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
+	if err != nil {
+		return gs, errors.Wrapf(err, "error setting Ready, Port and address on GameServer %s Status", gs.ObjectMeta.Name)
+	}
+
+	if addressPopulated {
+		c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Address and port populated")
+	}
+	c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "SDK.Ready() complete")
+	return gs, nil
+}
+
+// applyGameServerReadyContainerIDAnnotation updates the GameServer and its corresponding Pod with an annotation
+// indicating the ID of the container that is running the game server, once it's in a Running state.
+func (c *Controller) applyGameServerReadyContainerIDAnnotation(ctx context.Context, gsCopy *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, error) {
path: pkg/gameservers/controller.go
user: login: markmandel
body: Ah, it does edit the `gsCopy` here:

https://github.com/googleforgames/agones/blob/bcd0c0b3fc0ba8d032fa132df1c4d766b3a747f6/pkg/gameservers/controller.go#L994
created_at: 2025-04-29T03:52:18Z
author_association: MEMBER
diff_hunk: @@ -955,11 +955,37 @@ func (c *Controller) syncGameServerRequestReadyState(ctx context.Context, gs *ag
 		}
 	}
 
+	gsCopy, err = c.applyGameServerReadyContainerIDAnnotation(ctx, gsCopy, pod)
+	if err != nil {
+		return gs, err
+	}
+
+	gsCopy.Status.State = agonesv1.GameServerStateReady
+	gs, err = c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
+	if err != nil {
+		return gs, errors.Wrapf(err, "error setting Ready, Port and address on GameServer %s Status", gs.ObjectMeta.Name)
+	}
+
+	if addressPopulated {
+		c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Address and port populated")
+	}
+	c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "SDK.Ready() complete")
+	return gs, nil
+}
+
+// applyGameServerReadyContainerIDAnnotation updates the GameServer and its corresponding Pod with an annotation
+// indicating the ID of the container that is running the game server, once it's in a Running state.
+func (c *Controller) applyGameServerReadyContainerIDAnnotation(ctx context.Context, gsCopy *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, error) {
path: pkg/gameservers/controller.go
user: login: markmandel
body: Just gently bumping - I think this should be good to merge now, yeah?
created_at: 2025-04-30T17:38:58Z
author_association: MEMBER
diff_hunk: @@ -955,11 +955,37 @@ func (c *Controller) syncGameServerRequestReadyState(ctx context.Context, gs *ag
 		}
 	}
 
+	gsCopy, err = c.applyGameServerReadyContainerIDAnnotation(ctx, gsCopy, pod)
+	if err != nil {
+		return gs, err
+	}
+
+	gsCopy.Status.State = agonesv1.GameServerStateReady
+	gs, err = c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{})
+	if err != nil {
+		return gs, errors.Wrapf(err, "error setting Ready, Port and address on GameServer %s Status", gs.ObjectMeta.Name)
+	}
+
+	if addressPopulated {
+		c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "Address and port populated")
+	}
+	c.recorder.Event(gs, corev1.EventTypeNormal, string(gs.Status.State), "SDK.Ready() complete")
+	return gs, nil
+}
+
+// applyGameServerReadyContainerIDAnnotation updates the GameServer and its corresponding Pod with an annotation
+// indicating the ID of the container that is running the game server, once it's in a Running state.
+func (c *Controller) applyGameServerReadyContainerIDAnnotation(ctx context.Context, gsCopy *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, error) {
path: pkg/gameservers/controller.go
user: login: peterzhongyi
body: Oh I see. SG!
created_at: 2025-04-30T18:21:40Z
author_association: COLLABORATOR

PR comments:
user: login: agones-bot
created_at: 2025-04-07T03:16:09Z
body: 




**Build Failed :sob:**

_Build Id: 743d2942-f5ea-4307-87a4-0ac7f292afc7_

Status: FAILURE

 - [Cloud Build view](https://console.cloud.google.com/cloud-build/builds/743d2942-f5ea-4307-87a4-0ac7f292afc7?project=258182270954)
 - [Cloud Build log download](https://storage.googleapis.com/agones-build-logs/log-743d2942-f5ea-4307-87a4-0ac7f292afc7.txt)

To get permission to view the Cloud Build view, join the [agones-discuss](https://groups.google.com/forum/#!forum/agones-discuss) Google Group.

author_association: COLLABORATOR
user: login: markmandel
created_at: 2025-04-07T04:46:37Z
body: @igooch @gongmax can you cycle the autopilot clusters please? 😄 
author_association: MEMBER
user: login: agones-bot
created_at: 2025-04-07T23:21:00Z
body: 




**Build Succeeded :partying_face:**

_Build Id: bcd4442a-a271-4d0b-b317-8f0aa3cff6f3_

The following development artifacts have been built, and will exist for the next 30 days:

* image: [us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-eab7749](https://us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-eab7749)
* image: [us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-eab7749](https://us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-eab7749)
* image: [us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-eab7749-linux](https://us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-eab7749-linux)
* image: [us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-eab7749](https://us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-eab7749)
* image: [us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-eab7749](https://us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-eab7749)
* Linux C++ SDK (build): [agonessdk-1.49.0-dev-eab7749-linux-arch_64.tar.gz](https://storage.googleapis.com/agones-artifacts/cpp-sdk/agonessdk-1.49.0-dev-eab7749-linux-arch_64.tar.gz)
* SDK Server: [agonessdk-server-1.49.0-dev-eab7749.zip](https://storage.googleapis.com/agones-artifacts/sdk-server/agonessdk-server-1.49.0-dev-eab7749.zip)

A preview of the website (the last 30 builds are retained):
* https://eab7749-dot-preview-dot-agones-images.appspot.com/

To install this version:
```
git fetch https://github.com/googleforgames/agones.git pull/4146/head:pr_4146 && git checkout pr_4146
helm install agones ./install/helm/agones --namespace agones-system --set agones.image.registry=us-docker.pkg.dev/agones-images/ci --set agones.image.tag=1.49.0-dev-eab7749
```
author_association: COLLABORATOR
user: login: markmandel
created_at: 2025-04-10T00:32:55Z
body: Conflicts fixed. Gentle bump on review plz :)
author_association: MEMBER
user: login: agones-bot
created_at: 2025-04-10T01:28:47Z
body: 




**Build Succeeded :partying_face:**

_Build Id: 82100b9d-79d2-42a1-a84f-870207efca0f_

The following development artifacts have been built, and will exist for the next 30 days:

* image: [us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-b4ecf04](https://us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-b4ecf04)
* image: [us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-b4ecf04](https://us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-b4ecf04)
* image: [us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-b4ecf04-linux](https://us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-b4ecf04-linux)
* image: [us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-b4ecf04](https://us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-b4ecf04)
* image: [us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-b4ecf04](https://us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-b4ecf04)
* Linux C++ SDK (build): [agonessdk-1.49.0-dev-b4ecf04-linux-arch_64.tar.gz](https://storage.googleapis.com/agones-artifacts/cpp-sdk/agonessdk-1.49.0-dev-b4ecf04-linux-arch_64.tar.gz)
* SDK Server: [agonessdk-server-1.49.0-dev-b4ecf04.zip](https://storage.googleapis.com/agones-artifacts/sdk-server/agonessdk-server-1.49.0-dev-b4ecf04.zip)

A preview of the website (the last 30 builds are retained):
* https://b4ecf04-dot-preview-dot-agones-images.appspot.com/

To install this version:
```
git fetch https://github.com/googleforgames/agones.git pull/4146/head:pr_4146 && git checkout pr_4146
helm install agones ./install/helm/agones --namespace agones-system --set agones.image.registry=us-docker.pkg.dev/agones-images/ci --set agones.image.tag=1.49.0-dev-b4ecf04
```
author_association: COLLABORATOR
user: login: peterzhongyi
created_at: 2025-04-12T00:15:22Z
body: Thanks Mark! I'll take a look! Coincidentally, I just met with a potential customer who's evaluating Agones and they were specifically interested in GameServer's pod restart policy induced edge cases. So this PR might change a lot of things!
author_association: COLLABORATOR
user: login: agones-bot
created_at: 2025-04-12T00:38:11Z
body: 




**Build Failed :sob:**

_Build Id: e780bc6f-9ccc-432d-97d4-1274f44aadba_

Status: FAILURE

 - [Cloud Build view](https://console.cloud.google.com/cloud-build/builds/e780bc6f-9ccc-432d-97d4-1274f44aadba?project=258182270954)
 - [Cloud Build log download](https://storage.googleapis.com/agones-build-logs/log-e780bc6f-9ccc-432d-97d4-1274f44aadba.txt)

To get permission to view the Cloud Build view, join the [agones-discuss](https://groups.google.com/forum/#!forum/agones-discuss) Google Group.

author_association: COLLABORATOR
user: login: markmandel
created_at: 2025-04-12T03:27:23Z
body: Looks like it's time to recycle the autopilot clusters please.
author_association: MEMBER
user: login: igooch
created_at: 2025-04-14T15:27:48Z
body: > Looks like it's time to recycle the autopilot clusters please.

Updating now.
author_association: COLLABORATOR
user: login: peterzhongyi
created_at: 2025-04-14T20:02:46Z
body: /gcbrun
author_association: COLLABORATOR
user: login: agones-bot
created_at: 2025-04-14T21:11:05Z
body: 




**Build Succeeded :partying_face:**

_Build Id: 9aadf63a-9089-4c62-b4de-bbdf0cc35d7d_

The following development artifacts have been built, and will exist for the next 30 days:

* image: [us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-1e58065](https://us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-1e58065)
* image: [us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-1e58065](https://us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-1e58065)
* image: [us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-1e58065-linux](https://us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-1e58065-linux)
* image: [us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-1e58065](https://us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-1e58065)
* image: [us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-1e58065](https://us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-1e58065)
* Linux C++ SDK (build): [agonessdk-1.49.0-dev-1e58065-linux-arch_64.tar.gz](https://storage.googleapis.com/agones-artifacts/cpp-sdk/agonessdk-1.49.0-dev-1e58065-linux-arch_64.tar.gz)
* SDK Server: [agonessdk-server-1.49.0-dev-1e58065.zip](https://storage.googleapis.com/agones-artifacts/sdk-server/agonessdk-server-1.49.0-dev-1e58065.zip)

A preview of the website (the last 30 builds are retained):
* https://1e58065-dot-preview-dot-agones-images.appspot.com/

To install this version:
```
git fetch https://github.com/googleforgames/agones.git pull/4146/head:pr_4146 && git checkout pr_4146
helm install agones ./install/helm/agones --namespace agones-system --set agones.image.registry=us-docker.pkg.dev/agones-images/ci --set agones.image.tag=1.49.0-dev-1e58065
```
author_association: COLLABORATOR
user: login: agones-bot
created_at: 2025-04-15T02:14:28Z
body: 




**Build Succeeded :partying_face:**

_Build Id: 5be3d922-35be-4d70-9a00-76f5be90d09f_

The following development artifacts have been built, and will exist for the next 30 days:

* image: [us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-884674e](https://us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-884674e)
* image: [us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-884674e](https://us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-884674e)
* image: [us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-884674e-linux](https://us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-884674e-linux)
* image: [us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-884674e](https://us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-884674e)
* image: [us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-884674e](https://us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-884674e)
* Linux C++ SDK (build): [agonessdk-1.49.0-dev-884674e-linux-arch_64.tar.gz](https://storage.googleapis.com/agones-artifacts/cpp-sdk/agonessdk-1.49.0-dev-884674e-linux-arch_64.tar.gz)
* SDK Server: [agonessdk-server-1.49.0-dev-884674e.zip](https://storage.googleapis.com/agones-artifacts/sdk-server/agonessdk-server-1.49.0-dev-884674e.zip)

A preview of the website (the last 30 builds are retained):
* https://884674e-dot-preview-dot-agones-images.appspot.com/

To install this version:
```
git fetch https://github.com/googleforgames/agones.git pull/4146/head:pr_4146 && git checkout pr_4146
helm install agones ./install/helm/agones --namespace agones-system --set agones.image.registry=us-docker.pkg.dev/agones-images/ci --set agones.image.tag=1.49.0-dev-884674e
```
author_association: COLLABORATOR
user: login: agones-bot
created_at: 2025-04-17T03:54:22Z
body: 




**Build Succeeded :partying_face:**

_Build Id: bc079cc7-a5e7-4adb-a1f0-57408872e367_

The following development artifacts have been built, and will exist for the next 30 days:

* image: [us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-bcd0c0b](https://us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-bcd0c0b)
* image: [us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-bcd0c0b](https://us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-bcd0c0b)
* image: [us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-bcd0c0b-linux](https://us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-bcd0c0b-linux)
* image: [us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-bcd0c0b](https://us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-bcd0c0b)
* image: [us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-bcd0c0b](https://us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-bcd0c0b)
* Linux C++ SDK (build): [agonessdk-1.49.0-dev-bcd0c0b-linux-arch_64.tar.gz](https://storage.googleapis.com/agones-artifacts/cpp-sdk/agonessdk-1.49.0-dev-bcd0c0b-linux-arch_64.tar.gz)
* SDK Server: [agonessdk-server-1.49.0-dev-bcd0c0b.zip](https://storage.googleapis.com/agones-artifacts/sdk-server/agonessdk-server-1.49.0-dev-bcd0c0b.zip)

A preview of the website (the last 30 builds are retained):
* https://bcd0c0b-dot-preview-dot-agones-images.appspot.com/

To install this version:
```
git fetch https://github.com/googleforgames/agones.git pull/4146/head:pr_4146 && git checkout pr_4146
helm install agones ./install/helm/agones --namespace agones-system --set agones.image.registry=us-docker.pkg.dev/agones-images/ci --set agones.image.tag=1.49.0-dev-bcd0c0b
```
author_association: COLLABORATOR
user: login: agones-bot
created_at: 2025-04-29T04:56:06Z
body: 




**Build Succeeded :partying_face:**

_Build Id: accafeee-0a46-4579-8686-fdc0bdd786e8_

The following development artifacts have been built, and will exist for the next 30 days:

* image: [us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-cda7ff3](https://us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-cda7ff3)
* image: [us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-cda7ff3](https://us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-cda7ff3)
* image: [us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-cda7ff3-linux](https://us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-cda7ff3-linux)
* image: [us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-cda7ff3](https://us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-cda7ff3)
* image: [us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-cda7ff3](https://us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-cda7ff3)
* Linux C++ SDK (build): [agonessdk-1.49.0-dev-cda7ff3-linux-arch_64.tar.gz](https://storage.googleapis.com/agones-artifacts/cpp-sdk/agonessdk-1.49.0-dev-cda7ff3-linux-arch_64.tar.gz)
* SDK Server: [agonessdk-server-1.49.0-dev-cda7ff3.zip](https://storage.googleapis.com/agones-artifacts/sdk-server/agonessdk-server-1.49.0-dev-cda7ff3.zip)

A preview of the website (the last 30 builds are retained):
* https://cda7ff3-dot-preview-dot-agones-images.appspot.com/

To install this version:
```
git fetch https://github.com/googleforgames/agones.git pull/4146/head:pr_4146 && git checkout pr_4146
helm install agones ./install/helm/agones --namespace agones-system --set agones.image.registry=us-docker.pkg.dev/agones-images/ci --set agones.image.tag=1.49.0-dev-cda7ff3
```
author_association: COLLABORATOR
user: login: agones-bot
created_at: 2025-04-30T19:24:22Z
body: 




**Build Succeeded :partying_face:**

_Build Id: daa9c466-b770-4f65-94f0-0628d8791ff5_

The following development artifacts have been built, and will exist for the next 30 days:

* image: [us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-7d74c4e](https://us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-7d74c4e)
* image: [us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-7d74c4e](https://us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-7d74c4e)
* image: [us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-7d74c4e-linux](https://us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-7d74c4e-linux)
* image: [us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-7d74c4e](https://us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-7d74c4e)
* image: [us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-7d74c4e](https://us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-7d74c4e)
* Linux C++ SDK (build): [agonessdk-1.49.0-dev-7d74c4e-linux-arch_64.tar.gz](https://storage.googleapis.com/agones-artifacts/cpp-sdk/agonessdk-1.49.0-dev-7d74c4e-linux-arch_64.tar.gz)
* SDK Server: [agonessdk-server-1.49.0-dev-7d74c4e.zip](https://storage.googleapis.com/agones-artifacts/sdk-server/agonessdk-server-1.49.0-dev-7d74c4e.zip)

A preview of the website (the last 30 builds are retained):
* https://7d74c4e-dot-preview-dot-agones-images.appspot.com/

To install this version:
```
git fetch https://github.com/googleforgames/agones.git pull/4146/head:pr_4146 && git checkout pr_4146
helm install agones ./install/helm/agones --namespace agones-system --set agones.image.registry=us-docker.pkg.dev/agones-images/ci --set agones.image.tag=1.49.0-dev-7d74c4e
```
author_association: COLLABORATOR
user: login: agones-bot
created_at: 2025-04-30T21:50:14Z
body: 




**Build Succeeded :partying_face:**

_Build Id: aee3da24-e49f-4528-9892-388074c0fece_

The following development artifacts have been built, and will exist for the next 30 days:

* image: [us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-2166bee](https://us-docker.pkg.dev/agones-images/ci/agones-controller:1.49.0-dev-2166bee)
* image: [us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-2166bee](https://us-docker.pkg.dev/agones-images/ci/agones-extensions:1.49.0-dev-2166bee)
* image: [us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-2166bee-linux](https://us-docker.pkg.dev/agones-images/ci/agones-sdk:1.49.0-dev-2166bee-linux)
* image: [us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-2166bee](https://us-docker.pkg.dev/agones-images/ci/agones-ping:1.49.0-dev-2166bee)
* image: [us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-2166bee](https://us-docker.pkg.dev/agones-images/ci/agones-allocator:1.49.0-dev-2166bee)
* Linux C++ SDK (build): [agonessdk-1.49.0-dev-2166bee-linux-arch_64.tar.gz](https://storage.googleapis.com/agones-artifacts/cpp-sdk/agonessdk-1.49.0-dev-2166bee-linux-arch_64.tar.gz)
* SDK Server: [agonessdk-server-1.49.0-dev-2166bee.zip](https://storage.googleapis.com/agones-artifacts/sdk-server/agonessdk-server-1.49.0-dev-2166bee.zip)

A preview of the website (the last 30 builds are retained):
* https://2166bee-dot-preview-dot-agones-images.appspot.com/

To install this version:
```
git fetch https://github.com/googleforgames/agones.git pull/4146/head:pr_4146 && git checkout pr_4146
helm install agones ./install/helm/agones --namespace agones-system --set agones.image.registry=us-docker.pkg.dev/agones-images/ci --set agones.image.tag=1.49.0-dev-2166bee
```
author_association: COLLABORATOR