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 }