agones.dev/agones@v1.53.0/pkg/gameservers/controller_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 "encoding/json" 20 "errors" 21 "fmt" 22 "net/http" 23 "strconv" 24 "testing" 25 "time" 26 27 "agones.dev/agones/pkg/apis/agones" 28 agonesv1 "agones.dev/agones/pkg/apis/agones/v1" 29 "agones.dev/agones/pkg/cloudproduct/generic" 30 "agones.dev/agones/pkg/portallocator" 31 agtesting "agones.dev/agones/pkg/testing" 32 agruntime "agones.dev/agones/pkg/util/runtime" 33 "agones.dev/agones/pkg/util/webhooks" 34 "github.com/heptiolabs/healthcheck" 35 "github.com/sirupsen/logrus" 36 "github.com/stretchr/testify/assert" 37 "github.com/stretchr/testify/require" 38 "gomodules.xyz/jsonpatch/v2" 39 admissionv1 "k8s.io/api/admission/v1" 40 corev1 "k8s.io/api/core/v1" 41 k8serrors "k8s.io/apimachinery/pkg/api/errors" 42 "k8s.io/apimachinery/pkg/api/resource" 43 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 44 "k8s.io/apimachinery/pkg/labels" 45 "k8s.io/apimachinery/pkg/runtime" 46 "k8s.io/apimachinery/pkg/runtime/schema" 47 "k8s.io/apimachinery/pkg/util/intstr" 48 "k8s.io/apimachinery/pkg/util/validation/field" 49 "k8s.io/apimachinery/pkg/watch" 50 k8stesting "k8s.io/client-go/testing" 51 "k8s.io/client-go/tools/cache" 52 ) 53 54 const ( 55 ipFixture = "12.12.12.12" 56 ipv6Fixture = "2001:0db8:85a3:0000:0000:8a2e:0370:7334" 57 nodeFixtureName = "node1" 58 sidecarRunAsUser = 1000 59 ) 60 61 var GameServerKind = metav1.GroupVersionKind(agonesv1.SchemeGroupVersion.WithKind("GameServer")) 62 var PodKind = corev1.SchemeGroupVersion.WithKind("Pod") 63 64 func TestControllerSyncGameServer(t *testing.T) { 65 t.Parallel() 66 67 t.Run("Creating a new GameServer", func(t *testing.T) { 68 c, mocks := newFakeController() 69 updateCount := 0 70 podCreated := false 71 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 72 Spec: agonesv1.GameServerSpec{ 73 Ports: []agonesv1.GameServerPort{{ContainerPort: 7777}}, 74 Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ 75 Containers: []corev1.Container{{Name: "container", Image: "container/image"}}, 76 }, 77 }, 78 }, 79 } 80 81 node := corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeFixtureName}, 82 Status: corev1.NodeStatus{Addresses: []corev1.NodeAddress{{Address: ipFixture, Type: corev1.NodeExternalIP}}}} 83 84 fixture.ApplyDefaults() 85 86 watchPods := watch.NewFake() 87 mocks.KubeClient.AddWatchReactor("pods", k8stesting.DefaultWatchReactor(watchPods, nil)) 88 89 mocks.KubeClient.AddReactor("list", "nodes", func(_ k8stesting.Action) (bool, runtime.Object, error) { 90 return true, &corev1.NodeList{Items: []corev1.Node{node}}, nil 91 }) 92 mocks.KubeClient.AddReactor("create", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 93 ca := action.(k8stesting.CreateAction) 94 pod := ca.GetObject().(*corev1.Pod) 95 pod.Spec.NodeName = node.ObjectMeta.Name 96 pod.Status.PodIPs = []corev1.PodIP{{IP: ipv6Fixture}} 97 podCreated = true 98 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name) 99 watchPods.Add(pod) 100 // wait for the change to propagate 101 require.Eventually(t, func() bool { 102 list, err := c.podLister.List(labels.Everything()) 103 assert.NoError(t, err) 104 return len(list) == 1 105 }, 5*time.Second, time.Second) 106 return true, pod, nil 107 }) 108 mocks.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 109 gameServers := &agonesv1.GameServerList{Items: []agonesv1.GameServer{*fixture}} 110 return true, gameServers, nil 111 }) 112 mocks.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 113 ua := action.(k8stesting.UpdateAction) 114 gs := ua.GetObject().(*agonesv1.GameServer) 115 updateCount++ 116 expectedState := agonesv1.GameServerState("notastate") 117 switch updateCount { 118 case 1: 119 expectedState = agonesv1.GameServerStateCreating 120 case 2: 121 expectedState = agonesv1.GameServerStateStarting 122 case 3: 123 expectedState = agonesv1.GameServerStateScheduled 124 } 125 126 assert.Equal(t, expectedState, gs.Status.State) 127 if expectedState == agonesv1.GameServerStateScheduled { 128 assert.Equal(t, ipFixture, gs.Status.Address) 129 assert.Equal(t, []corev1.NodeAddress{ 130 {Address: ipFixture, Type: "ExternalIP"}, 131 {Address: ipv6Fixture, Type: "PodIP"}, 132 }, gs.Status.Addresses) 133 assert.NotEmpty(t, gs.Status.Ports[0].Port) 134 } 135 136 return true, gs, nil 137 }) 138 139 ctx, cancel := agtesting.StartInformers(mocks, c.gameServerSynced) 140 defer cancel() 141 142 err := c.portAllocator.Run(ctx) 143 assert.Nil(t, err) 144 145 err = c.syncGameServer(ctx, "default/test") 146 assert.Nil(t, err) 147 assert.Equal(t, 3, updateCount, "update reactor should fire thrice") 148 assert.True(t, podCreated, "pod should be created") 149 }) 150 151 t.Run("When a GameServer has been deleted, the sync operation should be a noop", func(t *testing.T) { 152 runReconcileDeleteGameServer(t, &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 153 Spec: newSingleContainerSpec(), 154 Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateReady}}) 155 }) 156 } 157 158 func runReconcileDeleteGameServer(t *testing.T, fixture *agonesv1.GameServer) { 159 c, mocks := newFakeController() 160 agonesWatch := watch.NewFake() 161 podAction := false 162 163 mocks.AgonesClient.AddWatchReactor("gameservers", k8stesting.DefaultWatchReactor(agonesWatch, nil)) 164 mocks.KubeClient.AddReactor("*", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 165 if action.GetVerb() == "update" || action.GetVerb() == "delete" || action.GetVerb() == "create" || action.GetVerb() == "patch" { 166 podAction = true 167 } 168 return false, nil, nil 169 }) 170 171 ctx, cancel := agtesting.StartInformers(mocks, c.gameServerSynced) 172 defer cancel() 173 174 agonesWatch.Delete(fixture) 175 176 err := c.syncGameServer(ctx, "default/test") 177 assert.Nil(t, err, fmt.Sprintf("Shouldn't be an error from syncGameServer: %+v", err)) 178 assert.False(t, podAction, "Nothing should happen to a Pod") 179 } 180 181 func TestControllerSyncGameServerWithDevIP(t *testing.T) { 182 t.Parallel() 183 184 templateDevGs := &agonesv1.GameServer{ 185 ObjectMeta: metav1.ObjectMeta{ 186 Name: "test", 187 Namespace: "default", 188 Annotations: map[string]string{agonesv1.DevAddressAnnotation: ipFixture}, 189 }, 190 Spec: agonesv1.GameServerSpec{ 191 Ports: []agonesv1.GameServerPort{{ContainerPort: 7777, HostPort: 7777, PortPolicy: agonesv1.Static}}, 192 }, 193 } 194 195 t.Run("Creating a new GameServer", func(t *testing.T) { 196 c, mocks := newFakeController() 197 updateCount := 0 198 199 fixture := templateDevGs.DeepCopy() 200 201 fixture.ApplyDefaults() 202 203 mocks.KubeClient.AddReactor("list", "nodes", func(_ k8stesting.Action) (bool, runtime.Object, error) { 204 return false, nil, k8serrors.NewMethodNotSupported(schema.GroupResource{}, "list nodes should not be called") 205 }) 206 mocks.KubeClient.AddReactor("create", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 207 return false, nil, k8serrors.NewMethodNotSupported(schema.GroupResource{}, "creating a pod with dev mode is not supported") 208 }) 209 mocks.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 210 gameServers := &agonesv1.GameServerList{Items: []agonesv1.GameServer{*fixture}} 211 return true, gameServers, nil 212 }) 213 mocks.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 214 ua := action.(k8stesting.UpdateAction) 215 gs := ua.GetObject().(*agonesv1.GameServer) 216 updateCount++ 217 expectedState := agonesv1.GameServerStateReady 218 219 assert.Equal(t, expectedState, gs.Status.State) 220 assert.Equal(t, ipFixture, gs.Status.Address) 221 assert.Equal(t, []corev1.NodeAddress{{Address: ipFixture, Type: "InternalIP"}}, gs.Status.Addresses) 222 assert.NotEmpty(t, gs.Status.Ports[0].Port) 223 224 return true, gs, nil 225 }) 226 227 ctx, cancel := agtesting.StartInformers(mocks, c.gameServerSynced) 228 defer cancel() 229 230 err := c.portAllocator.Run(ctx) 231 assert.Nil(t, err) 232 233 err = c.syncGameServer(ctx, "default/test") 234 assert.Nil(t, err) 235 assert.Equal(t, 1, updateCount, "update reactor should fire once") 236 }) 237 238 t.Run("GameServer with ReadyRequest State", func(t *testing.T) { 239 c, mocks := newFakeController() 240 241 updateCount := 0 242 243 gsFixture := templateDevGs.DeepCopy() 244 gsFixture.ApplyDefaults() 245 gsFixture.Status.State = agonesv1.GameServerStateRequestReady 246 247 mocks.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 248 gameServers := &agonesv1.GameServerList{Items: []agonesv1.GameServer{*gsFixture}} 249 return true, gameServers, nil 250 }) 251 mocks.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 252 ua := action.(k8stesting.UpdateAction) 253 gs := ua.GetObject().(*agonesv1.GameServer) 254 updateCount++ 255 256 assert.Equal(t, agonesv1.GameServerStateReady, gs.Status.State) 257 258 return true, gs, nil 259 }) 260 261 ctx, cancel := agtesting.StartInformers(mocks, c.gameServerSynced) 262 defer cancel() 263 264 err := c.portAllocator.Run(ctx) 265 assert.NoError(t, err, "should not error") 266 267 err = c.syncGameServer(ctx, "default/test") 268 assert.NoError(t, err, "should not error") 269 assert.Equal(t, 1, updateCount, "update reactor should fire once") 270 }) 271 272 t.Run("Allocated GameServer", func(t *testing.T) { 273 c, mocks := newFakeController() 274 275 fixture := templateDevGs.DeepCopy() 276 277 fixture.ApplyDefaults() 278 fixture.Status.State = agonesv1.GameServerStateAllocated 279 280 mocks.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 281 gameServers := &agonesv1.GameServerList{Items: []agonesv1.GameServer{*fixture}} 282 return true, gameServers, nil 283 }) 284 mocks.AgonesClient.AddReactor("update", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 285 require.Fail(t, "should not update") 286 return true, nil, nil 287 }) 288 289 ctx, cancel := agtesting.StartInformers(mocks, c.gameServerSynced) 290 defer cancel() 291 292 err := c.portAllocator.Run(ctx) 293 require.NoError(t, err) 294 295 err = c.syncGameServer(ctx, "default/test") 296 require.NoError(t, err) 297 }) 298 299 t.Run("When a GameServer has been deleted, the sync operation should be a noop", func(t *testing.T) { 300 runReconcileDeleteGameServer(t, &agonesv1.GameServer{ 301 ObjectMeta: metav1.ObjectMeta{ 302 Name: "test", 303 Namespace: "default", 304 Annotations: map[string]string{agonesv1.DevAddressAnnotation: ipFixture}, 305 }, 306 Spec: agonesv1.GameServerSpec{ 307 Ports: []agonesv1.GameServerPort{{ContainerPort: 7777, HostPort: 7777, PortPolicy: agonesv1.Static}}, 308 Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{ 309 Containers: []corev1.Container{{Name: "container", Image: "container/image"}}, 310 }, 311 }, 312 }, 313 }) 314 }) 315 } 316 317 func TestControllerWatchGameServers(t *testing.T) { 318 c, m := newFakeController() 319 fixture := agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, Spec: newSingleContainerSpec()} 320 fixture.ApplyDefaults() 321 pod, err := fixture.Pod(agtesting.FakeAPIHooks{}) 322 assert.Nil(t, err) 323 pod.ObjectMeta.Name = pod.ObjectMeta.GenerateName + "-pod" 324 325 gsWatch := watch.NewFake() 326 podWatch := watch.NewFake() 327 m.AgonesClient.AddWatchReactor("gameservers", k8stesting.DefaultWatchReactor(gsWatch, nil)) 328 m.KubeClient.AddWatchReactor("pods", k8stesting.DefaultWatchReactor(podWatch, nil)) 329 m.ExtClient.AddReactor("get", "customresourcedefinitions", func(_ k8stesting.Action) (bool, runtime.Object, error) { 330 return true, agtesting.NewEstablishedCRD(), nil 331 }) 332 333 received := make(chan string) 334 defer close(received) 335 336 h := func(_ context.Context, name string) error { 337 assert.Equal(t, "default/test", name) 338 received <- name 339 return nil 340 } 341 342 c.workerqueue.SyncHandler = h 343 c.creationWorkerQueue.SyncHandler = h 344 c.deletionWorkerQueue.SyncHandler = h 345 346 ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced) 347 defer cancel() 348 349 noStateChange := func(sync cache.InformerSynced) { 350 cache.WaitForCacheSync(ctx.Done(), sync) 351 select { 352 case <-received: 353 assert.Fail(t, "Should not be queued") 354 default: 355 } 356 } 357 358 podSynced := m.KubeInformerFactory.Core().V1().Pods().Informer().HasSynced 359 gsSynced := m.AgonesInformerFactory.Agones().V1().GameServers().Informer().HasSynced 360 361 go func() { 362 err := c.Run(ctx, 1) 363 assert.Nil(t, err, "Run should not error") 364 }() 365 366 logrus.Info("Adding first fixture") 367 gsWatch.Add(&fixture) 368 assert.Equal(t, "default/test", <-received) 369 podWatch.Add(pod) 370 noStateChange(podSynced) 371 372 // no state change 373 gsWatch.Modify(&fixture) 374 noStateChange(gsSynced) 375 376 // add a non game pod 377 nonGamePod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "default"}} 378 podWatch.Add(nonGamePod) 379 noStateChange(podSynced) 380 381 // no state change 382 gsWatch.Modify(&fixture) 383 noStateChange(gsSynced) 384 385 // no state change 386 gsWatch.Modify(&fixture) 387 noStateChange(gsSynced) 388 389 copyFixture := fixture.DeepCopy() 390 copyFixture.Status.State = agonesv1.GameServerStateStarting 391 logrus.Info("modify copyFixture") 392 gsWatch.Modify(copyFixture) 393 assert.Equal(t, "default/test", <-received) 394 395 // modify a gameserver with a deletion timestamp 396 now := metav1.Now() 397 deleted := copyFixture.DeepCopy() 398 deleted.ObjectMeta.DeletionTimestamp = &now 399 gsWatch.Modify(deleted) 400 assert.Equal(t, "default/test", <-received) 401 402 podWatch.Delete(pod) 403 assert.Equal(t, "default/test", <-received) 404 405 // add an unscheduled game pod 406 pod, err = fixture.Pod(agtesting.FakeAPIHooks{}) 407 assert.Nil(t, err) 408 pod.ObjectMeta.Name = pod.ObjectMeta.GenerateName + "-pod2" 409 podWatch.Add(pod) 410 noStateChange(podSynced) 411 412 // schedule it 413 podCopy := pod.DeepCopy() 414 podCopy.Spec.NodeName = nodeFixtureName 415 416 podWatch.Modify(podCopy) 417 assert.Equal(t, "default/test", <-received) 418 } 419 420 func TestControllerCreationMutationHandler(t *testing.T) { 421 t.Parallel() 422 423 type expected struct { 424 responseAllowed bool 425 patches []jsonpatch.JsonPatchOperation 426 nilPatch bool 427 } 428 429 var testCases = []struct { 430 description string 431 fixture interface{} 432 expected expected 433 }{ 434 { 435 description: "OK", 436 fixture: &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 437 Spec: newSingleContainerSpec()}, 438 expected: expected{ 439 responseAllowed: true, 440 patches: []jsonpatch.JsonPatchOperation{ 441 {Operation: "add", Path: "/metadata/finalizers", Value: []interface{}{"agones.dev/controller"}}, 442 {Operation: "add", Path: "/spec/ports/0/protocol", Value: "UDP"}}, 443 }, 444 }, 445 { 446 description: "Wrong request object, err expected", 447 fixture: "WRONG DATA", 448 expected: expected{nilPatch: true}, 449 }, 450 } 451 452 ext := newFakeExtensions() 453 454 for _, tc := range testCases { 455 t.Run(tc.description, func(t *testing.T) { 456 raw, err := json.Marshal(tc.fixture) 457 require.NoError(t, err) 458 459 review := admissionv1.AdmissionReview{ 460 Request: &admissionv1.AdmissionRequest{ 461 Kind: GameServerKind, 462 Operation: admissionv1.Create, 463 Object: runtime.RawExtension{ 464 Raw: raw, 465 }, 466 }, 467 Response: &admissionv1.AdmissionResponse{Allowed: true}, 468 } 469 470 result, err := ext.creationMutationHandler(review) 471 472 assert.NoError(t, err) 473 if tc.expected.nilPatch { 474 require.Nil(t, result.Response.PatchType) 475 } else { 476 assert.True(t, result.Response.Allowed) 477 assert.Equal(t, admissionv1.PatchTypeJSONPatch, *result.Response.PatchType) 478 479 patch := &jsonpatch.ByPath{} 480 err = json.Unmarshal(result.Response.Patch, patch) 481 require.NoError(t, err) 482 483 found := false 484 485 for _, expected := range tc.expected.patches { 486 for _, p := range *patch { 487 if assert.ObjectsAreEqual(p, expected) { 488 found = true 489 } 490 } 491 assert.True(t, found, "Could not find operation %#v in patch %v", expected, *patch) 492 } 493 } 494 }) 495 } 496 } 497 498 func TestControllerCreationValidationHandler(t *testing.T) { 499 t.Parallel() 500 501 ext := newFakeExtensions() 502 503 t.Run("valid gameserver", func(t *testing.T) { 504 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 505 Spec: newSingleContainerSpec()} 506 fixture.ApplyDefaults() 507 508 raw, err := json.Marshal(fixture) 509 require.NoError(t, err) 510 review := admissionv1.AdmissionReview{ 511 Request: &admissionv1.AdmissionRequest{ 512 Kind: GameServerKind, 513 Operation: admissionv1.Create, 514 Object: runtime.RawExtension{ 515 Raw: raw, 516 }, 517 }, 518 Response: &admissionv1.AdmissionResponse{Allowed: true}, 519 } 520 521 result, err := ext.creationValidationHandler(review) 522 require.NoError(t, err) 523 assert.True(t, result.Response.Allowed) 524 }) 525 526 t.Run("invalid gameserver", func(t *testing.T) { 527 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 528 Spec: agonesv1.GameServerSpec{ 529 Container: "NOPE!", 530 Ports: []agonesv1.GameServerPort{{ContainerPort: 7777}}, 531 Template: corev1.PodTemplateSpec{ 532 Spec: corev1.PodSpec{ 533 Containers: []corev1.Container{ 534 {Name: "container", Image: "container/image"}, 535 {Name: "container2", Image: "container/image"}, 536 }, 537 }, 538 }, 539 }, 540 } 541 raw, err := json.Marshal(fixture) 542 require.NoError(t, err) 543 review := admissionv1.AdmissionReview{ 544 Request: &admissionv1.AdmissionRequest{ 545 Kind: GameServerKind, 546 Operation: admissionv1.Create, 547 Object: runtime.RawExtension{ 548 Raw: raw, 549 }, 550 }, 551 Response: &admissionv1.AdmissionResponse{Allowed: true}, 552 } 553 554 result, err := ext.creationValidationHandler(review) 555 require.NoError(t, err) 556 assert.False(t, result.Response.Allowed) 557 assert.Equal(t, metav1.StatusFailure, review.Response.Result.Status) 558 assert.Equal(t, metav1.StatusReasonInvalid, review.Response.Result.Reason) 559 assert.Equal(t, review.Request.Kind.Kind, result.Response.Result.Details.Kind) 560 assert.Equal(t, review.Request.Kind.Group, result.Response.Result.Details.Group) 561 assert.NotEmpty(t, result.Response.Result.Details.Causes) 562 }) 563 564 t.Run("valid request object, error expected", func(t *testing.T) { 565 raw, err := json.Marshal("WRONG DATA") 566 require.NoError(t, err) 567 568 review := admissionv1.AdmissionReview{ 569 Request: &admissionv1.AdmissionRequest{ 570 Kind: GameServerKind, 571 Operation: admissionv1.Create, 572 Object: runtime.RawExtension{ 573 Raw: raw, 574 }, 575 }, 576 Response: &admissionv1.AdmissionResponse{Allowed: true}, 577 } 578 579 _, err = ext.creationValidationHandler(review) 580 if assert.Error(t, err) { 581 assert.Equal(t, `error unmarshalling GameServer json after schema validation: "WRONG DATA": json: cannot unmarshal string into Go value of type v1.GameServer`, err.Error()) 582 } 583 }) 584 } 585 586 func TestControllerCreationMutationHandlerPod(t *testing.T) { 587 t.Parallel() 588 ext := newFakeExtensions() 589 590 type expected struct { 591 patches []jsonpatch.JsonPatchOperation 592 } 593 594 t.Run("valid pod mutation for Passthrough portPolicy, containerPort should be the same as hostPort", func(t *testing.T) { 595 gameServerHostPort0 := float64(newPassthroughPortSingleContainerSpec().Spec.Containers[1].Ports[0].HostPort) 596 gameServerHostPort2 := float64(newPassthroughPortSingleContainerSpec().Spec.Containers[1].Ports[1].HostPort) 597 gameServerHostPort3 := float64(newPassthroughPortSingleContainerSpec().Spec.Containers[2].Ports[0].HostPort) 598 fixture := newPassthroughPortSingleContainerSpec() 599 raw, err := json.Marshal(fixture) 600 require.NoError(t, err) 601 review := admissionv1.AdmissionReview{ 602 Request: &admissionv1.AdmissionRequest{ 603 Kind: metav1.GroupVersionKind(PodKind), 604 Operation: admissionv1.Create, 605 Object: runtime.RawExtension{ 606 Raw: raw, 607 }, 608 }, 609 Response: &admissionv1.AdmissionResponse{Allowed: true}, 610 } 611 expected := expected{ 612 patches: []jsonpatch.JsonPatchOperation{ 613 {Operation: "replace", Path: "/spec/containers/1/ports/0/containerPort", Value: gameServerHostPort0}, 614 {Operation: "replace", Path: "/spec/containers/1/ports/1/containerPort", Value: gameServerHostPort2}, 615 {Operation: "replace", Path: "/spec/containers/2/ports/0/containerPort", Value: gameServerHostPort3}}, 616 } 617 618 result, err := ext.creationMutationHandlerPod(review) 619 assert.NoError(t, err) 620 patch := &jsonpatch.ByPath{} 621 err = json.Unmarshal(result.Response.Patch, patch) 622 found := false 623 624 for _, expected := range expected.patches { 625 for _, p := range *patch { 626 if assert.ObjectsAreEqual(p, expected) { 627 found = true 628 } 629 } 630 assert.True(t, found, "Could not find operation %#v in patch %v", expected, *patch) 631 } 632 633 require.NoError(t, err) 634 635 }) 636 } 637 638 func TestControllerSyncGameServerDeletionTimestamp(t *testing.T) { 639 t.Parallel() 640 641 t.Run("GameServer has a Pod", func(t *testing.T) { 642 c, mocks := newFakeController() 643 now := metav1.Now() 644 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", DeletionTimestamp: &now}, 645 Spec: newSingleContainerSpec()} 646 fixture.ApplyDefaults() 647 pod, err := fixture.Pod(agtesting.FakeAPIHooks{}) 648 assert.Nil(t, err) 649 650 deleted := false 651 mocks.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 652 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 653 }) 654 mocks.KubeClient.AddReactor("delete", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 655 deleted = true 656 da := action.(k8stesting.DeleteAction) 657 assert.Equal(t, pod.ObjectMeta.Name, da.GetName()) 658 return true, nil, nil 659 }) 660 661 ctx, cancel := agtesting.StartInformers(mocks, c.podSynced) 662 defer cancel() 663 664 result, err := c.syncGameServerDeletionTimestamp(ctx, fixture) 665 assert.NoError(t, err) 666 assert.True(t, deleted, "pod should be deleted") 667 assert.Equal(t, fixture, result) 668 agtesting.AssertEventContains(t, mocks.FakeRecorder.Events, fmt.Sprintf("%s %s %s", corev1.EventTypeNormal, 669 fixture.Status.State, "Deleting Pod "+pod.ObjectMeta.Name)) 670 }) 671 672 t.Run("Error on deleting pod", func(t *testing.T) { 673 c, mocks := newFakeController() 674 now := metav1.Now() 675 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", DeletionTimestamp: &now}, 676 Spec: newSingleContainerSpec()} 677 fixture.ApplyDefaults() 678 pod, err := fixture.Pod(agtesting.FakeAPIHooks{}) 679 assert.Nil(t, err) 680 681 mocks.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 682 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 683 }) 684 mocks.KubeClient.AddReactor("delete", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 685 return true, nil, errors.New("Delete-err") 686 }) 687 688 ctx, cancel := agtesting.StartInformers(mocks, c.podSynced) 689 defer cancel() 690 691 _, err = c.syncGameServerDeletionTimestamp(ctx, fixture) 692 if assert.Error(t, err) { 693 assert.Equal(t, `error deleting pod for GameServer. Name: test, Namespace: default: Delete-err`, err.Error()) 694 } 695 }) 696 697 t.Run("GameServer's Pods have been deleted", func(t *testing.T) { 698 c, mocks := newFakeController() 699 now := metav1.Now() 700 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", DeletionTimestamp: &now}, 701 Spec: newSingleContainerSpec()} 702 fixture.ApplyDefaults() 703 704 updated := false 705 mocks.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 706 updated = true 707 708 ua := action.(k8stesting.UpdateAction) 709 gs := ua.GetObject().(*agonesv1.GameServer) 710 assert.Equal(t, fixture.ObjectMeta.Name, gs.ObjectMeta.Name) 711 assert.Empty(t, gs.ObjectMeta.Finalizers) 712 713 return true, gs, nil 714 }) 715 ctx, cancel := agtesting.StartInformers(mocks, c.gameServerSynced) 716 defer cancel() 717 718 result, err := c.syncGameServerDeletionTimestamp(ctx, fixture) 719 assert.Nil(t, err) 720 assert.True(t, updated, "gameserver should be updated, to remove the finaliser") 721 assert.Equal(t, fixture.ObjectMeta.Name, result.ObjectMeta.Name) 722 assert.Empty(t, result.ObjectMeta.Finalizers) 723 }) 724 725 t.Run("Local development GameServer", func(t *testing.T) { 726 c, mocks := newFakeController() 727 now := metav1.Now() 728 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", 729 Annotations: map[string]string{agonesv1.DevAddressAnnotation: "1.1.1.1"}, 730 DeletionTimestamp: &now}, 731 Spec: newSingleContainerSpec()} 732 fixture.ApplyDefaults() 733 734 updated := false 735 mocks.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 736 updated = true 737 738 ua := action.(k8stesting.UpdateAction) 739 gs := ua.GetObject().(*agonesv1.GameServer) 740 assert.Equal(t, fixture.ObjectMeta.Name, gs.ObjectMeta.Name) 741 assert.Empty(t, gs.ObjectMeta.Finalizers) 742 743 return true, gs, nil 744 }) 745 746 ctx, cancel := agtesting.StartInformers(mocks, c.gameServerSynced) 747 defer cancel() 748 749 result, err := c.syncGameServerDeletionTimestamp(ctx, fixture) 750 assert.Nil(t, err) 751 assert.True(t, updated, "gameserver should be updated, to remove the finaliser") 752 assert.Equal(t, fixture.ObjectMeta.Name, result.ObjectMeta.Name) 753 assert.Empty(t, result.ObjectMeta.Finalizers) 754 }) 755 } 756 757 func TestControllerSyncGameServerPortAllocationState(t *testing.T) { 758 t.Parallel() 759 760 t.Run("Gameserver with port allocation state", func(t *testing.T) { 761 t.Parallel() 762 c, mocks := newFakeController() 763 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 764 Spec: agonesv1.GameServerSpec{ 765 Ports: []agonesv1.GameServerPort{{ContainerPort: 7777}}, 766 Template: corev1.PodTemplateSpec{ 767 Spec: corev1.PodSpec{ 768 Containers: []corev1.Container{{Name: "container", Image: "container/image"}}, 769 }, 770 }, 771 }, 772 Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStatePortAllocation}, 773 } 774 fixture.ApplyDefaults() 775 mocks.KubeClient.AddReactor("list", "nodes", func(_ k8stesting.Action) (bool, runtime.Object, error) { 776 return true, &corev1.NodeList{Items: []corev1.Node{{ObjectMeta: metav1.ObjectMeta{Name: nodeFixtureName}}}}, nil 777 }) 778 779 updated := false 780 781 mocks.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 782 updated = true 783 ua := action.(k8stesting.UpdateAction) 784 gs := ua.GetObject().(*agonesv1.GameServer) 785 assert.Equal(t, fixture.ObjectMeta.Name, gs.ObjectMeta.Name) 786 port := gs.Spec.Ports[0] 787 assert.Equal(t, agonesv1.Dynamic, port.PortPolicy) 788 assert.NotEqual(t, fixture.Spec.Ports[0].HostPort, port.HostPort) 789 assert.True(t, 10 <= port.HostPort && port.HostPort <= 20, "%s not in range", port.HostPort) 790 791 return true, gs, nil 792 }) 793 794 ctx, cancel := agtesting.StartInformers(mocks, c.gameServerSynced) 795 defer cancel() 796 err := c.portAllocator.Run(ctx) 797 require.NoError(t, err) 798 799 result, err := c.syncGameServerPortAllocationState(ctx, fixture) 800 require.NoError(t, err, "sync should not error") 801 assert.True(t, updated, "update should occur") 802 port := result.Spec.Ports[0] 803 assert.Equal(t, agonesv1.Dynamic, port.PortPolicy) 804 assert.NotEqual(t, fixture.Spec.Ports[0].HostPort, port.HostPort) 805 assert.True(t, 10 <= port.HostPort && port.HostPort <= 20, "%s not in range", port.HostPort) 806 }) 807 808 t.Run("Error on update", func(t *testing.T) { 809 t.Parallel() 810 c, mocks := newFakeController() 811 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 812 Spec: agonesv1.GameServerSpec{ 813 Ports: []agonesv1.GameServerPort{{ContainerPort: 7777}}, 814 Template: corev1.PodTemplateSpec{ 815 Spec: corev1.PodSpec{ 816 Containers: []corev1.Container{{Name: "container", Image: "container/image"}}, 817 }, 818 }, 819 }, 820 Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStatePortAllocation}, 821 } 822 fixture.ApplyDefaults() 823 mocks.KubeClient.AddReactor("list", "nodes", func(_ k8stesting.Action) (bool, runtime.Object, error) { 824 return true, &corev1.NodeList{Items: []corev1.Node{{ObjectMeta: metav1.ObjectMeta{Name: nodeFixtureName}}}}, nil 825 }) 826 827 mocks.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 828 ua := action.(k8stesting.UpdateAction) 829 gs := ua.GetObject().(*agonesv1.GameServer) 830 return true, gs, errors.New("update-err") 831 }) 832 833 ctx, cancel := agtesting.StartInformers(mocks, c.gameServerSynced) 834 defer cancel() 835 err := c.portAllocator.Run(ctx) 836 require.NoError(t, err) 837 838 _, err = c.syncGameServerPortAllocationState(ctx, fixture) 839 if assert.Error(t, err) { 840 assert.Equal(t, `error updating GameServer test to default values: update-err`, err.Error()) 841 } 842 }) 843 844 t.Run("Gameserver with unknown state", func(t *testing.T) { 845 testNoChange(t, "Unknown", func(c *Controller, fixture *agonesv1.GameServer) (*agonesv1.GameServer, error) { 846 return c.syncGameServerPortAllocationState(context.Background(), fixture) 847 }) 848 }) 849 850 t.Run("GameServer with non zero deletion datetime", func(t *testing.T) { 851 testWithNonZeroDeletionTimestamp(t, func(c *Controller, fixture *agonesv1.GameServer) (*agonesv1.GameServer, error) { 852 return c.syncGameServerPortAllocationState(context.Background(), fixture) 853 }) 854 }) 855 } 856 857 func TestControllerSyncGameServerCreatingState(t *testing.T) { 858 t.Parallel() 859 860 newFixture := func() *agonesv1.GameServer { 861 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 862 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateCreating}} 863 fixture.ApplyDefaults() 864 return fixture 865 } 866 867 t.Run("Testing TCPUDP protocol of static portpolicy", func(t *testing.T) { 868 c, m := newFakeController() 869 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 870 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateCreating}} 871 fixture.Spec.Ports[0].Name = "default" 872 fixture.Spec.Ports[0].HostPort = 7000 873 fixture.Spec.Ports[0].Protocol = agonesv1.ProtocolTCPUDP 874 fixture.ApplyDefaults() 875 podCreated := false 876 gsUpdated := false 877 878 var pod *corev1.Pod 879 m.KubeClient.AddReactor("create", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 880 podCreated = true 881 ca := action.(k8stesting.CreateAction) 882 pod = ca.GetObject().(*corev1.Pod) 883 assert.True(t, metav1.IsControlledBy(pod, fixture)) 884 return true, pod, nil 885 }) 886 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 887 gsUpdated = true 888 ua := action.(k8stesting.UpdateAction) 889 gs := ua.GetObject().(*agonesv1.GameServer) 890 assert.Equal(t, agonesv1.GameServerStateStarting, gs.Status.State) 891 assert.Len(t, gs.Spec.Ports, 2) 892 assert.Equal(t, "default-tcp", gs.Spec.Ports[0].Name) 893 assert.Equal(t, corev1.ProtocolTCP, gs.Spec.Ports[0].Protocol) 894 assert.Equal(t, "default-udp", gs.Spec.Ports[1].Name) 895 assert.Equal(t, corev1.ProtocolUDP, gs.Spec.Ports[1].Protocol) 896 return true, gs, nil 897 }) 898 899 ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced, c.podSynced) 900 defer cancel() 901 902 gs, err := c.syncGameServerCreatingState(ctx, fixture) 903 904 assert.NoError(t, err) 905 assert.True(t, podCreated, "Pod should have been created") 906 907 assert.Equal(t, agonesv1.GameServerStateStarting, gs.Status.State) 908 assert.True(t, gsUpdated, "GameServer should have been updated") 909 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Pod") 910 }) 911 912 t.Run("Testing TCP protocol of static portpolicy", func(t *testing.T) { 913 c, m := newFakeController() 914 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 915 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateCreating}} 916 fixture.Spec.Ports[0].Name = "tcp-port" 917 fixture.Spec.Ports[0].HostPort = 7000 918 fixture.Spec.Ports[0].Protocol = corev1.ProtocolTCP 919 fixture.ApplyDefaults() 920 podCreated := false 921 gsUpdated := false 922 923 var pod *corev1.Pod 924 m.KubeClient.AddReactor("create", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 925 podCreated = true 926 ca := action.(k8stesting.CreateAction) 927 pod = ca.GetObject().(*corev1.Pod) 928 assert.True(t, metav1.IsControlledBy(pod, fixture)) 929 return true, pod, nil 930 }) 931 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 932 gsUpdated = true 933 ua := action.(k8stesting.UpdateAction) 934 gs := ua.GetObject().(*agonesv1.GameServer) 935 assert.Equal(t, agonesv1.GameServerStateStarting, gs.Status.State) 936 assert.Len(t, gs.Spec.Ports, 1) 937 assert.Equal(t, "tcp-port", gs.Spec.Ports[0].Name) 938 assert.Equal(t, corev1.ProtocolTCP, gs.Spec.Ports[0].Protocol) 939 return true, gs, nil 940 }) 941 942 ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced, c.podSynced) 943 defer cancel() 944 945 gs, err := c.syncGameServerCreatingState(ctx, fixture) 946 947 assert.NoError(t, err) 948 assert.True(t, podCreated, "Pod should have been created") 949 950 assert.Equal(t, agonesv1.GameServerStateStarting, gs.Status.State) 951 assert.True(t, gsUpdated, "GameServer should have been updated") 952 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Pod") 953 }) 954 955 t.Run("Testing default protocol of static portpolicy", func(t *testing.T) { 956 c, m := newFakeController() 957 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 958 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateCreating}} 959 fixture.Spec.Ports[0].Name = "udp-port" 960 fixture.Spec.Ports[0].HostPort = 7000 961 fixture.ApplyDefaults() 962 podCreated := false 963 gsUpdated := false 964 965 var pod *corev1.Pod 966 m.KubeClient.AddReactor("create", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 967 podCreated = true 968 ca := action.(k8stesting.CreateAction) 969 pod = ca.GetObject().(*corev1.Pod) 970 assert.True(t, metav1.IsControlledBy(pod, fixture)) 971 return true, pod, nil 972 }) 973 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 974 gsUpdated = true 975 ua := action.(k8stesting.UpdateAction) 976 gs := ua.GetObject().(*agonesv1.GameServer) 977 assert.Equal(t, agonesv1.GameServerStateStarting, gs.Status.State) 978 assert.Len(t, gs.Spec.Ports, 1) 979 assert.Equal(t, "udp-port", gs.Spec.Ports[0].Name) 980 assert.Equal(t, corev1.ProtocolUDP, gs.Spec.Ports[0].Protocol) 981 return true, gs, nil 982 }) 983 984 ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced, c.podSynced) 985 defer cancel() 986 987 gs, err := c.syncGameServerCreatingState(ctx, fixture) 988 989 assert.NoError(t, err) 990 assert.True(t, podCreated, "Pod should have been created") 991 992 assert.Equal(t, agonesv1.GameServerStateStarting, gs.Status.State) 993 assert.True(t, gsUpdated, "GameServer should have been updated") 994 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Pod") 995 }) 996 997 t.Run("Syncing from Created State, with no issues", func(t *testing.T) { 998 c, m := newFakeController() 999 fixture := newFixture() 1000 podCreated := false 1001 gsUpdated := false 1002 1003 var pod *corev1.Pod 1004 m.KubeClient.AddReactor("create", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 1005 podCreated = true 1006 ca := action.(k8stesting.CreateAction) 1007 pod = ca.GetObject().(*corev1.Pod) 1008 assert.True(t, metav1.IsControlledBy(pod, fixture)) 1009 return true, pod, nil 1010 }) 1011 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1012 gsUpdated = true 1013 ua := action.(k8stesting.UpdateAction) 1014 gs := ua.GetObject().(*agonesv1.GameServer) 1015 assert.Equal(t, agonesv1.GameServerStateStarting, gs.Status.State) 1016 return true, gs, nil 1017 }) 1018 1019 ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced, c.podSynced) 1020 defer cancel() 1021 1022 gs, err := c.syncGameServerCreatingState(ctx, fixture) 1023 1024 assert.NoError(t, err) 1025 assert.True(t, podCreated, "Pod should have been created") 1026 1027 assert.Equal(t, agonesv1.GameServerStateStarting, gs.Status.State) 1028 assert.True(t, gsUpdated, "GameServer should have been updated") 1029 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Pod") 1030 }) 1031 1032 t.Run("Error on updating gs", func(t *testing.T) { 1033 c, m := newFakeController() 1034 fixture := newFixture() 1035 podCreated := false 1036 1037 var pod *corev1.Pod 1038 m.KubeClient.AddReactor("create", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 1039 podCreated = true 1040 ca := action.(k8stesting.CreateAction) 1041 pod = ca.GetObject().(*corev1.Pod) 1042 assert.True(t, metav1.IsControlledBy(pod, fixture)) 1043 return true, pod, nil 1044 }) 1045 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1046 ua := action.(k8stesting.UpdateAction) 1047 gs := ua.GetObject().(*agonesv1.GameServer) 1048 assert.Equal(t, agonesv1.GameServerStateStarting, gs.Status.State) 1049 return true, gs, errors.New("update-err") 1050 }) 1051 1052 ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced, c.podSynced) 1053 defer cancel() 1054 1055 _, err := c.syncGameServerCreatingState(ctx, fixture) 1056 require.True(t, podCreated, "Pod should have been created") 1057 1058 if assert.Error(t, err) { 1059 assert.Equal(t, `error updating GameServer test to Starting state: update-err`, err.Error()) 1060 } 1061 }) 1062 1063 t.Run("Previously started sync, created Pod, but didn't move to Starting", func(t *testing.T) { 1064 c, m := newFakeController() 1065 fixture := newFixture() 1066 podCreated := false 1067 gsUpdated := false 1068 pod, err := fixture.Pod(agtesting.FakeAPIHooks{}) 1069 assert.Nil(t, err) 1070 1071 m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1072 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 1073 }) 1074 m.KubeClient.AddReactor("create", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1075 podCreated = true 1076 return true, nil, nil 1077 }) 1078 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1079 gsUpdated = true 1080 ua := action.(k8stesting.UpdateAction) 1081 gs := ua.GetObject().(*agonesv1.GameServer) 1082 assert.Equal(t, agonesv1.GameServerStateStarting, gs.Status.State) 1083 return true, gs, nil 1084 }) 1085 1086 ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced, c.podSynced) 1087 defer cancel() 1088 1089 gs, err := c.syncGameServerCreatingState(ctx, fixture) 1090 assert.Nil(t, err) 1091 assert.Equal(t, agonesv1.GameServerStateStarting, gs.Status.State) 1092 assert.False(t, podCreated, "Pod should not have been created") 1093 assert.True(t, gsUpdated, "GameServer should have been updated") 1094 }) 1095 1096 t.Run("creates an invalid podspec", func(t *testing.T) { 1097 c, mocks := newFakeController() 1098 fixture := newFixture() 1099 podCreated := false 1100 gsUpdated := false 1101 1102 mocks.KubeClient.AddReactor("create", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1103 podCreated = true 1104 return true, nil, k8serrors.NewInvalid(schema.GroupKind{}, "test", field.ErrorList{}) 1105 }) 1106 mocks.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1107 gsUpdated = true 1108 ua := action.(k8stesting.UpdateAction) 1109 gs := ua.GetObject().(*agonesv1.GameServer) 1110 assert.Equal(t, agonesv1.GameServerStateError, gs.Status.State) 1111 return true, gs, nil 1112 }) 1113 1114 ctx, cancel := agtesting.StartInformers(mocks, c.gameServerSynced, c.podSynced) 1115 defer cancel() 1116 1117 gs, err := c.syncGameServerCreatingState(ctx, fixture) 1118 assert.Nil(t, err) 1119 1120 assert.True(t, podCreated, "attempt should have been made to create a pod") 1121 assert.True(t, gsUpdated, "GameServer should be updated") 1122 assert.Equal(t, agonesv1.GameServerStateError, gs.Status.State) 1123 }) 1124 1125 t.Run("GameServer with unknown state", func(t *testing.T) { 1126 testNoChange(t, "Unknown", func(c *Controller, fixture *agonesv1.GameServer) (*agonesv1.GameServer, error) { 1127 return c.syncGameServerCreatingState(context.Background(), fixture) 1128 }) 1129 }) 1130 1131 t.Run("GameServer with non zero deletion datetime", func(t *testing.T) { 1132 testWithNonZeroDeletionTimestamp(t, func(c *Controller, fixture *agonesv1.GameServer) (*agonesv1.GameServer, error) { 1133 return c.syncGameServerCreatingState(context.Background(), fixture) 1134 }) 1135 }) 1136 } 1137 1138 func TestControllerSyncGameServerStartingState(t *testing.T) { 1139 t.Parallel() 1140 1141 newFixture := func() *agonesv1.GameServer { 1142 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1143 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateStarting}} 1144 fixture.ApplyDefaults() 1145 return fixture 1146 } 1147 1148 node := corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeFixtureName}, Status: corev1.NodeStatus{Addresses: []corev1.NodeAddress{{Address: ipFixture, Type: corev1.NodeExternalIP}}}} 1149 1150 t.Run("sync from Stating state, with no issues", func(t *testing.T) { 1151 c, m := newFakeController() 1152 gsFixture := newFixture() 1153 gsFixture.ApplyDefaults() 1154 pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) 1155 assert.Nil(t, err) 1156 pod.Spec.NodeName = nodeFixtureName 1157 pod.Status.PodIPs = []corev1.PodIP{{IP: ipv6Fixture}} 1158 gsUpdated := false 1159 1160 m.KubeClient.AddReactor("list", "nodes", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1161 return true, &corev1.NodeList{Items: []corev1.Node{node}}, nil 1162 }) 1163 m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1164 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 1165 }) 1166 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1167 gsUpdated = true 1168 ua := action.(k8stesting.UpdateAction) 1169 gs := ua.GetObject().(*agonesv1.GameServer) 1170 assert.Equal(t, agonesv1.GameServerStateScheduled, gs.Status.State) 1171 return true, gs, nil 1172 }) 1173 1174 ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced, c.podSynced, c.nodeSynced) 1175 defer cancel() 1176 1177 gs, err := c.syncGameServerStartingState(ctx, gsFixture) 1178 require.NoError(t, err) 1179 1180 assert.True(t, gsUpdated) 1181 assert.Equal(t, gs.Status.NodeName, node.ObjectMeta.Name) 1182 assert.Equal(t, gs.Status.Address, ipFixture) 1183 assert.Equal(t, []corev1.NodeAddress{ 1184 {Address: ipFixture, Type: "ExternalIP"}, 1185 {Address: ipv6Fixture, Type: "PodIP"}, 1186 }, gs.Status.Addresses) 1187 1188 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Address and port populated") 1189 assert.NotEmpty(t, gs.Status.Ports) 1190 }) 1191 1192 t.Run("Error on podIPs not populated", func(t *testing.T) { 1193 c, m := newFakeController() 1194 gsFixture := newFixture() 1195 gsFixture.ApplyDefaults() 1196 pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) 1197 require.NoError(t, err) 1198 pod.Spec.NodeName = nodeFixtureName 1199 1200 m.KubeClient.AddReactor("list", "nodes", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1201 return true, &corev1.NodeList{Items: []corev1.Node{node}}, nil 1202 }) 1203 m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1204 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 1205 }) 1206 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1207 ua := action.(k8stesting.UpdateAction) 1208 gs := ua.GetObject().(*agonesv1.GameServer) 1209 assert.Equal(t, agonesv1.GameServerStateScheduled, gs.Status.State) 1210 return true, gs, errors.New("update-err") 1211 }) 1212 ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced, c.podSynced, c.nodeSynced) 1213 defer cancel() 1214 1215 _, err = c.syncGameServerStartingState(ctx, gsFixture) 1216 assert.Error(t, err) 1217 }) 1218 1219 t.Run("Error on update", func(t *testing.T) { 1220 c, m := newFakeController() 1221 gsFixture := newFixture() 1222 gsFixture.ApplyDefaults() 1223 pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) 1224 require.NoError(t, err) 1225 pod.Spec.NodeName = nodeFixtureName 1226 pod.Status.PodIPs = []corev1.PodIP{{IP: ipv6Fixture}} 1227 1228 m.KubeClient.AddReactor("list", "nodes", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1229 return true, &corev1.NodeList{Items: []corev1.Node{node}}, nil 1230 }) 1231 m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1232 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 1233 }) 1234 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1235 ua := action.(k8stesting.UpdateAction) 1236 gs := ua.GetObject().(*agonesv1.GameServer) 1237 assert.Equal(t, agonesv1.GameServerStateScheduled, gs.Status.State) 1238 return true, gs, errors.New("update-err") 1239 }) 1240 ctx, cancel := agtesting.StartInformers(m, c.gameServerSynced, c.podSynced, c.nodeSynced) 1241 defer cancel() 1242 1243 _, err = c.syncGameServerStartingState(ctx, gsFixture) 1244 if assert.Error(t, err) { 1245 assert.Equal(t, `error updating GameServer test to Scheduled state: update-err`, err.Error()) 1246 } 1247 }) 1248 1249 t.Run("GameServer with unknown state", func(t *testing.T) { 1250 testNoChange(t, "Unknown", func(c *Controller, fixture *agonesv1.GameServer) (*agonesv1.GameServer, error) { 1251 return c.syncGameServerStartingState(context.Background(), fixture) 1252 }) 1253 }) 1254 1255 t.Run("GameServer with non zero deletion datetime", func(t *testing.T) { 1256 testWithNonZeroDeletionTimestamp(t, func(c *Controller, fixture *agonesv1.GameServer) (*agonesv1.GameServer, error) { 1257 return c.syncGameServerStartingState(context.Background(), fixture) 1258 }) 1259 }) 1260 } 1261 1262 func TestControllerCreateGameServerPod(t *testing.T) { 1263 t.Parallel() 1264 1265 // TODO: remove mutex when "SidecarContainers" moves to stable. 1266 agruntime.FeatureTestMutex.Lock() 1267 defer agruntime.FeatureTestMutex.Unlock() 1268 1269 newFixture := func() *agonesv1.GameServer { 1270 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1271 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateCreating}} 1272 fixture.ApplyDefaults() 1273 return fixture 1274 } 1275 1276 t.Run("create pod, with no issues", func(t *testing.T) { 1277 c, m := newFakeController() 1278 fixture := newFixture() 1279 created := false 1280 1281 m.KubeClient.AddReactor("create", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 1282 created = true 1283 ca := action.(k8stesting.CreateAction) 1284 pod := ca.GetObject().(*corev1.Pod) 1285 1286 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name) 1287 assert.Equal(t, fixture.ObjectMeta.Namespace, pod.ObjectMeta.Namespace) 1288 assert.Equal(t, "sdk-service-account", pod.Spec.ServiceAccountName) 1289 assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"]) 1290 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Labels[agonesv1.GameServerPodLabel]) 1291 assert.True(t, metav1.IsControlledBy(pod, fixture)) 1292 1293 // gke.MutateGameServerPod assumes that a non-empty NodeSelector / Tolerations are user 1294 // intent. The generic cloudproduct that we use in unit tests does not manipulate these. 1295 // So we verify using the generic cloudproduct that NodeSelector/Tolerations are empty 1296 // as a change detector - if this test fails, gke.MutateGameServerPod will not work. 1297 assert.Empty(t, pod.Spec.NodeSelector) 1298 assert.Empty(t, pod.Spec.Tolerations) 1299 1300 // if sidecar feature enabled, should be 1 container and 1 initContainer, otherwise 2 containers 1301 var sidecarContainer corev1.Container 1302 var gsContainer corev1.Container 1303 if agruntime.FeatureEnabled(agruntime.FeatureSidecarContainers) { 1304 assert.Len(t, pod.Spec.Containers, 1, "Should have 1 container") 1305 assert.Len(t, pod.Spec.InitContainers, 1, "Should have init container") 1306 gsContainer = pod.Spec.Containers[0] 1307 sidecarContainer = pod.Spec.InitContainers[0] 1308 } else { 1309 assert.Len(t, pod.Spec.Containers, 2, "Should have a sidecar container") 1310 sidecarContainer = pod.Spec.Containers[0] 1311 gsContainer = pod.Spec.Containers[1] 1312 } 1313 1314 assert.Equal(t, sidecarContainer.Image, c.sidecarImage) 1315 assert.Equal(t, sidecarContainer.Resources.Limits.Cpu(), &c.sidecarCPULimit) 1316 assert.Equal(t, sidecarContainer.Resources.Requests.Cpu(), &c.sidecarCPURequest) 1317 assert.Equal(t, sidecarContainer.Resources.Limits.Memory(), &c.sidecarMemoryLimit) 1318 assert.Equal(t, sidecarContainer.Resources.Requests.Memory(), &c.sidecarMemoryRequest) 1319 assert.Len(t, sidecarContainer.Env, 5, "5 env vars") 1320 assert.Equal(t, "GAMESERVER_NAME", sidecarContainer.Env[0].Name) 1321 assert.Equal(t, fixture.ObjectMeta.Name, sidecarContainer.Env[0].Value) 1322 assert.Equal(t, "POD_NAMESPACE", sidecarContainer.Env[1].Name) 1323 assert.Equal(t, "FEATURE_GATES", sidecarContainer.Env[2].Name) 1324 assert.Equal(t, "LOG_LEVEL", sidecarContainer.Env[3].Name) 1325 assert.Equal(t, "REQUESTS_RATE_LIMIT", sidecarContainer.Env[4].Name) 1326 assert.Equal(t, "500ms", sidecarContainer.Env[4].Value) 1327 assert.Equal(t, string(fixture.Spec.SdkServer.LogLevel), sidecarContainer.Env[3].Value) 1328 assert.Equal(t, *sidecarContainer.SecurityContext.AllowPrivilegeEscalation, false) 1329 assert.Equal(t, *sidecarContainer.SecurityContext.RunAsNonRoot, true) 1330 assert.Equal(t, *sidecarContainer.SecurityContext.RunAsUser, int64(sidecarRunAsUser)) 1331 1332 assert.Equal(t, fixture.Spec.Ports[0].HostPort, gsContainer.Ports[0].HostPort) 1333 assert.Equal(t, fixture.Spec.Ports[0].ContainerPort, gsContainer.Ports[0].ContainerPort) 1334 assert.Equal(t, corev1.Protocol("UDP"), gsContainer.Ports[0].Protocol) 1335 assert.Equal(t, "/gshealthz", gsContainer.LivenessProbe.HTTPGet.Path) 1336 assert.Equal(t, gsContainer.LivenessProbe.HTTPGet.Port, intstr.FromInt(8080)) 1337 assert.Equal(t, intstr.FromInt(8080), gsContainer.LivenessProbe.HTTPGet.Port) 1338 assert.Equal(t, fixture.Spec.Health.InitialDelaySeconds, gsContainer.LivenessProbe.InitialDelaySeconds) 1339 assert.Equal(t, fixture.Spec.Health.PeriodSeconds, gsContainer.LivenessProbe.PeriodSeconds) 1340 assert.Equal(t, fixture.Spec.Health.FailureThreshold, gsContainer.LivenessProbe.FailureThreshold) 1341 assert.Len(t, gsContainer.VolumeMounts, 1) 1342 assert.Equal(t, "/var/run/secrets/kubernetes.io/serviceaccount", gsContainer.VolumeMounts[0].MountPath) 1343 1344 return true, pod, nil 1345 }) 1346 1347 gs, err := c.createGameServerPod(context.Background(), fixture) 1348 require.NoError(t, err) 1349 assert.Equal(t, fixture.Status.State, gs.Status.State) 1350 assert.True(t, created) 1351 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Pod") 1352 }) 1353 1354 t.Run("service account", func(t *testing.T) { 1355 c, m := newFakeController() 1356 fixture := newFixture() 1357 fixture.Spec.Template.Spec.ServiceAccountName = "foobar" 1358 1359 created := false 1360 1361 m.KubeClient.AddReactor("create", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 1362 created = true 1363 ca := action.(k8stesting.CreateAction) 1364 pod := ca.GetObject().(*corev1.Pod) 1365 if agruntime.FeatureEnabled(agruntime.FeatureSidecarContainers) { 1366 assert.Len(t, pod.Spec.InitContainers, 1, "Should have init container") 1367 } else { 1368 assert.Len(t, pod.Spec.Containers, 2, "Should have a sidecar container") 1369 } 1370 assert.Empty(t, pod.Spec.Containers[0].VolumeMounts) 1371 1372 return true, pod, nil 1373 }) 1374 1375 _, err := c.createGameServerPod(context.Background(), fixture) 1376 assert.Nil(t, err) 1377 assert.True(t, created) 1378 }) 1379 1380 t.Run("invalid podspec", func(t *testing.T) { 1381 c, mocks := newFakeController() 1382 fixture := newFixture() 1383 podCreated := false 1384 gsUpdated := false 1385 1386 mocks.KubeClient.AddReactor("create", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1387 podCreated = true 1388 return true, nil, k8serrors.NewInvalid(schema.GroupKind{}, "test", field.ErrorList{}) 1389 }) 1390 mocks.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1391 gsUpdated = true 1392 ua := action.(k8stesting.UpdateAction) 1393 gs := ua.GetObject().(*agonesv1.GameServer) 1394 assert.Equal(t, agonesv1.GameServerStateError, gs.Status.State) 1395 return true, gs, nil 1396 }) 1397 1398 gs, err := c.createGameServerPod(context.Background(), fixture) 1399 require.NoError(t, err) 1400 1401 assert.True(t, podCreated, "attempt should have been made to create a pod") 1402 assert.True(t, gsUpdated, "GameServer should be updated") 1403 if assert.NotEmpty(t, gs.Annotations[agonesv1.GameServerErroredAtAnnotation]) { 1404 gotTime, err := time.Parse(time.RFC3339, gs.Annotations[agonesv1.GameServerErroredAtAnnotation]) 1405 require.NoError(t, err) 1406 assert.WithinDuration(t, time.Now(), gotTime, time.Second) 1407 } 1408 assert.Equal(t, agonesv1.GameServerStateError, gs.Status.State) 1409 }) 1410 1411 t.Run("forbidden pods creation", func(t *testing.T) { 1412 c, mocks := newFakeController() 1413 fixture := newFixture() 1414 podCreated := false 1415 gsUpdated := false 1416 1417 mocks.KubeClient.AddReactor("create", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1418 podCreated = true 1419 return true, nil, k8serrors.NewForbidden(schema.GroupResource{}, "test", errors.New("test")) 1420 }) 1421 mocks.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1422 gsUpdated = true 1423 ua := action.(k8stesting.UpdateAction) 1424 gs := ua.GetObject().(*agonesv1.GameServer) 1425 assert.Equal(t, agonesv1.GameServerStateError, gs.Status.State) 1426 return true, gs, nil 1427 }) 1428 1429 gs, err := c.createGameServerPod(context.Background(), fixture) 1430 require.NoError(t, err) 1431 1432 assert.True(t, podCreated, "attempt should have been made to create a pod") 1433 assert.True(t, gsUpdated, "GameServer should be updated") 1434 if assert.NotEmpty(t, gs.Annotations[agonesv1.GameServerErroredAtAnnotation]) { 1435 gotTime, err := time.Parse(time.RFC3339, gs.Annotations[agonesv1.GameServerErroredAtAnnotation]) 1436 require.NoError(t, err) 1437 assert.WithinDuration(t, time.Now(), gotTime, time.Second) 1438 } 1439 assert.Equal(t, agonesv1.GameServerStateError, gs.Status.State) 1440 }) 1441 } 1442 1443 func TestControllerSyncGameServerRequestReadyState(t *testing.T) { 1444 t.Parallel() 1445 nodeName := "node" 1446 containerID := "1234" 1447 1448 agruntime.FeatureTestMutex.Lock() 1449 defer agruntime.FeatureTestMutex.Unlock() 1450 1451 t.Run("GameServer with ReadyRequest State", func(t *testing.T) { 1452 c, m := newFakeController() 1453 1454 gsFixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1455 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateRequestReady}} 1456 gsFixture.ApplyDefaults() 1457 gsFixture.Status.NodeName = nodeName 1458 pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) 1459 require.NoError(t, err) 1460 pod.Status.ContainerStatuses = []corev1.ContainerStatus{ 1461 { 1462 Name: gsFixture.Spec.Container, 1463 State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}}, 1464 ContainerID: containerID, 1465 }, 1466 } 1467 1468 gsUpdated := false 1469 podUpdated := false 1470 1471 m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1472 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 1473 }) 1474 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1475 gsUpdated = true 1476 ua := action.(k8stesting.UpdateAction) 1477 gs := ua.GetObject().(*agonesv1.GameServer) 1478 assert.Equal(t, agonesv1.GameServerStateReady, gs.Status.State) 1479 1480 // only set if not using sidecars. 1481 if !agruntime.FeatureEnabled(agruntime.FeatureSidecarContainers) { 1482 assert.Equal(t, containerID, gs.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]) 1483 } 1484 return true, gs, nil 1485 }) 1486 m.KubeClient.AddReactor("update", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 1487 podUpdated = true 1488 ua := action.(k8stesting.UpdateAction) 1489 pod := ua.GetObject().(*corev1.Pod) 1490 assert.Equal(t, containerID, pod.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]) 1491 return true, pod, nil 1492 }) 1493 1494 ctx, cancel := agtesting.StartInformers(m, c.podSynced) 1495 defer cancel() 1496 1497 gs, err := c.syncGameServerRequestReadyState(ctx, gsFixture) 1498 assert.NoError(t, err, "should not error") 1499 assert.True(t, gsUpdated, "GameServer wasn't updated") 1500 if agruntime.FeatureEnabled(agruntime.FeatureSidecarContainers) { 1501 assert.False(t, podUpdated, "Pod was updated") 1502 } else { 1503 assert.True(t, podUpdated, "Pod was not updated") 1504 } 1505 assert.Equal(t, agonesv1.GameServerStateReady, gs.Status.State) 1506 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "SDK.Ready() complete") 1507 }) 1508 1509 t.Run("Error on GameServer update", func(t *testing.T) { 1510 require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=false")) 1511 1512 c, m := newFakeController() 1513 1514 gsFixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1515 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateRequestReady}} 1516 gsFixture.ApplyDefaults() 1517 gsFixture.Status.NodeName = nodeName 1518 pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) 1519 require.NoError(t, err) 1520 pod.Status.ContainerStatuses = []corev1.ContainerStatus{ 1521 { 1522 Name: gsFixture.Spec.Container, 1523 State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}}, 1524 ContainerID: containerID, 1525 }, 1526 } 1527 podUpdated := false 1528 1529 m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1530 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 1531 }) 1532 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1533 ua := action.(k8stesting.UpdateAction) 1534 gs := ua.GetObject().(*agonesv1.GameServer) 1535 assert.Equal(t, agonesv1.GameServerStateReady, gs.Status.State) 1536 assert.Equal(t, containerID, gs.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]) 1537 return true, gs, errors.New("update-err") 1538 }) 1539 m.KubeClient.AddReactor("update", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 1540 podUpdated = true 1541 ua := action.(k8stesting.UpdateAction) 1542 pod := ua.GetObject().(*corev1.Pod) 1543 assert.Equal(t, containerID, pod.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]) 1544 return true, pod, nil 1545 }) 1546 1547 ctx, cancel := agtesting.StartInformers(m, c.podSynced) 1548 defer cancel() 1549 1550 _, err = c.syncGameServerRequestReadyState(ctx, gsFixture) 1551 assert.True(t, podUpdated, "pod was not updated") 1552 require.EqualError(t, err, "error setting Ready, Port and address on GameServer test Status: update-err") 1553 }) 1554 1555 t.Run("Error on pod update", func(t *testing.T) { 1556 c, m := newFakeController() 1557 1558 gsFixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1559 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateRequestReady}} 1560 gsFixture.ApplyDefaults() 1561 gsFixture.Status.NodeName = nodeName 1562 pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) 1563 require.NoError(t, err) 1564 pod.Status.ContainerStatuses = []corev1.ContainerStatus{ 1565 { 1566 Name: gsFixture.Spec.Container, 1567 State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}}, 1568 ContainerID: containerID, 1569 }, 1570 } 1571 gsUpdated := false 1572 podUpdated := false 1573 1574 m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1575 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 1576 }) 1577 m.AgonesClient.AddReactor("update", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1578 gsUpdated = true 1579 return true, nil, nil 1580 }) 1581 m.KubeClient.AddReactor("update", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 1582 podUpdated = true 1583 ua := action.(k8stesting.UpdateAction) 1584 pod := ua.GetObject().(*corev1.Pod) 1585 assert.Equal(t, containerID, pod.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]) 1586 return true, pod, errors.New("pod-error") 1587 }) 1588 1589 ctx, cancel := agtesting.StartInformers(m, c.podSynced) 1590 defer cancel() 1591 1592 _, err = c.syncGameServerRequestReadyState(ctx, gsFixture) 1593 assert.True(t, podUpdated, "pod was not updated") 1594 assert.False(t, gsUpdated, "GameServer was updated") 1595 require.EqualError(t, err, "error updating ready annotation on Pod: test: pod-error") 1596 }) 1597 1598 t.Run("Pod annotation already set", func(t *testing.T) { 1599 c, m := newFakeController() 1600 1601 gsFixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1602 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateRequestReady}} 1603 gsFixture.ApplyDefaults() 1604 gsFixture.Status.NodeName = nodeName 1605 pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) 1606 require.NoError(t, err) 1607 pod.ObjectMeta.Annotations = map[string]string{agonesv1.GameServerReadyContainerIDAnnotation: containerID} 1608 pod.Status.ContainerStatuses = []corev1.ContainerStatus{ 1609 { 1610 Name: gsFixture.Spec.Container, 1611 State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}}, 1612 ContainerID: containerID, 1613 }, 1614 } 1615 gsUpdated := false 1616 podUpdated := false 1617 1618 m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1619 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 1620 }) 1621 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1622 gsUpdated = true 1623 ua := action.(k8stesting.UpdateAction) 1624 gs := ua.GetObject().(*agonesv1.GameServer) 1625 assert.Equal(t, agonesv1.GameServerStateReady, gs.Status.State) 1626 assert.Equal(t, containerID, gs.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]) 1627 return true, gs, nil 1628 }) 1629 m.KubeClient.AddReactor("update", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1630 podUpdated = true 1631 return true, nil, nil 1632 }) 1633 1634 ctx, cancel := agtesting.StartInformers(m, c.podSynced) 1635 defer cancel() 1636 1637 gs, err := c.syncGameServerRequestReadyState(ctx, gsFixture) 1638 assert.NoError(t, err, "should not error") 1639 assert.True(t, gsUpdated, "GameServer wasn't updated") 1640 assert.False(t, podUpdated, "Pod was updated") 1641 assert.Equal(t, agonesv1.GameServerStateReady, gs.Status.State) 1642 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "SDK.Ready() complete") 1643 1644 }) 1645 1646 t.Run("GameServer without an Address, but RequestReady State", func(t *testing.T) { 1647 c, m := newFakeController() 1648 1649 gsFixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1650 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateRequestReady}} 1651 gsFixture.ApplyDefaults() 1652 pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) 1653 assert.Nil(t, err) 1654 pod.Spec.NodeName = nodeFixtureName 1655 pod.Status.ContainerStatuses = []corev1.ContainerStatus{ 1656 { 1657 Name: gsFixture.Spec.Container, 1658 State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}}, 1659 ContainerID: containerID, 1660 }, 1661 } 1662 gsUpdated := false 1663 podUpdated := false 1664 1665 ipFixture := "12.12.12.12" 1666 nodeFixture := corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: nodeFixtureName}, Status: corev1.NodeStatus{Addresses: []corev1.NodeAddress{{Address: ipFixture, Type: corev1.NodeExternalIP}}}} 1667 1668 m.KubeClient.AddReactor("list", "nodes", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1669 return true, &corev1.NodeList{Items: []corev1.Node{nodeFixture}}, nil 1670 }) 1671 1672 m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1673 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 1674 }) 1675 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1676 gsUpdated = true 1677 ua := action.(k8stesting.UpdateAction) 1678 gs := ua.GetObject().(*agonesv1.GameServer) 1679 assert.Equal(t, agonesv1.GameServerStateReady, gs.Status.State) 1680 assert.Equal(t, containerID, gs.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]) 1681 return true, gs, nil 1682 }) 1683 m.KubeClient.AddReactor("update", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 1684 podUpdated = true 1685 ua := action.(k8stesting.UpdateAction) 1686 pod := ua.GetObject().(*corev1.Pod) 1687 assert.Equal(t, containerID, pod.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]) 1688 return true, pod, nil 1689 }) 1690 1691 ctx, cancel := agtesting.StartInformers(m, c.podSynced, c.nodeSynced) 1692 defer cancel() 1693 1694 gs, err := c.syncGameServerRequestReadyState(ctx, gsFixture) 1695 assert.Nil(t, err, "should not error") 1696 assert.True(t, gsUpdated, "GameServer wasn't updated") 1697 assert.True(t, podUpdated, "Pod wasn't updated") 1698 assert.Equal(t, agonesv1.GameServerStateReady, gs.Status.State) 1699 1700 assert.Equal(t, gs.Status.NodeName, nodeFixture.ObjectMeta.Name) 1701 assert.Equal(t, gs.Status.Address, ipFixture) 1702 assert.Equal(t, []corev1.NodeAddress{{Address: ipFixture, Type: "ExternalIP"}}, gs.Status.Addresses) 1703 1704 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "Address and port populated") 1705 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "SDK.Ready() complete") 1706 }) 1707 1708 t.Run("GameServer with a GameServerReadyContainerIDAnnotation already", func(t *testing.T) { 1709 c, m := newFakeController() 1710 1711 gsFixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1712 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateRequestReady}} 1713 gsFixture.ApplyDefaults() 1714 gsFixture.Status.NodeName = nodeName 1715 gsFixture.Annotations[agonesv1.GameServerReadyContainerIDAnnotation] = "4321" 1716 pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) 1717 pod.Status.ContainerStatuses = []corev1.ContainerStatus{ 1718 { 1719 Name: gsFixture.Spec.Container, 1720 State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}}, 1721 ContainerID: containerID, 1722 }, 1723 } 1724 assert.Nil(t, err) 1725 gsUpdated := false 1726 podUpdated := false 1727 1728 m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1729 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 1730 }) 1731 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1732 gsUpdated = true 1733 ua := action.(k8stesting.UpdateAction) 1734 gs := ua.GetObject().(*agonesv1.GameServer) 1735 assert.Equal(t, agonesv1.GameServerStateReady, gs.Status.State) 1736 assert.NotEqual(t, containerID, gs.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]) 1737 1738 return true, gs, nil 1739 }) 1740 m.KubeClient.AddReactor("update", "pods", func(action k8stesting.Action) (bool, runtime.Object, error) { 1741 podUpdated = true 1742 ua := action.(k8stesting.UpdateAction) 1743 pod := ua.GetObject().(*corev1.Pod) 1744 assert.NotEqual(t, containerID, pod.Annotations[agonesv1.GameServerReadyContainerIDAnnotation]) 1745 return true, pod, nil 1746 }) 1747 1748 ctx, cancel := agtesting.StartInformers(m, c.podSynced) 1749 defer cancel() 1750 1751 gs, err := c.syncGameServerRequestReadyState(ctx, gsFixture) 1752 assert.NoError(t, err, "should not error") 1753 assert.True(t, gsUpdated, "GameServer wasn't updated") 1754 assert.True(t, podUpdated, "Pod wasn't updated") 1755 assert.Equal(t, agonesv1.GameServerStateReady, gs.Status.State) 1756 agtesting.AssertEventContains(t, m.FakeRecorder.Events, "SDK.Ready() complete") 1757 }) 1758 1759 for _, s := range []agonesv1.GameServerState{"Unknown", agonesv1.GameServerStateUnhealthy} { 1760 name := fmt.Sprintf("GameServer with %s state", s) 1761 t.Run(name, func(t *testing.T) { 1762 testNoChange(t, s, func(c *Controller, fixture *agonesv1.GameServer) (*agonesv1.GameServer, error) { 1763 return c.syncGameServerRequestReadyState(context.Background(), fixture) 1764 }) 1765 }) 1766 } 1767 1768 t.Run("GameServer whose pod is currently not in a running state, so should retry and not update", func(t *testing.T) { 1769 c, m := newFakeController() 1770 1771 gsFixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1772 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateRequestReady}} 1773 gsFixture.ApplyDefaults() 1774 gsFixture.Status.NodeName = nodeName 1775 pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) 1776 pod.Status.ContainerStatuses = []corev1.ContainerStatus{{Name: gsFixture.Spec.Container}} 1777 assert.Nil(t, err) 1778 gsUpdated := false 1779 podUpdated := false 1780 1781 m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1782 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 1783 }) 1784 m.AgonesClient.AddReactor("update", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1785 gsUpdated = true 1786 return true, nil, nil 1787 }) 1788 m.KubeClient.AddReactor("update", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1789 podUpdated = true 1790 return true, nil, nil 1791 }) 1792 1793 ctx, cancel := agtesting.StartInformers(m, c.podSynced) 1794 defer cancel() 1795 1796 _, err = c.syncGameServerRequestReadyState(ctx, gsFixture) 1797 assert.EqualError(t, err, "game server container for GameServer test in namespace default is not currently running, try again") 1798 assert.False(t, gsUpdated, "GameServer was updated") 1799 assert.False(t, podUpdated, "Pod was updated") 1800 }) 1801 1802 t.Run("GameServer whose pod is missing ContainerStatuses, so should retry and not update", func(t *testing.T) { 1803 c, m := newFakeController() 1804 1805 gsFixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1806 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateRequestReady}} 1807 gsFixture.ApplyDefaults() 1808 gsFixture.Status.NodeName = nodeName 1809 pod, err := gsFixture.Pod(agtesting.FakeAPIHooks{}) 1810 assert.Nil(t, err) 1811 gsUpdated := false 1812 podUpdated := false 1813 1814 m.KubeClient.AddReactor("list", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1815 return true, &corev1.PodList{Items: []corev1.Pod{*pod}}, nil 1816 }) 1817 m.AgonesClient.AddReactor("update", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1818 gsUpdated = true 1819 return true, nil, nil 1820 }) 1821 m.KubeClient.AddReactor("update", "pods", func(_ k8stesting.Action) (bool, runtime.Object, error) { 1822 podUpdated = true 1823 return true, nil, nil 1824 }) 1825 1826 ctx, cancel := agtesting.StartInformers(m, c.podSynced) 1827 defer cancel() 1828 1829 _, err = c.syncGameServerRequestReadyState(ctx, gsFixture) 1830 assert.EqualError(t, err, "game server container for GameServer test in namespace default not present in pod status, try again") 1831 assert.False(t, gsUpdated, "GameServer was updated") 1832 assert.False(t, podUpdated, "Pod was updated") 1833 }) 1834 1835 t.Run("GameServer with non zero deletion datetime", func(t *testing.T) { 1836 testWithNonZeroDeletionTimestamp(t, func(c *Controller, fixture *agonesv1.GameServer) (*agonesv1.GameServer, error) { 1837 return c.syncGameServerRequestReadyState(context.Background(), fixture) 1838 }) 1839 }) 1840 } 1841 1842 func TestMoveToErrorState(t *testing.T) { 1843 t.Parallel() 1844 1845 t.Run("Set GameServer to error state", func(t *testing.T) { 1846 c, m := newFakeController() 1847 1848 gsFixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1849 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateRequestReady}} 1850 gsFixture.ApplyDefaults() 1851 1852 gsUpdated := false 1853 1854 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1855 gsUpdated = true 1856 ua := action.(k8stesting.UpdateAction) 1857 gs := ua.GetObject().(*agonesv1.GameServer) 1858 assert.Equal(t, agonesv1.GameServerStateError, gs.Status.State) 1859 return true, gs, nil 1860 }) 1861 1862 ctx, cancel := agtesting.StartInformers(m, c.podSynced) 1863 defer cancel() 1864 1865 res, err := c.moveToErrorState(ctx, gsFixture, "some-data") 1866 require.NoError(t, err) 1867 require.NotNil(t, res) 1868 assert.True(t, gsUpdated) 1869 assert.Equal(t, agonesv1.GameServerStateError, res.Status.State) 1870 }) 1871 1872 t.Run("Error on update", func(t *testing.T) { 1873 c, m := newFakeController() 1874 1875 gsFixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1876 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateRequestReady}} 1877 gsFixture.ApplyDefaults() 1878 1879 m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1880 ua := action.(k8stesting.UpdateAction) 1881 gs := ua.GetObject().(*agonesv1.GameServer) 1882 assert.Equal(t, agonesv1.GameServerStateError, gs.Status.State) 1883 return true, gs, errors.New("update-err") 1884 }) 1885 1886 ctx, cancel := agtesting.StartInformers(m, c.podSynced) 1887 defer cancel() 1888 1889 _, err := c.moveToErrorState(ctx, gsFixture, "some-data") 1890 if assert.Error(t, err) { 1891 assert.Equal(t, `error moving GameServer test to Error State: update-err`, err.Error()) 1892 } 1893 }) 1894 } 1895 1896 func TestControllerSyncGameServerShutdownState(t *testing.T) { 1897 t.Parallel() 1898 1899 t.Run("GameServer with a Shutdown state", func(t *testing.T) { 1900 c, mocks := newFakeController() 1901 gsFixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1902 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateShutdown}} 1903 gsFixture.ApplyDefaults() 1904 checkDeleted := false 1905 1906 mocks.AgonesClient.AddReactor("delete", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1907 checkDeleted = true 1908 assert.Equal(t, "default", action.GetNamespace()) 1909 da := action.(k8stesting.DeleteAction) 1910 assert.Equal(t, "test", da.GetName()) 1911 1912 return true, nil, nil 1913 }) 1914 1915 ctx, cancel := agtesting.StartInformers(mocks, c.gameServerSynced) 1916 defer cancel() 1917 1918 err := c.syncGameServerShutdownState(ctx, gsFixture) 1919 assert.Nil(t, err) 1920 assert.True(t, checkDeleted, "GameServer should be deleted") 1921 assert.Contains(t, <-mocks.FakeRecorder.Events, "Deletion started") 1922 }) 1923 1924 t.Run("Error on delete", func(t *testing.T) { 1925 c, mocks := newFakeController() 1926 gsFixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 1927 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateShutdown}} 1928 gsFixture.ApplyDefaults() 1929 1930 mocks.AgonesClient.AddReactor("delete", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { 1931 assert.Equal(t, "default", action.GetNamespace()) 1932 da := action.(k8stesting.DeleteAction) 1933 assert.Equal(t, "test", da.GetName()) 1934 1935 return true, nil, errors.New("delete-err") 1936 }) 1937 1938 ctx, cancel := agtesting.StartInformers(mocks, c.gameServerSynced) 1939 defer cancel() 1940 1941 err := c.syncGameServerShutdownState(ctx, gsFixture) 1942 if assert.Error(t, err) { 1943 assert.Equal(t, `error deleting Game Server test: delete-err`, err.Error()) 1944 } 1945 }) 1946 1947 t.Run("GameServer with unknown state", func(t *testing.T) { 1948 testNoChange(t, "Unknown", func(c *Controller, fixture *agonesv1.GameServer) (*agonesv1.GameServer, error) { 1949 return fixture, c.syncGameServerShutdownState(context.Background(), fixture) 1950 }) 1951 }) 1952 1953 t.Run("GameServer with non zero deletion datetime", func(t *testing.T) { 1954 testWithNonZeroDeletionTimestamp(t, func(c *Controller, fixture *agonesv1.GameServer) (*agonesv1.GameServer, error) { 1955 return fixture, c.syncGameServerShutdownState(context.Background(), fixture) 1956 }) 1957 }) 1958 } 1959 1960 func TestControllerGameServerPod(t *testing.T) { 1961 t.Parallel() 1962 1963 agruntime.FeatureTestMutex.Lock() 1964 defer agruntime.FeatureTestMutex.Unlock() 1965 require.NoError(t, agruntime.ParseFeatures(string(agruntime.FeatureSidecarContainers)+"=false")) 1966 1967 setup := func() (*Controller, *agonesv1.GameServer, *watch.FakeWatcher, context.Context, context.CancelFunc) { 1968 c, mocks := newFakeController() 1969 fakeWatch := watch.NewFake() 1970 mocks.KubeClient.AddWatchReactor("pods", k8stesting.DefaultWatchReactor(fakeWatch, nil)) 1971 ctx, cancel := agtesting.StartInformers(mocks, c.gameServerSynced) 1972 gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gameserver", 1973 Namespace: defaultNs, UID: "1234"}, Spec: newSingleContainerSpec()} 1974 gs.ApplyDefaults() 1975 return c, gs, fakeWatch, ctx, cancel 1976 } 1977 1978 t.Run("no pod exists", func(t *testing.T) { 1979 c, gs, _, _, cancel := setup() 1980 defer cancel() 1981 1982 require.Never(t, func() bool { 1983 list, err := c.podLister.List(labels.Everything()) 1984 assert.NoError(t, err) 1985 return len(list) > 0 1986 }, time.Second, 100*time.Millisecond) 1987 _, err := c.gameServerPod(gs) 1988 assert.Error(t, err) 1989 assert.True(t, k8serrors.IsNotFound(err)) 1990 }) 1991 1992 t.Run("a pod exists", func(t *testing.T) { 1993 c, gs, fakeWatch, _, cancel := setup() 1994 1995 defer cancel() 1996 pod, err := gs.Pod(agtesting.FakeAPIHooks{}) 1997 require.NoError(t, err) 1998 1999 fakeWatch.Add(pod.DeepCopy()) 2000 require.Eventually(t, func() bool { 2001 list, err := c.podLister.List(labels.Everything()) 2002 assert.NoError(t, err) 2003 return len(list) == 1 2004 }, 5*time.Second, time.Second) 2005 2006 pod2, err := c.gameServerPod(gs) 2007 require.NoError(t, err) 2008 assert.Equal(t, pod, pod2) 2009 2010 fakeWatch.Delete(pod.DeepCopy()) 2011 require.Eventually(t, func() bool { 2012 list, err := c.podLister.List(labels.Everything()) 2013 assert.NoError(t, err) 2014 return len(list) == 0 2015 }, 5*time.Second, time.Second) 2016 _, err = c.gameServerPod(gs) 2017 assert.Error(t, err) 2018 assert.True(t, k8serrors.IsNotFound(err)) 2019 }) 2020 2021 t.Run("a pod exists, but isn't owned by the gameserver", func(t *testing.T) { 2022 c, gs, fakeWatch, ctx, cancel := setup() 2023 defer cancel() 2024 2025 pod := &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: gs.ObjectMeta.Name, Labels: map[string]string{agonesv1.GameServerPodLabel: gs.ObjectMeta.Name, "owned": "false"}}} 2026 fakeWatch.Add(pod.DeepCopy()) 2027 2028 // gate 2029 cache.WaitForCacheSync(ctx.Done(), c.podSynced) 2030 pod, err := c.podGetter.Pods(defaultNs).Get(ctx, pod.ObjectMeta.Name, metav1.GetOptions{}) 2031 require.NoError(t, err) 2032 assert.NotNil(t, pod) 2033 2034 _, err = c.gameServerPod(gs) 2035 assert.Error(t, err) 2036 assert.True(t, k8serrors.IsNotFound(err)) 2037 }) 2038 2039 t.Run("dev gameserver pod", func(t *testing.T) { 2040 c, _ := newFakeController() 2041 2042 gs := &agonesv1.GameServer{ 2043 ObjectMeta: metav1.ObjectMeta{Name: "gameserver", Namespace: defaultNs, 2044 Annotations: map[string]string{ 2045 agonesv1.DevAddressAnnotation: "1.1.1.1", 2046 }, 2047 UID: "1234"}, 2048 2049 Spec: newSingleContainerSpec()} 2050 2051 pod, err := c.gameServerPod(gs) 2052 require.NoError(t, err) 2053 assert.Empty(t, pod.ObjectMeta.Name) 2054 }) 2055 } 2056 2057 func TestControllerAddGameServerHealthCheck(t *testing.T) { 2058 c, _ := newFakeController() 2059 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 2060 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateCreating}} 2061 fixture.ApplyDefaults() 2062 2063 assert.False(t, fixture.Spec.Health.Disabled) 2064 pod, err := fixture.Pod(agtesting.FakeAPIHooks{}) 2065 require.NoError(t, err) 2066 err = c.addGameServerHealthCheck(fixture, pod) 2067 2068 assert.NoError(t, err) 2069 assert.Len(t, pod.Spec.Containers, 1) 2070 probe := pod.Spec.Containers[0].LivenessProbe 2071 require.NotNil(t, probe) 2072 assert.Equal(t, "/gshealthz", probe.HTTPGet.Path) 2073 assert.Equal(t, intstr.IntOrString{IntVal: 8080}, probe.HTTPGet.Port) 2074 assert.Equal(t, fixture.Spec.Health.FailureThreshold, probe.FailureThreshold) 2075 assert.Equal(t, fixture.Spec.Health.InitialDelaySeconds, probe.InitialDelaySeconds) 2076 assert.Equal(t, fixture.Spec.Health.PeriodSeconds, probe.PeriodSeconds) 2077 } 2078 2079 func TestControllerAddSDKServerEnvVars(t *testing.T) { 2080 2081 t.Run("legacy game server without ports set", func(t *testing.T) { 2082 // For backwards compatibility, verify that no variables are set if the ports 2083 // are not set on the game server. 2084 c, _ := newFakeController() 2085 gs := &agonesv1.GameServer{ 2086 ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "1234"}, 2087 Spec: newSingleContainerSpec(), 2088 } 2089 gs.ApplyDefaults() 2090 gs.Spec.SdkServer = agonesv1.SdkServer{} 2091 pod, err := gs.Pod(agtesting.FakeAPIHooks{}) 2092 require.NoError(t, err) 2093 before := pod.DeepCopy() 2094 c.addSDKServerEnvVars(gs, pod) 2095 assert.Equal(t, before, pod, "Error: pod unexpectedly modified. before = %v, after = %v", before, pod) 2096 }) 2097 2098 t.Run("game server without any environment", func(t *testing.T) { 2099 c, _ := newFakeController() 2100 gs := &agonesv1.GameServer{ 2101 ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "2345"}, 2102 Spec: newSingleContainerSpec(), 2103 } 2104 gs.ApplyDefaults() 2105 pod, err := gs.Pod(agtesting.FakeAPIHooks{}) 2106 require.NoError(t, err) 2107 c.addSDKServerEnvVars(gs, pod) 2108 assert.Len(t, pod.Spec.Containers, 1, "Expected 1 container, found %d", len(pod.Spec.Containers)) 2109 assert.Contains(t, pod.Spec.Containers[0].Env, corev1.EnvVar{Name: grpcPortEnvVar, Value: strconv.Itoa(int(gs.Spec.SdkServer.GRPCPort))}) 2110 assert.Contains(t, pod.Spec.Containers[0].Env, corev1.EnvVar{Name: httpPortEnvVar, Value: strconv.Itoa(int(gs.Spec.SdkServer.HTTPPort))}) 2111 }) 2112 2113 t.Run("game server without any conflicting env vars", func(t *testing.T) { 2114 c, _ := newFakeController() 2115 gs := &agonesv1.GameServer{ 2116 ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "3456"}, 2117 Spec: agonesv1.GameServerSpec{ 2118 Ports: []agonesv1.GameServerPort{{ContainerPort: 7777}}, 2119 Template: corev1.PodTemplateSpec{ 2120 Spec: corev1.PodSpec{ 2121 Containers: []corev1.Container{ 2122 { 2123 Name: "container", 2124 Image: "container/image", 2125 Env: []corev1.EnvVar{{Name: "one", Value: "value"}, {Name: "two", Value: "value"}}, 2126 }, 2127 }, 2128 }, 2129 }, 2130 }, 2131 } 2132 gs.ApplyDefaults() 2133 pod, err := gs.Pod(agtesting.FakeAPIHooks{}) 2134 require.NoError(t, err) 2135 c.addSDKServerEnvVars(gs, pod) 2136 assert.Len(t, pod.Spec.Containers, 1, "Expected 1 container, found %d", len(pod.Spec.Containers)) 2137 assert.Contains(t, pod.Spec.Containers[0].Env, corev1.EnvVar{Name: grpcPortEnvVar, Value: strconv.Itoa(int(gs.Spec.SdkServer.GRPCPort))}) 2138 assert.Contains(t, pod.Spec.Containers[0].Env, corev1.EnvVar{Name: httpPortEnvVar, Value: strconv.Itoa(int(gs.Spec.SdkServer.HTTPPort))}) 2139 }) 2140 2141 t.Run("game server with conflicting env vars", func(t *testing.T) { 2142 c, _ := newFakeController() 2143 gs := &agonesv1.GameServer{ 2144 ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "4567"}, 2145 Spec: agonesv1.GameServerSpec{ 2146 Ports: []agonesv1.GameServerPort{{ContainerPort: 7777}}, 2147 Template: corev1.PodTemplateSpec{ 2148 Spec: corev1.PodSpec{ 2149 Containers: []corev1.Container{ 2150 { 2151 Name: "container", 2152 Image: "container/image", 2153 Env: []corev1.EnvVar{{Name: grpcPortEnvVar, Value: "value"}, {Name: httpPortEnvVar, Value: "value"}}, 2154 }, 2155 }, 2156 }, 2157 }, 2158 }, 2159 } 2160 gs.ApplyDefaults() 2161 pod, err := gs.Pod(agtesting.FakeAPIHooks{}) 2162 require.NoError(t, err) 2163 c.addSDKServerEnvVars(gs, pod) 2164 assert.Len(t, pod.Spec.Containers, 1, "Expected 1 container, found %d", len(pod.Spec.Containers)) 2165 assert.Contains(t, pod.Spec.Containers[0].Env, corev1.EnvVar{Name: grpcPortEnvVar, Value: strconv.Itoa(int(gs.Spec.SdkServer.GRPCPort))}) 2166 assert.Contains(t, pod.Spec.Containers[0].Env, corev1.EnvVar{Name: httpPortEnvVar, Value: strconv.Itoa(int(gs.Spec.SdkServer.HTTPPort))}) 2167 }) 2168 2169 t.Run("game server with multiple containers", func(t *testing.T) { 2170 c, _ := newFakeController() 2171 gs := &agonesv1.GameServer{ 2172 ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "5678"}, 2173 Spec: agonesv1.GameServerSpec{ 2174 Container: "container1", 2175 Ports: []agonesv1.GameServerPort{{ContainerPort: 7777}}, 2176 Template: corev1.PodTemplateSpec{ 2177 Spec: corev1.PodSpec{ 2178 Containers: []corev1.Container{ 2179 { 2180 Name: "container1", 2181 Image: "container/gameserver", 2182 }, 2183 { 2184 Name: "container2", 2185 Image: "container/image2", 2186 Env: []corev1.EnvVar{{Name: "one", Value: "value"}, {Name: "two", Value: "value"}}, 2187 }, 2188 { 2189 Name: "container3", 2190 Image: "container/image2", 2191 Env: []corev1.EnvVar{{Name: grpcPortEnvVar, Value: "value"}, {Name: httpPortEnvVar, Value: "value"}}, 2192 }, 2193 }, 2194 }, 2195 }, 2196 }, 2197 } 2198 gs.ApplyDefaults() 2199 pod, err := gs.Pod(agtesting.FakeAPIHooks{}) 2200 require.NoError(t, err) 2201 c.addSDKServerEnvVars(gs, pod) 2202 for _, c := range pod.Spec.Containers { 2203 assert.Contains(t, c.Env, corev1.EnvVar{Name: grpcPortEnvVar, Value: strconv.Itoa(int(gs.Spec.SdkServer.GRPCPort))}) 2204 assert.Contains(t, c.Env, corev1.EnvVar{Name: httpPortEnvVar, Value: strconv.Itoa(int(gs.Spec.SdkServer.HTTPPort))}) 2205 } 2206 }) 2207 2208 t.Run("environment variables not applied to the sdkserver container", func(t *testing.T) { 2209 c, _ := newFakeController() 2210 gs := &agonesv1.GameServer{ 2211 ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "5678"}, 2212 Spec: agonesv1.GameServerSpec{ 2213 Container: "container1", 2214 Ports: []agonesv1.GameServerPort{{ContainerPort: 7777}}, 2215 Template: corev1.PodTemplateSpec{ 2216 Spec: corev1.PodSpec{ 2217 Containers: []corev1.Container{ 2218 { 2219 Name: "container1", 2220 Image: "container/gameserver", 2221 }, 2222 { 2223 Name: "container2", 2224 Image: "container/image2", 2225 Env: []corev1.EnvVar{{Name: "one", Value: "value"}, {Name: "two", Value: "value"}}, 2226 }, 2227 { 2228 Name: "container3", 2229 Image: "container/image2", 2230 Env: []corev1.EnvVar{{Name: grpcPortEnvVar, Value: "value"}, {Name: httpPortEnvVar, Value: "value"}}, 2231 }, 2232 }, 2233 }, 2234 }, 2235 }, 2236 } 2237 gs.ApplyDefaults() 2238 sidecar := c.sidecar(gs) 2239 pod, err := gs.Pod(agtesting.FakeAPIHooks{}, sidecar) 2240 require.NoError(t, err) 2241 c.addSDKServerEnvVars(gs, pod) 2242 for _, c := range pod.Spec.Containers { 2243 if c.Name == sdkserverSidecarName { 2244 assert.NotContains(t, c.Env, corev1.EnvVar{Name: grpcPortEnvVar, Value: strconv.Itoa(int(gs.Spec.SdkServer.GRPCPort))}) 2245 assert.NotContains(t, c.Env, corev1.EnvVar{Name: httpPortEnvVar, Value: strconv.Itoa(int(gs.Spec.SdkServer.HTTPPort))}) 2246 } else { 2247 assert.Contains(t, c.Env, corev1.EnvVar{Name: grpcPortEnvVar, Value: strconv.Itoa(int(gs.Spec.SdkServer.GRPCPort))}) 2248 assert.Contains(t, c.Env, corev1.EnvVar{Name: httpPortEnvVar, Value: strconv.Itoa(int(gs.Spec.SdkServer.HTTPPort))}) 2249 } 2250 } 2251 }) 2252 } 2253 2254 // testNoChange runs a test with a state that doesn't exist, to ensure a handler 2255 // doesn't do process anything beyond the state it is meant to handle. 2256 func testNoChange(t *testing.T, state agonesv1.GameServerState, f func(*Controller, *agonesv1.GameServer) (*agonesv1.GameServer, error)) { 2257 c, mocks := newFakeController() 2258 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, 2259 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: state}} 2260 fixture.ApplyDefaults() 2261 updated := false 2262 mocks.AgonesClient.AddReactor("update", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 2263 updated = true 2264 return true, nil, nil 2265 }) 2266 2267 result, err := f(c, fixture) 2268 require.NoError(t, err) 2269 assert.False(t, updated, "update should occur") 2270 assert.Equal(t, fixture, result) 2271 } 2272 2273 // testWithNonZeroDeletionTimestamp runs a test with a given state, but 2274 // the DeletionTimestamp set to Now() 2275 func testWithNonZeroDeletionTimestamp(t *testing.T, f func(*Controller, *agonesv1.GameServer) (*agonesv1.GameServer, error)) { 2276 c, mocks := newFakeController() 2277 now := metav1.Now() 2278 fixture := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", DeletionTimestamp: &now}, 2279 Spec: newSingleContainerSpec(), Status: agonesv1.GameServerStatus{State: agonesv1.GameServerStateShutdown}} 2280 fixture.ApplyDefaults() 2281 updated := false 2282 mocks.AgonesClient.AddReactor("update", "gameservers", func(_ k8stesting.Action) (bool, runtime.Object, error) { 2283 updated = true 2284 return true, nil, nil 2285 }) 2286 2287 result, err := f(c, fixture) 2288 require.NoError(t, err) 2289 assert.False(t, updated, "update should occur") 2290 assert.Equal(t, fixture, result) 2291 } 2292 2293 // newFakeController returns a controller, backed by the fake Clientset 2294 func newFakeController() (*Controller, agtesting.Mocks) { 2295 m := agtesting.NewMocks() 2296 c := NewController( 2297 generic.New(), 2298 healthcheck.NewHandler(), 2299 map[string]portallocator.PortRange{agonesv1.DefaultPortRange: {MinPort: 10, MaxPort: 20}}, 2300 "sidecar:dev", false, 2301 resource.MustParse("0.05"), resource.MustParse("0.1"), 2302 resource.MustParse("50Mi"), resource.MustParse("100Mi"), sidecarRunAsUser, 500*time.Millisecond, "sdk-service-account", 2303 m.KubeClient, m.KubeInformerFactory, m.ExtClient, m.AgonesClient, m.AgonesInformerFactory) 2304 c.recorder = m.FakeRecorder 2305 return c, m 2306 } 2307 2308 // newFakeExtensions return a fake extensions struct 2309 func newFakeExtensions() *Extensions { 2310 return NewExtensions(generic.New(), webhooks.NewWebHook(http.NewServeMux())) 2311 } 2312 2313 func newSingleContainerSpec() agonesv1.GameServerSpec { 2314 return agonesv1.GameServerSpec{ 2315 Ports: []agonesv1.GameServerPort{{ContainerPort: 7777, HostPort: 9999, PortPolicy: agonesv1.Static}}, 2316 Template: corev1.PodTemplateSpec{ 2317 Spec: corev1.PodSpec{ 2318 Containers: []corev1.Container{{Name: "container", Image: "container/image"}}, 2319 }, 2320 }, 2321 } 2322 } 2323 2324 // Assume container ports 0 and 1 are Passthrough ports for "example-server" container and container port 0 for "example-server-two" 2325 // The annotation would look like autopilot.gke.io/passthrough-port-assignment: '{"example-server":["0","1"], "example-server-two":[0]}' 2326 func newPassthroughPortSingleContainerSpec() corev1.Pod { 2327 passthroughContainerPortMap := "{\"example-server\":[0,1],\"example-server-two\":[0]}" 2328 return corev1.Pod{ 2329 ObjectMeta: metav1.ObjectMeta{ 2330 Annotations: map[string]string{agonesv1.PassthroughPortAssignmentAnnotation: passthroughContainerPortMap}, 2331 }, 2332 Spec: corev1.PodSpec{ 2333 Containers: []corev1.Container{ 2334 {Name: "agones-gameserver-sidecar", 2335 Image: "container/image", 2336 Env: []corev1.EnvVar{{Name: passthroughPortEnvVar, Value: "TRUE"}}}, 2337 {Name: "example-server", 2338 Image: "container2/image", 2339 Ports: []corev1.ContainerPort{ 2340 {HostPort: 7777, ContainerPort: 5555}, 2341 {HostPort: 7776, ContainerPort: 7797}, 2342 {HostPort: 7775, ContainerPort: 7793}}, 2343 Env: []corev1.EnvVar{{Name: passthroughPortEnvVar, Value: "TRUE"}}}, 2344 {Name: "example-server-two", 2345 Image: "container3/image", 2346 Ports: []corev1.ContainerPort{ 2347 {HostPort: 7745, ContainerPort: 7983}, 2348 {HostPort: 7312, ContainerPort: 7364}}, 2349 Env: []corev1.EnvVar{{Name: passthroughPortEnvVar, Value: "TRUE"}}}}, 2350 }, 2351 } 2352 }