agones.dev/agones@v1.53.0/pkg/gameservers/health_test.go (about)

     1  // Copyright 2018 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package gameservers
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"testing"
    21  	"time"
    22  
    23  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    24  	agtesting "agones.dev/agones/pkg/testing"
    25  	agruntime "agones.dev/agones/pkg/util/runtime"
    26  	"github.com/heptiolabs/healthcheck"
    27  	"github.com/stretchr/testify/assert"
    28  	"github.com/stretchr/testify/require"
    29  	corev1 "k8s.io/api/core/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/util/wait"
    33  	"k8s.io/apimachinery/pkg/watch"
    34  	k8stesting "k8s.io/client-go/testing"
    35  )
    36  
    37  func TestHealthControllerFailedContainer(t *testing.T) {
    38  	t.Parallel()
    39  
    40  	agruntime.FeatureTestMutex.Lock()
    41  	defer agruntime.FeatureTestMutex.Unlock()
    42  	require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=false"))
    43  
    44  	m := agtesting.NewMocks()
    45  	hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
    46  
    47  	gs := agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test"}, Spec: newSingleContainerSpec()}
    48  	gs.ApplyDefaults()
    49  
    50  	pod, err := gs.Pod(agtesting.FakeAPIHooks{})
    51  	require.NoError(t, err)
    52  	pod.Status = corev1.PodStatus{ContainerStatuses: []corev1.ContainerStatus{{Name: gs.Spec.Container,
    53  		State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{}}}}}
    54  
    55  	assert.True(t, hc.failedContainer(pod))
    56  	pod2 := pod.DeepCopy()
    57  
    58  	pod.Status.ContainerStatuses[0].State.Terminated = nil
    59  	assert.False(t, hc.failedContainer(pod))
    60  
    61  	pod.Status.ContainerStatuses[0].LastTerminationState.Terminated = &corev1.ContainerStateTerminated{}
    62  	assert.True(t, hc.failedContainer(pod))
    63  
    64  	pod2.Status.ContainerStatuses[0].Name = "Not a matching name"
    65  	assert.False(t, hc.failedContainer(pod2))
    66  }
    67  
    68  func TestHealthControllerFailedPod(t *testing.T) {
    69  	t.Parallel()
    70  
    71  	agruntime.FeatureTestMutex.Lock()
    72  	defer agruntime.FeatureTestMutex.Unlock()
    73  
    74  	// set sidecar feature flag
    75  	require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=true"))
    76  
    77  	m := agtesting.NewMocks()
    78  	hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
    79  
    80  	gs := agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test"}, Spec: newSingleContainerSpec()}
    81  	gs.ApplyDefaults()
    82  
    83  	pod, err := gs.Pod(agtesting.FakeAPIHooks{})
    84  	require.NoError(t, err)
    85  	assert.False(t, hc.failedPod(pod))
    86  
    87  	// set the pod to failed status
    88  	pod.Status.Phase = corev1.PodFailed
    89  	assert.True(t, hc.failedPod(pod))
    90  }
    91  
    92  func TestHealthUnschedulableWithNoFreePorts(t *testing.T) {
    93  	t.Parallel()
    94  
    95  	agruntime.FeatureTestMutex.Lock()
    96  	defer agruntime.FeatureTestMutex.Unlock()
    97  	require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=false"))
    98  
    99  	m := agtesting.NewMocks()
   100  	hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
   101  
   102  	gs := agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test"}, Spec: newSingleContainerSpec()}
   103  	gs.ApplyDefaults()
   104  
   105  	for name, tc := range map[string]struct {
   106  		message           string
   107  		waitOnFreePorts   bool
   108  		wantUnschedulable bool
   109  	}{
   110  		"unschedulable, terminal": {
   111  			message:           "0/4 nodes are available: 4 node(s) didn't have free ports for the requestedpod ports.",
   112  			wantUnschedulable: true,
   113  		},
   114  		"unschedulable, will wait on free ports": {
   115  			message:         "0/4 nodes are available: 4 node(s) didn't have free ports for the requestedpod ports.",
   116  			waitOnFreePorts: true,
   117  		},
   118  		"some other condition": {
   119  			message: "twas brillig and the slithy toves",
   120  		},
   121  	} {
   122  		t.Run(name, func(t *testing.T) {
   123  			pod, err := gs.Pod(agtesting.FakeAPIHooks{})
   124  			require.NoError(t, err)
   125  
   126  			pod.Status.Conditions = []corev1.PodCondition{
   127  				{Type: corev1.PodScheduled, Reason: corev1.PodReasonUnschedulable,
   128  					Message: tc.message},
   129  			}
   130  			hc.waitOnFreePorts = tc.waitOnFreePorts
   131  			assert.Equal(t, tc.wantUnschedulable, hc.unschedulableWithNoFreePorts(pod))
   132  		})
   133  	}
   134  }
   135  
   136  func TestHealthControllerSkipUnhealthyGameContainer(t *testing.T) {
   137  	t.Parallel()
   138  
   139  	agruntime.FeatureTestMutex.Lock()
   140  	defer agruntime.FeatureTestMutex.Unlock()
   141  	require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=false"))
   142  
   143  	type expected struct {
   144  		result bool
   145  		err    string
   146  	}
   147  
   148  	fixtures := map[string]struct {
   149  		setup    func(*agonesv1.GameServer, *corev1.Pod)
   150  		expected expected
   151  	}{
   152  		"scheduled and terminated container": {
   153  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod) {
   154  				gs.Status.State = agonesv1.GameServerStateScheduled
   155  				pod.Status.ContainerStatuses = []corev1.ContainerStatus{{
   156  					Name:  gs.Spec.Container,
   157  					State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{}},
   158  				}}
   159  			},
   160  			expected: expected{result: true},
   161  		},
   162  		"after ready and terminated container": {
   163  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod) {
   164  				gs.Status.State = agonesv1.GameServerStateReady
   165  				pod.Status.ContainerStatuses = []corev1.ContainerStatus{{
   166  					Name:  gs.Spec.Container,
   167  					State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{}},
   168  				}}
   169  			},
   170  			expected: expected{result: false},
   171  		},
   172  		"before ready, with no terminated container": {
   173  			setup: func(gs *agonesv1.GameServer, _ *corev1.Pod) {
   174  				gs.Status.State = agonesv1.GameServerStateScheduled
   175  			},
   176  			expected: expected{result: false},
   177  		},
   178  		"after ready, with no terminated container": {
   179  			setup: func(gs *agonesv1.GameServer, _ *corev1.Pod) {
   180  				gs.Status.State = agonesv1.GameServerStateAllocated
   181  			},
   182  			expected: expected{result: false},
   183  		},
   184  		"before ready, with a LastTerminated container": {
   185  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod) {
   186  				gs.Status.State = agonesv1.GameServerStateScheduled
   187  				pod.Status.ContainerStatuses = []corev1.ContainerStatus{{
   188  					Name:                 gs.Spec.Container,
   189  					LastTerminationState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{}},
   190  				}}
   191  			},
   192  			expected: expected{result: true},
   193  		},
   194  		"after ready, with a LastTerminated container, not matching": {
   195  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod) {
   196  				gs.Status.State = agonesv1.GameServerStateReady
   197  				gs.Annotations[agonesv1.GameServerReadyContainerIDAnnotation] = "4321"
   198  				pod.ObjectMeta.Annotations[agonesv1.GameServerReadyContainerIDAnnotation] = "4321"
   199  				pod.Status.ContainerStatuses = []corev1.ContainerStatus{{
   200  					ContainerID:          "1234",
   201  					Name:                 gs.Spec.Container,
   202  					LastTerminationState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{}},
   203  				}}
   204  			},
   205  			expected: expected{result: false},
   206  		},
   207  		"after ready, with a LastTerminated container, matching": {
   208  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod) {
   209  				gs.Status.State = agonesv1.GameServerStateReserved
   210  				gs.Annotations[agonesv1.GameServerReadyContainerIDAnnotation] = "1234"
   211  				pod.Annotations[agonesv1.GameServerReadyContainerIDAnnotation] = "1234"
   212  				pod.Status.ContainerStatuses = []corev1.ContainerStatus{{
   213  					ContainerID:          "1234",
   214  					Name:                 gs.Spec.Container,
   215  					LastTerminationState: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{}},
   216  				}}
   217  			},
   218  			expected: expected{result: true},
   219  		},
   220  		"pod is missing!": {
   221  			setup: func(_ *agonesv1.GameServer, pod *corev1.Pod) {
   222  				pod.ObjectMeta.Name = "missing"
   223  			},
   224  			expected: expected{result: false},
   225  		},
   226  		"annotations do not match": {
   227  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod) {
   228  				gs.Annotations[agonesv1.GameServerReadyContainerIDAnnotation] = "1234"
   229  				pod.Annotations[agonesv1.GameServerReadyContainerIDAnnotation] = ""
   230  			},
   231  			expected: expected{err: "pod and gameserver test data are out of sync, retrying"},
   232  		},
   233  	}
   234  
   235  	for k, v := range fixtures {
   236  		t.Run(k, func(t *testing.T) {
   237  			m := agtesting.NewMocks()
   238  			hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
   239  			gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: defaultNs}, Spec: newSingleContainerSpec()}
   240  			gs.ApplyDefaults()
   241  			pod, err := gs.Pod(agtesting.FakeAPIHooks{})
   242  			assert.NoError(t, err)
   243  
   244  			v.setup(gs, pod)
   245  
   246  			m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) {
   247  				return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil
   248  			})
   249  
   250  			result, err := hc.skipUnhealthyGameContainer(gs, pod)
   251  
   252  			if len(v.expected.err) > 0 {
   253  				require.EqualError(t, err, v.expected.err)
   254  			} else {
   255  				assert.NoError(t, err)
   256  			}
   257  			assert.Equal(t, v.expected.result, result)
   258  		})
   259  	}
   260  }
   261  
   262  func TestHealthControllerSyncGameServer(t *testing.T) {
   263  	t.Parallel()
   264  
   265  	type expected struct {
   266  		updated bool
   267  	}
   268  	fixtures := map[string]struct {
   269  		state     agonesv1.GameServerState
   270  		podStatus *corev1.PodStatus
   271  		feature   string
   272  		expected  expected
   273  	}{
   274  		"started": {
   275  			state: agonesv1.GameServerStateStarting,
   276  			expected: expected{
   277  				updated: true,
   278  			},
   279  		},
   280  		"shutdown": {
   281  			state: agonesv1.GameServerStateShutdown,
   282  			expected: expected{
   283  				updated: false,
   284  			},
   285  		},
   286  		"unhealthy": {
   287  			state: agonesv1.GameServerStateUnhealthy,
   288  			expected: expected{
   289  				updated: false,
   290  			},
   291  		},
   292  		"ready": {
   293  			state: agonesv1.GameServerStateReady,
   294  			expected: expected{
   295  				updated: true,
   296  			},
   297  		},
   298  		"allocated": {
   299  			state: agonesv1.GameServerStateAllocated,
   300  			expected: expected{
   301  				updated: true,
   302  			},
   303  		},
   304  		"container failed before ready": {
   305  			state: agonesv1.GameServerStateStarting,
   306  			podStatus: &corev1.PodStatus{ContainerStatuses: []corev1.ContainerStatus{
   307  				{Name: "container", State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{}}}}},
   308  			expected: expected{updated: false},
   309  		},
   310  		"container failed after ready": {
   311  			state:   agonesv1.GameServerStateAllocated,
   312  			feature: string(agruntime.FeatureSidecarContainers) + "=false",
   313  			podStatus: &corev1.PodStatus{ContainerStatuses: []corev1.ContainerStatus{
   314  				{Name: "container", State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{}}}}},
   315  			expected: expected{updated: true},
   316  		},
   317  		"container recovered and starting after queueing": {
   318  			state: agonesv1.GameServerStateStarting,
   319  			podStatus: &corev1.PodStatus{ContainerStatuses: []corev1.ContainerStatus{
   320  				{Name: "container", State: corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{}}}}},
   321  			expected: expected{updated: false},
   322  		},
   323  		"container recovered and ready after queueing": {
   324  			state: agonesv1.GameServerStateReady,
   325  			podStatus: &corev1.PodStatus{ContainerStatuses: []corev1.ContainerStatus{
   326  				{Name: "container", State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}}}}},
   327  			expected: expected{updated: false},
   328  		},
   329  		"container recovered and allocated after queueing": {
   330  			state: agonesv1.GameServerStateAllocated,
   331  			podStatus: &corev1.PodStatus{ContainerStatuses: []corev1.ContainerStatus{
   332  				{Name: "container", State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}}}}},
   333  			expected: expected{updated: false},
   334  		},
   335  		"pod failed": {
   336  			feature:   string(agruntime.FeatureSidecarContainers) + "=true",
   337  			state:     agonesv1.GameServerStateReady,
   338  			podStatus: &corev1.PodStatus{Phase: corev1.PodFailed},
   339  			expected:  expected{updated: true},
   340  		},
   341  	}
   342  
   343  	for name, test := range fixtures {
   344  		t.Run(name, func(t *testing.T) {
   345  			agruntime.FeatureTestMutex.Lock()
   346  			defer agruntime.FeatureTestMutex.Unlock()
   347  			if len(test.feature) > 0 {
   348  				require.NoError(t, agruntime.ParseFeatures(test.feature))
   349  			}
   350  
   351  			m := agtesting.NewMocks()
   352  			hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
   353  			hc.recorder = m.FakeRecorder
   354  
   355  			gs := agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "test"}, Spec: newSingleContainerSpec(),
   356  				Status: agonesv1.GameServerStatus{State: test.state}}
   357  			gs.ApplyDefaults()
   358  
   359  			got := false
   360  			updated := false
   361  			m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) {
   362  				list := &corev1.PodList{Items: []corev1.Pod{}}
   363  				if test.podStatus != nil {
   364  					pod, err := gs.Pod(agtesting.FakeAPIHooks{})
   365  					assert.NoError(t, err)
   366  					pod.Status = *test.podStatus
   367  					list.Items = append(list.Items, *pod)
   368  				}
   369  				return true, list, nil
   370  			})
   371  			m.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) {
   372  				got = true
   373  				return true, &agonesv1.GameServerList{Items: []agonesv1.GameServer{gs}}, nil
   374  			})
   375  			m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) {
   376  				updated = true
   377  				ua := action.(k8stesting.UpdateAction)
   378  				gsObj := ua.GetObject().(*agonesv1.GameServer)
   379  				assert.Equal(t, agonesv1.GameServerStateUnhealthy, gsObj.Status.State)
   380  				return true, gsObj, nil
   381  			})
   382  
   383  			ctx, cancel := agtesting.StartInformers(m, hc.gameServerSynced, hc.podSynced)
   384  			defer cancel()
   385  
   386  			err := hc.syncGameServer(ctx, "default/test")
   387  			assert.Nil(t, err, err)
   388  			assert.True(t, got, "GameServers Should be got!")
   389  
   390  			assert.Equal(t, test.expected.updated, updated, "updated test")
   391  		})
   392  	}
   393  }
   394  
   395  func TestHealthControllerSyncGameServerUpdateFailed(t *testing.T) {
   396  	t.Parallel()
   397  
   398  	m := agtesting.NewMocks()
   399  	hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
   400  	hc.recorder = m.FakeRecorder
   401  
   402  	gs := agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "test"}, Spec: newSingleContainerSpec(),
   403  		Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateAllocated}}
   404  	gs.ApplyDefaults()
   405  
   406  	m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) {
   407  		list := &corev1.PodList{Items: []corev1.Pod{}}
   408  		return true, list, nil
   409  	})
   410  	m.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) {
   411  		return true, &agonesv1.GameServerList{Items: []agonesv1.GameServer{gs}}, nil
   412  	})
   413  	m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) {
   414  		ua := action.(k8stesting.UpdateAction)
   415  		gsObj := ua.GetObject().(*agonesv1.GameServer)
   416  		assert.Equal(t, agonesv1.GameServerStateUnhealthy, gsObj.Status.State)
   417  		return true, gsObj, errors.New("update-err")
   418  	})
   419  
   420  	ctx, cancel := agtesting.StartInformers(m, hc.gameServerSynced, hc.podSynced)
   421  	defer cancel()
   422  
   423  	err := hc.syncGameServer(ctx, "default/test")
   424  
   425  	if assert.Error(t, err) {
   426  		assert.Equal(t, "error updating GameServer test/default to unhealthy: update-err", err.Error())
   427  	}
   428  }
   429  
   430  func TestHealthControllerRunNoSideCar(t *testing.T) {
   431  	t.Parallel()
   432  
   433  	agruntime.FeatureTestMutex.Lock()
   434  	defer agruntime.FeatureTestMutex.Unlock()
   435  	require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=false"))
   436  
   437  	m := agtesting.NewMocks()
   438  	hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
   439  	hc.recorder = m.FakeRecorder
   440  
   441  	gsWatch := watch.NewFake()
   442  	m.AgonesClient.AddWatchReactor("gameservers", k8stesting.DefaultWatchReactor(gsWatch, nil))
   443  
   444  	podWatch := watch.NewFake()
   445  	m.KubeClient.AddWatchReactor("pods", k8stesting.DefaultWatchReactor(podWatch, nil))
   446  
   447  	updated := make(chan bool)
   448  	defer close(updated)
   449  	m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) {
   450  		defer func() {
   451  			updated <- true
   452  		}()
   453  		ua := action.(k8stesting.UpdateAction)
   454  		gsObj := ua.GetObject().(*agonesv1.GameServer)
   455  		assert.Equal(t, agonesv1.GameServerStateUnhealthy, gsObj.Status.State)
   456  		return true, gsObj, nil
   457  	})
   458  
   459  	gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "test"}, Spec: newSingleContainerSpec(),
   460  		Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateReady}}
   461  	gs.ApplyDefaults()
   462  	pod, err := gs.Pod(agtesting.FakeAPIHooks{})
   463  	require.NoError(t, err)
   464  
   465  	stop, cancel := agtesting.StartInformers(m)
   466  	defer cancel()
   467  
   468  	gsWatch.Add(gs.DeepCopy())
   469  	podWatch.Add(pod.DeepCopy())
   470  
   471  	go hc.Run(stop, 1) // nolint: errcheck
   472  	err = wait.PollUntilContextTimeout(context.Background(), time.Second, 10*time.Second, true, func(_ context.Context) (bool, error) {
   473  		return hc.workerqueue.RunCount() == 1, nil
   474  	})
   475  	assert.NoError(t, err)
   476  
   477  	pod.Status.ContainerStatuses = []corev1.ContainerStatus{{Name: gs.Spec.Container, State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{}}}}
   478  	// gate
   479  	assert.True(t, hc.failedContainer(pod))
   480  	assert.False(t, hc.unschedulableWithNoFreePorts(pod))
   481  
   482  	podWatch.Modify(pod.DeepCopy())
   483  
   484  	select {
   485  	case <-updated:
   486  	case <-time.After(10 * time.Second):
   487  		assert.FailNow(t, "timeout on GameServer update")
   488  	}
   489  
   490  	agtesting.AssertEventContains(t, m.FakeRecorder.Events, string(agonesv1.GameServerStateUnhealthy))
   491  
   492  	pod.Status.ContainerStatuses = nil
   493  	pod.Status.Conditions = []corev1.PodCondition{
   494  		{Type: corev1.PodScheduled, Reason: corev1.PodReasonUnschedulable,
   495  			Message: "0/4 nodes are available: 4 node(s) didn't have free ports for the requestedpod ports."},
   496  	}
   497  	// gate
   498  	assert.True(t, hc.unschedulableWithNoFreePorts(pod))
   499  	assert.False(t, hc.failedContainer(pod))
   500  
   501  	podWatch.Modify(pod.DeepCopy())
   502  
   503  	select {
   504  	case <-updated:
   505  	case <-time.After(10 * time.Second):
   506  		assert.FailNow(t, "timeout on GameServer update")
   507  	}
   508  
   509  	agtesting.AssertEventContains(t, m.FakeRecorder.Events, string(agonesv1.GameServerStateUnhealthy))
   510  
   511  	podWatch.Delete(pod.DeepCopy())
   512  	select {
   513  	case <-updated:
   514  	case <-time.After(10 * time.Second):
   515  		assert.FailNow(t, "timeout on GameServer update")
   516  	}
   517  
   518  	agtesting.AssertEventContains(t, m.FakeRecorder.Events, string(agonesv1.GameServerStateUnhealthy))
   519  }
   520  
   521  func TestHealthControllerRunWithSideCar(t *testing.T) {
   522  	t.Parallel()
   523  
   524  	agruntime.FeatureTestMutex.Lock()
   525  	defer agruntime.FeatureTestMutex.Unlock()
   526  	require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=true"))
   527  
   528  	m := agtesting.NewMocks()
   529  	hc := NewHealthController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, false)
   530  	hc.recorder = m.FakeRecorder
   531  
   532  	gsWatch := watch.NewFake()
   533  	m.AgonesClient.AddWatchReactor("gameservers", k8stesting.DefaultWatchReactor(gsWatch, nil))
   534  
   535  	podWatch := watch.NewFake()
   536  	m.KubeClient.AddWatchReactor("pods", k8stesting.DefaultWatchReactor(podWatch, nil))
   537  
   538  	updated := make(chan bool)
   539  	defer close(updated)
   540  	m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) {
   541  		defer func() {
   542  			updated <- true
   543  		}()
   544  		ua := action.(k8stesting.UpdateAction)
   545  		gsObj := ua.GetObject().(*agonesv1.GameServer)
   546  		assert.Equal(t, agonesv1.GameServerStateUnhealthy, gsObj.Status.State)
   547  		return true, gsObj, nil
   548  	})
   549  
   550  	gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "test"}, Spec: newSingleContainerSpec(),
   551  		Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateReady}}
   552  	gs.ApplyDefaults()
   553  	pod, err := gs.Pod(agtesting.FakeAPIHooks{})
   554  	require.NoError(t, err)
   555  
   556  	stop, cancel := agtesting.StartInformers(m)
   557  	defer cancel()
   558  
   559  	gsWatch.Add(gs.DeepCopy())
   560  	podWatch.Add(pod.DeepCopy())
   561  
   562  	go hc.Run(stop, 1) // nolint: errcheck
   563  	err = wait.PollUntilContextTimeout(context.Background(), time.Second, 10*time.Second, true, func(_ context.Context) (bool, error) {
   564  		return hc.workerqueue.RunCount() == 1, nil
   565  	})
   566  	assert.NoError(t, err)
   567  
   568  	pod.Status = corev1.PodStatus{Phase: corev1.PodFailed}
   569  	// gate
   570  	require.True(t, hc.failedPod(pod))
   571  	require.False(t, hc.evictedPod(pod))
   572  
   573  	podWatch.Modify(pod.DeepCopy())
   574  
   575  	select {
   576  	case <-updated:
   577  	case <-time.After(10 * time.Second):
   578  		assert.FailNow(t, "timeout on GameServer update")
   579  	}
   580  
   581  	agtesting.AssertEventContains(t, m.FakeRecorder.Events, string(agonesv1.GameServerStateUnhealthy))
   582  
   583  	podWatch.Delete(pod.DeepCopy())
   584  	select {
   585  	case <-updated:
   586  	case <-time.After(10 * time.Second):
   587  		assert.FailNow(t, "timeout on GameServer update")
   588  	}
   589  
   590  	agtesting.AssertEventContains(t, m.FakeRecorder.Events, string(agonesv1.GameServerStateUnhealthy))
   591  }