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

     1  // Copyright 2025 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  	"testing"
    19  	"time"
    20  
    21  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    22  	agtesting "agones.dev/agones/pkg/testing"
    23  	"github.com/heptiolabs/healthcheck"
    24  	"github.com/stretchr/testify/require"
    25  	corev1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	k8stesting "k8s.io/client-go/testing"
    29  )
    30  
    31  func TestSucceededControllerSyncGameServer(t *testing.T) {
    32  	type expected struct {
    33  		updated     bool
    34  		updateTests func(t *testing.T, gs *agonesv1.GameServer)
    35  		postTests   func(t *testing.T, mocks agtesting.Mocks)
    36  	}
    37  	fixtures := map[string]struct {
    38  		setup    func(*agonesv1.GameServer, *corev1.Pod) (*agonesv1.GameServer, *corev1.Pod)
    39  		expected expected
    40  	}{
    41  		"pod exists but not in Succeeded state": {
    42  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, *corev1.Pod) {
    43  				return gs, pod
    44  			},
    45  			expected: expected{
    46  				updated:     false,
    47  				updateTests: func(_ *testing.T, _ *agonesv1.GameServer) {},
    48  				postTests:   func(_ *testing.T, _ agtesting.Mocks) {},
    49  			},
    50  		},
    51  		"pod exists and is in Succeeded state": {
    52  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, *corev1.Pod) {
    53  				pod.Status.Phase = corev1.PodSucceeded
    54  				return gs, pod
    55  			},
    56  			expected: expected{
    57  				updated: true,
    58  				updateTests: func(t *testing.T, gs *agonesv1.GameServer) {
    59  					require.Equal(t, agonesv1.GameServerStateShutdown, gs.Status.State)
    60  				},
    61  				postTests: func(t *testing.T, m agtesting.Mocks) {
    62  					agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Normal Shutdown Pod is in Succeeded state")
    63  				},
    64  			},
    65  		},
    66  		"pod doesn't exist": {
    67  			setup: func(gs *agonesv1.GameServer, _ *corev1.Pod) (*agonesv1.GameServer, *corev1.Pod) {
    68  				return gs, nil
    69  			},
    70  			expected: expected{
    71  				updated:     false,
    72  				updateTests: func(_ *testing.T, _ *agonesv1.GameServer) {},
    73  				postTests:   func(_ *testing.T, _ agtesting.Mocks) {},
    74  			},
    75  		},
    76  		"game server not found": {
    77  			setup: func(_ *agonesv1.GameServer, _ *corev1.Pod) (*agonesv1.GameServer, *corev1.Pod) {
    78  				return nil, nil
    79  			},
    80  			expected: expected{
    81  				updated:     false,
    82  				updateTests: func(_ *testing.T, _ *agonesv1.GameServer) {},
    83  				postTests:   func(_ *testing.T, _ agtesting.Mocks) {},
    84  			},
    85  		},
    86  		"game server is being deleted": {
    87  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, *corev1.Pod) {
    88  				now := metav1.Now()
    89  				gs.ObjectMeta.DeletionTimestamp = &now
    90  				pod.Status.Phase = corev1.PodSucceeded
    91  				return gs, pod
    92  			},
    93  			expected: expected{
    94  				updated:     false,
    95  				updateTests: func(_ *testing.T, _ *agonesv1.GameServer) {},
    96  				postTests:   func(_ *testing.T, _ agtesting.Mocks) {},
    97  			},
    98  		},
    99  		"game server is already in Shutdown state": {
   100  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, *corev1.Pod) {
   101  				gs.Status.State = agonesv1.GameServerStateShutdown
   102  				pod.Status.Phase = corev1.PodSucceeded
   103  				return gs, pod
   104  			},
   105  			expected: expected{
   106  				updated:     false,
   107  				updateTests: func(_ *testing.T, _ *agonesv1.GameServer) {},
   108  				postTests:   func(_ *testing.T, _ agtesting.Mocks) {},
   109  			},
   110  		},
   111  		"game server is in Error state": {
   112  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, *corev1.Pod) {
   113  				gs.Status.State = agonesv1.GameServerStateError
   114  				pod.Status.Phase = corev1.PodSucceeded
   115  				return gs, pod
   116  			},
   117  			expected: expected{
   118  				updated:     false,
   119  				updateTests: func(_ *testing.T, _ *agonesv1.GameServer) {},
   120  				postTests:   func(_ *testing.T, _ agtesting.Mocks) {},
   121  			},
   122  		},
   123  		"game server is in Unhealthy state": {
   124  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, *corev1.Pod) {
   125  				gs.Status.State = agonesv1.GameServerStateUnhealthy
   126  				pod.Status.Phase = corev1.PodSucceeded
   127  				return gs, pod
   128  			},
   129  			expected: expected{
   130  				updated:     false,
   131  				updateTests: func(_ *testing.T, _ *agonesv1.GameServer) {},
   132  				postTests:   func(_ *testing.T, _ agtesting.Mocks) {},
   133  			},
   134  		},
   135  		"pod is not a gameserver pod": {
   136  			setup: func(gs *agonesv1.GameServer, _ *corev1.Pod) (*agonesv1.GameServer, *corev1.Pod) {
   137  				pod := &corev1.Pod{ObjectMeta: gs.ObjectMeta}
   138  				pod.Status.Phase = corev1.PodSucceeded
   139  				return gs, pod
   140  			},
   141  			expected: expected{
   142  				updated:     false,
   143  				updateTests: func(_ *testing.T, _ *agonesv1.GameServer) {},
   144  				postTests:   func(_ *testing.T, _ agtesting.Mocks) {},
   145  			},
   146  		},
   147  		"pod is in terminating state": {
   148  			setup: func(gs *agonesv1.GameServer, pod *corev1.Pod) (*agonesv1.GameServer, *corev1.Pod) {
   149  				now := metav1.Now()
   150  				pod.ObjectMeta.DeletionTimestamp = &now
   151  				pod.Status.Phase = corev1.PodSucceeded
   152  				return gs, pod
   153  			},
   154  			expected: expected{
   155  				updated:     false,
   156  				updateTests: func(_ *testing.T, _ *agonesv1.GameServer) {},
   157  				postTests:   func(_ *testing.T, _ agtesting.Mocks) {},
   158  			},
   159  		},
   160  	}
   161  
   162  	for k, v := range fixtures {
   163  		t.Run(k, func(t *testing.T) {
   164  			m := agtesting.NewMocks()
   165  			c := NewSucceededController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory)
   166  			c.recorder = m.FakeRecorder
   167  
   168  			gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
   169  				Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{}}
   170  			gs.ApplyDefaults()
   171  
   172  			pod, err := gs.Pod(agtesting.FakeAPIHooks{})
   173  			require.NoError(t, err)
   174  
   175  			gs, pod = v.setup(gs, pod)
   176  			m.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) {
   177  				if gs != nil {
   178  					return true, &agonesv1.GameServerList{Items: []agonesv1.GameServer{*gs}}, nil
   179  				}
   180  				return true, &agonesv1.GameServerList{Items: []agonesv1.GameServer{}}, nil
   181  			})
   182  			m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) {
   183  				if pod != nil {
   184  					return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil
   185  				}
   186  				return true, &corev1.PodList{Items: []corev1.Pod{}}, nil
   187  			})
   188  
   189  			updated := false
   190  			m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) {
   191  				updated = true
   192  				ua := action.(k8stesting.UpdateAction)
   193  				gs := ua.GetObject().(*agonesv1.GameServer)
   194  				v.expected.updateTests(t, gs)
   195  				return true, gs, nil
   196  			})
   197  
   198  			// Use context explicitly to avoid unused import warning
   199  			ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced, c.podSynced)
   200  			defer cancel()
   201  
   202  			err = c.syncGameServer(ctx, "default/test")
   203  			require.NoError(t, err)
   204  			require.Equal(t, v.expected.updated, updated)
   205  			v.expected.postTests(t, m)
   206  		})
   207  	}
   208  }
   209  
   210  func TestSucceededControllerRun(t *testing.T) {
   211  	m := agtesting.NewMocks()
   212  	c := NewSucceededController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory)
   213  
   214  	gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
   215  		Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{}}
   216  	gs.ApplyDefaults()
   217  
   218  	pod, err := gs.Pod(agtesting.FakeAPIHooks{})
   219  	require.NoError(t, err)
   220  	pod.Status.Phase = corev1.PodSucceeded
   221  
   222  	m.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) {
   223  		return true, &agonesv1.GameServerList{Items: []agonesv1.GameServer{*gs}}, nil
   224  	})
   225  	m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) {
   226  		return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil
   227  	})
   228  
   229  	updated := make(chan bool, 10)
   230  	m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) {
   231  		ua := action.(k8stesting.UpdateAction)
   232  		gs := ua.GetObject().(*agonesv1.GameServer)
   233  		updated <- gs.Status.State == agonesv1.GameServerStateShutdown
   234  		return true, gs, nil
   235  	})
   236  
   237  	ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced, c.podSynced)
   238  	defer cancel()
   239  
   240  	go func() {
   241  		err := c.Run(ctx, 1)
   242  		require.NoError(t, err)
   243  	}()
   244  
   245  	select {
   246  	case <-time.After(5 * time.Second):
   247  		require.FailNow(t, "timeout waiting for GameServer to be marked as Shutdown")
   248  	case value := <-updated:
   249  		require.True(t, value)
   250  	}
   251  }