agones.dev/agones@v1.53.0/test/e2e/framework/framework.go (about) 1 // Copyright 2018 Google LLC All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package framework is a package helping setting up end-to-end testing across a 16 // Kubernetes cluster. 17 package framework 18 19 import ( 20 "bufio" 21 "context" 22 "encoding/json" 23 "flag" 24 "fmt" 25 "io" 26 "net" 27 "os" 28 "os/user" 29 "path/filepath" 30 "strings" 31 "testing" 32 "time" 33 34 "github.com/pkg/errors" 35 "github.com/sirupsen/logrus" 36 "github.com/spf13/pflag" 37 "github.com/spf13/viper" 38 "github.com/stretchr/testify/require" 39 corev1 "k8s.io/api/core/v1" 40 rbacv1 "k8s.io/api/rbac/v1" 41 "k8s.io/apimachinery/pkg/api/resource" 42 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 43 "k8s.io/apimachinery/pkg/labels" 44 k8sruntime "k8s.io/apimachinery/pkg/runtime" 45 "k8s.io/apimachinery/pkg/types" 46 "k8s.io/apimachinery/pkg/util/wait" 47 "k8s.io/client-go/kubernetes" 48 "k8s.io/client-go/kubernetes/scheme" 49 50 // required to use gcloud login see: https://github.com/kubernetes/client-go/issues/242 51 _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 52 53 agonesv1 "agones.dev/agones/pkg/apis/agones/v1" 54 allocationv1 "agones.dev/agones/pkg/apis/allocation/v1" 55 autoscaling "agones.dev/agones/pkg/apis/autoscaling/v1" 56 "agones.dev/agones/pkg/client/clientset/versioned" 57 "agones.dev/agones/pkg/util/runtime" 58 ) 59 60 // special labels that can be put on pods to trigger automatic cleanup. 61 const ( 62 AutoCleanupLabelKey = "agones.dev/e2e-test-auto-cleanup" 63 AutoCleanupLabelValue = "true" 64 ) 65 66 // NamespaceLabel is the label that is put on all namespaces that are created 67 // for e2e tests. 68 var NamespaceLabel = map[string]string{"owner": "e2e-test"} 69 70 // Framework is a testing framework 71 type Framework struct { 72 KubeClient kubernetes.Interface 73 AgonesClient versioned.Interface 74 GameServerImage string 75 PullSecret string 76 StressTestLevel int 77 PerfOutputDir string 78 Version string 79 Namespace string 80 CloudProduct string 81 WaitForState time.Duration // default time to wait for state changes, may change based on cloud product. 82 } 83 84 func newFramework(kubeconfig string, qps float32, burst int) (*Framework, error) { 85 logger := runtime.NewLoggerWithSource("framework") 86 config, err := runtime.InClusterBuildConfig(logger, kubeconfig) 87 if err != nil { 88 return nil, errors.Wrap(err, "build config from flags failed") 89 } 90 91 if qps > 0 { 92 config.QPS = qps 93 } 94 if burst > 0 { 95 config.Burst = burst 96 } 97 98 kubeClient, err := kubernetes.NewForConfig(config) 99 if err != nil { 100 return nil, errors.Wrap(err, "creating new kube-client failed") 101 } 102 103 agonesClient, err := versioned.NewForConfig(config) 104 if err != nil { 105 return nil, errors.Wrap(err, "creating new agones-client failed") 106 } 107 108 return &Framework{ 109 KubeClient: kubeClient, 110 AgonesClient: agonesClient, 111 }, nil 112 } 113 114 const ( 115 kubeconfigFlag = "kubeconfig" 116 gsimageFlag = "gameserver-image" 117 pullSecretFlag = "pullsecret" 118 stressTestLevelFlag = "stress" 119 perfOutputDirFlag = "perf-output" 120 versionFlag = "version" 121 namespaceFlag = "namespace" 122 cloudProductFlag = "cloud-product" 123 ) 124 125 // ParseTestFlags Parses go test flags separately because pflag package ignores flags with '-test.' prefix 126 // Related issues: 127 // https://github.com/spf13/pflag/issues/63 128 // https://github.com/spf13/pflag/issues/238 129 func ParseTestFlags() error { 130 // if we have a "___" in the arguments path, then this is IntelliJ running the test, so ignore this, as otherwise 131 // it breaks. 132 if strings.Contains(os.Args[0], "___") { 133 logrus.Info("Running test via Intellij. Skipping Test Flag Parsing") 134 return nil 135 } 136 137 var testFlags []string 138 for _, f := range os.Args[1:] { 139 if strings.HasPrefix(f, "-test.") { 140 testFlags = append(testFlags, f) 141 } 142 } 143 return flag.CommandLine.Parse(testFlags) 144 } 145 146 // NewFromFlags sets up the testing framework with the standard command line flags. 147 func NewFromFlags() (*Framework, error) { 148 usr, err := user.Current() 149 if err != nil { 150 return nil, err 151 } 152 153 viper.SetDefault(kubeconfigFlag, filepath.Join(usr.HomeDir, ".kube", "config")) 154 viper.SetDefault(gsimageFlag, "us-docker.pkg.dev/agones-images/examples/simple-game-server:0.39") 155 viper.SetDefault(pullSecretFlag, "") 156 viper.SetDefault(stressTestLevelFlag, 0) 157 viper.SetDefault(perfOutputDirFlag, "") 158 viper.SetDefault(versionFlag, "") 159 viper.SetDefault(runtime.FeatureGateFlag, "") 160 viper.SetDefault(namespaceFlag, "") 161 viper.SetDefault(cloudProductFlag, "generic") 162 163 pflag.String(kubeconfigFlag, viper.GetString(kubeconfigFlag), "kube config path, e.g. $HOME/.kube/config") 164 pflag.String(gsimageFlag, viper.GetString(gsimageFlag), "gameserver image to use for those tests") 165 pflag.String(pullSecretFlag, viper.GetString(pullSecretFlag), "optional secret to be used for pulling the gameserver and/or Agones SDK sidecar images") 166 pflag.Int(stressTestLevelFlag, viper.GetInt(stressTestLevelFlag), "enable stress test at given level 0-100") 167 pflag.String(perfOutputDirFlag, viper.GetString(perfOutputDirFlag), "write performance statistics to the specified directory") 168 pflag.String(versionFlag, viper.GetString(versionFlag), "agones controller version to be tested, consists of release version plus a short hash of the latest commit") 169 pflag.String(namespaceFlag, viper.GetString(namespaceFlag), "namespace is used to isolate test runs to their own namespaces") 170 pflag.String(cloudProductFlag, viper.GetString(cloudProductFlag), "cloud product of cluster references by kubeconfig; defaults to 'generic'; options are 'generic', 'gke-autopilot'") 171 runtime.FeaturesBindFlags() 172 pflag.Parse() 173 174 viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) 175 runtime.Must(viper.BindEnv(kubeconfigFlag)) 176 runtime.Must(viper.BindEnv(gsimageFlag)) 177 runtime.Must(viper.BindEnv(pullSecretFlag)) 178 runtime.Must(viper.BindEnv(stressTestLevelFlag)) 179 runtime.Must(viper.BindEnv(perfOutputDirFlag)) 180 runtime.Must(viper.BindEnv(versionFlag)) 181 runtime.Must(viper.BindEnv(namespaceFlag)) 182 runtime.Must(viper.BindEnv(cloudProductFlag)) 183 runtime.Must(viper.BindPFlags(pflag.CommandLine)) 184 runtime.Must(runtime.FeaturesBindEnv()) 185 runtime.Must(runtime.ParseFeaturesFromEnv()) 186 187 framework, err := newFramework(viper.GetString(kubeconfigFlag), 0, 0) 188 if err != nil { 189 return framework, err 190 } 191 framework.GameServerImage = viper.GetString(gsimageFlag) 192 framework.PullSecret = viper.GetString(pullSecretFlag) 193 framework.StressTestLevel = viper.GetInt(stressTestLevelFlag) 194 framework.PerfOutputDir = viper.GetString(perfOutputDirFlag) 195 framework.Version = viper.GetString(versionFlag) 196 framework.Namespace = viper.GetString(namespaceFlag) 197 framework.CloudProduct = viper.GetString(cloudProductFlag) 198 framework.WaitForState = 5 * time.Minute 199 if framework.CloudProduct == "gke-autopilot" { 200 framework.WaitForState = 10 * time.Minute // Autopilot can take a little while due to autoscaling, be a little liberal. 201 } 202 203 logrus.WithField("gameServerImage", framework.GameServerImage). 204 WithField("pullSecret", framework.PullSecret). 205 WithField("stressTestLevel", framework.StressTestLevel). 206 WithField("perfOutputDir", framework.PerfOutputDir). 207 WithField("version", framework.Version). 208 WithField("namespace", framework.Namespace). 209 WithField("cloudProduct", framework.CloudProduct). 210 WithField("featureGates", runtime.EncodeFeatures()). 211 Info("Starting e2e test(s)") 212 213 return framework, nil 214 } 215 216 // CreateGameServerAndWaitUntilReady Creates a GameServer and wait for its state to become ready. 217 func (f *Framework) CreateGameServerAndWaitUntilReady(t *testing.T, ns string, gs *agonesv1.GameServer) (*agonesv1.GameServer, error) { 218 t.Helper() 219 log := TestLogger(t) 220 newGs, err := f.AgonesClient.AgonesV1().GameServers(ns).Create(context.Background(), gs, metav1.CreateOptions{}) 221 if err != nil { 222 return nil, fmt.Errorf("creating %v GameServer instances failed (%v): %v", gs.Spec, gs.Name, err) 223 } 224 225 log.WithField("gs", newGs.ObjectMeta.Name).Info("GameServer created, waiting for Ready") 226 227 readyGs, err := f.WaitForGameServerState(t, newGs, agonesv1.GameServerStateReady, f.WaitForState) 228 229 if err != nil { 230 return readyGs, fmt.Errorf("waiting for %v GameServer instance readiness timed out (%v): %v", 231 gs.Spec, gs.Name, err) 232 } 233 234 expectedPortCount := len(gs.Spec.Ports) 235 if expectedPortCount > 0 { 236 for _, port := range gs.Spec.Ports { 237 if port.Protocol == agonesv1.ProtocolTCPUDP { 238 expectedPortCount++ 239 } 240 } 241 } 242 243 if len(readyGs.Status.Ports) != expectedPortCount { 244 return readyGs, fmt.Errorf("ready GameServer instance has %d port(s), want %d", len(readyGs.Status.Ports), expectedPortCount) 245 } 246 247 logrus.WithField("gs", newGs.ObjectMeta.Name).Info("GameServer Ready") 248 249 return readyGs, nil 250 } 251 252 // WaitForGameServerState Waits untils the gameserver reach a given state before the timeout expires (with a default logger) 253 func (f *Framework) WaitForGameServerState(t *testing.T, gs *agonesv1.GameServer, state agonesv1.GameServerState, 254 timeout time.Duration) (*agonesv1.GameServer, error) { 255 t.Helper() 256 log := TestLogger(t) 257 258 var checkGs *agonesv1.GameServer 259 260 err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, timeout, true, func(_ context.Context) (bool, error) { 261 var err error 262 checkGs, err = f.AgonesClient.AgonesV1().GameServers(gs.Namespace).Get(context.Background(), gs.Name, metav1.GetOptions{}) 263 264 if err != nil { 265 log.WithError(err).Warn("error retrieving GameServer") 266 return false, nil 267 } 268 269 checkState := checkGs.Status.State 270 if checkState == state { 271 log.WithField("gs", checkGs.ObjectMeta.Name). 272 WithField("currentState", checkState). 273 WithField("awaitingState", state).Info("GameServer states match") 274 return true, nil 275 } 276 if agonesv1.TerminalGameServerStates[checkState] { 277 log.WithField("gs", checkGs.ObjectMeta.Name). 278 WithField("currentState", checkState). 279 WithField("awaitingState", state).Error("GameServer reached terminal state") 280 return false, errors.Errorf("GameServer reached terminal state %s", checkState) 281 } 282 log.WithField("gs", checkGs.ObjectMeta.Name). 283 WithField("currentState", checkState). 284 WithField("awaitingState", state).Info("Waiting for states to match") 285 286 return false, nil 287 }) 288 289 return checkGs, errors.Wrapf(err, "waiting for GameServer %v/%v to be %v", 290 gs.Namespace, gs.Name, state) 291 } 292 293 // CycleAllocations repeatedly Allocates a GameServer in the Fleet (if one is available), once every specified period. 294 // Each Allocated GameServer gets deleted allocDuration after it was Allocated. 295 // GameServers will continue to be Allocated until a message is passed to the done channel. 296 func (f *Framework) CycleAllocations(ctx context.Context, t *testing.T, flt *agonesv1.Fleet, period time.Duration, allocDuration time.Duration) { 297 err := wait.PollUntilContextCancel(ctx, period, true, func(_ context.Context) (bool, error) { 298 gsa := GetAllocation(flt) 299 gsa, err := f.AgonesClient.AllocationV1().GameServerAllocations(flt.Namespace).Create(context.Background(), gsa, metav1.CreateOptions{}) 300 if err != nil || gsa.Status.State != allocationv1.GameServerAllocationAllocated { 301 // Ignore error. Could be that the buffer was empty, will try again next cycle. 302 return false, nil 303 } 304 305 // Deallocate after allocDuration. 306 go func(gsa *allocationv1.GameServerAllocation) { 307 time.Sleep(allocDuration) 308 err := f.AgonesClient.AgonesV1().GameServers(gsa.Namespace).Delete(context.Background(), gsa.Status.GameServerName, metav1.DeleteOptions{}) 309 require.NoError(t, err) 310 }(gsa) 311 312 return false, nil 313 }) 314 // Ignore wait timeout error, will always be returned when the context is cancelled at the end of the test. 315 if !wait.Interrupted(err) { 316 require.NoError(t, err) 317 } 318 } 319 320 // ScaleFleet will scale a Fleet with retries to a specified replica size. 321 func (f *Framework) ScaleFleet(t *testing.T, log *logrus.Entry, flt *agonesv1.Fleet, replicas int32) { 322 fleets := f.AgonesClient.AgonesV1().Fleets(f.Namespace) 323 ctx := context.Background() 324 325 require.Eventuallyf(t, func() bool { 326 flt, err := fleets.Get(ctx, flt.ObjectMeta.Name, metav1.GetOptions{}) 327 if err != nil { 328 log.WithError(err).Info("Could not get Fleet") 329 return false 330 } 331 332 fltCopy := flt.DeepCopy() 333 fltCopy.Spec.Replicas = replicas 334 _, err = fleets.Update(ctx, fltCopy, metav1.UpdateOptions{}) 335 if err != nil { 336 log.WithError(err).Info("Could not scale Fleet") 337 return false 338 } 339 340 return true 341 }, 5*time.Minute, time.Second, "Could not scale Fleet %s", flt.ObjectMeta.Name) 342 } 343 344 // AssertFleetCondition waits for the Fleet to be in a specific condition or fails the test if the condition can't be met in 5 minutes. 345 func (f *Framework) AssertFleetCondition(t *testing.T, flt *agonesv1.Fleet, condition func(*logrus.Entry, *agonesv1.Fleet) bool) { 346 err := f.WaitForFleetCondition(t, flt, condition) 347 require.NoError(t, err, "error waiting for fleet condition on fleet: %v", flt.Name) 348 } 349 350 // WaitForFleetCondition waits for the Fleet to be in a specific condition or returns an error if the condition can't be met in 5 minutes. 351 func (f *Framework) WaitForFleetCondition(t *testing.T, flt *agonesv1.Fleet, condition func(*logrus.Entry, *agonesv1.Fleet) bool) error { 352 log := TestLogger(t).WithField("fleet", flt.Name) 353 log.Info("waiting for fleet condition") 354 err := wait.PollUntilContextTimeout(context.Background(), 2*time.Second, f.WaitForState, true, func(_ context.Context) (bool, error) { 355 fleet, err := f.AgonesClient.AgonesV1().Fleets(flt.ObjectMeta.Namespace).Get(context.Background(), flt.ObjectMeta.Name, metav1.GetOptions{}) 356 if err != nil { 357 return true, err 358 } 359 360 return condition(log, fleet), nil 361 }) 362 if err != nil { 363 // save this to be returned later. 364 resultErr := err 365 log.WithField("fleetStatus", fmt.Sprintf("%+v", flt.Status)).WithError(err). 366 Info("error waiting for fleet condition, dumping Fleet and Gameserver data") 367 368 f.LogEvents(t, log, flt.ObjectMeta.Namespace, flt) 369 370 gsList, err := f.ListGameServersFromFleet(flt) 371 require.NoError(t, err) 372 373 for i := range gsList { 374 gs := gsList[i] 375 log = log.WithField("gs", gs.ObjectMeta.Name) 376 log.WithField("status", fmt.Sprintf("%+v", gs.Status)).Info("GameServer state dump:") 377 f.LogEvents(t, log, gs.ObjectMeta.Namespace, &gs) 378 } 379 380 return resultErr 381 } 382 return nil 383 } 384 385 // WaitForFleetAutoScalerCondition waits for the FleetAutoscaler to be in a specific condition or fails the test if the condition can't be met in 2 minutes. 386 // nolint: dupl 387 func (f *Framework) WaitForFleetAutoScalerCondition(t *testing.T, fas *autoscaling.FleetAutoscaler, condition func(log *logrus.Entry, fas *autoscaling.FleetAutoscaler) bool) { 388 log := TestLogger(t).WithField("fleetautoscaler", fas.Name) 389 log.Info("waiting for fleetautoscaler condition") 390 err := wait.PollUntilContextTimeout(context.Background(), 2*time.Second, 2*time.Minute, true, func(_ context.Context) (bool, error) { 391 fleetautoscaler, err := f.AgonesClient.AutoscalingV1().FleetAutoscalers(fas.ObjectMeta.Namespace).Get(context.Background(), fas.ObjectMeta.Name, metav1.GetOptions{}) 392 if err != nil { 393 return true, err 394 } 395 396 return condition(log, fleetautoscaler), nil 397 }) 398 require.NoError(t, err, "error waiting for fleetautoscaler condition on fleetautoscaler %v", fas.Name) 399 } 400 401 // ListGameServersFromFleet lists GameServers from a particular fleet 402 func (f *Framework) ListGameServersFromFleet(flt *agonesv1.Fleet) ([]agonesv1.GameServer, error) { 403 var results []agonesv1.GameServer 404 405 opts := metav1.ListOptions{LabelSelector: labels.Set{agonesv1.FleetNameLabel: flt.ObjectMeta.Name}.String()} 406 gsSetList, err := f.AgonesClient.AgonesV1().GameServerSets(flt.ObjectMeta.Namespace).List(context.Background(), opts) 407 if err != nil { 408 return results, err 409 } 410 411 for i := range gsSetList.Items { 412 gsSet := &gsSetList.Items[i] 413 opts := metav1.ListOptions{LabelSelector: labels.Set{agonesv1.GameServerSetGameServerLabel: gsSet.ObjectMeta.Name}.String()} 414 gsList, err := f.AgonesClient.AgonesV1().GameServers(flt.ObjectMeta.Namespace).List(context.Background(), opts) 415 if err != nil { 416 return results, err 417 } 418 419 results = append(results, gsList.Items...) 420 } 421 422 return results, nil 423 } 424 425 // FleetReadyCount returns the ready count in a fleet 426 func FleetReadyCount(amount int32) func(*logrus.Entry, *agonesv1.Fleet) bool { 427 return func(log *logrus.Entry, fleet *agonesv1.Fleet) bool { 428 log.WithField("fleetStatus", fmt.Sprintf("%+v", fleet.Status)).WithField("fleet", fleet.ObjectMeta.Name).WithField("expected", amount).Info("Checking Fleet Ready replicas") 429 return fleet.Status.ReadyReplicas == amount 430 } 431 } 432 433 // WaitForFleetGameServersCondition waits for all GameServers for a given fleet to match 434 // a condition specified by a callback. 435 func (f *Framework) WaitForFleetGameServersCondition(flt *agonesv1.Fleet, 436 cond func(server *agonesv1.GameServer) bool) error { 437 return f.WaitForFleetGameServerListCondition(flt, 438 func(servers []agonesv1.GameServer) bool { 439 for i := range servers { 440 gs := &servers[i] 441 if !cond(gs) { 442 return false 443 } 444 } 445 return true 446 }) 447 } 448 449 // WaitForFleetGameServerListCondition waits for the list of GameServers to match a condition 450 // specified by a callback and the size of GameServers to match fleet's Spec.Replicas. 451 func (f *Framework) WaitForFleetGameServerListCondition(flt *agonesv1.Fleet, 452 cond func(servers []agonesv1.GameServer) bool) error { 453 return wait.PollUntilContextTimeout(context.Background(), 2*time.Second, f.WaitForState, true, func(_ context.Context) (done bool, err error) { 454 gsList, err := f.ListGameServersFromFleet(flt) 455 if err != nil { 456 return false, err 457 } 458 if int32(len(gsList)) != flt.Spec.Replicas { 459 return false, nil 460 } 461 return cond(gsList), nil 462 }) 463 } 464 465 // NewStatsCollector returns new instance of statistics collector, 466 // which can be used to emit performance statistics for load tests and stress tests. 467 func (f *Framework) NewStatsCollector(name, version string) *StatsCollector { 468 if f.StressTestLevel > 0 { 469 name = fmt.Sprintf("stress_%v_%v", f.StressTestLevel, name) 470 } 471 return &StatsCollector{name: name, outputDir: f.PerfOutputDir, version: version} 472 } 473 474 // CleanUp Delete all Agones resources in a given namespace. 475 func (f *Framework) CleanUp(ns string) error { 476 logrus.Info("Cleaning up now.") 477 defer logrus.Info("Finished cleanup.") 478 agonesV1 := f.AgonesClient.AgonesV1() 479 deleteOptions := metav1.DeleteOptions{} 480 listOptions := metav1.ListOptions{} 481 482 // find and delete pods created by tests and labeled with our special label 483 pods := f.KubeClient.CoreV1().Pods(ns) 484 ctx := context.Background() 485 podList, err := pods.List(ctx, metav1.ListOptions{ 486 LabelSelector: AutoCleanupLabelKey + "=" + AutoCleanupLabelValue, 487 }) 488 if err != nil { 489 return err 490 } 491 492 for i := range podList.Items { 493 p := &podList.Items[i] 494 if err := pods.Delete(ctx, p.ObjectMeta.Name, deleteOptions); err != nil { 495 return err 496 } 497 } 498 499 err = agonesV1.Fleets(ns).DeleteCollection(ctx, deleteOptions, listOptions) 500 if err != nil { 501 return err 502 } 503 504 err = f.AgonesClient.AutoscalingV1().FleetAutoscalers(ns).DeleteCollection(ctx, deleteOptions, listOptions) 505 if err != nil { 506 return err 507 } 508 509 return agonesV1.GameServers(ns). 510 DeleteCollection(ctx, deleteOptions, listOptions) 511 } 512 513 // CreateAndApplyAllocation creates and applies an Allocation to a Fleet 514 func (f *Framework) CreateAndApplyAllocation(t *testing.T, flt *agonesv1.Fleet) *allocationv1.GameServerAllocation { 515 gsa := GetAllocation(flt) 516 gsa, err := f.AgonesClient.AllocationV1().GameServerAllocations(flt.ObjectMeta.Namespace).Create(context.Background(), gsa, metav1.CreateOptions{}) 517 require.NoError(t, err) 518 require.Equal(t, string(allocationv1.GameServerAllocationAllocated), string(gsa.Status.State)) 519 return gsa 520 } 521 522 // SendGameServerUDP sends a message to a gameserver and returns its reply 523 // finds the first udp port from the spec to send the message to, 524 // returns error if no Ports were allocated 525 func (f *Framework) SendGameServerUDP(t *testing.T, gs *agonesv1.GameServer, msg string) (string, error) { 526 if len(gs.Status.Ports) == 0 { 527 return "", errors.New("Empty Ports array") 528 } 529 530 // use first udp port 531 for _, p := range gs.Spec.Ports { 532 if p.Protocol == corev1.ProtocolUDP { 533 return f.SendGameServerUDPToPort(t, gs, p.Name, msg) 534 } 535 } 536 return "", errors.New("No UDP ports") 537 } 538 539 // SendGameServerUDPToPort sends a message to a gameserver at the named port and returns its reply 540 // returns error if no Ports were allocated or a port of the specified name doesn't exist 541 func (f *Framework) SendGameServerUDPToPort(t *testing.T, gs *agonesv1.GameServer, portName string, msg string) (string, error) { 542 log := TestLogger(t) 543 if len(gs.Status.Ports) == 0 { 544 return "", errors.New("Empty Ports array") 545 } 546 var port agonesv1.GameServerStatusPort 547 for _, p := range gs.Status.Ports { 548 if p.Name == portName { 549 port = p 550 } 551 } 552 address := fmt.Sprintf("%s:%d", gs.Status.Address, port.Port) 553 reply, err := f.SendUDP(t, address, msg) 554 555 if err != nil { 556 log.WithField("gs", gs.ObjectMeta.Name).WithField("status", fmt.Sprintf("%+v", gs.Status)).Info("Failed to send UDP packet to GameServer. Dumping Events!") 557 f.LogEvents(t, log, gs.ObjectMeta.Namespace, gs) 558 } 559 560 return reply, err 561 } 562 563 // SendUDP sends a message to an address, and returns its reply if 564 // it returns one in 10 seconds. Will retry 5 times, in case UDP packets drop. 565 func (f *Framework) SendUDP(t *testing.T, address, msg string) (string, error) { 566 log := TestLogger(t).WithField("address", address) 567 b := make([]byte, 1024) 568 var n int 569 // sometimes we get I/O timeout, so let's do a retry 570 err := wait.PollUntilContextTimeout(context.Background(), 2*time.Second, time.Minute, true, func(_ context.Context) (bool, error) { 571 conn, err := net.Dial("udp", address) 572 if err != nil { 573 log.WithError(err).Info("could not dial address") 574 return false, nil 575 } 576 577 defer func() { 578 err = conn.Close() 579 }() 580 581 _, err = conn.Write([]byte(msg)) 582 if err != nil { 583 log.WithError(err).Info("could not write message to address") 584 return false, nil 585 } 586 587 err = conn.SetReadDeadline(time.Now().Add(10 * time.Second)) 588 if err != nil { 589 log.WithError(err).Info("Could not set read deadline") 590 return false, nil 591 } 592 593 n, err = conn.Read(b) 594 if err != nil { 595 log.WithError(err).Info("Could not read from address") 596 } 597 598 return err == nil, nil 599 }) 600 601 if err != nil { 602 return "", errors.Wrap(err, "timed out attempting to send UDP packet to address") 603 } 604 605 return string(b[:n]), nil 606 } 607 608 // SendGameServerTCP sends a message to a gameserver and returns its reply 609 // finds the first tcp port from the spec to send the message to, 610 // returns error if no Ports were allocated 611 func SendGameServerTCP(gs *agonesv1.GameServer, msg string) (string, error) { 612 if len(gs.Status.Ports) == 0 { 613 return "", errors.New("Empty Ports array") 614 } 615 616 // use first tcp port 617 for _, p := range gs.Spec.Ports { 618 if p.Protocol == corev1.ProtocolTCP { 619 return SendGameServerTCPToPort(gs, p.Name, msg) 620 } 621 } 622 return "", errors.New("No TCP ports") 623 } 624 625 // SendGameServerTCPToPort sends a message to a gameserver at the named port and returns its reply 626 // returns error if no Ports were allocated or a port of the specified name doesn't exist 627 func SendGameServerTCPToPort(gs *agonesv1.GameServer, portName string, msg string) (string, error) { 628 if len(gs.Status.Ports) == 0 { 629 return "", errors.New("Empty Ports array") 630 } 631 var port agonesv1.GameServerStatusPort 632 for _, p := range gs.Status.Ports { 633 if p.Name == portName { 634 port = p 635 } 636 } 637 address := fmt.Sprintf("%s:%d", gs.Status.Address, port.Port) 638 return SendTCP(address, msg) 639 } 640 641 // SendTCP sends a message to an address, and returns its reply if 642 // it returns one in 30 seconds 643 func SendTCP(address, msg string) (string, error) { 644 conn, err := net.Dial("tcp", address) 645 if err != nil { 646 return "", err 647 } 648 649 if err := conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil { 650 return "", err 651 } 652 653 defer func() { 654 if err := conn.Close(); err != nil { 655 logrus.Warn("Could not close TCP connection") 656 } 657 }() 658 659 // writes to the tcp connection 660 _, err = fmt.Fprintln(conn, msg) 661 if err != nil { 662 return "", err 663 } 664 665 response, err := bufio.NewReader(conn).ReadString('\n') 666 if err != nil { 667 return "", err 668 } 669 670 return response, nil 671 } 672 673 // GetAllocation returns a GameServerAllocation that is looking for a Ready 674 // GameServer from this fleet. 675 func GetAllocation(f *agonesv1.Fleet) *allocationv1.GameServerAllocation { 676 // get an allocation 677 return &allocationv1.GameServerAllocation{ 678 Spec: allocationv1.GameServerAllocationSpec{ 679 Selectors: []allocationv1.GameServerSelector{ 680 {LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{agonesv1.FleetNameLabel: f.ObjectMeta.Name}}}, 681 }, 682 }} 683 } 684 685 // CreateNamespace creates a namespace and a service account in the test cluster 686 func (f *Framework) CreateNamespace(namespace string) error { 687 kubeCore := f.KubeClient.CoreV1() 688 ns := &corev1.Namespace{ 689 ObjectMeta: metav1.ObjectMeta{ 690 Name: namespace, 691 Labels: NamespaceLabel, 692 }, 693 } 694 695 ctx := context.Background() 696 697 options := metav1.CreateOptions{} 698 if _, err := kubeCore.Namespaces().Create(ctx, ns, options); err != nil { 699 return errors.Errorf("creating namespace %s failed: %s", namespace, err.Error()) 700 } 701 logrus.Infof("Namespace %s is created", namespace) 702 703 saName := "agones-sdk" 704 if _, err := kubeCore.ServiceAccounts(namespace).Create(ctx, &corev1.ServiceAccount{ 705 ObjectMeta: metav1.ObjectMeta{ 706 Name: saName, 707 Namespace: namespace, 708 Labels: map[string]string{"app": "agones"}, 709 }, 710 }, options); err != nil { 711 err = errors.Errorf("creating ServiceAccount %s in namespace %s failed: %s", saName, namespace, err.Error()) 712 derr := f.DeleteNamespace(namespace) 713 if derr != nil { 714 return errors.Wrap(err, derr.Error()) 715 } 716 return err 717 } 718 logrus.Infof("ServiceAccount %s/%s is created", namespace, saName) 719 720 rb := &rbacv1.RoleBinding{ 721 ObjectMeta: metav1.ObjectMeta{ 722 Name: "agones-sdk-access", 723 Namespace: namespace, 724 Labels: map[string]string{"app": "agones"}, 725 }, 726 RoleRef: rbacv1.RoleRef{ 727 APIGroup: "rbac.authorization.k8s.io", 728 Kind: "ClusterRole", 729 Name: "agones-sdk", 730 }, 731 Subjects: []rbacv1.Subject{ 732 { 733 Kind: "ServiceAccount", 734 Name: saName, 735 Namespace: namespace, 736 }, 737 }, 738 } 739 if _, err := f.KubeClient.RbacV1().RoleBindings(namespace).Create(ctx, rb, options); err != nil { 740 err = errors.Errorf("creating RoleBinding for service account %q in namespace %q failed: %s", saName, namespace, err.Error()) 741 derr := f.DeleteNamespace(namespace) 742 if derr != nil { 743 return errors.Wrap(err, derr.Error()) 744 } 745 return err 746 } 747 logrus.Infof("RoleBinding %s/%s is created", namespace, rb.Name) 748 749 return nil 750 } 751 752 // DeleteNamespace deletes a namespace from the test cluster 753 func (f *Framework) DeleteNamespace(namespace string) error { 754 kubeCore := f.KubeClient.CoreV1() 755 ctx := context.Background() 756 757 // Remove finalizers 758 pods, err := kubeCore.Pods(namespace).List(ctx, metav1.ListOptions{}) 759 if err != nil { 760 return errors.Errorf("listing pods in namespace %s failed: %s", namespace, err) 761 } 762 for i := range pods.Items { 763 pod := &pods.Items[i] 764 if len(pod.Finalizers) > 0 { 765 pod.Finalizers = nil 766 payload := []patchRemoveNoValue{{ 767 Op: "remove", 768 Path: "/metadata/finalizers", 769 }} 770 payloadBytes, _ := json.Marshal(payload) 771 if _, err := kubeCore.Pods(namespace).Patch(ctx, pod.Name, types.JSONPatchType, payloadBytes, metav1.PatchOptions{}); err != nil { 772 return errors.Wrapf(err, "updating pod %s failed", pod.GetName()) 773 } 774 } 775 } 776 777 if err := kubeCore.Namespaces().Delete(ctx, namespace, metav1.DeleteOptions{}); err != nil { 778 return errors.Wrapf(err, "deleting namespace %s failed", namespace) 779 } 780 logrus.Infof("Namespace %s is deleted", namespace) 781 return nil 782 } 783 784 type patchRemoveNoValue struct { 785 Op string `json:"op"` 786 Path string `json:"path"` 787 } 788 789 // DefaultGameServer provides a default GameServer fixture, based on parameters 790 // passed to the Test Framework. 791 func (f *Framework) DefaultGameServer(namespace string) *agonesv1.GameServer { 792 gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{GenerateName: "game-server", Namespace: namespace}, 793 Spec: agonesv1.GameServerSpec{ 794 Container: "game-server", 795 Ports: []agonesv1.GameServerPort{{ 796 ContainerPort: 7654, 797 Name: "udp-port", 798 PortPolicy: agonesv1.Dynamic, 799 Protocol: corev1.ProtocolUDP, 800 }}, 801 Template: corev1.PodTemplateSpec{ 802 Spec: corev1.PodSpec{ 803 Containers: []corev1.Container{{ 804 Name: "game-server", 805 Image: f.GameServerImage, 806 ImagePullPolicy: corev1.PullIfNotPresent, 807 Resources: corev1.ResourceRequirements{ 808 Requests: corev1.ResourceList{ 809 corev1.ResourceCPU: resource.MustParse("30m"), 810 corev1.ResourceMemory: resource.MustParse("32Mi"), 811 }, 812 Limits: corev1.ResourceList{ 813 corev1.ResourceCPU: resource.MustParse("30m"), 814 corev1.ResourceMemory: resource.MustParse("32Mi"), 815 }, 816 }, 817 }}, 818 }, 819 }, 820 }, 821 } 822 823 if f.PullSecret != "" { 824 gs.Spec.Template.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{ 825 Name: f.PullSecret}} 826 } 827 828 return gs 829 } 830 831 // LogEvents logs all the events for a given Kubernetes objects. Useful for debugging why something 832 // went wrong. 833 func (f *Framework) LogEvents(t *testing.T, log *logrus.Entry, namespace string, objOrRef k8sruntime.Object) { 834 log.WithField("kind", objOrRef.GetObjectKind().GroupVersionKind().Kind).Info("Dumping Events:") 835 events, err := f.KubeClient.CoreV1().Events(namespace).Search(scheme.Scheme, objOrRef) 836 require.NoError(t, err, "error searching for events") 837 for i := range events.Items { 838 event := events.Items[i] 839 log.WithField("lastTimestamp", event.LastTimestamp).WithField("type", event.Type).WithField("reason", event.Reason).WithField("message", event.Message).Info("Event!") 840 } 841 } 842 843 // LogPodContainers takes a Pod as an argument and attempts to output the current and previous logs from each container 844 // in that Pod It uses the framework's KubeClient to retrieve the logs and outputs them using the provided logger. 845 func (f *Framework) LogPodContainers(t *testing.T, pod *corev1.Pod) { 846 log := TestLogger(t) 847 log.WithField("pod", pod.Name).WithField("namespace", pod.Namespace).Info("Logs for Pod:") 848 849 // sub-function so defer will fire on each printLogs, rather than at the end. 850 printLogs := func(container corev1.Container, previous bool) { 851 logOptions := &corev1.PodLogOptions{ 852 Container: container.Name, 853 Follow: false, 854 Previous: previous, 855 } 856 857 req := f.KubeClient.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, logOptions) 858 podLogs, err := req.Stream(context.Background()) 859 log = log.WithField("options", logOptions) 860 861 if err != nil { 862 log.WithError(err).Warn("Error opening log stream for container") 863 return 864 } 865 defer podLogs.Close() // nolint:errcheck,staticcheck 866 867 logBytes, err := io.ReadAll(podLogs) 868 if err != nil { 869 log.WithError(err).WithField("options", logOptions).Warn("Error reading logs for container") 870 return 871 } 872 873 log.Info("---Logs for container---") 874 lines := strings.Split(string(logBytes), "\n") 875 for _, line := range lines { 876 if line == "" { 877 continue 878 } 879 log.Info(line) 880 } 881 log.Info("---End of container logs---") 882 } 883 884 // run through the container list twice, so we group current vs previous logs nicely. 885 for _, container := range pod.Spec.Containers { 886 printLogs(container, false) 887 } 888 889 for _, container := range pod.Spec.Containers { 890 printLogs(container, true) 891 } 892 893 } 894 895 // SkipOnCloudProduct skips the test if the e2e was invoked with --cloud-product=<product>. 896 func (f *Framework) SkipOnCloudProduct(t *testing.T, product, reason string) { 897 if f.CloudProduct == product { 898 t.Skipf("skipping test on cloud product %s: %s", product, reason) 899 } 900 } 901 902 // TestLogger returns the standard logger for helper functions. 903 func TestLogger(t *testing.T) *logrus.Entry { 904 return logrus.WithField("test", t.Name()) 905 }