agones.dev/agones@v1.53.0/pkg/gameservers/migration_test.go (about) 1 // Copyright 2020 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 "testing" 20 "time" 21 22 agonesv1 "agones.dev/agones/pkg/apis/agones/v1" 23 agtesting "agones.dev/agones/pkg/testing" 24 "github.com/heptiolabs/healthcheck" 25 "github.com/sirupsen/logrus" 26 "github.com/stretchr/testify/assert" 27 "github.com/stretchr/testify/require" 28 corev1 "k8s.io/api/core/v1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/watch" 32 k8stesting "k8s.io/client-go/testing" 33 "k8s.io/client-go/tools/cache" 34 ) 35 36 func TestMigrationControllerSyncGameServer(t *testing.T) { 37 t.Parallel() 38 39 ipChangeFixture := "99.99.99.99" 40 nodeNameChangeFixture := "nodeChange" 41 42 type expected struct { 43 updated bool 44 updateTests func(t *testing.T, gs *agonesv1.GameServer) 45 postTests func(t *testing.T, mocks agtesting.Mocks) 46 } 47 fixtures := map[string]struct { 48 setup func(*agonesv1.GameServer, *corev1.Pod, *corev1.Node) (*agonesv1.GameServer, *corev1.Pod, *corev1.Node) 49 expected expected 50 }{ 51 "no change, gameserver nodeName not yet set": { 52 setup: func(gs *agonesv1.GameServer, pod *corev1.Pod, node *corev1.Node) (*agonesv1.GameServer, *corev1.Pod, *corev1.Node) { 53 return gs, pod, node 54 }, 55 expected: expected{ 56 updated: false, 57 updateTests: func(_ *testing.T, _ *agonesv1.GameServer) {}, 58 postTests: func(t *testing.T, m agtesting.Mocks) { 59 agtesting.AssertNoEvent(t, m.FakeRecorder.Events) 60 }, 61 }, 62 }, 63 "no change, with same address": { 64 setup: func(gs *agonesv1.GameServer, pod *corev1.Pod, node *corev1.Node) (*agonesv1.GameServer, *corev1.Pod, *corev1.Node) { 65 gs.Status.NodeName = nodeFixtureName 66 gs.Status.Address = ipFixture 67 return gs, pod, node 68 }, 69 expected: expected{ 70 updated: false, 71 updateTests: func(_ *testing.T, _ *agonesv1.GameServer) {}, 72 postTests: func(t *testing.T, m agtesting.Mocks) { 73 agtesting.AssertNoEvent(t, m.FakeRecorder.Events) 74 }, 75 }, 76 }, 77 "change before ready, ip only change": { 78 setup: func(gs *agonesv1.GameServer, pod *corev1.Pod, node *corev1.Node) (*agonesv1.GameServer, *corev1.Pod, *corev1.Node) { 79 gs.Status.NodeName = nodeFixtureName 80 gs.Status.Address = ipFixture 81 gs.Status.State = agonesv1.GameServerStateScheduled 82 node.Status.Addresses[0].Address = ipChangeFixture 83 return gs, pod, node 84 }, 85 expected: expected{ 86 updated: true, 87 updateTests: func(t *testing.T, gs *agonesv1.GameServer) { 88 assert.Equal(t, ipChangeFixture, gs.Status.Address) 89 }, 90 postTests: func(t *testing.T, m agtesting.Mocks) { 91 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Warning Scheduled Address updated due to Node migration") 92 }, 93 }, 94 }, 95 "change before ready, full node change": { 96 setup: func(gs *agonesv1.GameServer, pod *corev1.Pod, node *corev1.Node) (*agonesv1.GameServer, *corev1.Pod, *corev1.Node) { 97 gs.Status.NodeName = nodeFixtureName 98 gs.Status.Address = ipFixture 99 gs.Status.State = agonesv1.GameServerStateScheduled 100 101 // full node name change 102 pod.Spec.NodeName = nodeNameChangeFixture 103 node.ObjectMeta.Name = nodeNameChangeFixture 104 node.Status.Addresses[0].Address = ipChangeFixture 105 return gs, pod, node 106 }, 107 expected: expected{ 108 updated: true, 109 updateTests: func(t *testing.T, gs *agonesv1.GameServer) { 110 assert.Equal(t, ipChangeFixture, gs.Status.Address) 111 assert.Equal(t, nodeNameChangeFixture, gs.Status.NodeName) 112 }, 113 postTests: func(t *testing.T, m agtesting.Mocks) { 114 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Warning Scheduled Address updated due to Node migration") 115 }, 116 }, 117 }, 118 "change after ready": { 119 setup: func(gs *agonesv1.GameServer, pod *corev1.Pod, node *corev1.Node) (*agonesv1.GameServer, *corev1.Pod, *corev1.Node) { 120 gs.Status.NodeName = nodeFixtureName 121 gs.Status.Address = ipFixture 122 gs.Status.State = agonesv1.GameServerStateAllocated 123 124 // full node name change 125 pod.Spec.NodeName = nodeNameChangeFixture 126 node.ObjectMeta.Name = nodeNameChangeFixture 127 node.Status.Addresses[0].Address = ipChangeFixture 128 129 return gs, pod, node 130 }, 131 expected: expected{ 132 updated: true, 133 updateTests: func(t *testing.T, gs *agonesv1.GameServer) { 134 assert.Equal(t, agonesv1.GameServerStateUnhealthy, gs.Status.State) 135 }, 136 postTests: func(t *testing.T, m agtesting.Mocks) { 137 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Warning Unhealthy Node migration occurred") 138 }, 139 }, 140 }, 141 } 142 143 for k, v := range fixtures { 144 t.Run(k, func(t *testing.T) { 145 m := agtesting.NewMocks() 146 c := NewMigrationController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, nilSyncPodPortsToGameServer) 147 c.recorder = m.FakeRecorder 148 149 gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 150 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{}} 151 gs.ApplyDefaults() 152 153 pod, err := gs.Pod(agtesting.FakeAPIHooks{}) 154 require.NoError(t, err) 155 pod.Spec.NodeName = nodeFixtureName 156 157 node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeFixtureName}, 158 Status: corev1.NodeStatus{Addresses: []corev1.NodeAddress{{Address: ipFixture, Type: corev1.NodeExternalIP}}}} 159 160 gs, pod, node = v.setup(gs, pod, node) 161 162 // populate 163 m.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 164 return true, &agonesv1.GameServerList{Items: []agonesv1.GameServer{*gs}}, nil 165 }) 166 m.KubeClient.AddReactor("list", "nodes", func(_ k8stesting.Action) (bool, runtime.Object, error) { 167 return true, 168 &corev1.NodeList{Items: []corev1.Node{*node}}, nil 169 }) 170 m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 171 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 172 }) 173 174 // check values 175 updated := false 176 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 177 updated = true 178 ua := action.(k8stesting.UpdateAction) 179 gs := ua.GetObject().(*agonesv1.GameServer) 180 v.expected.updateTests(t, gs) 181 return true, gs, nil 182 }) 183 184 ctx, cancel := agtesting.StartInformers(m, c.nodeSynced, c.gameServerSynced, c.podSynced) 185 defer cancel() 186 187 err = c.syncGameServer(ctx, "default/test") 188 assert.NoError(t, err) 189 assert.Equal(t, v.expected.updated, updated) 190 v.expected.postTests(t, m) 191 }) 192 } 193 } 194 195 func TestMigrationControllerRun(t *testing.T) { 196 logrus.SetLevel(logrus.DebugLevel) 197 198 m := agtesting.NewMocks() 199 c := NewMigrationController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, nilSyncPodPortsToGameServer) 200 201 node := corev1.Node{ 202 ObjectMeta: metav1.ObjectMeta{ 203 Name: nodeFixtureName, 204 }, 205 Status: corev1.NodeStatus{ 206 Addresses: []corev1.NodeAddress{ 207 { 208 Type: corev1.NodeExternalIP, 209 Address: ipFixture, 210 }, 211 }, 212 }, 213 } 214 nodeChangedName := nodeFixtureName + "+changed" 215 nodeChanged := corev1.Node{ 216 ObjectMeta: metav1.ObjectMeta{Name: nodeChangedName}, 217 Status: corev1.NodeStatus{ 218 Addresses: []corev1.NodeAddress{ 219 { 220 Type: corev1.NodeExternalIP, 221 Address: "no", 222 }, 223 }, 224 }, 225 } 226 227 gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 228 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{ 229 NodeName: nodeFixtureName, 230 Address: ipFixture, 231 State: agonesv1.GameServerStateAllocated, 232 }} 233 gs.ApplyDefaults() 234 235 gsPod, err := gs.Pod(agtesting.FakeAPIHooks{}) 236 require.NoError(t, err) 237 gsPod.Spec.NodeName = nodeFixtureName 238 239 received := make(chan string) 240 h := func(_ context.Context, name string) error { 241 assert.Equal(t, "default/test", name) 242 received <- name 243 return nil 244 } 245 246 podWatch := watch.NewFake() 247 m.KubeClient.AddWatchReactor("pods", k8stesting.DefaultWatchReactor(podWatch, nil)) 248 249 nodeWatch := watch.NewFake() 250 m.KubeClient.AddWatchReactor("nodes", k8stesting.DefaultWatchReactor(nodeWatch, nil)) 251 252 gsWatch := watch.NewFake() 253 m.AgonesClient.AddWatchReactor("gameservers", k8stesting.DefaultWatchReactor(gsWatch, nil)) 254 255 c.workerqueue.SyncHandler = h 256 257 ctx, cancel := agtesting.StartInformers(m, c.nodeSynced, c.gameServerSynced, c.podSynced) 258 defer cancel() 259 260 go func() { 261 err := c.Run(ctx, 1) 262 assert.Nil(t, err, "Run should not error") 263 }() 264 265 noChange := func() { 266 require.True(t, cache.WaitForCacheSync(ctx.Done(), c.nodeSynced, c.gameServerSynced, c.podSynced)) 267 select { 268 case <-received: 269 require.Fail(t, "should not be updated") 270 case <-time.After(1 * time.Second): 271 } 272 } 273 274 result := func() { 275 require.True(t, cache.WaitForCacheSync(ctx.Done(), c.nodeSynced, c.gameServerSynced, c.podSynced)) 276 select { 277 case res := <-received: 278 require.Equal(t, "default/test", res) 279 case <-time.After(2 * time.Second): 280 require.Fail(t, "did not receive queue") 281 } 282 } 283 284 // initial pod, no gameserver, no nodes 285 logrus.Info("initial pod, no gameserver, no node") 286 gsPod.ObjectMeta.Labels["change"] = "no-pod-no-gameserver-no-node" 287 podWatch.Add(gsPod.DeepCopy()) 288 noChange() 289 290 // pod with gameserver, no nodes 291 logrus.Info("pod with gameserver, no node") 292 gsWatch.Add(gs.DeepCopy()) 293 require.True(t, cache.WaitForCacheSync(ctx.Done(), c.gameServerSynced)) 294 gsPod.ObjectMeta.Labels["change"] = "pod-gameserver-no-node" 295 podWatch.Modify(gsPod.DeepCopy()) 296 noChange() 297 298 // pod with gameserver, and nodes 299 logrus.Info("pod with gameserver, and node") 300 nodeWatch.Add(node.DeepCopy()) 301 nodeWatch.Add(nodeChanged.DeepCopy()) 302 require.True(t, cache.WaitForCacheSync(ctx.Done(), c.nodeSynced)) 303 gsPod.ObjectMeta.Labels["change"] = "pod-gameserver-node" 304 podWatch.Modify(gsPod.DeepCopy()) 305 noChange() 306 307 // pod with a different NodeName to the Node. 308 logrus.Info("pod with a different NodeName to the Node.") 309 gsPod.Spec.NodeName = nodeChangedName 310 gsPod.ObjectMeta.Labels["change"] = "pod-gameserver-node-changed" 311 podWatch.Modify(gsPod.DeepCopy()) 312 result() 313 314 // deleted pod 315 now := metav1.Now() 316 logrus.Info("deleted pod") 317 gsPod.ObjectMeta.DeletionTimestamp = &now 318 podWatch.Modify(gsPod.DeepCopy()) 319 noChange() 320 321 // non gameserver pod 322 pod := corev1.Pod{ObjectMeta: metav1.ObjectMeta{ 323 Name: "test2", 324 Namespace: "default", 325 }} 326 pod.Spec.NodeName = nodeFixtureName 327 podWatch.Add(pod.DeepCopy()) 328 noChange() 329 330 pod.Spec.NodeName = nodeChangedName 331 podWatch.Modify(pod.DeepCopy()) 332 noChange() 333 } 334 335 func TestMigrationControllerAnyAddressMatch(t *testing.T) { 336 fixtures := map[string]struct { 337 matches bool 338 gsAddress string 339 nodeAddresses []corev1.NodeAddress 340 }{ 341 "NodeHostName matches": { 342 matches: true, 343 gsAddress: "NodeHostName", 344 nodeAddresses: []corev1.NodeAddress{{Address: "NodeHostName", Type: corev1.NodeHostName}}, 345 }, 346 "NodeExternalDNS matches": { 347 matches: true, 348 gsAddress: "NodeExternalDNS", 349 nodeAddresses: []corev1.NodeAddress{{Address: "NodeExternalDNS", Type: corev1.NodeExternalDNS}}, 350 }, 351 "NodeExternalIP matches": { 352 matches: true, 353 gsAddress: "NodeExternalIP", 354 nodeAddresses: []corev1.NodeAddress{{Address: "NodeExternalIP", Type: corev1.NodeExternalIP}}, 355 }, 356 "NodeInternalDNS matches": { 357 matches: true, 358 gsAddress: "NodeInternalDNS", 359 nodeAddresses: []corev1.NodeAddress{{Address: "NodeInternalDNS", Type: corev1.NodeInternalDNS}}, 360 }, 361 "NodeInternalIP matches": { 362 matches: true, 363 gsAddress: "NodeInternalIP", 364 nodeAddresses: []corev1.NodeAddress{{Address: "NodeInternalIP", Type: corev1.NodeInternalIP}}, 365 }, 366 "no matches": { 367 matches: false, 368 gsAddress: "no-match", 369 nodeAddresses: []corev1.NodeAddress{ 370 {Address: "NodeHostName", Type: corev1.NodeHostName}, 371 {Address: "NodeExternalDNS", Type: corev1.NodeExternalDNS}, 372 {Address: "NodeExternalIP", Type: corev1.NodeExternalIP}, 373 {Address: "NodeInternalDNS", Type: corev1.NodeInternalDNS}, 374 {Address: "NodeInternalIP", Type: corev1.NodeInternalIP}, 375 }, 376 }, 377 "matches one of many 1": { 378 matches: true, 379 gsAddress: "NodeInternalDNS", 380 nodeAddresses: []corev1.NodeAddress{ 381 {Address: "NodeHostName", Type: corev1.NodeHostName}, 382 {Address: "NodeExternalDNS", Type: corev1.NodeExternalDNS}, 383 {Address: "NodeExternalIP", Type: corev1.NodeExternalIP}, 384 {Address: "NodeInternalDNS", Type: corev1.NodeInternalDNS}, 385 {Address: "NodeInternalIP", Type: corev1.NodeInternalIP}, 386 }, 387 }, 388 "matches one of many 2": { 389 matches: true, 390 gsAddress: "NodeInternalIP", 391 nodeAddresses: []corev1.NodeAddress{ 392 {Address: "NodeHostName", Type: corev1.NodeHostName}, 393 {Address: "NodeExternalDNS", Type: corev1.NodeExternalDNS}, 394 {Address: "NodeExternalIP", Type: corev1.NodeExternalIP}, 395 {Address: "NodeInternalDNS", Type: corev1.NodeInternalDNS}, 396 {Address: "NodeInternalIP", Type: corev1.NodeInternalIP}, 397 }, 398 }, 399 } 400 for k, v := range fixtures { 401 t.Run(k, func(t *testing.T) { 402 m := agtesting.NewMocks() 403 c := NewMigrationController(healthcheck.NewHandler(), m.KubeClient, m.AgonesClient, m.KubeInformerFactory, m.AgonesInformerFactory, nilSyncPodPortsToGameServer) 404 gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 405 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{ 406 Address: v.gsAddress, 407 }} 408 node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeFixtureName}, 409 Status: corev1.NodeStatus{Addresses: v.nodeAddresses}} 410 411 matches := c.anyAddressMatch(node, gs) 412 assert.Equal(t, v.matches, matches) 413 }) 414 } 415 } 416 417 func nilSyncPodPortsToGameServer(*agonesv1.GameServer, *corev1.Pod) error { return nil }