agones.dev/agones@v1.54.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 // Autopilot can take a little while due to autoscaling, be a little liberal. 201 // Keeping it under 10m so we don't get stack track dumps at 10m as unit tests can't be extended past 10m. 202 framework.WaitForState = 8 * time.Minute 203 } 204 205 logrus.WithField("gameServerImage", framework.GameServerImage). 206 WithField("pullSecret", framework.PullSecret). 207 WithField("stressTestLevel", framework.StressTestLevel). 208 WithField("perfOutputDir", framework.PerfOutputDir). 209 WithField("version", framework.Version). 210 WithField("namespace", framework.Namespace). 211 WithField("cloudProduct", framework.CloudProduct). 212 WithField("featureGates", runtime.EncodeFeatures()). 213 Info("Starting e2e test(s)") 214 215 return framework, nil 216 } 217 218 // CreateGameServerAndWaitUntilReady Creates a GameServer and wait for its state to become ready. 219 func (f *Framework) CreateGameServerAndWaitUntilReady(t *testing.T, ns string, gs *agonesv1.GameServer) (*agonesv1.GameServer, error) { 220 t.Helper() 221 log := TestLogger(t) 222 newGs, err := f.AgonesClient.AgonesV1().GameServers(ns).Create(context.Background(), gs, metav1.CreateOptions{}) 223 if err != nil { 224 return nil, fmt.Errorf("creating %v GameServer instances failed (%v): %v", gs.Spec, gs.Name, err) 225 } 226 227 log.WithField("gs", newGs.ObjectMeta.Name).Info("GameServer created, waiting for Ready") 228 229 readyGs, err := f.WaitForGameServerState(t, newGs, agonesv1.GameServerStateReady, f.WaitForState) 230 231 if err != nil { 232 return readyGs, fmt.Errorf("waiting for %v GameServer instance readiness timed out (%v): %v", 233 gs.Spec, gs.Name, err) 234 } 235 236 expectedPortCount := len(gs.Spec.Ports) 237 if expectedPortCount > 0 { 238 for _, port := range gs.Spec.Ports { 239 if port.Protocol == agonesv1.ProtocolTCPUDP { 240 expectedPortCount++ 241 } 242 } 243 } 244 245 if len(readyGs.Status.Ports) != expectedPortCount { 246 return readyGs, fmt.Errorf("ready GameServer instance has %d port(s), want %d", len(readyGs.Status.Ports), expectedPortCount) 247 } 248 249 logrus.WithField("gs", newGs.ObjectMeta.Name).Info("GameServer Ready") 250 251 return readyGs, nil 252 } 253 254 // WaitForGameServerState Waits untils the gameserver reach a given state before the timeout expires (with a default logger) 255 func (f *Framework) WaitForGameServerState(t *testing.T, gs *agonesv1.GameServer, state agonesv1.GameServerState, 256 timeout time.Duration) (*agonesv1.GameServer, error) { 257 t.Helper() 258 log := TestLogger(t) 259 260 var checkGs *agonesv1.GameServer 261 262 err := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, timeout, true, func(_ context.Context) (bool, error) { 263 var err error 264 checkGs, err = f.AgonesClient.AgonesV1().GameServers(gs.Namespace).Get(context.Background(), gs.Name, metav1.GetOptions{}) 265 266 if err != nil { 267 log.WithError(err).Warn("error retrieving GameServer") 268 return false, nil 269 } 270 271 checkState := checkGs.Status.State 272 if checkState == state { 273 log.WithField("gs", checkGs.ObjectMeta.Name). 274 WithField("currentState", checkState). 275 WithField("awaitingState", state).Info("GameServer states match") 276 return true, nil 277 } 278 if agonesv1.TerminalGameServerStates[checkState] { 279 log.WithField("gs", checkGs.ObjectMeta.Name). 280 WithField("currentState", checkState). 281 WithField("awaitingState", state).Error("GameServer reached terminal state") 282 return false, errors.Errorf("GameServer reached terminal state %s", checkState) 283 } 284 log.WithField("gs", checkGs.ObjectMeta.Name). 285 WithField("currentState", checkState). 286 WithField("awaitingState", state).Info("Waiting for states to match") 287 288 return false, nil 289 }) 290 291 return checkGs, errors.Wrapf(err, "waiting for GameServer %v/%v to be %v", 292 gs.Namespace, gs.Name, state) 293 } 294 295 // CycleAllocations repeatedly Allocates a GameServer in the Fleet (if one is available), once every specified period. 296 // Each Allocated GameServer gets deleted allocDuration after it was Allocated. 297 // GameServers will continue to be Allocated until a message is passed to the done channel. 298 func (f *Framework) CycleAllocations(ctx context.Context, t *testing.T, flt *agonesv1.Fleet, period time.Duration, allocDuration time.Duration) { 299 err := wait.PollUntilContextCancel(ctx, period, true, func(_ context.Context) (bool, error) { 300 gsa := GetAllocation(flt) 301 gsa, err := f.AgonesClient.AllocationV1().GameServerAllocations(flt.Namespace).Create(context.Background(), gsa, metav1.CreateOptions{}) 302 if err != nil || gsa.Status.State != allocationv1.GameServerAllocationAllocated { 303 // Ignore error. Could be that the buffer was empty, will try again next cycle. 304 return false, nil 305 } 306 307 // Deallocate after allocDuration. 308 go func(gsa *allocationv1.GameServerAllocation) { 309 time.Sleep(allocDuration) 310 err := f.AgonesClient.AgonesV1().GameServers(gsa.Namespace).Delete(context.Background(), gsa.Status.GameServerName, metav1.DeleteOptions{}) 311 require.NoError(t, err) 312 }(gsa) 313 314 return false, nil 315 }) 316 // Ignore wait timeout error, will always be returned when the context is cancelled at the end of the test. 317 if !wait.Interrupted(err) { 318 require.NoError(t, err) 319 } 320 } 321 322 // ScaleFleet will scale a Fleet with retries to a specified replica size. 323 func (f *Framework) ScaleFleet(t *testing.T, log *logrus.Entry, flt *agonesv1.Fleet, replicas int32) { 324 fleets := f.AgonesClient.AgonesV1().Fleets(f.Namespace) 325 ctx := context.Background() 326 327 require.Eventuallyf(t, func() bool { 328 flt, err := fleets.Get(ctx, flt.ObjectMeta.Name, metav1.GetOptions{}) 329 if err != nil { 330 log.WithError(err).Info("Could not get Fleet") 331 return false 332 } 333 334 fltCopy := flt.DeepCopy() 335 fltCopy.Spec.Replicas = replicas 336 _, err = fleets.Update(ctx, fltCopy, metav1.UpdateOptions{}) 337 if err != nil { 338 log.WithError(err).Info("Could not scale Fleet") 339 return false 340 } 341 342 return true 343 }, 5*time.Minute, time.Second, "Could not scale Fleet %s", flt.ObjectMeta.Name) 344 } 345 346 // 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. 347 func (f *Framework) AssertFleetCondition(t *testing.T, flt *agonesv1.Fleet, condition func(*logrus.Entry, *agonesv1.Fleet) bool) { 348 err := f.WaitForFleetCondition(t, flt, condition) 349 require.NoError(t, err, "error waiting for fleet condition on fleet: %v", flt.Name) 350 } 351 352 // 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. 353 func (f *Framework) WaitForFleetCondition(t *testing.T, flt *agonesv1.Fleet, condition func(*logrus.Entry, *agonesv1.Fleet) bool) error { 354 log := TestLogger(t).WithField("fleet", flt.Name) 355 log.Info("waiting for fleet condition") 356 err := wait.PollUntilContextTimeout(context.Background(), 2*time.Second, f.WaitForState, true, func(_ context.Context) (bool, error) { 357 fleet, err := f.AgonesClient.AgonesV1().Fleets(flt.ObjectMeta.Namespace).Get(context.Background(), flt.ObjectMeta.Name, metav1.GetOptions{}) 358 if err != nil { 359 return true, err 360 } 361 362 return condition(log, fleet), nil 363 }) 364 if err != nil { 365 // save this to be returned later. 366 resultErr := err 367 log.WithField("fleetStatus", fmt.Sprintf("%+v", flt.Status)).WithError(err). 368 Info("error waiting for fleet condition, dumping Fleet and Gameserver data") 369 370 f.LogEvents(t, log, flt.ObjectMeta.Namespace, flt) 371 372 gsList, err := f.ListGameServersFromFleet(flt) 373 require.NoError(t, err) 374 375 for i := range gsList { 376 gs := gsList[i] 377 log = log.WithField("gs", gs.ObjectMeta.Name) 378 log.WithField("status", fmt.Sprintf("%+v", gs.Status)).Info("GameServer state dump:") 379 f.LogEvents(t, log, gs.ObjectMeta.Namespace, &gs) 380 } 381 382 return resultErr 383 } 384 return nil 385 } 386 387 // 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. 388 // nolint: dupl 389 func (f *Framework) WaitForFleetAutoScalerCondition(t *testing.T, fas *autoscaling.FleetAutoscaler, condition func(log *logrus.Entry, fas *autoscaling.FleetAutoscaler) bool) { 390 log := TestLogger(t).WithField("fleetautoscaler", fas.Name) 391 log.Info("waiting for fleetautoscaler condition") 392 err := wait.PollUntilContextTimeout(context.Background(), 2*time.Second, 2*time.Minute, true, func(_ context.Context) (bool, error) { 393 fleetautoscaler, err := f.AgonesClient.AutoscalingV1().FleetAutoscalers(fas.ObjectMeta.Namespace).Get(context.Background(), fas.ObjectMeta.Name, metav1.GetOptions{}) 394 if err != nil { 395 return true, err 396 } 397 398 return condition(log, fleetautoscaler), nil 399 }) 400 require.NoError(t, err, "error waiting for fleetautoscaler condition on fleetautoscaler %v", fas.Name) 401 } 402 403 // ListGameServersFromFleet lists GameServers from a particular fleet 404 func (f *Framework) ListGameServersFromFleet(flt *agonesv1.Fleet) ([]agonesv1.GameServer, error) { 405 var results []agonesv1.GameServer 406 407 opts := metav1.ListOptions{LabelSelector: labels.Set{agonesv1.FleetNameLabel: flt.ObjectMeta.Name}.String()} 408 gsSetList, err := f.AgonesClient.AgonesV1().GameServerSets(flt.ObjectMeta.Namespace).List(context.Background(), opts) 409 if err != nil { 410 return results, err 411 } 412 413 for i := range gsSetList.Items { 414 gsSet := &gsSetList.Items[i] 415 opts := metav1.ListOptions{LabelSelector: labels.Set{agonesv1.GameServerSetGameServerLabel: gsSet.ObjectMeta.Name}.String()} 416 gsList, err := f.AgonesClient.AgonesV1().GameServers(flt.ObjectMeta.Namespace).List(context.Background(), opts) 417 if err != nil { 418 return results, err 419 } 420 421 results = append(results, gsList.Items...) 422 } 423 424 return results, nil 425 } 426 427 // FleetReadyCount returns the ready count in a fleet 428 func FleetReadyCount(amount int32) func(*logrus.Entry, *agonesv1.Fleet) bool { 429 return func(log *logrus.Entry, fleet *agonesv1.Fleet) bool { 430 log.WithField("fleetStatus", fmt.Sprintf("%+v", fleet.Status)).WithField("fleet", fleet.ObjectMeta.Name).WithField("expected", amount).Info("Checking Fleet Ready replicas") 431 return fleet.Status.ReadyReplicas == amount 432 } 433 } 434 435 // WaitForFleetGameServersCondition waits for all GameServers for a given fleet to match 436 // a condition specified by a callback. 437 func (f *Framework) WaitForFleetGameServersCondition(flt *agonesv1.Fleet, 438 cond func(server *agonesv1.GameServer) bool) error { 439 return f.WaitForFleetGameServerListCondition(flt, 440 func(servers []agonesv1.GameServer) bool { 441 for i := range servers { 442 gs := &servers[i] 443 if !cond(gs) { 444 return false 445 } 446 } 447 return true 448 }) 449 } 450 451 // WaitForFleetGameServerListCondition waits for the list of GameServers to match a condition 452 // specified by a callback and the size of GameServers to match fleet's Spec.Replicas. 453 func (f *Framework) WaitForFleetGameServerListCondition(flt *agonesv1.Fleet, 454 cond func(servers []agonesv1.GameServer) bool) error { 455 return wait.PollUntilContextTimeout(context.Background(), 2*time.Second, f.WaitForState, true, func(_ context.Context) (done bool, err error) { 456 gsList, err := f.ListGameServersFromFleet(flt) 457 if err != nil { 458 return false, err 459 } 460 if int32(len(gsList)) != flt.Spec.Replicas { 461 return false, nil 462 } 463 return cond(gsList), nil 464 }) 465 } 466 467 // NewStatsCollector returns new instance of statistics collector, 468 // which can be used to emit performance statistics for load tests and stress tests. 469 func (f *Framework) NewStatsCollector(name, version string) *StatsCollector { 470 if f.StressTestLevel > 0 { 471 name = fmt.Sprintf("stress_%v_%v", f.StressTestLevel, name) 472 } 473 return &StatsCollector{name: name, outputDir: f.PerfOutputDir, version: version} 474 } 475 476 // CleanUp Delete all Agones resources in a given namespace. 477 func (f *Framework) CleanUp(ns string) error { 478 logrus.Info("Cleaning up now.") 479 defer logrus.Info("Finished cleanup.") 480 agonesV1 := f.AgonesClient.AgonesV1() 481 deleteOptions := metav1.DeleteOptions{} 482 listOptions := metav1.ListOptions{} 483 484 // find and delete pods created by tests and labeled with our special label 485 pods := f.KubeClient.CoreV1().Pods(ns) 486 ctx := context.Background() 487 podList, err := pods.List(ctx, metav1.ListOptions{ 488 LabelSelector: AutoCleanupLabelKey + "=" + AutoCleanupLabelValue, 489 }) 490 if err != nil { 491 return err 492 } 493 494 for i := range podList.Items { 495 p := &podList.Items[i] 496 if err := pods.Delete(ctx, p.ObjectMeta.Name, deleteOptions); err != nil { 497 return err 498 } 499 } 500 501 err = agonesV1.Fleets(ns).DeleteCollection(ctx, deleteOptions, listOptions) 502 if err != nil { 503 return err 504 } 505 506 err = f.AgonesClient.AutoscalingV1().FleetAutoscalers(ns).DeleteCollection(ctx, deleteOptions, listOptions) 507 if err != nil { 508 return err 509 } 510 511 return agonesV1.GameServers(ns). 512 DeleteCollection(ctx, deleteOptions, listOptions) 513 } 514 515 // CreateAndApplyAllocation creates and applies an Allocation to a Fleet 516 func (f *Framework) CreateAndApplyAllocation(t *testing.T, flt *agonesv1.Fleet) *allocationv1.GameServerAllocation { 517 gsa := GetAllocation(flt) 518 gsa, err := f.AgonesClient.AllocationV1().GameServerAllocations(flt.ObjectMeta.Namespace).Create(context.Background(), gsa, metav1.CreateOptions{}) 519 require.NoError(t, err) 520 require.Equal(t, string(allocationv1.GameServerAllocationAllocated), string(gsa.Status.State)) 521 return gsa 522 } 523 524 // SendGameServerUDP sends a message to a gameserver and returns its reply 525 // finds the first udp port from the spec to send the message to, 526 // returns error if no Ports were allocated 527 func (f *Framework) SendGameServerUDP(t *testing.T, gs *agonesv1.GameServer, msg string) (string, error) { 528 if len(gs.Status.Ports) == 0 { 529 return "", errors.New("Empty Ports array") 530 } 531 532 // use first udp port 533 for _, p := range gs.Spec.Ports { 534 if p.Protocol == corev1.ProtocolUDP { 535 return f.SendGameServerUDPToPort(t, gs, p.Name, msg) 536 } 537 } 538 return "", errors.New("No UDP ports") 539 } 540 541 // SendGameServerUDPToPort sends a message to a gameserver at the named port and returns its reply 542 // returns error if no Ports were allocated or a port of the specified name doesn't exist 543 func (f *Framework) SendGameServerUDPToPort(t *testing.T, gs *agonesv1.GameServer, portName string, msg string) (string, error) { 544 log := TestLogger(t) 545 if len(gs.Status.Ports) == 0 { 546 return "", errors.New("Empty Ports array") 547 } 548 var port agonesv1.GameServerStatusPort 549 for _, p := range gs.Status.Ports { 550 if p.Name == portName { 551 port = p 552 } 553 } 554 address := fmt.Sprintf("%s:%d", gs.Status.Address, port.Port) 555 reply, err := f.SendUDP(t, address, msg) 556 557 if err != nil { 558 log.WithField("gs", gs.ObjectMeta.Name).WithField("status", fmt.Sprintf("%+v", gs.Status)).Info("Failed to send UDP packet to GameServer. Dumping Events!") 559 f.LogEvents(t, log, gs.ObjectMeta.Namespace, gs) 560 } 561 562 return reply, err 563 } 564 565 // SendUDP sends a message to an address, and returns its reply if 566 // it returns one in 10 seconds. Will retry 5 times, in case UDP packets drop. 567 func (f *Framework) SendUDP(t *testing.T, address, msg string) (string, error) { 568 log := TestLogger(t).WithField("address", address) 569 b := make([]byte, 1024) 570 var n int 571 // sometimes we get I/O timeout, so let's do a retry 572 err := wait.PollUntilContextTimeout(context.Background(), 2*time.Second, time.Minute, true, func(_ context.Context) (bool, error) { 573 conn, err := net.Dial("udp", address) 574 if err != nil { 575 log.WithError(err).Info("could not dial address") 576 return false, nil 577 } 578 579 defer func() { 580 err = conn.Close() 581 }() 582 583 _, err = conn.Write([]byte(msg)) 584 if err != nil { 585 log.WithError(err).Info("could not write message to address") 586 return false, nil 587 } 588 589 err = conn.SetReadDeadline(time.Now().Add(10 * time.Second)) 590 if err != nil { 591 log.WithError(err).Info("Could not set read deadline") 592 return false, nil 593 } 594 595 n, err = conn.Read(b) 596 if err != nil { 597 log.WithError(err).Info("Could not read from address") 598 } 599 600 return err == nil, nil 601 }) 602 603 if err != nil { 604 return "", errors.Wrap(err, "timed out attempting to send UDP packet to address") 605 } 606 607 return string(b[:n]), nil 608 } 609 610 // SendGameServerTCP sends a message to a gameserver and returns its reply 611 // finds the first tcp port from the spec to send the message to, 612 // returns error if no Ports were allocated 613 func SendGameServerTCP(gs *agonesv1.GameServer, msg string) (string, error) { 614 if len(gs.Status.Ports) == 0 { 615 return "", errors.New("Empty Ports array") 616 } 617 618 // use first tcp port 619 for _, p := range gs.Spec.Ports { 620 if p.Protocol == corev1.ProtocolTCP { 621 return SendGameServerTCPToPort(gs, p.Name, msg) 622 } 623 } 624 return "", errors.New("No TCP ports") 625 } 626 627 // SendGameServerTCPToPort sends a message to a gameserver at the named port and returns its reply 628 // returns error if no Ports were allocated or a port of the specified name doesn't exist 629 func SendGameServerTCPToPort(gs *agonesv1.GameServer, portName string, msg string) (string, error) { 630 if len(gs.Status.Ports) == 0 { 631 return "", errors.New("Empty Ports array") 632 } 633 var port agonesv1.GameServerStatusPort 634 for _, p := range gs.Status.Ports { 635 if p.Name == portName { 636 port = p 637 } 638 } 639 address := fmt.Sprintf("%s:%d", gs.Status.Address, port.Port) 640 return SendTCP(address, msg) 641 } 642 643 // SendTCP sends a message to an address, and returns its reply if 644 // it returns one in 30 seconds 645 func SendTCP(address, msg string) (string, error) { 646 conn, err := net.Dial("tcp", address) 647 if err != nil { 648 return "", err 649 } 650 651 if err := conn.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil { 652 return "", err 653 } 654 655 defer func() { 656 if err := conn.Close(); err != nil { 657 logrus.Warn("Could not close TCP connection") 658 } 659 }() 660 661 // writes to the tcp connection 662 _, err = fmt.Fprintln(conn, msg) 663 if err != nil { 664 return "", err 665 } 666 667 response, err := bufio.NewReader(conn).ReadString('\n') 668 if err != nil { 669 return "", err 670 } 671 672 return response, nil 673 } 674 675 // GetAllocation returns a GameServerAllocation that is looking for a Ready 676 // GameServer from this fleet. 677 func GetAllocation(f *agonesv1.Fleet) *allocationv1.GameServerAllocation { 678 // get an allocation 679 return &allocationv1.GameServerAllocation{ 680 Spec: allocationv1.GameServerAllocationSpec{ 681 Selectors: []allocationv1.GameServerSelector{ 682 {LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{agonesv1.FleetNameLabel: f.ObjectMeta.Name}}}, 683 }, 684 }} 685 } 686 687 // CreateNamespace creates a namespace and a service account in the test cluster 688 func (f *Framework) CreateNamespace(namespace string) error { 689 kubeCore := f.KubeClient.CoreV1() 690 ns := &corev1.Namespace{ 691 ObjectMeta: metav1.ObjectMeta{ 692 Name: namespace, 693 Labels: NamespaceLabel, 694 }, 695 } 696 697 ctx := context.Background() 698 699 options := metav1.CreateOptions{} 700 if _, err := kubeCore.Namespaces().Create(ctx, ns, options); err != nil { 701 return errors.Errorf("creating namespace %s failed: %s", namespace, err.Error()) 702 } 703 logrus.Infof("Namespace %s is created", namespace) 704 705 saName := "agones-sdk" 706 if _, err := kubeCore.ServiceAccounts(namespace).Create(ctx, &corev1.ServiceAccount{ 707 ObjectMeta: metav1.ObjectMeta{ 708 Name: saName, 709 Namespace: namespace, 710 Labels: map[string]string{"app": "agones"}, 711 }, 712 }, options); err != nil { 713 err = errors.Errorf("creating ServiceAccount %s in namespace %s failed: %s", saName, namespace, err.Error()) 714 derr := f.DeleteNamespace(namespace) 715 if derr != nil { 716 return errors.Wrap(err, derr.Error()) 717 } 718 return err 719 } 720 logrus.Infof("ServiceAccount %s/%s is created", namespace, saName) 721 722 rb := &rbacv1.RoleBinding{ 723 ObjectMeta: metav1.ObjectMeta{ 724 Name: "agones-sdk-access", 725 Namespace: namespace, 726 Labels: map[string]string{"app": "agones"}, 727 }, 728 RoleRef: rbacv1.RoleRef{ 729 APIGroup: "rbac.authorization.k8s.io", 730 Kind: "ClusterRole", 731 Name: "agones-sdk", 732 }, 733 Subjects: []rbacv1.Subject{ 734 { 735 Kind: "ServiceAccount", 736 Name: saName, 737 Namespace: namespace, 738 }, 739 }, 740 } 741 if _, err := f.KubeClient.RbacV1().RoleBindings(namespace).Create(ctx, rb, options); err != nil { 742 err = errors.Errorf("creating RoleBinding for service account %q in namespace %q failed: %s", saName, namespace, err.Error()) 743 derr := f.DeleteNamespace(namespace) 744 if derr != nil { 745 return errors.Wrap(err, derr.Error()) 746 } 747 return err 748 } 749 logrus.Infof("RoleBinding %s/%s is created", namespace, rb.Name) 750 751 return nil 752 } 753 754 // DeleteNamespace deletes a namespace from the test cluster 755 func (f *Framework) DeleteNamespace(namespace string) error { 756 kubeCore := f.KubeClient.CoreV1() 757 ctx := context.Background() 758 759 // Remove finalizers 760 pods, err := kubeCore.Pods(namespace).List(ctx, metav1.ListOptions{}) 761 if err != nil { 762 return errors.Errorf("listing pods in namespace %s failed: %s", namespace, err) 763 } 764 for i := range pods.Items { 765 pod := &pods.Items[i] 766 if len(pod.Finalizers) > 0 { 767 pod.Finalizers = nil 768 payload := []patchRemoveNoValue{{ 769 Op: "remove", 770 Path: "/metadata/finalizers", 771 }} 772 payloadBytes, _ := json.Marshal(payload) 773 if _, err := kubeCore.Pods(namespace).Patch(ctx, pod.Name, types.JSONPatchType, payloadBytes, metav1.PatchOptions{}); err != nil { 774 return errors.Wrapf(err, "updating pod %s failed", pod.GetName()) 775 } 776 } 777 } 778 779 if err := kubeCore.Namespaces().Delete(ctx, namespace, metav1.DeleteOptions{}); err != nil { 780 return errors.Wrapf(err, "deleting namespace %s failed", namespace) 781 } 782 logrus.Infof("Namespace %s is deleted", namespace) 783 return nil 784 } 785 786 type patchRemoveNoValue struct { 787 Op string `json:"op"` 788 Path string `json:"path"` 789 } 790 791 // DefaultGameServer provides a default GameServer fixture, based on parameters 792 // passed to the Test Framework. 793 func (f *Framework) DefaultGameServer(namespace string) *agonesv1.GameServer { 794 gs := &agonesv1.GameServer{ObjectMeta: metav1.ObjectMeta{GenerateName: "game-server", Namespace: namespace}, 795 Spec: agonesv1.GameServerSpec{ 796 Container: "game-server", 797 Ports: []agonesv1.GameServerPort{{ 798 ContainerPort: 7654, 799 Name: "udp-port", 800 PortPolicy: agonesv1.Dynamic, 801 Protocol: corev1.ProtocolUDP, 802 }}, 803 Template: corev1.PodTemplateSpec{ 804 Spec: corev1.PodSpec{ 805 Containers: []corev1.Container{{ 806 Name: "game-server", 807 Image: f.GameServerImage, 808 ImagePullPolicy: corev1.PullIfNotPresent, 809 Resources: corev1.ResourceRequirements{ 810 Requests: corev1.ResourceList{ 811 corev1.ResourceCPU: resource.MustParse("30m"), 812 corev1.ResourceMemory: resource.MustParse("32Mi"), 813 }, 814 Limits: corev1.ResourceList{ 815 corev1.ResourceCPU: resource.MustParse("30m"), 816 corev1.ResourceMemory: resource.MustParse("32Mi"), 817 }, 818 }, 819 }}, 820 }, 821 }, 822 }, 823 } 824 825 if f.PullSecret != "" { 826 gs.Spec.Template.Spec.ImagePullSecrets = []corev1.LocalObjectReference{{ 827 Name: f.PullSecret}} 828 } 829 830 return gs 831 } 832 833 // LogEvents logs all the events for a given Kubernetes objects. Useful for debugging why something 834 // went wrong. 835 func (f *Framework) LogEvents(t *testing.T, log *logrus.Entry, namespace string, objOrRef k8sruntime.Object) { 836 log.WithField("kind", objOrRef.GetObjectKind().GroupVersionKind().Kind).Info("Dumping Events:") 837 events, err := f.KubeClient.CoreV1().Events(namespace).SearchWithContext(context.Background(), scheme.Scheme, objOrRef) 838 require.NoError(t, err, "error searching for events") 839 for i := range events.Items { 840 event := events.Items[i] 841 log.WithField("lastTimestamp", event.LastTimestamp).WithField("type", event.Type).WithField("reason", event.Reason).WithField("message", event.Message).Info("Event!") 842 } 843 } 844 845 // LogPodContainers takes a Pod as an argument and attempts to output the current and previous logs from each container 846 // in that Pod It uses the framework's KubeClient to retrieve the logs and outputs them using the provided logger. 847 func (f *Framework) LogPodContainers(t *testing.T, pod *corev1.Pod) { 848 log := TestLogger(t) 849 log.WithField("pod", pod.Name).WithField("namespace", pod.Namespace).Info("Logs for Pod:") 850 851 // sub-function so defer will fire on each printLogs, rather than at the end. 852 printLogs := func(container corev1.Container, previous bool) { 853 logOptions := &corev1.PodLogOptions{ 854 Container: container.Name, 855 Follow: false, 856 Previous: previous, 857 } 858 859 req := f.KubeClient.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, logOptions) 860 podLogs, err := req.Stream(context.Background()) 861 log = log.WithField("options", logOptions) 862 863 if err != nil { 864 log.WithError(err).Warn("Error opening log stream for container") 865 return 866 } 867 defer podLogs.Close() // nolint:errcheck,staticcheck 868 869 logBytes, err := io.ReadAll(podLogs) 870 if err != nil { 871 log.WithError(err).WithField("options", logOptions).Warn("Error reading logs for container") 872 return 873 } 874 875 log.Info("---Logs for container---") 876 lines := strings.Split(string(logBytes), "\n") 877 for _, line := range lines { 878 if line == "" { 879 continue 880 } 881 log.Info(line) 882 } 883 log.Info("---End of container logs---") 884 } 885 886 // run through the container list twice, so we group current vs previous logs nicely. 887 for _, container := range pod.Spec.Containers { 888 printLogs(container, false) 889 } 890 891 for _, container := range pod.Spec.Containers { 892 printLogs(container, true) 893 } 894 895 } 896 897 // SkipOnCloudProduct skips the test if the e2e was invoked with --cloud-product=<product>. 898 func (f *Framework) SkipOnCloudProduct(t *testing.T, product, reason string) { 899 if f.CloudProduct == product { 900 t.Skipf("skipping test on cloud product %s: %s", product, reason) 901 } 902 } 903 904 // TestLogger returns the standard logger for helper functions. 905 func TestLogger(t *testing.T) *logrus.Entry { 906 return logrus.WithField("test", t.Name()) 907 }