agones.dev/agones@v1.53.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.Invalid(field.NewPath("spec", "template", "metadata", "labels"), longNameLen64, "name part must be no more than 63 characters"), 611 }, 612 }, 613 { 614 description: "Long label value is invalid", 615 gs: GameServer{ 616 ObjectMeta: metav1.ObjectMeta{ 617 GenerateName: "ok-name", 618 }, 619 TypeMeta: metav1.TypeMeta{ 620 Kind: "test-kind", 621 }, 622 Spec: GameServerSpec{ 623 Container: "my_image", 624 Template: corev1.PodTemplateSpec{ 625 Spec: corev1.PodSpec{ 626 Containers: []corev1.Container{ 627 {Name: "my_image", Image: "foo/my_image"}, 628 }, 629 }, 630 ObjectMeta: metav1.ObjectMeta{ 631 Labels: map[string]string{"agones.dev/longValueKey": longNameLen64}, 632 }, 633 }, 634 }, 635 }, 636 applyDefaults: false, 637 want: field.ErrorList{ 638 field.Invalid(field.NewPath("spec", "template", "metadata", "labels"), longNameLen64, "must be no more than 63 characters"), 639 }, 640 }, 641 { 642 description: "Long annotation key is invalid", 643 gs: GameServer{ 644 ObjectMeta: metav1.ObjectMeta{ 645 GenerateName: "ok-name", 646 }, 647 TypeMeta: metav1.TypeMeta{ 648 Kind: "test-kind", 649 }, 650 Spec: GameServerSpec{ 651 Container: "my_image", 652 Template: corev1.PodTemplateSpec{ 653 Spec: corev1.PodSpec{ 654 Containers: []corev1.Container{ 655 {Name: "my_image", Image: "foo/my_image"}, 656 }, 657 }, 658 ObjectMeta: metav1.ObjectMeta{ 659 Annotations: map[string]string{longNameLen64: longNameLen64}, 660 }, 661 }, 662 }, 663 }, 664 applyDefaults: false, 665 want: field.ErrorList{ 666 field.Invalid(field.NewPath("spec", "template", "metadata", "annotations"), longNameLen64, "name part must be no more than 63 characters"), 667 }, 668 }, 669 { 670 description: "Invalid character in annotation key", 671 gs: GameServer{ 672 ObjectMeta: metav1.ObjectMeta{ 673 GenerateName: "ok-name", 674 }, 675 TypeMeta: metav1.TypeMeta{ 676 Kind: "test-kind", 677 }, 678 Spec: GameServerSpec{ 679 Container: "my_image", 680 Template: corev1.PodTemplateSpec{ 681 Spec: corev1.PodSpec{ 682 Containers: []corev1.Container{ 683 {Name: "my_image", Image: "foo/my_image"}, 684 }, 685 }, 686 ObjectMeta: metav1.ObjectMeta{ 687 Annotations: map[string]string{"agones.dev/short±Name": longNameLen64}, 688 }, 689 }, 690 }, 691 }, 692 applyDefaults: false, 693 want: field.ErrorList{ 694 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]')"), 695 }, 696 }, 697 { 698 description: "Valid annotation key", 699 gs: GameServer{ 700 ObjectMeta: metav1.ObjectMeta{ 701 GenerateName: "ok-name", 702 }, 703 TypeMeta: metav1.TypeMeta{ 704 Kind: "test-kind", 705 }, 706 Spec: GameServerSpec{ 707 Container: "my_image", 708 Template: corev1.PodTemplateSpec{ 709 Spec: corev1.PodSpec{ 710 Containers: []corev1.Container{ 711 {Name: "my_image", Image: "foo/my_image"}, 712 }, 713 }, 714 ObjectMeta: metav1.ObjectMeta{ 715 Annotations: map[string]string{"agones.dev/shortName": longNameLen64}, 716 }, 717 }, 718 }, 719 }, 720 applyDefaults: false, 721 }, 722 { 723 description: "Check ContainerPort and HostPort with different policies", 724 gs: GameServer{ 725 ObjectMeta: metav1.ObjectMeta{ 726 GenerateName: "ok-name", 727 }, 728 TypeMeta: metav1.TypeMeta{ 729 Kind: "test-kind", 730 }, 731 Spec: GameServerSpec{ 732 Ports: []GameServerPort{ 733 {Name: "one", PortPolicy: Passthrough, ContainerPort: 1294}, 734 {PortPolicy: Passthrough, Name: "two", HostPort: 7890}, 735 {PortPolicy: Dynamic, Name: "three", HostPort: 7890, ContainerPort: 1294}, 736 }, 737 Container: "my_image", 738 Template: corev1.PodTemplateSpec{ 739 Spec: corev1.PodSpec{ 740 Containers: []corev1.Container{ 741 {Name: "my_image", Image: "foo/my_image"}, 742 }, 743 }, 744 }, 745 }, 746 }, 747 applyDefaults: true, 748 want: field.ErrorList{ 749 field.Required(field.NewPath("spec", "ports").Index(0).Child("containerPort"), "ContainerPort cannot be specified with Passthrough PortPolicy"), 750 field.Forbidden(field.NewPath("spec", "ports").Index(1).Child("hostPort"), "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy"), 751 field.Forbidden(field.NewPath("spec", "ports").Index(2).Child("hostPort"), "HostPort cannot be specified with a Dynamic or Passthrough PortPolicy"), 752 }, 753 }, 754 { 755 description: "PortPolicy must be Static with HostPort specified", 756 gs: GameServer{ 757 ObjectMeta: metav1.ObjectMeta{ 758 Name: "dev-game", 759 Namespace: "default", 760 Annotations: map[string]string{DevAddressAnnotation: ipFixture}, 761 }, 762 Spec: GameServerSpec{ 763 Ports: []GameServerPort{ 764 {PortPolicy: Passthrough, Name: "main", HostPort: 7890, ContainerPort: 7777}, 765 }, 766 Container: "my_image", 767 Template: corev1.PodTemplateSpec{ 768 Spec: corev1.PodSpec{ 769 Containers: []corev1.Container{ 770 {Name: "my_image", Image: "foo/my_image"}, 771 }, 772 }, 773 }, 774 }, 775 }, 776 applyDefaults: true, 777 want: field.ErrorList{ 778 field.Required( 779 field.NewPath("spec", "ports").Index(0).Child("portPolicy"), 780 "PortPolicy must be Static"), 781 }, 782 }, 783 { 784 description: "ContainerPort is less than zero", 785 gs: GameServer{ 786 ObjectMeta: metav1.ObjectMeta{ 787 Name: "dev-game", 788 Namespace: "default", 789 }, 790 Spec: GameServerSpec{ 791 Ports: []GameServerPort{{ 792 Name: "main", 793 ContainerPort: -4, 794 PortPolicy: Dynamic, 795 }}, 796 Container: "testing", 797 Template: corev1.PodTemplateSpec{ 798 Spec: corev1.PodSpec{Containers: []corev1.Container{ 799 {Name: "testing", Image: "testing/image"}, 800 }}, 801 }, 802 }, 803 }, 804 applyDefaults: false, 805 want: field.ErrorList{ 806 field.Required( 807 field.NewPath("spec", "ports").Index(0).Child("containerPort"), 808 "ContainerPort must be defined for Dynamic and Static PortPolicies", 809 ), 810 }, 811 }, 812 { 813 description: "CPU Request > Limit", 814 gs: GameServer{ 815 ObjectMeta: metav1.ObjectMeta{ 816 Name: "dev-game", 817 Namespace: "default", 818 }, 819 Spec: GameServerSpec{ 820 Ports: []GameServerPort{{ 821 Name: "main", 822 ContainerPort: 7777, 823 PortPolicy: Dynamic, 824 }}, 825 Container: "testing", 826 Template: corev1.PodTemplateSpec{ 827 Spec: corev1.PodSpec{Containers: []corev1.Container{ 828 { 829 Name: "testing", 830 Image: "testing/image", 831 Resources: corev1.ResourceRequirements{ 832 Requests: corev1.ResourceList{ 833 corev1.ResourceCPU: resource.MustParse("50m"), 834 corev1.ResourceMemory: resource.MustParse("32Mi"), 835 }, 836 Limits: corev1.ResourceList{ 837 corev1.ResourceCPU: resource.MustParse("30m"), 838 corev1.ResourceMemory: resource.MustParse("32Mi"), 839 }, 840 }, 841 }, 842 }}, 843 }, 844 }, 845 }, 846 applyDefaults: false, 847 want: field.ErrorList{ 848 field.Invalid( 849 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"), 850 "50m", 851 "must be less than or equal to cpu limit of 30m", 852 ), 853 }, 854 }, 855 { 856 description: "CPU negative request", 857 gs: GameServer{ 858 ObjectMeta: metav1.ObjectMeta{ 859 Name: "dev-game", 860 Namespace: "default", 861 }, 862 Spec: GameServerSpec{ 863 Ports: []GameServerPort{{ 864 Name: "main", 865 ContainerPort: 7777, 866 PortPolicy: Dynamic, 867 }}, 868 Container: "testing", 869 Template: corev1.PodTemplateSpec{ 870 Spec: corev1.PodSpec{Containers: []corev1.Container{ 871 { 872 Name: "testing", 873 Image: "testing/image", 874 Resources: corev1.ResourceRequirements{ 875 Requests: corev1.ResourceList{ 876 corev1.ResourceCPU: resource.MustParse("-30m"), 877 corev1.ResourceMemory: resource.MustParse("32Mi"), 878 }, 879 Limits: corev1.ResourceList{ 880 corev1.ResourceCPU: resource.MustParse("30m"), 881 corev1.ResourceMemory: resource.MustParse("32Mi"), 882 }, 883 }, 884 }, 885 }}, 886 }, 887 }, 888 }, 889 applyDefaults: false, 890 want: field.ErrorList{ 891 field.Invalid( 892 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests").Key("cpu"), 893 "-30m", 894 "must be greater than or equal to 0", 895 ), 896 }, 897 }, 898 { 899 description: "CPU negative limit", 900 gs: GameServer{ 901 ObjectMeta: metav1.ObjectMeta{ 902 Name: "dev-game", 903 Namespace: "default", 904 }, 905 Spec: GameServerSpec{ 906 Ports: []GameServerPort{{ 907 Name: "main", 908 ContainerPort: 7777, 909 PortPolicy: Dynamic, 910 }}, 911 Container: "testing", 912 Template: corev1.PodTemplateSpec{ 913 Spec: corev1.PodSpec{Containers: []corev1.Container{ 914 { 915 Name: "testing", 916 Image: "testing/image", 917 Resources: corev1.ResourceRequirements{ 918 Requests: corev1.ResourceList{ 919 corev1.ResourceCPU: resource.MustParse("30m"), 920 corev1.ResourceMemory: resource.MustParse("32Mi"), 921 }, 922 Limits: corev1.ResourceList{ 923 corev1.ResourceCPU: resource.MustParse("-30m"), 924 corev1.ResourceMemory: resource.MustParse("32Mi"), 925 }, 926 }, 927 }, 928 }}, 929 }, 930 }, 931 }, 932 applyDefaults: false, 933 want: field.ErrorList{ 934 field.Invalid( 935 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "limits").Key("cpu"), 936 "-30m", 937 "must be greater than or equal to 0", 938 ), 939 field.Invalid( 940 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"), 941 "30m", 942 "must be less than or equal to cpu limit of -30m", 943 ), 944 }, 945 }, 946 { 947 description: "Memory Request > Limit", 948 gs: GameServer{ 949 ObjectMeta: metav1.ObjectMeta{ 950 Name: "dev-game", 951 Namespace: "default", 952 }, 953 Spec: GameServerSpec{ 954 Ports: []GameServerPort{{ 955 Name: "main", 956 ContainerPort: 7777, 957 PortPolicy: Dynamic, 958 }}, 959 Container: "testing", 960 Template: corev1.PodTemplateSpec{ 961 Spec: corev1.PodSpec{Containers: []corev1.Container{ 962 { 963 Name: "testing", 964 Image: "testing/image", 965 Resources: corev1.ResourceRequirements{ 966 Requests: corev1.ResourceList{ 967 corev1.ResourceCPU: resource.MustParse("30m"), 968 corev1.ResourceMemory: resource.MustParse("55Mi"), 969 }, 970 Limits: corev1.ResourceList{ 971 corev1.ResourceCPU: resource.MustParse("30m"), 972 corev1.ResourceMemory: resource.MustParse("32Mi"), 973 }, 974 }, 975 }, 976 }}, 977 }, 978 }, 979 }, 980 applyDefaults: false, 981 want: field.ErrorList{ 982 field.Invalid( 983 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"), 984 "55Mi", 985 "must be less than or equal to memory limit of 32Mi", 986 ), 987 }, 988 }, 989 { 990 description: "Memory negative request", 991 gs: GameServer{ 992 ObjectMeta: metav1.ObjectMeta{ 993 Name: "dev-game", 994 Namespace: "default", 995 }, 996 Spec: GameServerSpec{ 997 Ports: []GameServerPort{{ 998 Name: "main", 999 ContainerPort: 7777, 1000 PortPolicy: Dynamic, 1001 }}, 1002 Container: "testing", 1003 Template: corev1.PodTemplateSpec{ 1004 Spec: corev1.PodSpec{Containers: []corev1.Container{ 1005 { 1006 Name: "testing", 1007 Image: "testing/image", 1008 Resources: corev1.ResourceRequirements{ 1009 Requests: corev1.ResourceList{ 1010 corev1.ResourceCPU: resource.MustParse("30m"), 1011 corev1.ResourceMemory: resource.MustParse("-32Mi"), 1012 }, 1013 Limits: corev1.ResourceList{ 1014 corev1.ResourceCPU: resource.MustParse("30m"), 1015 corev1.ResourceMemory: resource.MustParse("32Mi"), 1016 }, 1017 }, 1018 }, 1019 }}, 1020 }, 1021 }, 1022 }, 1023 applyDefaults: false, 1024 want: field.ErrorList{ 1025 field.Invalid( 1026 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests").Key("memory"), 1027 "-32Mi", 1028 "must be greater than or equal to 0", 1029 ), 1030 }, 1031 }, 1032 { 1033 description: "Memory negative limit", 1034 gs: GameServer{ 1035 ObjectMeta: metav1.ObjectMeta{ 1036 Name: "dev-game", 1037 Namespace: "default", 1038 }, 1039 Spec: GameServerSpec{ 1040 Ports: []GameServerPort{{ 1041 Name: "main", 1042 ContainerPort: 7777, 1043 PortPolicy: Dynamic, 1044 }}, 1045 Container: "testing", 1046 Template: corev1.PodTemplateSpec{ 1047 Spec: corev1.PodSpec{Containers: []corev1.Container{ 1048 { 1049 Name: "testing", 1050 Image: "testing/image", 1051 Resources: corev1.ResourceRequirements{ 1052 Requests: corev1.ResourceList{ 1053 corev1.ResourceCPU: resource.MustParse("30m"), 1054 corev1.ResourceMemory: resource.MustParse("32Mi"), 1055 }, 1056 Limits: corev1.ResourceList{ 1057 corev1.ResourceCPU: resource.MustParse("30m"), 1058 corev1.ResourceMemory: resource.MustParse("-32Mi"), 1059 }, 1060 }, 1061 }, 1062 }}, 1063 }, 1064 }, 1065 }, 1066 applyDefaults: false, 1067 want: field.ErrorList{ 1068 field.Invalid( 1069 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "limits").Key("memory"), 1070 "-32Mi", 1071 "must be greater than or equal to 0", 1072 ), 1073 field.Invalid( 1074 field.NewPath("spec", "template", "spec", "containers").Index(0).Child("resources", "requests"), 1075 "32Mi", 1076 "must be less than or equal to memory limit of -32Mi", 1077 ), 1078 }, 1079 }, 1080 } 1081 1082 for _, tc := range testCases { 1083 t.Run(tc.description, func(t *testing.T) { 1084 if tc.applyDefaults { 1085 tc.gs.ApplyDefaults() 1086 } 1087 1088 errs := tc.gs.Validate(fakeAPIHooks{}) 1089 assert.ElementsMatch(t, tc.want, errs, "ErrorList check") 1090 }) 1091 } 1092 } 1093 1094 func TestGameServerValidateFeatures(t *testing.T) { 1095 t.Parallel() 1096 runtime.FeatureTestMutex.Lock() 1097 defer runtime.FeatureTestMutex.Unlock() 1098 1099 portContainerName := "another-container" 1100 1101 testCases := []struct { 1102 description string 1103 feature string 1104 gs GameServer 1105 want field.ErrorList 1106 }{ 1107 { 1108 description: "Invalid container name, container was not found", 1109 gs: GameServer{ 1110 ObjectMeta: metav1.ObjectMeta{ 1111 Name: "dev-game", 1112 Namespace: "default", 1113 }, 1114 Spec: GameServerSpec{ 1115 Ports: []GameServerPort{ 1116 { 1117 Name: "main", 1118 ContainerPort: 7777, 1119 PortPolicy: Dynamic, 1120 Container: &portContainerName, 1121 }, 1122 }, 1123 Container: "testing", 1124 Template: corev1.PodTemplateSpec{ 1125 Spec: corev1.PodSpec{Containers: []corev1.Container{ 1126 {Name: "testing", Image: "testing/image"}, 1127 }}, 1128 }, 1129 }, 1130 }, 1131 want: field.ErrorList{ 1132 field.Invalid( 1133 field.NewPath("spec", "ports").Index(0).Child("container"), 1134 "another-container", 1135 "Container must be empty or the name of a container in the pod template", 1136 ), 1137 }, 1138 }, 1139 { 1140 description: "Multiple container ports, OK scenario", 1141 gs: GameServer{ 1142 ObjectMeta: metav1.ObjectMeta{ 1143 Name: "dev-game", 1144 Namespace: "default", 1145 }, 1146 Spec: GameServerSpec{ 1147 Ports: []GameServerPort{ 1148 { 1149 Name: "main", 1150 ContainerPort: 7777, 1151 PortPolicy: Dynamic, 1152 }, 1153 }, 1154 Container: "testing", 1155 Template: corev1.PodTemplateSpec{ 1156 Spec: corev1.PodSpec{Containers: []corev1.Container{ 1157 {Name: "testing", Image: "testing/image"}, 1158 }}, 1159 }, 1160 }, 1161 }, 1162 }, 1163 { 1164 description: "PlayerTracking is disabled, Players field specified", 1165 feature: fmt.Sprintf("%s=false", runtime.FeaturePlayerTracking), 1166 gs: GameServer{ 1167 Spec: GameServerSpec{ 1168 Container: "testing", 1169 Players: &PlayersSpec{InitialCapacity: 10}, 1170 Template: corev1.PodTemplateSpec{ 1171 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1172 }, 1173 }, 1174 }, 1175 want: field.ErrorList{ 1176 field.Forbidden( 1177 field.NewPath("spec", "players"), 1178 "Value cannot be set unless feature flag PlayerTracking is enabled", 1179 ), 1180 }, 1181 }, 1182 { 1183 description: "PlayerTracking is enabled, Players field specified", 1184 feature: fmt.Sprintf("%s=true", runtime.FeaturePlayerTracking), 1185 gs: GameServer{ 1186 Spec: GameServerSpec{ 1187 Container: "testing", 1188 Players: &PlayersSpec{InitialCapacity: 10}, 1189 Template: corev1.PodTemplateSpec{ 1190 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1191 }, 1192 }, 1193 }, 1194 }, 1195 { 1196 description: "CountsAndLists is disabled, Counters field specified", 1197 feature: fmt.Sprintf("%s=false", runtime.FeatureCountsAndLists), 1198 gs: GameServer{ 1199 Spec: GameServerSpec{ 1200 Container: "testing", 1201 Counters: map[string]CounterStatus{}, 1202 Template: corev1.PodTemplateSpec{ 1203 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1204 }, 1205 }, 1206 }, 1207 want: field.ErrorList{ 1208 field.Forbidden( 1209 field.NewPath("spec", "counters"), 1210 "Value cannot be set unless feature flag CountsAndLists is enabled", 1211 ), 1212 }, 1213 }, 1214 { 1215 description: "CountsAndLists is disabled, Lists field specified", 1216 feature: fmt.Sprintf("%s=false", runtime.FeatureCountsAndLists), 1217 gs: GameServer{ 1218 Spec: GameServerSpec{ 1219 Container: "testing", 1220 Lists: map[string]ListStatus{}, 1221 Template: corev1.PodTemplateSpec{ 1222 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1223 }, 1224 }, 1225 }, 1226 want: field.ErrorList{ 1227 field.Forbidden( 1228 field.NewPath("spec", "lists"), 1229 "Value cannot be set unless feature flag CountsAndLists is enabled", 1230 ), 1231 }, 1232 }, 1233 { 1234 description: "CountsAndLists is enabled, Counters field specified", 1235 feature: fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists), 1236 gs: GameServer{ 1237 Spec: GameServerSpec{ 1238 Container: "testing", 1239 Counters: map[string]CounterStatus{}, 1240 Template: corev1.PodTemplateSpec{ 1241 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1242 }, 1243 }, 1244 }, 1245 }, 1246 { 1247 description: "CountsAndLists is enabled, Lists field specified", 1248 feature: fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists), 1249 gs: GameServer{ 1250 Spec: GameServerSpec{ 1251 Container: "testing", 1252 Lists: map[string]ListStatus{}, 1253 Template: corev1.PodTemplateSpec{ 1254 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1255 }, 1256 }, 1257 }, 1258 }, 1259 { 1260 description: "PortPolicyNone is disabled, PortPolicy field set to None", 1261 feature: fmt.Sprintf("%s=false", runtime.FeaturePortPolicyNone), 1262 gs: GameServer{ 1263 Spec: GameServerSpec{ 1264 Ports: []GameServerPort{ 1265 { 1266 Name: "main", 1267 ContainerPort: 7777, 1268 PortPolicy: None, 1269 }, 1270 }, 1271 Container: "testing", 1272 Lists: map[string]ListStatus{}, 1273 Template: corev1.PodTemplateSpec{ 1274 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1275 }, 1276 }, 1277 }, 1278 want: field.ErrorList{ 1279 field.Forbidden( 1280 field.NewPath("spec.ports[0].portPolicy"), 1281 "Value cannot be set to None unless feature flag PortPolicyNone is enabled", 1282 ), 1283 }, 1284 }, 1285 { 1286 description: "PortPolicyNone is enabled, PortPolicy field set to None", 1287 feature: fmt.Sprintf("%s=true", runtime.FeaturePortPolicyNone), 1288 gs: GameServer{ 1289 Spec: GameServerSpec{ 1290 Ports: []GameServerPort{ 1291 { 1292 Name: "main", 1293 ContainerPort: 7777, 1294 PortPolicy: None, 1295 }, 1296 }, 1297 Container: "testing", 1298 Lists: map[string]ListStatus{}, 1299 Template: corev1.PodTemplateSpec{ 1300 Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "testing", Image: "testing/image"}}}, 1301 }, 1302 }, 1303 }, 1304 }, 1305 } 1306 1307 for _, tc := range testCases { 1308 t.Run(tc.description, func(t *testing.T) { 1309 err := runtime.ParseFeatures(tc.feature) 1310 assert.NoError(t, err) 1311 1312 errs := tc.gs.Validate(fakeAPIHooks{}) 1313 assert.ElementsMatch(t, tc.want, errs, "ErrorList check") 1314 }) 1315 } 1316 } 1317 1318 func TestGameServerPodNoErrors(t *testing.T) { 1319 t.Parallel() 1320 fixture := defaultGameServer() 1321 fixture.ApplyDefaults() 1322 1323 pod, err := fixture.Pod(fakeAPIHooks{}) 1324 assert.Nil(t, err, "Pod should not return an error") 1325 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name) 1326 assert.Equal(t, fixture.ObjectMeta.Name, pod.Spec.Hostname) 1327 assert.Equal(t, fixture.ObjectMeta.Namespace, pod.ObjectMeta.Namespace) 1328 assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"]) 1329 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Labels[GameServerPodLabel]) 1330 assert.Equal(t, fixture.Spec.Container, pod.ObjectMeta.Annotations[GameServerContainerAnnotation]) 1331 assert.True(t, metav1.IsControlledBy(pod, fixture)) 1332 assert.Equal(t, fixture.Spec.Ports[0].HostPort, pod.Spec.Containers[0].Ports[0].HostPort) 1333 assert.Equal(t, fixture.Spec.Ports[0].ContainerPort, pod.Spec.Containers[0].Ports[0].ContainerPort) 1334 assert.Equal(t, corev1.Protocol("UDP"), pod.Spec.Containers[0].Ports[0].Protocol) 1335 assert.True(t, metav1.IsControlledBy(pod, fixture)) 1336 } 1337 1338 func TestGameServerPodHostName(t *testing.T) { 1339 t.Parallel() 1340 1341 fixture := defaultGameServer() 1342 fixture.ObjectMeta.Name = "test-1.0" 1343 fixture.ApplyDefaults() 1344 pod, err := fixture.Pod(fakeAPIHooks{}) 1345 require.NoError(t, err) 1346 assert.Equal(t, "test-1-0", pod.Spec.Hostname) 1347 1348 fixture = defaultGameServer() 1349 fixture.ApplyDefaults() 1350 expected := "ORANGE" 1351 fixture.Spec.Template.Spec.Hostname = expected 1352 pod, err = fixture.Pod(fakeAPIHooks{}) 1353 require.NoError(t, err) 1354 assert.Equal(t, expected, pod.Spec.Hostname) 1355 } 1356 1357 func TestGameServerPodContainerNotFoundErrReturned(t *testing.T) { 1358 t.Parallel() 1359 1360 containerName1 := "Container1" 1361 fixture := &GameServer{ 1362 ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", UID: "1234"}, 1363 Spec: GameServerSpec{ 1364 Container: "can-not-find-this-name", 1365 Ports: []GameServerPort{ 1366 { 1367 Container: &containerName1, 1368 ContainerPort: 7777, 1369 HostPort: 9999, 1370 PortPolicy: Static, 1371 }, 1372 }, 1373 Template: corev1.PodTemplateSpec{ 1374 Spec: corev1.PodSpec{ 1375 Containers: []corev1.Container{{Name: "Container2", Image: "container/image"}}, 1376 }, 1377 }, 1378 }, Status: GameServerStatus{State: GameServerStateCreating}, 1379 } 1380 1381 _, err := fixture.Pod(fakeAPIHooks{}) 1382 if assert.NotNil(t, err, "Pod should return an error") { 1383 assert.Equal(t, "failed to find container named Container1 in pod spec", err.Error()) 1384 } 1385 } 1386 1387 func TestGameServerPodWithSidecarNoErrors(t *testing.T) { 1388 t.Parallel() 1389 runtime.FeatureTestMutex.Lock() 1390 defer runtime.FeatureTestMutex.Unlock() 1391 require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=false")) 1392 1393 fixture := defaultGameServer() 1394 fixture.ApplyDefaults() 1395 1396 sidecar := corev1.Container{Name: "sidecar", Image: "container/sidecar"} 1397 fixture.Spec.Template.Spec.ServiceAccountName = "other-agones-sdk" 1398 pod, err := fixture.Pod(fakeAPIHooks{}, sidecar) 1399 assert.Nil(t, err, "Pod should not return an error") 1400 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name) 1401 assert.Len(t, pod.Spec.Containers, 2, "Should have two containers") 1402 assert.Equal(t, "other-agones-sdk", pod.Spec.ServiceAccountName) 1403 assert.Equal(t, "sidecar", pod.Spec.Containers[0].Name) 1404 assert.Equal(t, "container", pod.Spec.Containers[1].Name) 1405 assert.True(t, metav1.IsControlledBy(pod, fixture)) 1406 } 1407 1408 func TestGameServerPodWithInitSidecarNoErrors(t *testing.T) { 1409 t.Parallel() 1410 runtime.FeatureTestMutex.Lock() 1411 defer runtime.FeatureTestMutex.Unlock() 1412 require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=true")) 1413 1414 fixture := defaultGameServer() 1415 fixture.ApplyDefaults() 1416 1417 sidecar := corev1.Container{Name: "sidecar", Image: "container/sidecar"} 1418 fixture.Spec.Template.Spec.ServiceAccountName = "other-agones-sdk" 1419 pod, err := fixture.Pod(fakeAPIHooks{}, sidecar) 1420 assert.Nil(t, err, "Pod should not return an error") 1421 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name) 1422 assert.Len(t, pod.Spec.Containers, 1, "Should have one containers") 1423 assert.Equal(t, "other-agones-sdk", pod.Spec.ServiceAccountName) 1424 assert.Equal(t, "sidecar", pod.Spec.InitContainers[0].Name) 1425 assert.Equal(t, corev1.ContainerRestartPolicyAlways, *pod.Spec.InitContainers[0].RestartPolicy) 1426 assert.Equal(t, "container", pod.Spec.Containers[0].Name) 1427 assert.True(t, metav1.IsControlledBy(pod, fixture)) 1428 assert.Equal(t, pod.Spec.RestartPolicy, corev1.RestartPolicyNever) 1429 } 1430 1431 func TestGameServerPodWithInitSidecarPrependsToExistingInitContainers(t *testing.T) { 1432 t.Parallel() 1433 runtime.FeatureTestMutex.Lock() 1434 defer runtime.FeatureTestMutex.Unlock() 1435 require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureSidecarContainers)+"=true")) 1436 1437 fixture := defaultGameServer() 1438 // Add existing init containers to the template 1439 existingInitContainer1 := corev1.Container{Name: "existing-init-1", Image: "existing/init1"} 1440 existingInitContainer2 := corev1.Container{Name: "existing-init-2", Image: "existing/init2"} 1441 fixture.Spec.Template.Spec.InitContainers = []corev1.Container{existingInitContainer1, existingInitContainer2} 1442 fixture.ApplyDefaults() 1443 1444 sidecar1 := corev1.Container{Name: "sidecar-1", Image: "container/sidecar1"} 1445 sidecar2 := corev1.Container{Name: "sidecar-2", Image: "container/sidecar2"} 1446 pod, err := fixture.Pod(fakeAPIHooks{}, sidecar1, sidecar2) 1447 require.NoError(t, err) 1448 1449 // Verify that sidecars are placed at the beginning of InitContainers 1450 assert.Len(t, pod.Spec.InitContainers, 4, "Should have four init containers") 1451 assert.Equal(t, "sidecar-1", pod.Spec.InitContainers[0].Name, "First sidecar should be at index 0") 1452 assert.Equal(t, "sidecar-2", pod.Spec.InitContainers[1].Name, "Second sidecar should be at index 1") 1453 assert.Equal(t, "existing-init-1", pod.Spec.InitContainers[2].Name, "First existing init container should be at index 2") 1454 assert.Equal(t, "existing-init-2", pod.Spec.InitContainers[3].Name, "Second existing init container should be at index 3") 1455 1456 // Verify restart policies 1457 assert.Equal(t, corev1.ContainerRestartPolicyAlways, *pod.Spec.InitContainers[0].RestartPolicy) 1458 assert.Equal(t, corev1.ContainerRestartPolicyAlways, *pod.Spec.InitContainers[1].RestartPolicy) 1459 } 1460 1461 func TestGameServerPodWithMultiplePortAllocations(t *testing.T) { 1462 fixture := defaultGameServer() 1463 containerName := "authContainer" 1464 fixture.Spec.Template.Spec.Containers = append(fixture.Spec.Template.Spec.Containers, corev1.Container{ 1465 Name: containerName, 1466 }) 1467 fixture.Spec.Ports = append(fixture.Spec.Ports, GameServerPort{ 1468 Name: "containerPort", 1469 PortPolicy: Dynamic, 1470 Container: &containerName, 1471 ContainerPort: 5000, 1472 }) 1473 fixture.Spec.Container = fixture.Spec.Template.Spec.Containers[0].Name 1474 fixture.ApplyDefaults() 1475 1476 pod, err := fixture.Pod(fakeAPIHooks{}) 1477 assert.NoError(t, err, "Pod should not return an error") 1478 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Name) 1479 assert.Equal(t, fixture.ObjectMeta.Namespace, pod.ObjectMeta.Namespace) 1480 assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"]) 1481 assert.Equal(t, fixture.ObjectMeta.Name, pod.ObjectMeta.Labels[GameServerPodLabel]) 1482 assert.Equal(t, fixture.Spec.Container, pod.ObjectMeta.Annotations[GameServerContainerAnnotation]) 1483 assert.Equal(t, fixture.Spec.Ports[0].HostPort, pod.Spec.Containers[0].Ports[0].HostPort) 1484 assert.Equal(t, fixture.Spec.Ports[0].ContainerPort, pod.Spec.Containers[0].Ports[0].ContainerPort) 1485 assert.Equal(t, *fixture.Spec.Ports[0].Container, pod.Spec.Containers[0].Name) 1486 assert.Equal(t, corev1.Protocol("UDP"), pod.Spec.Containers[0].Ports[0].Protocol) 1487 assert.True(t, metav1.IsControlledBy(pod, fixture)) 1488 assert.Equal(t, fixture.Spec.Ports[1].HostPort, pod.Spec.Containers[1].Ports[0].HostPort) 1489 assert.Equal(t, fixture.Spec.Ports[1].ContainerPort, pod.Spec.Containers[1].Ports[0].ContainerPort) 1490 assert.Equal(t, *fixture.Spec.Ports[1].Container, pod.Spec.Containers[1].Name) 1491 assert.Equal(t, corev1.Protocol("UDP"), pod.Spec.Containers[1].Ports[0].Protocol) 1492 } 1493 1494 func TestGameServerPodObjectMeta(t *testing.T) { 1495 fixture := &GameServer{ 1496 ObjectMeta: metav1.ObjectMeta{Name: "lucy"}, 1497 Spec: GameServerSpec{Container: "goat"}, 1498 } 1499 1500 for desc, tc := range map[string]struct { 1501 scheduling apis.SchedulingStrategy 1502 wantSafe string 1503 }{ 1504 "packed": { 1505 scheduling: apis.Packed, 1506 }, 1507 "distributed": { 1508 scheduling: apis.Distributed, 1509 }, 1510 } { 1511 t.Run(desc, func(t *testing.T) { 1512 gs := fixture.DeepCopy() 1513 gs.Spec.Scheduling = tc.scheduling 1514 pod := &corev1.Pod{} 1515 1516 gs.podObjectMeta(pod) 1517 1518 assert.Equal(t, gs.ObjectMeta.Name, pod.ObjectMeta.Name) 1519 assert.Equal(t, gs.ObjectMeta.Namespace, pod.ObjectMeta.Namespace) 1520 assert.Equal(t, GameServerLabelRole, pod.ObjectMeta.Labels[RoleLabel]) 1521 assert.Equal(t, "gameserver", pod.ObjectMeta.Labels[agones.GroupName+"/role"]) 1522 assert.Equal(t, gs.ObjectMeta.Name, pod.ObjectMeta.Labels[GameServerPodLabel]) 1523 assert.Equal(t, "goat", pod.ObjectMeta.Annotations[GameServerContainerAnnotation]) 1524 assert.True(t, metav1.IsControlledBy(pod, gs)) 1525 assert.Equal(t, tc.wantSafe, pod.ObjectMeta.Annotations[PodSafeToEvictAnnotation]) 1526 }) 1527 } 1528 } 1529 1530 func TestGameServerPodScheduling(t *testing.T) { 1531 fixture := &corev1.Pod{Spec: corev1.PodSpec{}} 1532 1533 t.Run("packed", func(t *testing.T) { 1534 gs := &GameServer{Spec: GameServerSpec{Scheduling: apis.Packed}} 1535 pod := fixture.DeepCopy() 1536 gs.podScheduling(pod) 1537 1538 assert.Len(t, pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution, 1) 1539 wpat := pod.Spec.Affinity.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0] 1540 assert.Equal(t, int32(100), wpat.Weight) 1541 assert.Contains(t, wpat.PodAffinityTerm.LabelSelector.String(), GameServerLabelRole) 1542 assert.Contains(t, wpat.PodAffinityTerm.LabelSelector.String(), RoleLabel) 1543 }) 1544 1545 t.Run("distributed", func(t *testing.T) { 1546 gs := &GameServer{Spec: GameServerSpec{Scheduling: apis.Distributed}} 1547 pod := fixture.DeepCopy() 1548 gs.podScheduling(pod) 1549 assert.Empty(t, pod.Spec.Affinity) 1550 }) 1551 } 1552 1553 func TestGameServerDisableServiceAccount(t *testing.T) { 1554 t.Parallel() 1555 1556 gs := &GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "1234"}, Spec: GameServerSpec{ 1557 Template: corev1.PodTemplateSpec{ 1558 Spec: corev1.PodSpec{ 1559 Containers: []corev1.Container{{Name: "container", Image: "container/image"}}, 1560 }, 1561 }, 1562 }} 1563 1564 gs.ApplyDefaults() 1565 pod, err := gs.Pod(fakeAPIHooks{}) 1566 assert.NoError(t, err) 1567 assert.Len(t, pod.Spec.Containers, 1) 1568 assert.Empty(t, pod.Spec.Containers[0].VolumeMounts) 1569 1570 err = gs.DisableServiceAccount(pod) 1571 assert.NoError(t, err) 1572 assert.Len(t, pod.Spec.Containers, 1) 1573 assert.Len(t, pod.Spec.Containers[0].VolumeMounts, 1) 1574 assert.Equal(t, "/var/run/secrets/kubernetes.io/serviceaccount", pod.Spec.Containers[0].VolumeMounts[0].MountPath) 1575 } 1576 1577 func TestGameServerPassthroughPortAnnotation(t *testing.T) { 1578 t.Parallel() 1579 runtime.FeatureTestMutex.Lock() 1580 defer runtime.FeatureTestMutex.Unlock() 1581 require.NoError(t, runtime.ParseFeatures(string(runtime.FeatureAutopilotPassthroughPort)+"=true")) 1582 containerOne := "containerOne" 1583 containerTwo := "containerTwo" 1584 containerThree := "containerThree" 1585 containerFour := "containerFour" 1586 gs := &GameServer{ObjectMeta: metav1.ObjectMeta{Name: "gameserver", UID: "1234"}, Spec: GameServerSpec{ 1587 Container: "containerOne", 1588 Ports: []GameServerPort{ 1589 {Name: "defaultDynamicOne", PortPolicy: Dynamic, ContainerPort: 7659, Container: &containerOne}, 1590 {Name: "defaultPassthroughOne", PortPolicy: Passthrough, Container: &containerOne}, 1591 {Name: "defaultPassthroughTwo", PortPolicy: Passthrough, Container: &containerTwo}, 1592 {Name: "defaultDynamicTwo", PortPolicy: Dynamic, ContainerPort: 7654, Container: &containerTwo}, 1593 {Name: "defaultDynamicThree", PortPolicy: Dynamic, ContainerPort: 7660, Container: &containerThree}, 1594 {Name: "defaultDynamicThree", PortPolicy: Dynamic, ContainerPort: 7661, Container: &containerThree}, 1595 {Name: "defaultDynamicThree", PortPolicy: Dynamic, ContainerPort: 7662, Container: &containerThree}, 1596 {Name: "defaulPassthroughThree", PortPolicy: Passthrough, Container: &containerThree}, 1597 {Name: "defaultPassthroughFour", PortPolicy: Passthrough, Container: &containerFour}, 1598 }, 1599 Template: corev1.PodTemplateSpec{ 1600 Spec: corev1.PodSpec{ 1601 Containers: []corev1.Container{ 1602 {Name: "containerOne", Image: "container/image"}, 1603 {Name: "containerTwo", Image: "container/image"}, 1604 {Name: "containerThree", Image: "container/image"}, 1605 {Name: "containerFour", Image: "container/image"}, 1606 }, 1607 }, 1608 }, 1609 }} 1610 1611 passthroughContainerPortMap := "{\"containerFour\":[0],\"containerOne\":[1],\"containerThree\":[3],\"containerTwo\":[0]}" 1612 1613 gs.ApplyDefaults() 1614 pod, err := gs.Pod(fakeAPIHooks{}) 1615 assert.NoError(t, err) 1616 assert.Len(t, pod.Spec.Containers, 4) 1617 assert.Empty(t, pod.Spec.Containers[0].VolumeMounts) 1618 assert.Equal(t, pod.ObjectMeta.Annotations[PassthroughPortAssignmentAnnotation], passthroughContainerPortMap) 1619 1620 err = gs.DisableServiceAccount(pod) 1621 assert.NoError(t, err) 1622 assert.Len(t, pod.Spec.Containers, 4) 1623 assert.Len(t, pod.Spec.Containers[0].VolumeMounts, 1) 1624 assert.Equal(t, "/var/run/secrets/kubernetes.io/serviceaccount", pod.Spec.Containers[0].VolumeMounts[0].MountPath) 1625 } 1626 1627 func TestGameServerCountPorts(t *testing.T) { 1628 fixture := &GameServer{Spec: GameServerSpec{Ports: []GameServerPort{ 1629 {PortPolicy: Dynamic}, 1630 {PortPolicy: Dynamic}, 1631 {PortPolicy: Dynamic}, 1632 {PortPolicy: Static}, 1633 }}} 1634 1635 assert.Equal(t, 3, fixture.CountPorts(func(policy PortPolicy) bool { 1636 return policy == Dynamic 1637 })) 1638 assert.Equal(t, 1, fixture.CountPorts(func(policy PortPolicy) bool { 1639 return policy == Static 1640 })) 1641 } 1642 1643 func TestGameServerCountPortsForRange(t *testing.T) { 1644 fixture := &GameServer{Spec: GameServerSpec{Ports: []GameServerPort{ 1645 {PortPolicy: Dynamic, Range: "test"}, 1646 {PortPolicy: Dynamic}, 1647 {PortPolicy: Dynamic, Range: "test"}, 1648 {PortPolicy: Static, Range: "test"}, 1649 }}} 1650 1651 assert.Equal(t, 2, fixture.CountPortsForRange("test", func(policy PortPolicy) bool { 1652 return policy == Dynamic 1653 })) 1654 assert.Equal(t, 1, fixture.CountPortsForRange("test", func(policy PortPolicy) bool { 1655 return policy == Static 1656 })) 1657 } 1658 1659 func TestGameServerPatch(t *testing.T) { 1660 fixture := &GameServer{ 1661 ObjectMeta: metav1.ObjectMeta{Name: "lucy", ResourceVersion: "1234"}, 1662 Spec: GameServerSpec{Container: "goat"}, 1663 } 1664 1665 delta := fixture.DeepCopy() 1666 delta.Spec.Container = "bear" 1667 1668 patch, err := fixture.Patch(delta) 1669 assert.Nil(t, err) 1670 1671 assert.Contains(t, string(patch), `{"op":"replace","path":"/spec/container","value":"bear"}`) 1672 assert.Contains(t, string(patch), `{"op":"test","path":"/metadata/resourceVersion","value":"1234"}`) 1673 } 1674 1675 func TestGameServerGetDevAddress(t *testing.T) { 1676 devGs := &GameServer{ 1677 ObjectMeta: metav1.ObjectMeta{ 1678 Name: "dev-game", 1679 Namespace: "default", 1680 Annotations: map[string]string{DevAddressAnnotation: ipFixture}, 1681 }, 1682 Spec: GameServerSpec{ 1683 Ports: []GameServerPort{{HostPort: 7777, PortPolicy: Static}}, 1684 Template: corev1.PodTemplateSpec{ 1685 Spec: corev1.PodSpec{ 1686 Containers: []corev1.Container{{Name: "container", Image: "container/image"}}, 1687 }, 1688 }, 1689 }, 1690 } 1691 1692 devAddress, isDev := devGs.GetDevAddress() 1693 assert.True(t, isDev, "dev-game should had a dev-address") 1694 assert.Equal(t, ipFixture, devAddress, "dev-address IP address should be 127.1.1.1") 1695 1696 regularGs := devGs.DeepCopy() 1697 regularGs.ObjectMeta.Annotations = map[string]string{} 1698 devAddress, isDev = regularGs.GetDevAddress() 1699 assert.False(t, isDev, "dev-game should NOT have a dev-address") 1700 assert.Equal(t, "", devAddress, "dev-address IP address should be 127.1.1.1") 1701 } 1702 1703 func TestGameServerIsDeletable(t *testing.T) { 1704 gs := &GameServer{Status: GameServerStatus{State: GameServerStateStarting}} 1705 assert.True(t, gs.IsDeletable()) 1706 1707 gs.Status.State = GameServerStateAllocated 1708 assert.False(t, gs.IsDeletable()) 1709 1710 gs.Status.State = GameServerStateReserved 1711 assert.False(t, gs.IsDeletable()) 1712 1713 now := metav1.Now() 1714 gs.ObjectMeta.DeletionTimestamp = &now 1715 assert.True(t, gs.IsDeletable()) 1716 1717 gs.Status.State = GameServerStateAllocated 1718 assert.True(t, gs.IsDeletable()) 1719 1720 gs.Status.State = GameServerStateReady 1721 assert.True(t, gs.IsDeletable()) 1722 } 1723 1724 func TestGameServerIsBeforeReady(t *testing.T) { 1725 fixtures := []struct { 1726 state GameServerState 1727 expected bool 1728 }{ 1729 {GameServerStatePortAllocation, true}, 1730 {GameServerStateCreating, true}, 1731 {GameServerStateStarting, true}, 1732 {GameServerStateScheduled, true}, 1733 {GameServerStateRequestReady, true}, 1734 {GameServerStateReady, false}, 1735 {GameServerStateShutdown, false}, 1736 {GameServerStateError, false}, 1737 {GameServerStateUnhealthy, false}, 1738 {GameServerStateReserved, false}, 1739 {GameServerStateAllocated, false}, 1740 } 1741 1742 for _, test := range fixtures { 1743 t.Run(string(test.state), func(t *testing.T) { 1744 gs := &GameServer{Status: GameServerStatus{State: test.state}} 1745 assert.Equal(t, test.expected, gs.IsBeforeReady(), test.state) 1746 }) 1747 } 1748 } 1749 1750 func TestGameServerApplyToPodContainer(t *testing.T) { 1751 t.Parallel() 1752 type expected struct { 1753 err string 1754 tty bool 1755 } 1756 1757 testCases := []struct { 1758 description string 1759 gs *GameServer 1760 expected expected 1761 }{ 1762 { 1763 description: "OK, no error", 1764 gs: &GameServer{ 1765 Spec: GameServerSpec{ 1766 Container: "mycontainer", 1767 Template: corev1.PodTemplateSpec{ 1768 Spec: corev1.PodSpec{ 1769 Containers: []corev1.Container{ 1770 {Name: "mycontainer", Image: "foo/mycontainer"}, 1771 {Name: "notmycontainer", Image: "foo/notmycontainer"}, 1772 }, 1773 }, 1774 }, 1775 }, 1776 }, 1777 expected: expected{ 1778 err: "", 1779 tty: true, 1780 }, 1781 }, 1782 { 1783 description: "container not found, error is returned", 1784 gs: &GameServer{ 1785 Spec: GameServerSpec{ 1786 Container: "mycontainer-WRONG-NAME", 1787 Template: corev1.PodTemplateSpec{ 1788 Spec: corev1.PodSpec{ 1789 Containers: []corev1.Container{ 1790 {Name: "mycontainer", Image: "foo/mycontainer"}, 1791 {Name: "notmycontainer", Image: "foo/notmycontainer"}, 1792 }, 1793 }, 1794 }, 1795 }, 1796 }, 1797 expected: expected{ 1798 err: "failed to find container named mycontainer-WRONG-NAME in pod spec", 1799 tty: false, 1800 }, 1801 }, 1802 } 1803 1804 for _, tc := range testCases { 1805 t.Run(tc.description, func(t *testing.T) { 1806 pod := &corev1.Pod{Spec: *tc.gs.Spec.Template.Spec.DeepCopy()} 1807 result := tc.gs.ApplyToPodContainer(pod, tc.gs.Spec.Container, func(c corev1.Container) corev1.Container { 1808 // easy thing to change and test for 1809 c.TTY = true 1810 return c 1811 }) 1812 1813 if tc.expected.err != "" && assert.NotNil(t, result) { 1814 assert.Equal(t, tc.expected.err, result.Error()) 1815 } 1816 assert.Equal(t, tc.expected.tty, pod.Spec.Containers[0].TTY) 1817 assert.False(t, pod.Spec.Containers[1].TTY) 1818 }) 1819 } 1820 } 1821 1822 func defaultGameServer() *GameServer { 1823 return &GameServer{ 1824 ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default", UID: "1234"}, 1825 Spec: GameServerSpec{ 1826 Ports: []GameServerPort{ 1827 { 1828 ContainerPort: 7777, 1829 HostPort: 9999, 1830 PortPolicy: Static, 1831 }, 1832 }, 1833 Template: corev1.PodTemplateSpec{ 1834 Spec: corev1.PodSpec{ 1835 Containers: []corev1.Container{{Name: "container", Image: "container/image"}}, 1836 }, 1837 }, 1838 }, Status: GameServerStatus{State: GameServerStateCreating}, 1839 } 1840 } 1841 1842 func TestGameServerUpdateCount(t *testing.T) { 1843 t.Parallel() 1844 1845 testCases := map[string]struct { 1846 gs GameServer 1847 name string 1848 action string 1849 amount int64 1850 want CounterStatus 1851 wantErr bool 1852 }{ 1853 "counter not in game server no-op and error": { 1854 gs: GameServer{Status: GameServerStatus{ 1855 Counters: map[string]CounterStatus{ 1856 "foos": { 1857 Count: 0, 1858 Capacity: 100, 1859 }, 1860 }, 1861 }}, 1862 name: "foo", 1863 action: "Increment", 1864 amount: 1, 1865 wantErr: true, 1866 }, 1867 "negative amount no-op and error": { 1868 gs: GameServer{Status: GameServerStatus{ 1869 Counters: map[string]CounterStatus{ 1870 "foos": { 1871 Count: 1, 1872 Capacity: 100, 1873 }, 1874 }, 1875 }}, 1876 name: "foos", 1877 action: "Decrement", 1878 amount: -1, 1879 want: CounterStatus{ 1880 Count: 1, 1881 Capacity: 100, 1882 }, 1883 wantErr: true, 1884 }, 1885 "increment by 1": { 1886 gs: GameServer{Status: GameServerStatus{ 1887 Counters: map[string]CounterStatus{ 1888 "players": { 1889 Count: 0, 1890 Capacity: 100, 1891 }, 1892 }, 1893 }}, 1894 name: "players", 1895 action: "Increment", 1896 amount: 1, 1897 want: CounterStatus{ 1898 Count: 1, 1899 Capacity: 100, 1900 }, 1901 wantErr: false, 1902 }, 1903 "decrement by 10": { 1904 gs: GameServer{Status: GameServerStatus{ 1905 Counters: map[string]CounterStatus{ 1906 "bars": { 1907 Count: 99, 1908 Capacity: 100, 1909 }, 1910 }, 1911 }}, 1912 name: "bars", 1913 action: "Decrement", 1914 amount: 10, 1915 want: CounterStatus{ 1916 Count: 89, 1917 Capacity: 100, 1918 }, 1919 wantErr: false, 1920 }, 1921 "incorrect action no-op and error": { 1922 gs: GameServer{Status: GameServerStatus{ 1923 Counters: map[string]CounterStatus{ 1924 "bazes": { 1925 Count: 99, 1926 Capacity: 100, 1927 }, 1928 }, 1929 }}, 1930 name: "bazes", 1931 action: "decrement", 1932 amount: 10, 1933 want: CounterStatus{ 1934 Count: 99, 1935 Capacity: 100, 1936 }, 1937 wantErr: true, 1938 }, 1939 "decrement beyond zero truncated": { 1940 gs: GameServer{Status: GameServerStatus{ 1941 Counters: map[string]CounterStatus{ 1942 "baz": { 1943 Count: 99, 1944 Capacity: 100, 1945 }, 1946 }, 1947 }}, 1948 name: "baz", 1949 action: "Decrement", 1950 amount: 100, 1951 want: CounterStatus{ 1952 Count: 0, 1953 Capacity: 100, 1954 }, 1955 wantErr: false, 1956 }, 1957 "increment beyond capacity truncated": { 1958 gs: GameServer{Status: GameServerStatus{ 1959 Counters: map[string]CounterStatus{ 1960 "splayers": { 1961 Count: 99, 1962 Capacity: 100, 1963 }, 1964 }, 1965 }}, 1966 name: "splayers", 1967 action: "Increment", 1968 amount: 2, 1969 want: CounterStatus{ 1970 Count: 100, 1971 Capacity: 100, 1972 }, 1973 wantErr: false, 1974 }, 1975 } 1976 1977 for test, testCase := range testCases { 1978 t.Run(test, func(t *testing.T) { 1979 err := testCase.gs.UpdateCount(testCase.name, testCase.action, testCase.amount) 1980 if err != nil { 1981 assert.True(t, testCase.wantErr) 1982 } else { 1983 assert.False(t, testCase.wantErr) 1984 } 1985 if counter, ok := testCase.gs.Status.Counters[testCase.name]; ok { 1986 assert.Equal(t, testCase.want, counter) 1987 } 1988 }) 1989 } 1990 } 1991 1992 func TestGameServerUpdateCounterCapacity(t *testing.T) { 1993 t.Parallel() 1994 1995 testCases := map[string]struct { 1996 gs GameServer 1997 name string 1998 capacity int64 1999 want CounterStatus 2000 wantErr bool 2001 }{ 2002 "counter not in game server no-op with error": { 2003 gs: GameServer{Status: GameServerStatus{ 2004 Counters: map[string]CounterStatus{ 2005 "foos": { 2006 Count: 0, 2007 Capacity: 100, 2008 }, 2009 }, 2010 }}, 2011 name: "foo", 2012 capacity: 1000, 2013 wantErr: true, 2014 }, 2015 "capacity less than zero no-op with error": { 2016 gs: GameServer{Status: GameServerStatus{ 2017 Counters: map[string]CounterStatus{ 2018 "foos": { 2019 Count: 0, 2020 Capacity: 100, 2021 }, 2022 }, 2023 }}, 2024 name: "foos", 2025 capacity: -1000, 2026 want: CounterStatus{ 2027 Count: 0, 2028 Capacity: 100, 2029 }, 2030 wantErr: true, 2031 }, 2032 "update capacity": { 2033 gs: GameServer{Status: GameServerStatus{ 2034 Counters: map[string]CounterStatus{ 2035 "sessions": { 2036 Count: 0, 2037 Capacity: 100, 2038 }, 2039 }, 2040 }}, 2041 name: "sessions", 2042 capacity: 9223372036854775807, 2043 want: CounterStatus{ 2044 Count: 0, 2045 Capacity: 9223372036854775807, 2046 }, 2047 }, 2048 } 2049 2050 for test, testCase := range testCases { 2051 t.Run(test, func(t *testing.T) { 2052 err := testCase.gs.UpdateCounterCapacity(testCase.name, testCase.capacity) 2053 if err != nil { 2054 assert.True(t, testCase.wantErr) 2055 } else { 2056 assert.False(t, testCase.wantErr) 2057 } 2058 if counter, ok := testCase.gs.Status.Counters[testCase.name]; ok { 2059 assert.Equal(t, testCase.want, counter) 2060 } 2061 }) 2062 } 2063 } 2064 2065 func TestGameServerUpdateListCapacity(t *testing.T) { 2066 t.Parallel() 2067 2068 testCases := map[string]struct { 2069 gs GameServer 2070 name string 2071 capacity int64 2072 want ListStatus 2073 wantErr bool 2074 }{ 2075 "list not in game server no-op with error": { 2076 gs: GameServer{Status: GameServerStatus{ 2077 Lists: map[string]ListStatus{ 2078 "things": { 2079 Values: []string{}, 2080 Capacity: 100, 2081 }, 2082 }, 2083 }}, 2084 name: "thing", 2085 capacity: 1000, 2086 wantErr: true, 2087 }, 2088 "update list capacity": { 2089 gs: GameServer{Status: GameServerStatus{ 2090 Lists: map[string]ListStatus{ 2091 "things": { 2092 Values: []string{}, 2093 Capacity: 100, 2094 }, 2095 }, 2096 }}, 2097 name: "things", 2098 capacity: 1000, 2099 want: ListStatus{ 2100 Values: []string{}, 2101 Capacity: 1000, 2102 }, 2103 wantErr: false, 2104 }, 2105 "list capacity above max no-op with error": { 2106 gs: GameServer{Status: GameServerStatus{ 2107 Lists: map[string]ListStatus{ 2108 "slings": { 2109 Values: []string{}, 2110 Capacity: 100, 2111 }, 2112 }, 2113 }}, 2114 name: "slings", 2115 capacity: 10000, 2116 want: ListStatus{ 2117 Values: []string{}, 2118 Capacity: 100, 2119 }, 2120 wantErr: true, 2121 }, 2122 "list capacity less than zero no-op with error": { 2123 gs: GameServer{Status: GameServerStatus{ 2124 Lists: map[string]ListStatus{ 2125 "flings": { 2126 Values: []string{}, 2127 Capacity: 999, 2128 }, 2129 }, 2130 }}, 2131 name: "flings", 2132 capacity: -100, 2133 want: ListStatus{ 2134 Values: []string{}, 2135 Capacity: 999, 2136 }, 2137 wantErr: true, 2138 }, 2139 } 2140 2141 for test, testCase := range testCases { 2142 t.Run(test, func(t *testing.T) { 2143 err := testCase.gs.UpdateListCapacity(testCase.name, testCase.capacity) 2144 if err != nil { 2145 assert.True(t, testCase.wantErr) 2146 } else { 2147 assert.False(t, testCase.wantErr) 2148 } 2149 if list, ok := testCase.gs.Status.Lists[testCase.name]; ok { 2150 assert.Equal(t, testCase.want, list) 2151 } 2152 }) 2153 } 2154 } 2155 2156 func TestGameServerAppendListValues(t *testing.T) { 2157 t.Parallel() 2158 2159 var nilSlice []string 2160 2161 testCases := map[string]struct { 2162 gs GameServer 2163 name string 2164 values []string 2165 want ListStatus 2166 wantErr bool 2167 }{ 2168 "list not in game server no-op with error": { 2169 gs: GameServer{Status: GameServerStatus{ 2170 Lists: map[string]ListStatus{ 2171 "things": { 2172 Values: []string{}, 2173 Capacity: 100, 2174 }, 2175 }, 2176 }}, 2177 name: "thing", 2178 values: []string{"thing1", "thing2", "thing3"}, 2179 wantErr: true, 2180 }, 2181 "append values": { 2182 gs: GameServer{Status: GameServerStatus{ 2183 Lists: map[string]ListStatus{ 2184 "things": { 2185 Values: []string{"thing1"}, 2186 Capacity: 100, 2187 }, 2188 }, 2189 }}, 2190 name: "things", 2191 values: []string{"thing2", "thing3"}, 2192 want: ListStatus{ 2193 Values: []string{"thing1", "thing2", "thing3"}, 2194 Capacity: 100, 2195 }, 2196 wantErr: false, 2197 }, 2198 "append values with silent drop of duplicates": { 2199 gs: GameServer{Status: GameServerStatus{ 2200 Lists: map[string]ListStatus{ 2201 "games": { 2202 Values: []string{"game0"}, 2203 Capacity: 10, 2204 }, 2205 }, 2206 }}, 2207 name: "games", 2208 values: []string{"game1", "game2", "game2", "game1"}, 2209 want: ListStatus{ 2210 Values: []string{"game0", "game1", "game2"}, 2211 Capacity: 10, 2212 }, 2213 wantErr: false, 2214 }, 2215 "append values with silent drop of duplicates in original list": { 2216 gs: GameServer{Status: GameServerStatus{ 2217 Lists: map[string]ListStatus{ 2218 "objects": { 2219 Values: []string{"object1", "object2"}, 2220 Capacity: 10, 2221 }, 2222 }, 2223 }}, 2224 name: "objects", 2225 values: []string{"object2", "object1", "object3", "object3"}, 2226 want: ListStatus{ 2227 Values: []string{"object1", "object2", "object3"}, 2228 Capacity: 10, 2229 }, 2230 wantErr: false, 2231 }, 2232 "append nil values": { 2233 gs: GameServer{Status: GameServerStatus{ 2234 Lists: map[string]ListStatus{ 2235 "blings": { 2236 Values: []string{"bling1"}, 2237 Capacity: 10, 2238 }, 2239 }, 2240 }}, 2241 name: "blings", 2242 values: nilSlice, 2243 want: ListStatus{ 2244 Values: []string{"bling1"}, 2245 Capacity: 10, 2246 }, 2247 wantErr: true, 2248 }, 2249 "append too many values truncates list": { 2250 gs: GameServer{Status: GameServerStatus{ 2251 Lists: map[string]ListStatus{ 2252 "bananaslugs": { 2253 Values: []string{"bananaslugs1", "bananaslug2", "bananaslug3"}, 2254 Capacity: 5, 2255 }, 2256 }, 2257 }}, 2258 name: "bananaslugs", 2259 values: []string{"bananaslug4", "bananaslug5", "bananaslug6"}, 2260 want: ListStatus{ 2261 Values: []string{"bananaslugs1", "bananaslug2", "bananaslug3", "bananaslug4", "bananaslug5"}, 2262 Capacity: 5, 2263 }, 2264 wantErr: false, 2265 }, 2266 } 2267 2268 for test, testCase := range testCases { 2269 t.Run(test, func(t *testing.T) { 2270 err := testCase.gs.AppendListValues(testCase.name, testCase.values) 2271 if err != nil { 2272 assert.True(t, testCase.wantErr) 2273 } else { 2274 assert.False(t, testCase.wantErr) 2275 } 2276 if list, ok := testCase.gs.Status.Lists[testCase.name]; ok { 2277 assert.Equal(t, testCase.want, list) 2278 } 2279 }) 2280 } 2281 } 2282 2283 func TestGameServerDeleteListValues(t *testing.T) { 2284 t.Parallel() 2285 2286 testCases := map[string]struct { 2287 gs GameServer 2288 name string 2289 want ListStatus 2290 values []string 2291 wantErr bool 2292 }{ 2293 "list not in game server no-op and error": { 2294 gs: GameServer{Status: GameServerStatus{ 2295 Lists: map[string]ListStatus{ 2296 "foos": { 2297 Values: []string{"foo", "bar", "bax"}, 2298 Capacity: 100, 2299 }, 2300 }, 2301 }}, 2302 name: "foo", 2303 values: []string{"bar", "baz"}, 2304 wantErr: true, 2305 }, 2306 "delete list value - one value not present": { 2307 gs: GameServer{Status: GameServerStatus{ 2308 Lists: map[string]ListStatus{ 2309 "foo": { 2310 Values: []string{"foo", "bar", "bax"}, 2311 Capacity: 100, 2312 }, 2313 }, 2314 }}, 2315 name: "foo", 2316 values: []string{"bar", "baz"}, 2317 wantErr: false, 2318 want: ListStatus{ 2319 Values: []string{"foo", "bax"}, 2320 Capacity: 100, 2321 }, 2322 }, 2323 } 2324 2325 for test, testCase := range testCases { 2326 t.Run(test, func(t *testing.T) { 2327 err := testCase.gs.DeleteListValues(testCase.name, testCase.values) 2328 if err != nil { 2329 assert.True(t, testCase.wantErr) 2330 } else { 2331 assert.False(t, testCase.wantErr) 2332 } 2333 if list, ok := testCase.gs.Status.Lists[testCase.name]; ok { 2334 assert.Equal(t, testCase.want, list) 2335 } 2336 }) 2337 } 2338 } 2339 2340 func TestMergeRemoveDuplicates(t *testing.T) { 2341 t.Parallel() 2342 2343 testCases := map[string]struct { 2344 str1 []string 2345 str2 []string 2346 want []string 2347 }{ 2348 "empty string arrays": { 2349 str1: []string{}, 2350 str2: []string{}, 2351 want: []string{}, 2352 }, 2353 "no duplicates": { 2354 str1: []string{"one"}, 2355 str2: []string{"two", "three"}, 2356 want: []string{"one", "two", "three"}, 2357 }, 2358 "remove one duplicate": { 2359 str1: []string{"one", "one", "one"}, 2360 str2: []string{"one", "one", "one"}, 2361 want: []string{"one"}, 2362 }, 2363 "remove multiple duplicates": { 2364 str1: []string{"one", "two"}, 2365 str2: []string{"two", "one"}, 2366 want: []string{"one", "two"}, 2367 }, 2368 } 2369 2370 for test, testCase := range testCases { 2371 t.Run(test, func(t *testing.T) { 2372 got := MergeRemoveDuplicates(testCase.str1, testCase.str2) 2373 assert.Equal(t, testCase.want, got) 2374 }) 2375 } 2376 }