agones.dev/agones@v1.54.0/pkg/apis/agones/v1/gameserver_test.go (about) 1 // Copyright 2017 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 v1 16 17 import ( 18 "fmt" 19 "strings" 20 "testing" 21 "time" 22 23 "agones.dev/agones/pkg" 24 "agones.dev/agones/pkg/apis" 25 "agones.dev/agones/pkg/apis/agones" 26 "agones.dev/agones/pkg/util/runtime" 27 "github.com/stretchr/testify/assert" 28 "github.com/stretchr/testify/require" 29 corev1 "k8s.io/api/core/v1" 30 "k8s.io/apimachinery/pkg/api/resource" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/util/validation" 33 "k8s.io/apimachinery/pkg/util/validation/field" 34 ) 35 36 const ( 37 ipFixture = "127.1.1.1" 38 ) 39 40 func TestStatus(t *testing.T) { 41 testCases := map[string]struct { 42 hostPort int32 43 containerPort int32 44 portPolicy PortPolicy 45 expected GameServerStatusPort 46 }{ 47 "PortPolicy Dynamic, should use hostPort": { 48 hostPort: 7788, 49 containerPort: 7777, 50 portPolicy: Dynamic, 51 expected: GameServerStatusPort{Name: "test-name", Port: 7788}, 52 }, 53 "PortPolicy Static - should use hostPort": { 54 hostPort: 7788, 55 containerPort: 7777, 56 portPolicy: Static, 57 expected: GameServerStatusPort{Name: "test-name", Port: 7788}, 58 }, 59 "PortPolicy Passthrough - should use hostPort": { 60 hostPort: 7788, 61 containerPort: 7777, 62 portPolicy: Passthrough, 63 expected: GameServerStatusPort{Name: "test-name", Port: 7788}, 64 }, 65 "PortPolicy None - should use containerPort and ignore hostPort": { 66 hostPort: 7788, 67 containerPort: 7777, 68 portPolicy: None, 69 expected: GameServerStatusPort{Name: "test-name", Port: 7777}, 70 }, 71 } 72 runtime.FeatureTestMutex.Lock() 73 defer runtime.FeatureTestMutex.Unlock() 74 require.NoError(t, runtime.ParseFeatures(string(runtime.FeaturePortPolicyNone)+"=true")) 75 76 for _, tc := range testCases { 77 name := "test-name" 78 p := GameServerPort{Name: name, HostPort: tc.hostPort, ContainerPort: tc.containerPort, PortPolicy: tc.portPolicy} 79 80 res := p.Status() 81 assert.Equal(t, tc.expected, res) 82 83 } 84 } 85 86 func TestIsBeingDeleted(t *testing.T) { 87 deletionTimestamp := metav1.Date(2009, 11, 17, 20, 34, 58, 651387237, time.UTC) 88 testCases := []struct { 89 description string 90 gs *GameServer 91 expected bool 92 }{ 93 { 94 description: "ready gs, is not being deleted", 95 gs: &GameServer{ 96 ObjectMeta: metav1.ObjectMeta{ 97 DeletionTimestamp: nil, 98 }, 99 Status: GameServerStatus{State: GameServerStateReady}, 100 }, 101 expected: false, 102 }, 103 { 104 description: "DeletionTimestamp is set, gs is being deleted", 105 gs: &GameServer{ 106 ObjectMeta: metav1.ObjectMeta{ 107 DeletionTimestamp: &deletionTimestamp, 108 }, 109 Status: GameServerStatus{State: GameServerStateReady}, 110 }, 111 expected: true, 112 }, 113 { 114 description: "gs status is GameServerStateShutdown, gs is being deleted", 115 gs: &GameServer{ 116 ObjectMeta: metav1.ObjectMeta{ 117 DeletionTimestamp: nil, 118 }, 119 Status: GameServerStatus{State: GameServerStateShutdown}, 120 }, 121 expected: true, 122 }, 123 { 124 description: "gs status is GameServerStateShutdown and DeletionTimestamp is set, gs is being deleted", 125 gs: &GameServer{ 126 ObjectMeta: metav1.ObjectMeta{ 127 DeletionTimestamp: &deletionTimestamp, 128 }, 129 Status: GameServerStatus{State: GameServerStateShutdown}, 130 }, 131 expected: true, 132 }, 133 } 134 135 for _, tc := range testCases { 136 t.Run(tc.description, func(t *testing.T) { 137 result := tc.gs.IsBeingDeleted() 138 assert.Equal(t, tc.expected, result) 139 }) 140 } 141 } 142 143 func TestGameServerApplyDefaults(t *testing.T) { 144 t.Parallel() 145 146 ten := int64(10) 147 148 defaultGameServerAnd := func(f func(gss *GameServerSpec)) GameServer { 149 gs := GameServer{ 150 Spec: GameServerSpec{ 151 Ports: []GameServerPort{{ContainerPort: 999}}, 152 Template: corev1.PodTemplateSpec{ 153 Spec: corev1.PodSpec{Containers: []corev1.Container{ 154 {Name: "testing", Image: "testing/image"}, 155 }}, 156 }, 157 }, 158 } 159 f(&gs.Spec) 160 return gs 161 } 162 type expected struct { 163 container string 164 protocol corev1.Protocol 165 state GameServerState 166 policy PortPolicy 167 portRange string 168 health Health 169 scheduling apis.SchedulingStrategy 170 sdkServer SdkServer 171 alphaPlayerCapacity *int64 172 counterSpec map[string]CounterStatus 173 listSpec map[string]ListStatus 174 evictionSafeSpec EvictionSafe 175 evictionSafeStatus EvictionSafe 176 } 177 wantDefaultAnd := func(f func(e *expected)) expected { 178 e := expected{ 179 container: "testing", 180 protocol: "UDP", 181 portRange: DefaultPortRange, 182 state: GameServerStatePortAllocation, 183 policy: Dynamic, 184 scheduling: apis.Packed, 185 health: Health{ 186 Disabled: false, 187 FailureThreshold: 3, 188 InitialDelaySeconds: 5, 189 PeriodSeconds: 5, 190 }, 191 sdkServer: SdkServer{ 192 LogLevel: SdkServerLogLevelInfo, 193 GRPCPort: 9357, 194 HTTPPort: 9358, 195 }, 196 evictionSafeSpec: EvictionSafeNever, 197 evictionSafeStatus: EvictionSafeNever, 198 } 199 f(&e) 200 return e 201 } 202 data := map[string]struct { 203 gameServer GameServer 204 container string 205 featureFlags string 206 expected expected 207 }{ 208 "set basic defaults on a very simple gameserver": { 209 gameServer: defaultGameServerAnd(func(_ *GameServerSpec) {}), 210 expected: wantDefaultAnd(func(_ *expected) {}), 211 }, 212 "PlayerTracking=true": { 213 featureFlags: string(runtime.FeaturePlayerTracking) + "=true", 214 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 215 gss.Players = &PlayersSpec{InitialCapacity: 10} 216 }), 217 expected: wantDefaultAnd(func(e *expected) { 218 e.alphaPlayerCapacity = &ten 219 }), 220 }, 221 "CountsAndLists=true, Counters": { 222 featureFlags: string(runtime.FeatureCountsAndLists) + "=true", 223 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 224 gss.Counters = make(map[string]CounterStatus) 225 gss.Counters["games"] = CounterStatus{Count: 1, Capacity: 100} 226 }), 227 expected: wantDefaultAnd(func(e *expected) { 228 e.counterSpec = make(map[string]CounterStatus) 229 e.counterSpec["games"] = CounterStatus{Count: 1, Capacity: 100} 230 }), 231 }, 232 "CountsAndLists=true, Lists": { 233 featureFlags: string(runtime.FeatureCountsAndLists) + "=true", 234 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 235 gss.Lists = make(map[string]ListStatus) 236 gss.Lists["players"] = ListStatus{Capacity: 100, Values: []string{"foo", "bar"}} 237 }), 238 expected: wantDefaultAnd(func(e *expected) { 239 e.listSpec = make(map[string]ListStatus) 240 e.listSpec["players"] = ListStatus{Capacity: 100, Values: []string{"foo", "bar"}} 241 }), 242 }, 243 "defaults on passthrough": { 244 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 245 gss.Ports[0].PortPolicy = Passthrough 246 }), 247 expected: wantDefaultAnd(func(e *expected) { 248 e.policy = Passthrough 249 }), 250 }, 251 "defaults are already set": { 252 gameServer: GameServer{ 253 Spec: GameServerSpec{ 254 Container: "testing2", 255 Ports: []GameServerPort{{ 256 Protocol: "TCP", 257 Range: DefaultPortRange, 258 PortPolicy: Static, 259 }}, 260 Health: Health{ 261 Disabled: false, 262 PeriodSeconds: 12, 263 InitialDelaySeconds: 11, 264 FailureThreshold: 10, 265 }, 266 Template: corev1.PodTemplateSpec{ 267 Spec: corev1.PodSpec{ 268 Containers: []corev1.Container{ 269 {Name: "testing", Image: "testing/image"}, 270 {Name: "testing2", Image: "testing/image2"}, 271 }, 272 }, 273 }, 274 SdkServer: SdkServer{ 275 LogLevel: SdkServerLogLevelInfo, 276 GRPCPort: 9357, 277 HTTPPort: 9358, 278 }, 279 }, 280 Status: GameServerStatus{State: "TestState"}, 281 }, 282 expected: wantDefaultAnd(func(e *expected) { 283 e.container = "testing2" 284 e.protocol = "TCP" 285 e.state = "TestState" 286 e.health = Health{ 287 Disabled: false, 288 FailureThreshold: 10, 289 InitialDelaySeconds: 11, 290 PeriodSeconds: 12, 291 } 292 }), 293 }, 294 "set basic defaults on static gameserver": { 295 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 296 gss.Ports[0].PortPolicy = Static 297 }), 298 expected: wantDefaultAnd(func(e *expected) { 299 e.state = GameServerStateCreating 300 e.policy = Static 301 }), 302 }, 303 "health is disabled": { 304 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 305 gss.Health = Health{Disabled: true} 306 }), 307 expected: wantDefaultAnd(func(e *expected) { 308 e.health = Health{Disabled: true} 309 }), 310 }, 311 "convert from legacy single port to multiple": { 312 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 313 gss.Ports[0] = GameServerPort{ 314 ContainerPort: 777, 315 HostPort: 777, 316 PortPolicy: Static, 317 Protocol: corev1.ProtocolTCP, 318 } 319 }), 320 expected: wantDefaultAnd(func(e *expected) { 321 e.protocol = "TCP" 322 e.state = GameServerStateCreating 323 }), 324 }, 325 "set Debug logging level": { 326 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 327 gss.SdkServer = SdkServer{LogLevel: SdkServerLogLevelDebug} 328 }), 329 expected: wantDefaultAnd(func(e *expected) { 330 e.sdkServer.LogLevel = SdkServerLogLevelDebug 331 }), 332 }, 333 "set gRPC and HTTP ports on SDK Server": { 334 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 335 gss.SdkServer = SdkServer{ 336 LogLevel: SdkServerLogLevelError, 337 GRPCPort: 19357, 338 HTTPPort: 19358, 339 } 340 }), 341 expected: wantDefaultAnd(func(e *expected) { 342 e.sdkServer = SdkServer{ 343 LogLevel: SdkServerLogLevelError, 344 GRPCPort: 19357, 345 HTTPPort: 19358, 346 } 347 }), 348 }, 349 "defaults are eviction.safe: Never": { 350 gameServer: defaultGameServerAnd(func(_ *GameServerSpec) {}), 351 expected: wantDefaultAnd(func(e *expected) { 352 e.evictionSafeSpec = EvictionSafeNever 353 e.evictionSafeStatus = EvictionSafeNever 354 }), 355 }, 356 "eviction.safe: Always": { 357 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 358 gss.Eviction = &Eviction{Safe: EvictionSafeAlways} 359 }), 360 expected: wantDefaultAnd(func(e *expected) { 361 e.evictionSafeSpec = EvictionSafeAlways 362 e.evictionSafeStatus = EvictionSafeAlways 363 }), 364 }, 365 "eviction.safe: OnUpgrade": { 366 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 367 gss.Eviction = &Eviction{Safe: EvictionSafeOnUpgrade} 368 }), 369 expected: wantDefaultAnd(func(e *expected) { 370 e.evictionSafeSpec = EvictionSafeOnUpgrade 371 e.evictionSafeStatus = EvictionSafeOnUpgrade 372 }), 373 }, 374 "eviction.safe: Never": { 375 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 376 gss.Eviction = &Eviction{Safe: EvictionSafeNever} 377 }), 378 expected: wantDefaultAnd(func(e *expected) { 379 e.evictionSafeSpec = EvictionSafeNever 380 e.evictionSafeStatus = EvictionSafeNever 381 }), 382 }, 383 "eviction.safe: Always inferred from safe-to-evict=true": { 384 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 385 gss.Template.ObjectMeta.Annotations = map[string]string{PodSafeToEvictAnnotation: "true"} 386 }), 387 expected: wantDefaultAnd(func(e *expected) { 388 e.evictionSafeSpec = EvictionSafeNever 389 e.evictionSafeStatus = EvictionSafeAlways 390 }), 391 }, 392 "Nothing inferred from safe-to-evict=false": { 393 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 394 gss.Template.ObjectMeta.Annotations = map[string]string{PodSafeToEvictAnnotation: "false"} 395 }), 396 expected: wantDefaultAnd(func(e *expected) { 397 e.evictionSafeSpec = EvictionSafeNever 398 e.evictionSafeStatus = EvictionSafeNever 399 }), 400 }, 401 "safe-to-evict=false AND eviction.safe: Always => eviction.safe: Always": { 402 gameServer: defaultGameServerAnd(func(gss *GameServerSpec) { 403 gss.Eviction = &Eviction{Safe: EvictionSafeAlways} 404 gss.Template.ObjectMeta.Annotations = map[string]string{PodSafeToEvictAnnotation: "false"} 405 }), 406 expected: wantDefaultAnd(func(e *expected) { 407 e.evictionSafeSpec = EvictionSafeAlways 408 e.evictionSafeStatus = EvictionSafeAlways 409 }), 410 }, 411 } 412 413 runtime.FeatureTestMutex.Lock() 414 defer runtime.FeatureTestMutex.Unlock() 415 416 for name, test := range data { 417 t.Run(name, func(t *testing.T) { 418 err := runtime.ParseFeatures(test.featureFlags) 419 assert.NoError(t, err) 420 421 test.gameServer.ApplyDefaults() 422 423 assert.Equal(t, pkg.Version, test.gameServer.Annotations[VersionAnnotation]) 424 425 spec := test.gameServer.Spec 426 assert.Contains(t, test.gameServer.ObjectMeta.Finalizers, FinalizerName) 427 assert.Equal(t, test.expected.container, spec.Container) 428 assert.Equal(t, test.expected.protocol, spec.Ports[0].Protocol) 429 assert.Equal(t, test.expected.portRange, spec.Ports[0].Range) 430 assert.Equal(t, test.expected.state, test.gameServer.Status.State) 431 assert.Equal(t, test.expected.scheduling, test.gameServer.Spec.Scheduling) 432 assert.Equal(t, test.expected.health, test.gameServer.Spec.Health) 433 assert.Equal(t, test.expected.sdkServer, test.gameServer.Spec.SdkServer) 434 if test.expected.alphaPlayerCapacity != nil { 435 assert.Equal(t, *test.expected.alphaPlayerCapacity, test.gameServer.Status.Players.Capacity) 436 } else { 437 assert.Nil(t, test.gameServer.Spec.Players) 438 assert.Nil(t, test.gameServer.Status.Players) 439 } 440 if len(test.expected.evictionSafeSpec) > 0 { 441 assert.Equal(t, test.expected.evictionSafeSpec, spec.Eviction.Safe) 442 } else { 443 assert.Nil(t, spec.Eviction) 444 } 445 if len(test.expected.evictionSafeStatus) > 0 { 446 assert.Equal(t, test.expected.evictionSafeStatus, test.gameServer.Status.Eviction.Safe) 447 } else { 448 assert.Nil(t, test.gameServer.Status.Eviction) 449 } 450 if test.expected.counterSpec != nil { 451 assert.Equal(t, test.expected.counterSpec, test.gameServer.Status.Counters) 452 } else { 453 assert.Nil(t, test.gameServer.Spec.Counters) 454 assert.Nil(t, test.gameServer.Status.Counters) 455 } 456 if test.expected.listSpec != nil { 457 assert.Equal(t, test.expected.listSpec, test.gameServer.Status.Lists) 458 } else { 459 assert.Nil(t, test.gameServer.Spec.Lists) 460 assert.Nil(t, test.gameServer.Status.Lists) 461 } 462 }) 463 } 464 } 465 466 // nolint:dupl 467 func TestGameServerValidate(t *testing.T) { 468 t.Parallel() 469 470 longNameLen64 := strings.Repeat("f", validation.LabelValueMaxLength+1) 471 472 testCases := []struct { 473 description string 474 gs GameServer 475 applyDefaults bool 476 want field.ErrorList 477 }{ 478 { 479 description: "Valid game server", 480 gs: GameServer{ 481 Spec: GameServerSpec{ 482 Template: corev1.PodTemplateSpec{ 483 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 484 }, 485 }, 486 }, 487 applyDefaults: true, 488 }, 489 { 490 description: "Invalid gs: container, containerPort, hostPort", 491 gs: GameServer{ 492 Spec: GameServerSpec{ 493 Container: "", 494 Ports: []GameServerPort{{ 495 Name: "main", 496 HostPort: 5001, 497 PortPolicy: Dynamic, 498 }, { 499 Name: "sidecar", 500 HostPort: 5002, 501 PortPolicy: Static, 502 ContainerPort: 5002, 503 }}, 504 Template: corev1.PodTemplateSpec{ 505 Spec: corev1.PodSpec{Containers: []corev1.Container{ 506 {Name: "testing", Image: "testing/image"}, 507 {Name: "anothertest", Image: "testing/image"}, 508 }}, 509 }, 510 }, 511 }, 512 applyDefaults: false, 513 want: field.ErrorList{ 514 field.Required(field.NewPath("spec", "container"), "Container is required when using multiple containers in the pod template"), 515 field.Invalid(field.NewPath("spec", "container"), "", "Could not find a container named "), 516 field.Required(field.NewPath("spec", "ports").Index(0).Child("containerPort"), "ContainerPort must be defined for Dynamic and Static PortPolicies"), 517 field.Forbidden(field.NewPath("spec", "ports").Index(0).Child("hostPort"), "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy"), 518 }, 519 }, 520 { 521 description: "DevAddressAnnotation: Invalid IP, no host port", 522 gs: GameServer{ 523 ObjectMeta: metav1.ObjectMeta{ 524 Name: "dev-game", 525 Namespace: "default", 526 Annotations: map[string]string{DevAddressAnnotation: "invalid-ip"}, 527 }, 528 Spec: GameServerSpec{ 529 Ports: []GameServerPort{{Name: "main", ContainerPort: 7777, PortPolicy: Static}}, 530 }, 531 }, 532 applyDefaults: false, 533 want: field.ErrorList{ 534 field.Invalid(field.NewPath("metadata").Child("annotations", "agones.dev/dev-address"), "invalid-ip", "must be a valid IP address"), 535 field.Required(field.NewPath("spec").Child("ports").Index(0).Child("hostPort"), "agones.dev/dev-address"), 536 }, 537 }, 538 { 539 description: "Long gs name", 540 gs: GameServer{ 541 ObjectMeta: metav1.ObjectMeta{ 542 Name: longNameLen64, 543 }, 544 TypeMeta: metav1.TypeMeta{ 545 Kind: "test-kind", 546 }, 547 Spec: GameServerSpec{ 548 Container: "my_image", 549 Template: corev1.PodTemplateSpec{ 550 Spec: corev1.PodSpec{ 551 Containers: []corev1.Container{ 552 {Name: "my_image", Image: "foo/my_image"}, 553 }, 554 }, 555 }, 556 }, 557 }, 558 applyDefaults: false, 559 want: field.ErrorList{ 560 field.TooLongMaxLength(field.NewPath("metadata", "name"), longNameLen64, 63), 561 }, 562 }, 563 { 564 description: "Long gs GenerateName is not validated on agones side", 565 gs: GameServer{ 566 ObjectMeta: metav1.ObjectMeta{ 567 GenerateName: longNameLen64, 568 }, 569 TypeMeta: metav1.TypeMeta{ 570 Kind: "test-kind", 571 }, 572 Spec: GameServerSpec{ 573 Container: "my_image", 574 Template: corev1.PodTemplateSpec{ 575 Spec: corev1.PodSpec{ 576 Containers: []corev1.Container{ 577 {Name: "my_image", Image: "foo/my_image"}, 578 }, 579 }, 580 }, 581 }, 582 }, 583 applyDefaults: false, 584 }, 585 { 586 description: "Long label key is invalid", 587 gs: GameServer{ 588 ObjectMeta: metav1.ObjectMeta{ 589 GenerateName: longNameLen64, 590 }, 591 TypeMeta: metav1.TypeMeta{ 592 Kind: "test-kind", 593 }, 594 Spec: GameServerSpec{ 595 Container: "my_image", 596 Template: corev1.PodTemplateSpec{ 597 Spec: corev1.PodSpec{ 598 Containers: []corev1.Container{ 599 {Name: "my_image", Image: "foo/my_image"}, 600 }, 601 }, 602 ObjectMeta: metav1.ObjectMeta{ 603 Labels: map[string]string{longNameLen64: ""}, 604 }, 605 }, 606 }, 607 }, 608 applyDefaults: false, 609 want: field.ErrorList{ 610 &field.Error{ 611 Type: field.ErrorTypeInvalid, 612 Field: field.NewPath("spec", "template", "metadata", "labels").String(), 613 BadValue: longNameLen64, 614 Detail: "name part must be no more than 63 characters", 615 Origin: "labelKey", 616 }, 617 }, 618 }, 619 { 620 description: "Long label value is invalid", 621 gs: GameServer{ 622 ObjectMeta: metav1.ObjectMeta{ 623 GenerateName: "ok-name", 624 }, 625 TypeMeta: metav1.TypeMeta{ 626 Kind: "test-kind", 627 }, 628 Spec: GameServerSpec{ 629 Container: "my_image", 630 Template: corev1.PodTemplateSpec{ 631 Spec: corev1.PodSpec{ 632 Containers: []corev1.Container{ 633 {Name: "my_image", Image: "foo/my_image"}, 634 }, 635 }, 636 ObjectMeta: metav1.ObjectMeta{ 637 Labels: map[string]string{"agones.dev/longValueKey": longNameLen64}, 638 }, 639 }, 640 }, 641 }, 642 applyDefaults: false, 643 want: field.ErrorList{ 644 field.Invalid(field.NewPath("spec", "template", "metadata", "labels"), longNameLen64, "must be no more than 63 characters"), 645 }, 646 }, 647 { 648 description: "Long annotation key is invalid", 649 gs: GameServer{ 650 ObjectMeta: metav1.ObjectMeta{ 651 GenerateName: "ok-name", 652 }, 653 TypeMeta: metav1.TypeMeta{ 654 Kind: "test-kind", 655 }, 656 Spec: GameServerSpec{ 657 Container: "my_image", 658 Template: corev1.PodTemplateSpec{ 659 Spec: corev1.PodSpec{ 660 Containers: []corev1.Container{ 661 {Name: "my_image", Image: "foo/my_image"}, 662 }, 663 }, 664 ObjectMeta: metav1.ObjectMeta{ 665 Annotations: map[string]string{longNameLen64: longNameLen64}, 666 }, 667 }, 668 }, 669 }, 670 applyDefaults: false, 671 want: field.ErrorList{ 672 field.Invalid(field.NewPath("spec", "template", "metadata", "annotations"), longNameLen64, "name part must be no more than 63 characters"), 673 }, 674 }, 675 { 676 description: "Invalid character in annotation key", 677 gs: GameServer{ 678 ObjectMeta: metav1.ObjectMeta{ 679 GenerateName: "ok-name", 680 }, 681 TypeMeta: metav1.TypeMeta{ 682 Kind: "test-kind", 683 }, 684 Spec: GameServerSpec{ 685 Container: "my_image", 686 Template: corev1.PodTemplateSpec{ 687 Spec: corev1.PodSpec{ 688 Containers: []corev1.Container{ 689 {Name: "my_image", Image: "foo/my_image"}, 690 }, 691 }, 692 ObjectMeta: metav1.ObjectMeta{ 693 Annotations: map[string]string{"agones.dev/short±Name": longNameLen64}, 694 }, 695 }, 696 }, 697 }, 698 applyDefaults: false, 699 want: field.ErrorList{ 700 field.Invalid(field.NewPath("spec", "template", "metadata", "annotations"), "agones.dev/short±Name", "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')"), 701 }, 702 }, 703 { 704 description: "Valid annotation key", 705 gs: GameServer{ 706 ObjectMeta: metav1.ObjectMeta{ 707 GenerateName: "ok-name", 708 }, 709 TypeMeta: metav1.TypeMeta{ 710 Kind: "test-kind", 711 }, 712 Spec: GameServerSpec{ 713 Container: "my_image", 714 Template: corev1.PodTemplateSpec{ 715 Spec: corev1.PodSpec{ 716 Containers: []corev1.Container{ 717 {Name: "my_image", Image: "foo/my_image"}, 718 }, 719 }, 720 ObjectMeta: metav1.ObjectMeta{ 721 Annotations: map[string]string{"agones.dev/shortName": longNameLen64}, 722 }, 723 }, 724 }, 725 }, 726 applyDefaults: false, 727 }, 728 { 729 description: "Check ContainerPort and HostPort with different policies", 730 gs: GameServer{ 731 ObjectMeta: metav1.ObjectMeta{ 732 GenerateName: "ok-name", 733 }, 734 TypeMeta: metav1.TypeMeta{ 735 Kind: "test-kind", 736 }, 737 Spec: GameServerSpec{ 738 Ports: []GameServerPort{ 739 {Name: "one", PortPolicy: Passthrough, ContainerPort: 1294}, 740 {PortPolicy: Passthrough, Name: "two", HostPort: 7890}, 741 {PortPolicy: Dynamic, Name: "three", HostPort: 7890, ContainerPort: 1294}, 742 }, 743 Container: "my_image", 744 Template: corev1.PodTemplateSpec{ 745 Spec: corev1.PodSpec{ 746 Containers: []corev1.Container{ 747 {Name: "my_image", Image: "foo/my_image"}, 748 }, 749 }, 750 }, 751 }, 752 }, 753 applyDefaults: true, 754 want: field.ErrorList{ 755 field.Required(field.NewPath("spec", "ports").Index(0).Child("containerPort"), "ContainerPort cannot be specified with Passthrough PortPolicy"), 756 field.Forbidden(field.NewPath("spec", "ports").Index(1).Child("hostPort"), "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy"), 757 field.Forbidden(field.NewPath("spec", "ports").Index(2).Child("hostPort"), "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy"), 758 }, 759 }, 760 { 761 description: "PortPolicy must be Static with HostPort specified", 762 gs: GameServer{ 763 ObjectMeta: metav1.ObjectMeta{ 764 Name: "dev-game", 765 Namespace: "default", 766 Annotations: map[string]string{DevAddressAnnotation: ipFixture}, 767 }, 768 Spec: GameServerSpec{ 769 Ports: []GameServerPort{ 770 {PortPolicy: Passthrough, Name: "main", HostPort: 7890, ContainerPort: 7777}, 771 }, 772 Container: "my_image", 773 Template: corev1.PodTemplateSpec{ 774 Spec: corev1.PodSpec{ 775 Containers: []corev1.Container{ 776 {Name: "my_image", Image: "foo/my_image"}, 777 }, 778 }, 779 }, 780 }, 781 }, 782 applyDefaults: true, 783 want: field.ErrorList{ 784 field.Required( 785 field.NewPath("spec", "ports").Index(0).Child("portPolicy"), 786 "PortPolicy must be Static"), 787 }, 788 }, 789 { 790 description: "ContainerPort is less than zero", 791 gs: GameServer{ 792 ObjectMeta: metav1.ObjectMeta{ 793 Name: "dev-game", 794 Namespace: "default", 795 }, 796 Spec: GameServerSpec{ 797 Ports: []GameServerPort{{ 798 Name: "main", 799 ContainerPort: -4, 800 PortPolicy: Dynamic, 801 }}, 802 Container: "testing", 803 Template: corev1.PodTemplateSpec{ 804 Spec: corev1.PodSpec{Containers: []corev1.Container{ 805 {Name: "testing", Image: "testing/image"}, 806 }}, 807 }, 808 }, 809 }, 810 applyDefaults: false, 811 want: field.ErrorList{ 812 field.Required( 813 field.NewPath("spec", "ports").Index(0).Child("containerPort"), 814 "ContainerPort must be defined for Dynamic and Static PortPolicies", 815 ), 816 }, 817 }, 818 { 819 description: "CPU Request > Limit", 820 gs: GameServer{ 821 ObjectMeta: metav1.ObjectMeta{ 822 Name: "dev-game", 823 Namespace: "default", 824 }, 825 Spec: GameServerSpec{ 826 Ports: []GameServerPort{{ 827 Name: "main", 828 ContainerPort: 7777, 829 PortPolicy: Dynamic, 830 }}, 831 Container: "testing", 832 Template: corev1.PodTemplateSpec{ 833 Spec: corev1.PodSpec{Containers: []corev1.Container{ 834 { 835 Name: "testing", 836 Image: "testing/image", 837 Resources: corev1.ResourceRequirements{ 838 Requests: corev1.ResourceList{ 839 corev1.ResourceCPU: resource.MustParse("50m"), 840 corev1.ResourceMemory: resource.MustParse("32Mi"), 841 }, 842 Limits: corev1.ResourceList{ 843 corev1.ResourceCPU: resource.MustParse("30m"), 844 corev1.ResourceMemory: resource.MustParse("32Mi"), 845 }, 846 }, 847 }, 848 }}, 849 }, 850 }, 851 }, 852 applyDefaults: false, 853 want: field.ErrorList{ 854 field.Invalid( 855 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"), 856 "50m", 857 "must be less than or equal to cpu limit of 30m", 858 ), 859 }, 860 }, 861 { 862 description: "CPU negative request", 863 gs: GameServer{ 864 ObjectMeta: metav1.ObjectMeta{ 865 Name: "dev-game", 866 Namespace: "default", 867 }, 868 Spec: GameServerSpec{ 869 Ports: []GameServerPort{{ 870 Name: "main", 871 ContainerPort: 7777, 872 PortPolicy: Dynamic, 873 }}, 874 Container: "testing", 875 Template: corev1.PodTemplateSpec{ 876 Spec: corev1.PodSpec{Containers: []corev1.Container{ 877 { 878 Name: "testing", 879 Image: "testing/image", 880 Resources: corev1.ResourceRequirements{ 881 Requests: corev1.ResourceList{ 882 corev1.ResourceCPU: resource.MustParse("-30m"), 883 corev1.ResourceMemory: resource.MustParse("32Mi"), 884 }, 885 Limits: corev1.ResourceList{ 886 corev1.ResourceCPU: resource.MustParse("30m"), 887 corev1.ResourceMemory: resource.MustParse("32Mi"), 888 }, 889 }, 890 }, 891 }}, 892 }, 893 }, 894 }, 895 applyDefaults: false, 896 want: field.ErrorList{ 897 field.Invalid( 898 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests").Key("cpu"), 899 "-30m", 900 "must be greater than or equal to 0", 901 ), 902 }, 903 }, 904 { 905 description: "CPU negative limit", 906 gs: GameServer{ 907 ObjectMeta: metav1.ObjectMeta{ 908 Name: "dev-game", 909 Namespace: "default", 910 }, 911 Spec: GameServerSpec{ 912 Ports: []GameServerPort{{ 913 Name: "main", 914 ContainerPort: 7777, 915 PortPolicy: Dynamic, 916 }}, 917 Container: "testing", 918 Template: corev1.PodTemplateSpec{ 919 Spec: corev1.PodSpec{Containers: []corev1.Container{ 920 { 921 Name: "testing", 922 Image: "testing/image", 923 Resources: corev1.ResourceRequirements{ 924 Requests: corev1.ResourceList{ 925 corev1.ResourceCPU: resource.MustParse("30m"), 926 corev1.ResourceMemory: resource.MustParse("32Mi"), 927 }, 928 Limits: corev1.ResourceList{ 929 corev1.ResourceCPU: resource.MustParse("-30m"), 930 corev1.ResourceMemory: resource.MustParse("32Mi"), 931 }, 932 }, 933 }, 934 }}, 935 }, 936 }, 937 }, 938 applyDefaults: false, 939 want: field.ErrorList{ 940 field.Invalid( 941 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "limits").Key("cpu"), 942 "-30m", 943 "must be greater than or equal to 0", 944 ), 945 field.Invalid( 946 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"), 947 "30m", 948 "must be less than or equal to cpu limit of -30m", 949 ), 950 }, 951 }, 952 { 953 description: "Memory Request > Limit", 954 gs: GameServer{ 955 ObjectMeta: metav1.ObjectMeta{ 956 Name: "dev-game", 957 Namespace: "default", 958 }, 959 Spec: GameServerSpec{ 960 Ports: []GameServerPort{{ 961 Name: "main", 962 ContainerPort: 7777, 963 PortPolicy: Dynamic, 964 }}, 965 Container: "testing", 966 Template: corev1.PodTemplateSpec{ 967 Spec: corev1.PodSpec{Containers: []corev1.Container{ 968 { 969 Name: "testing", 970 Image: "testing/image", 971 Resources: corev1.ResourceRequirements{ 972 Requests: corev1.ResourceList{ 973 corev1.ResourceCPU: resource.MustParse("30m"), 974 corev1.ResourceMemory: resource.MustParse("55Mi"), 975 }, 976 Limits: corev1.ResourceList{ 977 corev1.ResourceCPU: resource.MustParse("30m"), 978 corev1.ResourceMemory: resource.MustParse("32Mi"), 979 }, 980 }, 981 }, 982 }}, 983 }, 984 }, 985 }, 986 applyDefaults: false, 987 want: field.ErrorList{ 988 field.Invalid( 989 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"), 990 "55Mi", 991 "must be less than or equal to memory limit of 32Mi", 992 ), 993 }, 994 }, 995 { 996 description: "Memory negative request", 997 gs: GameServer{ 998 ObjectMeta: metav1.ObjectMeta{ 999 Name: "dev-game", 1000 Namespace: "default", 1001 }, 1002 Spec: GameServerSpec{ 1003 Ports: []GameServerPort{{ 1004 Name: "main", 1005 ContainerPort: 7777, 1006 PortPolicy: Dynamic, 1007 }}, 1008 Container: "testing", 1009 Template: corev1.PodTemplateSpec{ 1010 Spec: corev1.PodSpec{Containers: []corev1.Container{ 1011 { 1012 Name: "testing", 1013 Image: "testing/image", 1014 Resources: corev1.ResourceRequirements{ 1015 Requests: corev1.ResourceList{ 1016 corev1.ResourceCPU: resource.MustParse("30m"), 1017 corev1.ResourceMemory: resource.MustParse("-32Mi"), 1018 }, 1019 Limits: corev1.ResourceList{ 1020 corev1.ResourceCPU: resource.MustParse("30m"), 1021 corev1.ResourceMemory: resource.MustParse("32Mi"), 1022 }, 1023 }, 1024 }, 1025 }}, 1026 }, 1027 }, 1028 }, 1029 applyDefaults: false, 1030 want: field.ErrorList{ 1031 field.Invalid( 1032 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests").Key("memory"), 1033 "-32Mi", 1034 "must be greater than or equal to 0", 1035 ), 1036 }, 1037 }, 1038 { 1039 description: "Memory negative limit", 1040 gs: GameServer{ 1041 ObjectMeta: metav1.ObjectMeta{ 1042 Name: "dev-game", 1043 Namespace: "default", 1044 }, 1045 Spec: GameServerSpec{ 1046 Ports: []GameServerPort{{ 1047 Name: "main", 1048 ContainerPort: 7777, 1049 PortPolicy: Dynamic, 1050 }}, 1051 Container: "testing", 1052 Template: corev1.PodTemplateSpec{ 1053 Spec: corev1.PodSpec{Containers: []corev1.Container{ 1054 { 1055 Name: "testing", 1056 Image: "testing/image", 1057 Resources: corev1.ResourceRequirements{ 1058 Requests: corev1.ResourceList{ 1059 corev1.ResourceCPU: resource.MustParse("30m"), 1060 corev1.ResourceMemory: resource.MustParse("32Mi"), 1061 }, 1062 Limits: corev1.ResourceList{ 1063 corev1.ResourceCPU: resource.MustParse("30m"), 1064 corev1.ResourceMemory: resource.MustParse("-32Mi"), 1065 }, 1066 }, 1067 }, 1068 }}, 1069 }, 1070 }, 1071 }, 1072 applyDefaults: false, 1073 want: field.ErrorList{ 1074 field.Invalid( 1075 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "limits").Key("memory"), 1076 "-32Mi", 1077 "must be greater than or equal to 0", 1078 ), 1079 field.Invalid( 1080 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"), 1081 "32Mi", 1082 "must be less than or equal to memory limit of -32Mi", 1083 ), 1084 }, 1085 }, 1086 } 1087 1088 for _, tc := range testCases { 1089 t.Run(tc.description, func(t *testing.T) { 1090 if tc.applyDefaults { 1091 tc.gs.ApplyDefaults() 1092 } 1093 1094 errs := tc.gs.Validate(fakeAPIHooks{}) 1095 assert.ElementsMatch(t, tc.want, errs, "ErrorList check") 1096 }) 1097 } 1098 } 1099 1100 func TestGameServerValidateFeatures(t *testing.T) { 1101 t.Parallel() 1102 runtime.FeatureTestMutex.Lock() 1103 defer runtime.FeatureTestMutex.Unlock() 1104 1105 portContainerName := "another-container" 1106 1107 testCases := []struct { 1108 description string 1109 feature string 1110 gs GameServer 1111 want field.ErrorList 1112 }{ 1113 { 1114 description: "Invalid container name, container was not found", 1115 gs: GameServer{ 1116 ObjectMeta: metav1.ObjectMeta{ 1117 Name: "dev-game", 1118 Namespace: "default", 1119 }, 1120 Spec: GameServerSpec{ 1121 Ports: []GameServerPort{ 1122 { 1123 Name: "main", 1124 ContainerPort: 7777, 1125 PortPolicy: Dynamic, 1126 Container: &portContainerName, 1127 }, 1128 }, 1129 Container: "testing", 1130 Template: corev1.PodTemplateSpec{ 1131 Spec: corev1.PodSpec{Containers: []corev1.Container{ 1132 {Name: "testing", Image: "testing/image"}, 1133 }}, 1134 }, 1135 }, 1136 }, 1137 want: field.ErrorList{ 1138 field.Invalid( 1139 field.NewPath("spec", "ports").Index(0).Child("container"), 1140 "another-container", 1141 "Container must be empty or the name of a container in the pod template", 1142 ), 1143 }, 1144 }, 1145 { 1146 description: "Multiple container ports, OK scenario", 1147 gs: GameServer{ 1148 ObjectMeta: metav1.ObjectMeta{ 1149 Name: "dev-game", 1150 Namespace: "default", 1151 }, 1152 Spec: GameServerSpec{ 1153 Ports: []GameServerPort{ 1154 { 1155 Name: "main", 1156 ContainerPort: 7777, 1157 PortPolicy: Dynamic, 1158 }, 1159 }, 1160 Container: "testing", 1161 Template: corev1.PodTemplateSpec{ 1162 Spec: corev1.PodSpec{Containers: []corev1.Container{ 1163 {Name: "testing", Image: "testing/image"}, 1164 }}, 1165 }, 1166 }, 1167 }, 1168 }, 1169 { 1170 description: "PlayerTracking is disabled, Players field specified", 1171 feature: fmt.Sprintf("%s=false", runtime.FeaturePlayerTracking), 1172 gs: GameServer{ 1173 Spec: GameServerSpec{ 1174 Container: "testing", 1175 Players: &PlayersSpec{InitialCapacity: 10}, 1176 Template: corev1.PodTemplateSpec{ 1177 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1178 }, 1179 }, 1180 }, 1181 want: field.ErrorList{ 1182 field.Forbidden( 1183 field.NewPath("spec", "players"), 1184 "Value cannot be set unless feature flag PlayerTracking is enabled", 1185 ), 1186 }, 1187 }, 1188 { 1189 description: "PlayerTracking is enabled, Players field specified", 1190 feature: fmt.Sprintf("%s=true", runtime.FeaturePlayerTracking), 1191 gs: GameServer{ 1192 Spec: GameServerSpec{ 1193 Container: "testing", 1194 Players: &PlayersSpec{InitialCapacity: 10}, 1195 Template: corev1.PodTemplateSpec{ 1196 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1197 }, 1198 }, 1199 }, 1200 }, 1201 { 1202 description: "CountsAndLists is disabled, Counters field specified", 1203 feature: fmt.Sprintf("%s=false", runtime.FeatureCountsAndLists), 1204 gs: GameServer{ 1205 Spec: GameServerSpec{ 1206 Container: "testing", 1207 Counters: map[string]CounterStatus{}, 1208 Template: corev1.PodTemplateSpec{ 1209 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1210 }, 1211 }, 1212 }, 1213 want: field.ErrorList{ 1214 field.Forbidden( 1215 field.NewPath("spec", "counters"), 1216 "Value cannot be set unless feature flag CountsAndLists is enabled", 1217 ), 1218 }, 1219 }, 1220 { 1221 description: "CountsAndLists is disabled, Lists field specified", 1222 feature: fmt.Sprintf("%s=false", runtime.FeatureCountsAndLists), 1223 gs: GameServer{ 1224 Spec: GameServerSpec{ 1225 Container: "testing", 1226 Lists: map[string]ListStatus{}, 1227 Template: corev1.PodTemplateSpec{ 1228 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1229 }, 1230 }, 1231 }, 1232 want: field.ErrorList{ 1233 field.Forbidden( 1234 field.NewPath("spec", "lists"), 1235 "Value cannot be set unless feature flag CountsAndLists is enabled", 1236 ), 1237 }, 1238 }, 1239 { 1240 description: "CountsAndLists is enabled, Counters field specified", 1241 feature: fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists), 1242 gs: GameServer{ 1243 Spec: GameServerSpec{ 1244 Container: "testing", 1245 Counters: map[string]CounterStatus{}, 1246 Template: corev1.PodTemplateSpec{ 1247 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1248 }, 1249 }, 1250 }, 1251 }, 1252 { 1253 description: "CountsAndLists is enabled, Lists field specified", 1254 feature: fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists), 1255 gs: GameServer{ 1256 Spec: GameServerSpec{ 1257 Container: "testing", 1258 Lists: map[string]ListStatus{}, 1259 Template: corev1.PodTemplateSpec{ 1260 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1261 }, 1262 }, 1263 }, 1264 }, 1265 { 1266 description: "PortPolicyNone is disabled, PortPolicy field set to None", 1267 feature: fmt.Sprintf("%s=false", runtime.FeaturePortPolicyNone), 1268 gs: GameServer{ 1269 Spec: GameServerSpec{ 1270 Ports: []GameServerPort{ 1271 { 1272 Name: "main", 1273 ContainerPort: 7777, 1274 PortPolicy: None, 1275 }, 1276 }, 1277 Container: "testing", 1278 Lists: map[string]ListStatus{}, 1279 Template: corev1.PodTemplateSpec{ 1280 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1281 }, 1282 }, 1283 }, 1284 want: field.ErrorList{ 1285 field.Forbidden( 1286 field.NewPath("spec.ports[0].portPolicy"), 1287 "Value cannot be set to None unless feature flag PortPolicyNone is enabled", 1288 ), 1289 }, 1290 }, 1291 { 1292 description: "PortPolicyNone is enabled, PortPolicy field set to None", 1293 feature: fmt.Sprintf("%s=true", runtime.FeaturePortPolicyNone), 1294 gs: GameServer{ 1295 Spec: GameServerSpec{ 1296 Ports: []GameServerPort{ 1297 { 1298 Name: "main", 1299 ContainerPort: 7777, 1300 PortPolicy: None, 1301 }, 1302 }, 1303 Container: "testing", 1304 Lists: map[string]ListStatus{}, 1305 Template: corev1.PodTemplateSpec{ 1306 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1307 }, 1308 }, 1309 }, 1310 }, 1311 } 1312 1313 for _, tc := range testCases { 1314 t.Run(tc.description, func(t *testing.T) { 1315 err := runtime.ParseFeatures(tc.feature) 1316 assert.NoError(t, err) 1317 1318 errs := tc.gs.Validate(fakeAPIHooks{}) 1319 assert.ElementsMatch(t, tc.want, errs, "ErrorList check") 1320 }) 1321 } 1322 } 1323 1324 func TestGameServerPodNoErrors(t *testing.T) { 1325 t.Parallel() 1326 fixture := defaultGameServer() 1327 fixture.ApplyDefaults() 1328 1329 pod, err := fixture.Pod(fakeAPIHooks{}) 1330 assert.Nil(t, err, "Pod should not return an error") 1331 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name) 1332 assert.Equal(t, fixture.ObjectMeta.Name, pod.Spec.Hostname) 1333 assert.Equal(t, fixture.ObjectMeta.Namespace, pod.ObjectMeta.Namespace) 1334 assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"]) 1335 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Labels[GameServerPodLabel]) 1336 assert.Equal(t, fixture.Spec.Container, pod.ObjectMeta.Annotations[GameServerContainerAnnotation]) 1337 assert.True(t, metav1.IsControlledBy(pod, fixture)) 1338 assert.Equal(t, fixture.Spec.Ports[0].HostPort, pod.Spec.Containers[0].Ports[0].HostPort) 1339 assert.Equal(t, fixture.Spec.Ports[0].ContainerPort, pod.Spec.Containers[0].Ports[0].ContainerPort) 1340 assert.Equal(t, corev1.Protocol("UDP"), pod.Spec.Containers[0].Ports[0].Protocol) 1341 assert.True(t, metav1.IsControlledBy(pod, fixture)) 1342 } 1343 1344 func TestGameServerPodHostName(t *testing.T) { 1345 t.Parallel() 1346 1347 fixture := defaultGameServer() 1348 fixture.ObjectMeta.Name = "test-1.0" 1349 fixture.ApplyDefaults() 1350 pod, err := fixture.Pod(fakeAPIHooks{}) 1351 require.NoError(t, err) 1352 assert.Equal(t, "test-1-0", pod.Spec.Hostname) 1353 1354 fixture = defaultGameServer() 1355 fixture.ApplyDefaults() 1356 expected := "ORANGE" 1357 fixture.Spec.Template.Spec.Hostname = expected 1358 pod, err = fixture.Pod(fakeAPIHooks{}) 1359 require.NoError(t, err) 1360 assert.Equal(t, expected, pod.Spec.Hostname) 1361 } 1362 1363 func TestGameServerPodContainerNotFoundErrReturned(t *testing.T) { 1364 t.Parallel() 1365 1366 containerName1 := "Container1" 1367 fixture := &GameServer{ 1368 ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", UID: "1234"}, 1369 Spec: GameServerSpec{ 1370 Container: "can-not-find-this-name", 1371 Ports: []GameServerPort{ 1372 { 1373 Container: &containerName1, 1374 ContainerPort: 7777, 1375 HostPort: 9999, 1376 PortPolicy: Static, 1377 }, 1378 }, 1379 Template: corev1.PodTemplateSpec{ 1380 Spec: corev1.PodSpec{ 1381 Containers: []corev1.Container{{Name: "Container2", Image: "container/image"}}, 1382 }, 1383 }, 1384 }, Status: GameServerStatus{State: GameServerStateCreating}, 1385 } 1386 1387 _, err := fixture.Pod(fakeAPIHooks{}) 1388 if assert.NotNil(t, err, "Pod should return an error") { 1389 assert.Equal(t, "failed to find container named Container1 in pod spec", err.Error()) 1390 } 1391 } 1392 1393 func TestGameServerPodWithSidecarNoErrors(t *testing.T) { 1394 t.Parallel() 1395 runtime.FeatureTestMutex.Lock() 1396 defer runtime.FeatureTestMutex.Unlock() 1397 require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=false")) 1398 1399 fixture := defaultGameServer() 1400 fixture.ApplyDefaults() 1401 1402 sidecar := corev1.Container{Name: "sidecar", Image: "container/sidecar"} 1403 fixture.Spec.Template.Spec.ServiceAccountName = "other-agones-sdk" 1404 pod, err := fixture.Pod(fakeAPIHooks{}, sidecar) 1405 assert.Nil(t, err, "Pod should not return an error") 1406 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name) 1407 assert.Len(t, pod.Spec.Containers, 2, "Should have two containers") 1408 assert.Equal(t, "other-agones-sdk", pod.Spec.ServiceAccountName) 1409 assert.Equal(t, "sidecar", pod.Spec.Containers[0].Name) 1410 assert.Equal(t, "container", pod.Spec.Containers[1].Name) 1411 assert.True(t, metav1.IsControlledBy(pod, fixture)) 1412 } 1413 1414 func TestGameServerPodWithInitSidecarNoErrors(t *testing.T) { 1415 t.Parallel() 1416 runtime.FeatureTestMutex.Lock() 1417 defer runtime.FeatureTestMutex.Unlock() 1418 require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=true")) 1419 1420 fixture := defaultGameServer() 1421 fixture.ApplyDefaults() 1422 1423 sidecar := corev1.Container{Name: "sidecar", Image: "container/sidecar"} 1424 fixture.Spec.Template.Spec.ServiceAccountName = "other-agones-sdk" 1425 pod, err := fixture.Pod(fakeAPIHooks{}, sidecar) 1426 assert.Nil(t, err, "Pod should not return an error") 1427 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name) 1428 assert.Len(t, pod.Spec.Containers, 1, "Should have one containers") 1429 assert.Equal(t, "other-agones-sdk", pod.Spec.ServiceAccountName) 1430 assert.Equal(t, "sidecar", pod.Spec.InitContainers[0].Name) 1431 assert.Equal(t, corev1.ContainerRestartPolicyAlways, *pod.Spec.InitContainers[0].RestartPolicy) 1432 assert.Equal(t, "container", pod.Spec.Containers[0].Name) 1433 assert.True(t, metav1.IsControlledBy(pod, fixture)) 1434 assert.Equal(t, pod.Spec.RestartPolicy, corev1.RestartPolicyNever) 1435 } 1436 1437 func TestGameServerPodWithInitSidecarPrependsToExistingInitContainers(t *testing.T) { 1438 t.Parallel() 1439 runtime.FeatureTestMutex.Lock() 1440 defer runtime.FeatureTestMutex.Unlock() 1441 require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=true")) 1442 1443 fixture := defaultGameServer() 1444 // Add existing init containers to the template 1445 existingInitContainer1 := corev1.Container{Name: "existing-init-1", Image: "existing/init1"} 1446 existingInitContainer2 := corev1.Container{Name: "existing-init-2", Image: "existing/init2"} 1447 fixture.Spec.Template.Spec.InitContainers = []corev1.Container{existingInitContainer1, existingInitContainer2} 1448 fixture.ApplyDefaults() 1449 1450 sidecar1 := corev1.Container{Name: "sidecar-1", Image: "container/sidecar1"} 1451 sidecar2 := corev1.Container{Name: "sidecar-2", Image: "container/sidecar2"} 1452 pod, err := fixture.Pod(fakeAPIHooks{}, sidecar1, sidecar2) 1453 require.NoError(t, err) 1454 1455 // Verify that sidecars are placed at the beginning of InitContainers 1456 assert.Len(t, pod.Spec.InitContainers, 4, "Should have four init containers") 1457 assert.Equal(t, "sidecar-1", pod.Spec.InitContainers[0].Name, "First sidecar should be at index 0") 1458 assert.Equal(t, "sidecar-2", pod.Spec.InitContainers[1].Name, "Second sidecar should be at index 1") 1459 assert.Equal(t, "existing-init-1", pod.Spec.InitContainers[2].Name, "First existing init container should be at index 2") 1460 assert.Equal(t, "existing-init-2", pod.Spec.InitContainers[3].Name, "Second existing init container should be at index 3") 1461 1462 // Verify restart policies 1463 assert.Equal(t, corev1.ContainerRestartPolicyAlways, *pod.Spec.InitContainers[0].RestartPolicy) 1464 assert.Equal(t, corev1.ContainerRestartPolicyAlways, *pod.Spec.InitContainers[1].RestartPolicy) 1465 } 1466 1467 func TestGameServerPodWithMultiplePortAllocations(t *testing.T) { 1468 fixture := defaultGameServer() 1469 containerName := "authContainer" 1470 fixture.Spec.Template.Spec.Containers = append(fixture.Spec.Template.Spec.Containers, corev1.Container{ 1471 Name: containerName, 1472 }) 1473 fixture.Spec.Ports = append(fixture.Spec.Ports, GameServerPort{ 1474 Name: "containerPort", 1475 PortPolicy: Dynamic, 1476 Container: &containerName, 1477 ContainerPort: 5000, 1478 }) 1479 fixture.Spec.Container = fixture.Spec.Template.Spec.Containers[0].Name 1480 fixture.ApplyDefaults() 1481 1482 pod, err := fixture.Pod(fakeAPIHooks{}) 1483 assert.NoError(t, err, "Pod should not return an error") 1484 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name) 1485 assert.Equal(t, fixture.ObjectMeta.Namespace, pod.ObjectMeta.Namespace) 1486 assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"]) 1487 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Labels[GameServerPodLabel]) 1488 assert.Equal(t, fixture.Spec.Container, pod.ObjectMeta.Annotations[GameServerContainerAnnotation]) 1489 assert.Equal(t, fixture.Spec.Ports[0].HostPort, pod.Spec.Containers[0].Ports[0].HostPort) 1490 assert.Equal(t, fixture.Spec.Ports[0].ContainerPort, pod.Spec.Containers[0].Ports[0].ContainerPort) 1491 assert.Equal(t, *fixture.Spec.Ports[0].Container, pod.Spec.Containers[0].Name) 1492 assert.Equal(t, corev1.Protocol("UDP"), pod.Spec.Containers[0].Ports[0].Protocol) 1493 assert.True(t, metav1.IsControlledBy(pod, fixture)) 1494 assert.Equal(t, fixture.Spec.Ports[1].HostPort, pod.Spec.Containers[1].Ports[0].HostPort) 1495 assert.Equal(t, fixture.Spec.Ports[1].ContainerPort, pod.Spec.Containers[1].Ports[0].ContainerPort) 1496 assert.Equal(t, *fixture.Spec.Ports[1].Container, pod.Spec.Containers[1].Name) 1497 assert.Equal(t, corev1.Protocol("UDP"), pod.Spec.Containers[1].Ports[0].Protocol) 1498 } 1499 1500 func TestGameServerPodObjectMeta(t *testing.T) { 1501 fixture := &GameServer{ 1502 ObjectMeta: metav1.ObjectMeta{Name: "lucy"}, 1503 Spec: GameServerSpec{Container: "goat"}, 1504 } 1505 1506 for desc, tc := range map[string]struct { 1507 scheduling apis.SchedulingStrategy 1508 wantSafe string 1509 }{ 1510 "packed": { 1511 scheduling: apis.Packed, 1512 }, 1513 "distributed": { 1514 scheduling: apis.Distributed, 1515 }, 1516 } { 1517 t.Run(desc, func(t *testing.T) { 1518 gs := fixture.DeepCopy() 1519 gs.Spec.Scheduling = tc.scheduling 1520 pod := &corev1.Pod{} 1521 1522 gs.podObjectMeta(pod) 1523 1524 assert.Equal(t, gs.ObjectMeta.Name, pod.ObjectMeta.Name) 1525 assert.Equal(t, gs.ObjectMeta.Namespace, pod.ObjectMeta.Namespace) 1526 assert.Equal(t, GameServerLabelRole, pod.ObjectMeta.Labels[RoleLabel]) 1527 assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"]) 1528 assert.Equal(t, gs.ObjectMeta.Name, pod.ObjectMeta.Labels[GameServerPodLabel]) 1529 assert.Equal(t, "goat", pod.ObjectMeta.Annotations[GameServerContainerAnnotation]) 1530 assert.True(t, metav1.IsControlledBy(pod, gs)) 1531 assert.Equal(t, tc.wantSafe, pod.ObjectMeta.Annotations[PodSafeToEvictAnnotation]) 1532 }) 1533 } 1534 } 1535 1536 func TestGameServerPodScheduling(t *testing.T) { 1537 fixture := &corev1.Pod{Spec: corev1.PodSpec{}} 1538 1539 t.Run("packed", func(t *testing.T) { 1540 gs := &GameServer{Spec: GameServerSpec{Scheduling: apis.Packed}} 1541 pod := fixture.DeepCopy() 1542 gs.podScheduling(pod) 1543 1544 assert.Len(t, pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution, 1) 1545 wpat := pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0] 1546 assert.Equal(t, int32(100), wpat.Weight) 1547 assert.Contains(t, wpat.PodAffinityTerm.LabelSelector.String(), GameServerLabelRole) 1548 assert.Contains(t, wpat.PodAffinityTerm.LabelSelector.String(), RoleLabel) 1549 }) 1550 1551 t.Run("distributed", func(t *testing.T) { 1552 gs := &GameServer{Spec: GameServerSpec{Scheduling: apis.Distributed}} 1553 pod := fixture.DeepCopy() 1554 gs.podScheduling(pod) 1555 assert.Empty(t, pod.Spec.Affinity) 1556 }) 1557 } 1558 1559 func TestGameServerDisableServiceAccount(t *testing.T) { 1560 t.Parallel() 1561 1562 gs := &GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "1234"}, Spec: GameServerSpec{ 1563 Template: corev1.PodTemplateSpec{ 1564 Spec: corev1.PodSpec{ 1565 Containers: []corev1.Container{{Name: "container", Image: "container/image"}}, 1566 }, 1567 }, 1568 }} 1569 1570 gs.ApplyDefaults() 1571 pod, err := gs.Pod(fakeAPIHooks{}) 1572 assert.NoError(t, err) 1573 assert.Len(t, pod.Spec.Containers, 1) 1574 assert.Empty(t, pod.Spec.Containers[0].VolumeMounts) 1575 1576 err = gs.DisableServiceAccount(pod) 1577 assert.NoError(t, err) 1578 assert.Len(t, pod.Spec.Containers, 1) 1579 assert.Len(t, pod.Spec.Containers[0].VolumeMounts, 1) 1580 assert.Equal(t, "/var/run/secrets/kubernetes.io/serviceaccount", pod.Spec.Containers[0].VolumeMounts[0].MountPath) 1581 } 1582 1583 func TestGameServerPassthroughPortAnnotation(t *testing.T) { 1584 t.Parallel() 1585 runtime.FeatureTestMutex.Lock() 1586 defer runtime.FeatureTestMutex.Unlock() 1587 containerOne := "containerOne" 1588 containerTwo := "containerTwo" 1589 containerThree := "containerThree" 1590 containerFour := "containerFour" 1591 gs := &GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "1234"}, Spec: GameServerSpec{ 1592 Container: "containerOne", 1593 Ports: []GameServerPort{ 1594 {Name: "defaultDynamicOne", PortPolicy: Dynamic, ContainerPort: 7659, Container: &containerOne}, 1595 {Name: "defaultPassthroughOne", PortPolicy: Passthrough, Container: &containerOne}, 1596 {Name: "defaultPassthroughTwo", PortPolicy: Passthrough, Container: &containerTwo}, 1597 {Name: "defaultDynamicTwo", PortPolicy: Dynamic, ContainerPort: 7654, Container: &containerTwo}, 1598 {Name: "defaultDynamicThree", PortPolicy: Dynamic, ContainerPort: 7660, Container: &containerThree}, 1599 {Name: "defaultDynamicThree", PortPolicy: Dynamic, ContainerPort: 7661, Container: &containerThree}, 1600 {Name: "defaultDynamicThree", PortPolicy: Dynamic, ContainerPort: 7662, Container: &containerThree}, 1601 {Name: "defaulPassthroughThree", PortPolicy: Passthrough, Container: &containerThree}, 1602 {Name: "defaultPassthroughFour", PortPolicy: Passthrough, Container: &containerFour}, 1603 }, 1604 Template: corev1.PodTemplateSpec{ 1605 Spec: corev1.PodSpec{ 1606 Containers: []corev1.Container{ 1607 {Name: "containerOne", Image: "container/image"}, 1608 {Name: "containerTwo", Image: "container/image"}, 1609 {Name: "containerThree", Image: "container/image"}, 1610 {Name: "containerFour", Image: "container/image"}, 1611 }, 1612 }, 1613 }, 1614 }} 1615 1616 passthroughContainerPortMap := "{\"containerFour\":[0],\"containerOne\":[1],\"containerThree\":[3],\"containerTwo\":[0]}" 1617 1618 gs.ApplyDefaults() 1619 pod, err := gs.Pod(fakeAPIHooks{}) 1620 assert.NoError(t, err) 1621 assert.Len(t, pod.Spec.Containers, 4) 1622 assert.Empty(t, pod.Spec.Containers[0].VolumeMounts) 1623 assert.Equal(t, pod.ObjectMeta.Annotations[PassthroughPortAssignmentAnnotation], passthroughContainerPortMap) 1624 1625 err = gs.DisableServiceAccount(pod) 1626 assert.NoError(t, err) 1627 assert.Len(t, pod.Spec.Containers, 4) 1628 assert.Len(t, pod.Spec.Containers[0].VolumeMounts, 1) 1629 assert.Equal(t, "/var/run/secrets/kubernetes.io/serviceaccount", pod.Spec.Containers[0].VolumeMounts[0].MountPath) 1630 } 1631 1632 func TestGameServerCountPorts(t *testing.T) { 1633 fixture := &GameServer{Spec: GameServerSpec{Ports: []GameServerPort{ 1634 {PortPolicy: Dynamic}, 1635 {PortPolicy: Dynamic}, 1636 {PortPolicy: Dynamic}, 1637 {PortPolicy: Static}, 1638 }}} 1639 1640 assert.Equal(t, 3, fixture.CountPorts(func(policy PortPolicy) bool { 1641 return policy == Dynamic 1642 })) 1643 assert.Equal(t, 1, fixture.CountPorts(func(policy PortPolicy) bool { 1644 return policy == Static 1645 })) 1646 } 1647 1648 func TestGameServerCountPortsForRange(t *testing.T) { 1649 fixture := &GameServer{Spec: GameServerSpec{Ports: []GameServerPort{ 1650 {PortPolicy: Dynamic, Range: "test"}, 1651 {PortPolicy: Dynamic}, 1652 {PortPolicy: Dynamic, Range: "test"}, 1653 {PortPolicy: Static, Range: "test"}, 1654 }}} 1655 1656 assert.Equal(t, 2, fixture.CountPortsForRange("test", func(policy PortPolicy) bool { 1657 return policy == Dynamic 1658 })) 1659 assert.Equal(t, 1, fixture.CountPortsForRange("test", func(policy PortPolicy) bool { 1660 return policy == Static 1661 })) 1662 } 1663 1664 func TestGameServerPatch(t *testing.T) { 1665 fixture := &GameServer{ 1666 ObjectMeta: metav1.ObjectMeta{Name: "lucy", ResourceVersion: "1234"}, 1667 Spec: GameServerSpec{Container: "goat"}, 1668 } 1669 1670 delta := fixture.DeepCopy() 1671 delta.Spec.Container = "bear" 1672 1673 patch, err := fixture.Patch(delta) 1674 assert.Nil(t, err) 1675 1676 assert.Contains(t, string(patch), `{"op":"replace","path":"/spec/container","value":"bear"}`) 1677 assert.Contains(t, string(patch), `{"op":"test","path":"/metadata/resourceVersion","value":"1234"}`) 1678 } 1679 1680 func TestGameServerGetDevAddress(t *testing.T) { 1681 devGs := &GameServer{ 1682 ObjectMeta: metav1.ObjectMeta{ 1683 Name: "dev-game", 1684 Namespace: "default", 1685 Annotations: map[string]string{DevAddressAnnotation: ipFixture}, 1686 }, 1687 Spec: GameServerSpec{ 1688 Ports: []GameServerPort{{HostPort: 7777, PortPolicy: Static}}, 1689 Template: corev1.PodTemplateSpec{ 1690 Spec: corev1.PodSpec{ 1691 Containers: []corev1.Container{{Name: "container", Image: "container/image"}}, 1692 }, 1693 }, 1694 }, 1695 } 1696 1697 devAddress, isDev := devGs.GetDevAddress() 1698 assert.True(t, isDev, "dev-game should had a dev-address") 1699 assert.Equal(t, ipFixture, devAddress, "dev-address IP address should be 127.1.1.1") 1700 1701 regularGs := devGs.DeepCopy() 1702 regularGs.ObjectMeta.Annotations = map[string]string{} 1703 devAddress, isDev = regularGs.GetDevAddress() 1704 assert.False(t, isDev, "dev-game should NOT have a dev-address") 1705 assert.Equal(t, "", devAddress, "dev-address IP address should be 127.1.1.1") 1706 } 1707 1708 func TestGameServerIsDeletable(t *testing.T) { 1709 gs := &GameServer{Status: GameServerStatus{State: GameServerStateStarting}} 1710 assert.True(t, gs.IsDeletable()) 1711 1712 gs.Status.State = GameServerStateAllocated 1713 assert.False(t, gs.IsDeletable()) 1714 1715 gs.Status.State = GameServerStateReserved 1716 assert.False(t, gs.IsDeletable()) 1717 1718 now := metav1.Now() 1719 gs.ObjectMeta.DeletionTimestamp = &now 1720 assert.True(t, gs.IsDeletable()) 1721 1722 gs.Status.State = GameServerStateAllocated 1723 assert.True(t, gs.IsDeletable()) 1724 1725 gs.Status.State = GameServerStateReady 1726 assert.True(t, gs.IsDeletable()) 1727 } 1728 1729 func TestGameServerIsBeforeReady(t *testing.T) { 1730 fixtures := []struct { 1731 state GameServerState 1732 expected bool 1733 }{ 1734 {GameServerStatePortAllocation, true}, 1735 {GameServerStateCreating, true}, 1736 {GameServerStateStarting, true}, 1737 {GameServerStateScheduled, true}, 1738 {GameServerStateRequestReady, true}, 1739 {GameServerStateReady, false}, 1740 {GameServerStateShutdown, false}, 1741 {GameServerStateError, false}, 1742 {GameServerStateUnhealthy, false}, 1743 {GameServerStateReserved, false}, 1744 {GameServerStateAllocated, false}, 1745 } 1746 1747 for _, test := range fixtures { 1748 t.Run(string(test.state), func(t *testing.T) { 1749 gs := &GameServer{Status: GameServerStatus{State: test.state}} 1750 assert.Equal(t, test.expected, gs.IsBeforeReady(), test.state) 1751 }) 1752 } 1753 } 1754 1755 func TestGameServerApplyToPodContainer(t *testing.T) { 1756 t.Parallel() 1757 type expected struct { 1758 err string 1759 tty bool 1760 } 1761 1762 testCases := []struct { 1763 description string 1764 gs *GameServer 1765 expected expected 1766 }{ 1767 { 1768 description: "OK, no error", 1769 gs: &GameServer{ 1770 Spec: GameServerSpec{ 1771 Container: "mycontainer", 1772 Template: corev1.PodTemplateSpec{ 1773 Spec: corev1.PodSpec{ 1774 Containers: []corev1.Container{ 1775 {Name: "mycontainer", Image: "foo/mycontainer"}, 1776 {Name: "notmycontainer", Image: "foo/notmycontainer"}, 1777 }, 1778 }, 1779 }, 1780 }, 1781 }, 1782 expected: expected{ 1783 err: "", 1784 tty: true, 1785 }, 1786 }, 1787 { 1788 description: "container not found, error is returned", 1789 gs: &GameServer{ 1790 Spec: GameServerSpec{ 1791 Container: "mycontainer-WRONG-NAME", 1792 Template: corev1.PodTemplateSpec{ 1793 Spec: corev1.PodSpec{ 1794 Containers: []corev1.Container{ 1795 {Name: "mycontainer", Image: "foo/mycontainer"}, 1796 {Name: "notmycontainer", Image: "foo/notmycontainer"}, 1797 }, 1798 }, 1799 }, 1800 }, 1801 }, 1802 expected: expected{ 1803 err: "failed to find container named mycontainer-WRONG-NAME in pod spec", 1804 tty: false, 1805 }, 1806 }, 1807 } 1808 1809 for _, tc := range testCases { 1810 t.Run(tc.description, func(t *testing.T) { 1811 pod := &corev1.Pod{Spec: *tc.gs.Spec.Template.Spec.DeepCopy()} 1812 result := tc.gs.ApplyToPodContainer(pod, tc.gs.Spec.Container, func(c corev1.Container) corev1.Container { 1813 // easy thing to change and test for 1814 c.TTY = true 1815 return c 1816 }) 1817 1818 if tc.expected.err != "" && assert.NotNil(t, result) { 1819 assert.Equal(t, tc.expected.err, result.Error()) 1820 } 1821 assert.Equal(t, tc.expected.tty, pod.Spec.Containers[0].TTY) 1822 assert.False(t, pod.Spec.Containers[1].TTY) 1823 }) 1824 } 1825 } 1826 1827 func defaultGameServer() *GameServer { 1828 return &GameServer{ 1829 ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", UID: "1234"}, 1830 Spec: GameServerSpec{ 1831 Ports: []GameServerPort{ 1832 { 1833 ContainerPort: 7777, 1834 HostPort: 9999, 1835 PortPolicy: Static, 1836 }, 1837 }, 1838 Template: corev1.PodTemplateSpec{ 1839 Spec: corev1.PodSpec{ 1840 Containers: []corev1.Container{{Name: "container", Image: "container/image"}}, 1841 }, 1842 }, 1843 }, Status: GameServerStatus{State: GameServerStateCreating}, 1844 } 1845 } 1846 1847 func TestGameServerUpdateCount(t *testing.T) { 1848 t.Parallel() 1849 1850 testCases := map[string]struct { 1851 gs GameServer 1852 name string 1853 action string 1854 amount int64 1855 want CounterStatus 1856 wantErr bool 1857 }{ 1858 "counter not in game server no-op and error": { 1859 gs: GameServer{Status: GameServerStatus{ 1860 Counters: map[string]CounterStatus{ 1861 "foos": { 1862 Count: 0, 1863 Capacity: 100, 1864 }, 1865 }, 1866 }}, 1867 name: "foo", 1868 action: "Increment", 1869 amount: 1, 1870 wantErr: true, 1871 }, 1872 "negative amount no-op and error": { 1873 gs: GameServer{Status: GameServerStatus{ 1874 Counters: map[string]CounterStatus{ 1875 "foos": { 1876 Count: 1, 1877 Capacity: 100, 1878 }, 1879 }, 1880 }}, 1881 name: "foos", 1882 action: "Decrement", 1883 amount: -1, 1884 want: CounterStatus{ 1885 Count: 1, 1886 Capacity: 100, 1887 }, 1888 wantErr: true, 1889 }, 1890 "increment by 1": { 1891 gs: GameServer{Status: GameServerStatus{ 1892 Counters: map[string]CounterStatus{ 1893 "players": { 1894 Count: 0, 1895 Capacity: 100, 1896 }, 1897 }, 1898 }}, 1899 name: "players", 1900 action: "Increment", 1901 amount: 1, 1902 want: CounterStatus{ 1903 Count: 1, 1904 Capacity: 100, 1905 }, 1906 wantErr: false, 1907 }, 1908 "decrement by 10": { 1909 gs: GameServer{Status: GameServerStatus{ 1910 Counters: map[string]CounterStatus{ 1911 "bars": { 1912 Count: 99, 1913 Capacity: 100, 1914 }, 1915 }, 1916 }}, 1917 name: "bars", 1918 action: "Decrement", 1919 amount: 10, 1920 want: CounterStatus{ 1921 Count: 89, 1922 Capacity: 100, 1923 }, 1924 wantErr: false, 1925 }, 1926 "incorrect action no-op and error": { 1927 gs: GameServer{Status: GameServerStatus{ 1928 Counters: map[string]CounterStatus{ 1929 "bazes": { 1930 Count: 99, 1931 Capacity: 100, 1932 }, 1933 }, 1934 }}, 1935 name: "bazes", 1936 action: "decrement", 1937 amount: 10, 1938 want: CounterStatus{ 1939 Count: 99, 1940 Capacity: 100, 1941 }, 1942 wantErr: true, 1943 }, 1944 "decrement beyond zero truncated": { 1945 gs: GameServer{Status: GameServerStatus{ 1946 Counters: map[string]CounterStatus{ 1947 "baz": { 1948 Count: 99, 1949 Capacity: 100, 1950 }, 1951 }, 1952 }}, 1953 name: "baz", 1954 action: "Decrement", 1955 amount: 100, 1956 want: CounterStatus{ 1957 Count: 0, 1958 Capacity: 100, 1959 }, 1960 wantErr: false, 1961 }, 1962 "increment beyond capacity truncated": { 1963 gs: GameServer{Status: GameServerStatus{ 1964 Counters: map[string]CounterStatus{ 1965 "splayers": { 1966 Count: 99, 1967 Capacity: 100, 1968 }, 1969 }, 1970 }}, 1971 name: "splayers", 1972 action: "Increment", 1973 amount: 2, 1974 want: CounterStatus{ 1975 Count: 100, 1976 Capacity: 100, 1977 }, 1978 wantErr: false, 1979 }, 1980 } 1981 1982 for test, testCase := range testCases { 1983 t.Run(test, func(t *testing.T) { 1984 err := testCase.gs.UpdateCount(testCase.name, testCase.action, testCase.amount) 1985 if err != nil { 1986 assert.True(t, testCase.wantErr) 1987 } else { 1988 assert.False(t, testCase.wantErr) 1989 } 1990 if counter, ok := testCase.gs.Status.Counters[testCase.name]; ok { 1991 assert.Equal(t, testCase.want, counter) 1992 } 1993 }) 1994 } 1995 } 1996 1997 func TestGameServerUpdateCounterCapacity(t *testing.T) { 1998 t.Parallel() 1999 2000 testCases := map[string]struct { 2001 gs GameServer 2002 name string 2003 capacity int64 2004 want CounterStatus 2005 wantErr bool 2006 }{ 2007 "counter not in game server no-op with error": { 2008 gs: GameServer{Status: GameServerStatus{ 2009 Counters: map[string]CounterStatus{ 2010 "foos": { 2011 Count: 0, 2012 Capacity: 100, 2013 }, 2014 }, 2015 }}, 2016 name: "foo", 2017 capacity: 1000, 2018 wantErr: true, 2019 }, 2020 "capacity less than zero no-op with error": { 2021 gs: GameServer{Status: GameServerStatus{ 2022 Counters: map[string]CounterStatus{ 2023 "foos": { 2024 Count: 0, 2025 Capacity: 100, 2026 }, 2027 }, 2028 }}, 2029 name: "foos", 2030 capacity: -1000, 2031 want: CounterStatus{ 2032 Count: 0, 2033 Capacity: 100, 2034 }, 2035 wantErr: true, 2036 }, 2037 "update capacity": { 2038 gs: GameServer{Status: GameServerStatus{ 2039 Counters: map[string]CounterStatus{ 2040 "sessions": { 2041 Count: 0, 2042 Capacity: 100, 2043 }, 2044 }, 2045 }}, 2046 name: "sessions", 2047 capacity: 9223372036854775807, 2048 want: CounterStatus{ 2049 Count: 0, 2050 Capacity: 9223372036854775807, 2051 }, 2052 }, 2053 } 2054 2055 for test, testCase := range testCases { 2056 t.Run(test, func(t *testing.T) { 2057 err := testCase.gs.UpdateCounterCapacity(testCase.name, testCase.capacity) 2058 if err != nil { 2059 assert.True(t, testCase.wantErr) 2060 } else { 2061 assert.False(t, testCase.wantErr) 2062 } 2063 if counter, ok := testCase.gs.Status.Counters[testCase.name]; ok { 2064 assert.Equal(t, testCase.want, counter) 2065 } 2066 }) 2067 } 2068 } 2069 2070 func TestGameServerUpdateListCapacity(t *testing.T) { 2071 t.Parallel() 2072 2073 testCases := map[string]struct { 2074 gs GameServer 2075 name string 2076 capacity int64 2077 want ListStatus 2078 wantErr bool 2079 }{ 2080 "list not in game server no-op with error": { 2081 gs: GameServer{Status: GameServerStatus{ 2082 Lists: map[string]ListStatus{ 2083 "things": { 2084 Values: []string{}, 2085 Capacity: 100, 2086 }, 2087 }, 2088 }}, 2089 name: "thing", 2090 capacity: 1000, 2091 wantErr: true, 2092 }, 2093 "update list capacity": { 2094 gs: GameServer{Status: GameServerStatus{ 2095 Lists: map[string]ListStatus{ 2096 "things": { 2097 Values: []string{}, 2098 Capacity: 100, 2099 }, 2100 }, 2101 }}, 2102 name: "things", 2103 capacity: 1000, 2104 want: ListStatus{ 2105 Values: []string{}, 2106 Capacity: 1000, 2107 }, 2108 wantErr: false, 2109 }, 2110 "list capacity above max no-op with error": { 2111 gs: GameServer{Status: GameServerStatus{ 2112 Lists: map[string]ListStatus{ 2113 "slings": { 2114 Values: []string{}, 2115 Capacity: 100, 2116 }, 2117 }, 2118 }}, 2119 name: "slings", 2120 capacity: 10000, 2121 want: ListStatus{ 2122 Values: []string{}, 2123 Capacity: 100, 2124 }, 2125 wantErr: true, 2126 }, 2127 "list capacity less than zero no-op with error": { 2128 gs: GameServer{Status: GameServerStatus{ 2129 Lists: map[string]ListStatus{ 2130 "flings": { 2131 Values: []string{}, 2132 Capacity: 999, 2133 }, 2134 }, 2135 }}, 2136 name: "flings", 2137 capacity: -100, 2138 want: ListStatus{ 2139 Values: []string{}, 2140 Capacity: 999, 2141 }, 2142 wantErr: true, 2143 }, 2144 } 2145 2146 for test, testCase := range testCases { 2147 t.Run(test, func(t *testing.T) { 2148 err := testCase.gs.UpdateListCapacity(testCase.name, testCase.capacity) 2149 if err != nil { 2150 assert.True(t, testCase.wantErr) 2151 } else { 2152 assert.False(t, testCase.wantErr) 2153 } 2154 if list, ok := testCase.gs.Status.Lists[testCase.name]; ok { 2155 assert.Equal(t, testCase.want, list) 2156 } 2157 }) 2158 } 2159 } 2160 2161 func TestGameServerAppendListValues(t *testing.T) { 2162 t.Parallel() 2163 2164 var nilSlice []string 2165 2166 testCases := map[string]struct { 2167 gs GameServer 2168 name string 2169 values []string 2170 want ListStatus 2171 wantErr bool 2172 }{ 2173 "list not in game server no-op with error": { 2174 gs: GameServer{Status: GameServerStatus{ 2175 Lists: map[string]ListStatus{ 2176 "things": { 2177 Values: []string{}, 2178 Capacity: 100, 2179 }, 2180 }, 2181 }}, 2182 name: "thing", 2183 values: []string{"thing1", "thing2", "thing3"}, 2184 wantErr: true, 2185 }, 2186 "append values": { 2187 gs: GameServer{Status: GameServerStatus{ 2188 Lists: map[string]ListStatus{ 2189 "things": { 2190 Values: []string{"thing1"}, 2191 Capacity: 100, 2192 }, 2193 }, 2194 }}, 2195 name: "things", 2196 values: []string{"thing2", "thing3"}, 2197 want: ListStatus{ 2198 Values: []string{"thing1", "thing2", "thing3"}, 2199 Capacity: 100, 2200 }, 2201 wantErr: false, 2202 }, 2203 "append values with silent drop of duplicates": { 2204 gs: GameServer{Status: GameServerStatus{ 2205 Lists: map[string]ListStatus{ 2206 "games": { 2207 Values: []string{"game0"}, 2208 Capacity: 10, 2209 }, 2210 }, 2211 }}, 2212 name: "games", 2213 values: []string{"game1", "game2", "game2", "game1"}, 2214 want: ListStatus{ 2215 Values: []string{"game0", "game1", "game2"}, 2216 Capacity: 10, 2217 }, 2218 wantErr: false, 2219 }, 2220 "append values with silent drop of duplicates in original list": { 2221 gs: GameServer{Status: GameServerStatus{ 2222 Lists: map[string]ListStatus{ 2223 "objects": { 2224 Values: []string{"object1", "object2"}, 2225 Capacity: 10, 2226 }, 2227 }, 2228 }}, 2229 name: "objects", 2230 values: []string{"object2", "object1", "object3", "object3"}, 2231 want: ListStatus{ 2232 Values: []string{"object1", "object2", "object3"}, 2233 Capacity: 10, 2234 }, 2235 wantErr: false, 2236 }, 2237 "append nil values": { 2238 gs: GameServer{Status: GameServerStatus{ 2239 Lists: map[string]ListStatus{ 2240 "blings": { 2241 Values: []string{"bling1"}, 2242 Capacity: 10, 2243 }, 2244 }, 2245 }}, 2246 name: "blings", 2247 values: nilSlice, 2248 want: ListStatus{ 2249 Values: []string{"bling1"}, 2250 Capacity: 10, 2251 }, 2252 wantErr: true, 2253 }, 2254 "append too many values truncates list": { 2255 gs: GameServer{Status: GameServerStatus{ 2256 Lists: map[string]ListStatus{ 2257 "bananaslugs": { 2258 Values: []string{"bananaslugs1", "bananaslug2", "bananaslug3"}, 2259 Capacity: 5, 2260 }, 2261 }, 2262 }}, 2263 name: "bananaslugs", 2264 values: []string{"bananaslug4", "bananaslug5", "bananaslug6"}, 2265 want: ListStatus{ 2266 Values: []string{"bananaslugs1", "bananaslug2", "bananaslug3", "bananaslug4", "bananaslug5"}, 2267 Capacity: 5, 2268 }, 2269 wantErr: false, 2270 }, 2271 } 2272 2273 for test, testCase := range testCases { 2274 t.Run(test, func(t *testing.T) { 2275 err := testCase.gs.AppendListValues(testCase.name, testCase.values) 2276 if err != nil { 2277 assert.True(t, testCase.wantErr) 2278 } else { 2279 assert.False(t, testCase.wantErr) 2280 } 2281 if list, ok := testCase.gs.Status.Lists[testCase.name]; ok { 2282 assert.Equal(t, testCase.want, list) 2283 } 2284 }) 2285 } 2286 } 2287 2288 func TestGameServerDeleteListValues(t *testing.T) { 2289 t.Parallel() 2290 2291 testCases := map[string]struct { 2292 gs GameServer 2293 name string 2294 want ListStatus 2295 values []string 2296 wantErr bool 2297 }{ 2298 "list not in game server no-op and error": { 2299 gs: GameServer{Status: GameServerStatus{ 2300 Lists: map[string]ListStatus{ 2301 "foos": { 2302 Values: []string{"foo", "bar", "bax"}, 2303 Capacity: 100, 2304 }, 2305 }, 2306 }}, 2307 name: "foo", 2308 values: []string{"bar", "baz"}, 2309 wantErr: true, 2310 }, 2311 "delete list value - one value not present": { 2312 gs: GameServer{Status: GameServerStatus{ 2313 Lists: map[string]ListStatus{ 2314 "foo": { 2315 Values: []string{"foo", "bar", "bax"}, 2316 Capacity: 100, 2317 }, 2318 }, 2319 }}, 2320 name: "foo", 2321 values: []string{"bar", "baz"}, 2322 wantErr: false, 2323 want: ListStatus{ 2324 Values: []string{"foo", "bax"}, 2325 Capacity: 100, 2326 }, 2327 }, 2328 } 2329 2330 for test, testCase := range testCases { 2331 t.Run(test, func(t *testing.T) { 2332 err := testCase.gs.DeleteListValues(testCase.name, testCase.values) 2333 if err != nil { 2334 assert.True(t, testCase.wantErr) 2335 } else { 2336 assert.False(t, testCase.wantErr) 2337 } 2338 if list, ok := testCase.gs.Status.Lists[testCase.name]; ok { 2339 assert.Equal(t, testCase.want, list) 2340 } 2341 }) 2342 } 2343 } 2344 2345 func TestMergeRemoveDuplicates(t *testing.T) { 2346 t.Parallel() 2347 2348 testCases := map[string]struct { 2349 str1 []string 2350 str2 []string 2351 want []string 2352 }{ 2353 "empty string arrays": { 2354 str1: []string{}, 2355 str2: []string{}, 2356 want: []string{}, 2357 }, 2358 "no duplicates": { 2359 str1: []string{"one"}, 2360 str2: []string{"two", "three"}, 2361 want: []string{"one", "two", "three"}, 2362 }, 2363 "remove one duplicate": { 2364 str1: []string{"one", "one", "one"}, 2365 str2: []string{"one", "one", "one"}, 2366 want: []string{"one"}, 2367 }, 2368 "remove multiple duplicates": { 2369 str1: []string{"one", "two"}, 2370 str2: []string{"two", "one"}, 2371 want: []string{"one", "two"}, 2372 }, 2373 } 2374 2375 for test, testCase := range testCases { 2376 t.Run(test, func(t *testing.T) { 2377 got := MergeRemoveDuplicates(testCase.str1, testCase.str2) 2378 assert.Equal(t, testCase.want, got) 2379 }) 2380 } 2381 }