agones.dev/agones@v1.54.0/test/e2e/allochelper/helper_func.go (about) 1 // Copyright 2023 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 allochelper is a package for helper function that is used by e2e tests 16 package allochelper 17 18 import ( 19 "context" 20 "crypto/rand" 21 "crypto/rsa" 22 "crypto/tls" 23 "crypto/x509" 24 "crypto/x509/pkix" 25 "encoding/pem" 26 "fmt" 27 "math/big" 28 "net" 29 "testing" 30 "time" 31 32 pb "agones.dev/agones/pkg/allocation/go" 33 agonesv1 "agones.dev/agones/pkg/apis/agones/v1" 34 multiclusterv1 "agones.dev/agones/pkg/apis/multicluster/v1" 35 e2e "agones.dev/agones/test/e2e/framework" 36 "github.com/pkg/errors" 37 "github.com/sirupsen/logrus" 38 "github.com/stretchr/testify/assert" 39 "github.com/stretchr/testify/require" 40 "google.golang.org/grpc" 41 "google.golang.org/grpc/backoff" 42 "google.golang.org/grpc/credentials" 43 k8serrors "k8s.io/apimachinery/pkg/api/errors" 44 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 45 "k8s.io/apimachinery/pkg/labels" 46 "k8s.io/apimachinery/pkg/util/wait" 47 ) 48 49 const ( 50 agonesSystemNamespace = "agones-system" 51 allocatorServiceName = "agones-allocator" 52 allocatorTLSName = "allocator-tls" 53 tlsCrtTag = "tls.crt" 54 tlsKeyTag = "tls.key" 55 allocatorReqURLFmt = "%s:%d" 56 allocatorClientSecretName = "allocator-client.default" 57 allocatorClientSecretNamespace = "default" 58 replicasCount = 5 59 60 gRPCRetryPolicy = `{ 61 "methodConfig": [{ 62 "name": [{}], 63 "waitForReady": true, 64 65 "retryPolicy": { 66 "MaxAttempts": 4, 67 "InitialBackoff": ".01s", 68 "MaxBackoff": ".01s", 69 "BackoffMultiplier": 1.0, 70 "RetryableStatusCodes": [ "UNAVAILABLE" ] 71 } 72 }] 73 }` 74 ) 75 76 // CopyDefaultAllocatorClientSecret copys the allocator client secret 77 func CopyDefaultAllocatorClientSecret(ctx context.Context, t *testing.T, toNamespace string, framework *e2e.Framework) { 78 kubeCore := framework.KubeClient.CoreV1() 79 clientSecret, err := kubeCore.Secrets(allocatorClientSecretNamespace).Get(ctx, allocatorClientSecretName, metav1.GetOptions{}) 80 if err != nil { 81 t.Fatalf("Could not retrieve default allocator client secret %s/%s: %v", allocatorClientSecretNamespace, allocatorClientSecretName, err) 82 } 83 clientSecret.ObjectMeta.Namespace = toNamespace 84 clientSecret.ResourceVersion = "" 85 _, err = kubeCore.Secrets(toNamespace).Create(ctx, clientSecret, metav1.CreateOptions{}) 86 if err != nil { 87 t.Fatalf("Could not copy default allocator client %s/%s secret to namespace %s: %v", allocatorClientSecretNamespace, allocatorClientSecretName, toNamespace, err) 88 } 89 } 90 91 // CreateAllocationPolicy create a allocation policy 92 func CreateAllocationPolicy(ctx context.Context, t *testing.T, framework *e2e.Framework, p *multiclusterv1.GameServerAllocationPolicy) { 93 t.Helper() 94 95 mc := framework.AgonesClient.MulticlusterV1() 96 policy, err := mc.GameServerAllocationPolicies(p.Namespace).Create(ctx, p, metav1.CreateOptions{}) 97 if err != nil { 98 t.Fatalf("creating allocation policy failed: %s", err) 99 } 100 t.Logf("created allocation policy %v", policy) 101 } 102 103 // GetAllocatorEndpoint gets the allocator LB endpoint 104 func GetAllocatorEndpoint(ctx context.Context, t *testing.T, framework *e2e.Framework) (string, int32) { 105 kubeCore := framework.KubeClient.CoreV1() 106 svc, err := kubeCore.Services(agonesSystemNamespace).Get(ctx, allocatorServiceName, metav1.GetOptions{}) 107 if !assert.Nil(t, err) { 108 t.FailNow() 109 } 110 if !assert.NotNil(t, svc.Status.LoadBalancer) { 111 t.FailNow() 112 } 113 if !assert.Equal(t, 1, len(svc.Status.LoadBalancer.Ingress)) { 114 t.FailNow() 115 } 116 if !assert.NotNil(t, 0, svc.Status.LoadBalancer.Ingress[0].IP) { 117 t.FailNow() 118 } 119 120 port := svc.Spec.Ports[0] 121 return svc.Status.LoadBalancer.Ingress[0].IP, port.Port 122 } 123 124 // CreateRemoteClusterDialOptions creates a grpc client dial option with proper certs to make a remote call. 125 func CreateRemoteClusterDialOptions(ctx context.Context, namespace, clientSecretName string, tlsCA []byte, framework *e2e.Framework) ([]grpc.DialOption, error) { 126 tlsConfig, err := GetTLSConfig(ctx, namespace, clientSecretName, tlsCA, framework) 127 if err != nil { 128 return nil, err 129 } 130 131 return []grpc.DialOption{ 132 grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), 133 grpc.WithDefaultServiceConfig(gRPCRetryPolicy), 134 grpc.WithConnectParams(grpc.ConnectParams{ 135 Backoff: backoff.Config{ 136 BaseDelay: time.Duration(100) * time.Millisecond, 137 Multiplier: 1.6, 138 Jitter: 0.2, 139 MaxDelay: 30 * time.Second, 140 }, 141 MinConnectTimeout: time.Second, 142 }), 143 }, nil 144 } 145 146 // GetTLSConfig gets the namesapce client secret 147 func GetTLSConfig(ctx context.Context, namespace, clientSecretName string, tlsCA []byte, framework *e2e.Framework) (*tls.Config, error) { 148 kubeCore := framework.KubeClient.CoreV1() 149 clientSecret, err := kubeCore.Secrets(namespace).Get(ctx, clientSecretName, metav1.GetOptions{}) 150 if err != nil { 151 return nil, errors.Errorf("getting client secret %s/%s failed: %s", namespace, clientSecretName, err) 152 } 153 154 // Create http client using cert 155 clientCert := clientSecret.Data[tlsCrtTag] 156 clientKey := clientSecret.Data[tlsKeyTag] 157 if clientCert == nil || clientKey == nil { 158 return nil, errors.New("missing certificate") 159 } 160 161 // Load client cert 162 cert, err := tls.X509KeyPair(clientCert, clientKey) 163 if err != nil { 164 return nil, err 165 } 166 167 rootCA := x509.NewCertPool() 168 if !rootCA.AppendCertsFromPEM(tlsCA) { 169 return nil, errors.New("could not append PEM format CA cert") 170 } 171 172 return &tls.Config{ 173 Certificates: []tls.Certificate{cert}, 174 RootCAs: rootCA, 175 }, nil 176 } 177 178 // CreateFleet creates a game server fleet 179 func CreateFleet(ctx context.Context, namespace string, framework *e2e.Framework) (*agonesv1.Fleet, error) { 180 return CreateFleetWithOpts(ctx, namespace, framework, func(*agonesv1.Fleet) {}) 181 } 182 183 // CreateFleetWithOpts creates a game server fleet with the designated options 184 func CreateFleetWithOpts(ctx context.Context, namespace string, framework *e2e.Framework, opts func(fleet *agonesv1.Fleet)) (*agonesv1.Fleet, error) { 185 fleets := framework.AgonesClient.AgonesV1().Fleets(namespace) 186 fleet := defaultFleet(namespace, framework) 187 opts(fleet) 188 return fleets.Create(ctx, fleet, metav1.CreateOptions{}) 189 } 190 191 // RefreshAllocatorTLSCerts refreshes the allocator TLS cert with a newly generated cert 192 func RefreshAllocatorTLSCerts(ctx context.Context, t *testing.T, host string, framework *e2e.Framework) []byte { 193 t.Helper() 194 195 pub, priv := generateTLSCertPair(t, host) 196 // verify key pair 197 if _, err := tls.X509KeyPair(pub, priv); err != nil { 198 t.Fatalf("generated key pair failed create cert: %s", err) 199 } 200 201 kubeCore := framework.KubeClient.CoreV1() 202 203 require.Eventually(t, func() bool { 204 s, err := kubeCore.Secrets(agonesSystemNamespace).Get(ctx, allocatorTLSName, metav1.GetOptions{}) 205 if err != nil { 206 t.Logf("failed getting secret %s/%s failed: %s", agonesSystemNamespace, allocatorTLSName, err) 207 return false 208 } 209 210 s.Data[tlsCrtTag] = pub 211 s.Data[tlsKeyTag] = priv 212 if _, err := kubeCore.Secrets(agonesSystemNamespace).Update(ctx, s, metav1.UpdateOptions{}); err != nil { 213 t.Logf("failed updating secrets failed: %s", err) 214 return false 215 } 216 217 return true 218 }, time.Minute, time.Second, "Could not update Secret") 219 220 t.Logf("Allocator TLS is refreshed with public CA: %s for endpoint %s", string(pub), host) 221 return pub 222 } 223 224 func generateTLSCertPair(t *testing.T, host string) ([]byte, []byte) { 225 t.Helper() 226 227 priv, err := rsa.GenerateKey(rand.Reader, 2048) 228 if err != nil { 229 t.Fatalf("generating RSA key failed: %s", err) 230 } 231 232 notBefore := time.Now() 233 notAfter := notBefore.Add(time.Hour) 234 235 serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 236 serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 237 if err != nil { 238 t.Fatalf("generating serial number failed: %s", err) 239 } 240 241 template := x509.Certificate{ 242 SerialNumber: serialNumber, 243 Subject: pkix.Name{ 244 CommonName: host, 245 Organization: []string{"testing"}, 246 }, 247 NotBefore: notBefore, 248 NotAfter: notAfter, 249 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 250 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, 251 SignatureAlgorithm: x509.SHA1WithRSA, 252 BasicConstraintsValid: true, 253 IsCA: true, 254 } 255 256 if host != "" { 257 template.IPAddresses = []net.IP{net.ParseIP(host)} 258 } 259 derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) 260 if err != nil { 261 t.Fatalf("creating certificate failed: %s", err) 262 } 263 pemPubBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) 264 privBytes, err := x509.MarshalPKCS8PrivateKey(priv) 265 if err != nil { 266 t.Fatalf("marshalling private key failed: %v", err) 267 } 268 pemPrivBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}) 269 270 return pemPubBytes, pemPrivBytes 271 } 272 273 // ValidateAllocatorResponse validates the response returned by the allcoator 274 func ValidateAllocatorResponse(t *testing.T, resp *pb.AllocationResponse) { 275 t.Helper() 276 if !assert.NotNil(t, resp) { 277 return 278 } 279 assert.Greater(t, len(resp.Ports), 0) 280 assert.NotEmpty(t, resp.GameServerName) 281 assert.NotEmpty(t, resp.Address) 282 assert.NotEmpty(t, resp.Addresses) 283 assert.NotEmpty(t, resp.NodeName) 284 assert.NotEmpty(t, resp.Metadata.Labels) 285 assert.NotEmpty(t, resp.Metadata.Annotations) 286 } 287 288 // DeleteAgonesPod deletes an Agones pod with the specified namespace and podname 289 func DeleteAgonesPod(ctx context.Context, podName string, namespace string, framework *e2e.Framework) error { 290 policy := metav1.DeletePropagationBackground 291 err := framework.KubeClient.CoreV1().Pods(namespace).Delete(ctx, podName, 292 metav1.DeleteOptions{PropagationPolicy: &policy}) 293 return err 294 } 295 296 // GetAllocatorClient creates an allocator client and ensures that it can be connected to. Returns 297 // a client that has at least once successfully allocated from a fleet. The fleet used to test 298 // the client is leaked. 299 func GetAllocatorClient(ctx context.Context, t *testing.T, framework *e2e.Framework) (pb.AllocationServiceClient, error) { 300 logger := e2e.TestLogger(t) 301 ip, port := GetAllocatorEndpoint(ctx, t, framework) 302 requestURL := fmt.Sprintf(allocatorReqURLFmt, ip, port) 303 tlsCA := RefreshAllocatorTLSCerts(ctx, t, ip, framework) 304 305 flt, err := CreateFleet(ctx, framework.Namespace, framework) 306 if !assert.Nil(t, err) { 307 return nil, err 308 } 309 framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) 310 311 dialOpts, err := CreateRemoteClusterDialOptions(ctx, allocatorClientSecretNamespace, allocatorClientSecretName, tlsCA, framework) 312 if err != nil { 313 return nil, err 314 } 315 316 conn, err := grpc.NewClient(requestURL, dialOpts...) 317 require.NoError(t, err, "Failed grpc.NewClient") 318 go func() { 319 for { 320 state := conn.GetState() 321 logger.Infof("allocation client state: %v", state) 322 if notDone := conn.WaitForStateChange(ctx, state); !notDone { 323 break 324 } 325 } 326 _ = conn.Close() 327 }() 328 329 grpcClient := pb.NewAllocationServiceClient(conn) 330 331 request := &pb.AllocationRequest{ 332 Namespace: framework.Namespace, 333 PreferredGameServerSelectors: []*pb.GameServerSelector{{MatchLabels: map[string]string{agonesv1.FleetNameLabel: flt.ObjectMeta.Name}}}, 334 Scheduling: pb.AllocationRequest_Packed, 335 Metadata: &pb.MetaPatch{Labels: map[string]string{"gslabel": "allocatedbytest"}}, 336 } 337 338 var response *pb.AllocationResponse 339 err = wait.PollUntilContextTimeout(ctx, 5*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) { 340 response, err = grpcClient.Allocate(ctx, request) 341 if err != nil { 342 logger.WithError(err).Info("Failed grpc allocation request while waiting for certs to stabilize") 343 return false, nil 344 } 345 ValidateAllocatorResponse(t, response) 346 err = DeleteAgonesPod(ctx, response.GameServerName, framework.Namespace, framework) 347 assert.NoError(t, err, "Failed to delete game server pod %s", response.GameServerName) 348 return true, nil 349 }) 350 if err != nil { 351 return nil, err 352 } 353 354 return grpcClient, nil 355 } 356 357 // CleanupNamespaces cleans up the framework namespace 358 func CleanupNamespaces(ctx context.Context, framework *e2e.Framework) error { 359 // list all e2e namespaces 360 opts := metav1.ListOptions{LabelSelector: labels.Set(e2e.NamespaceLabel).String()} 361 list, err := framework.KubeClient.CoreV1().Namespaces().List(ctx, opts) 362 if err != nil { 363 return err 364 } 365 366 // loop through them, and delete them 367 for _, ns := range list.Items { 368 if err := framework.DeleteNamespace(ns.ObjectMeta.Name); err != nil { 369 cause := errors.Cause(err) 370 if k8serrors.IsConflict(cause) { 371 logrus.WithError(cause).Warn("namespace already being deleted") 372 continue 373 } 374 // here just in case we need to catch other errors 375 logrus.WithField("reason", k8serrors.ReasonForError(cause)).Info("cause for namespace deletion error") 376 return cause 377 } 378 } 379 380 return nil 381 } 382 383 // From fleet_test 384 // defaultFleet returns a default fleet configuration 385 func defaultFleet(namespace string, framework *e2e.Framework) *agonesv1.Fleet { 386 gs := framework.DefaultGameServer(namespace) 387 return fleetWithGameServerSpec(&gs.Spec, namespace) 388 } 389 390 // fleetWithGameServerSpec returns a fleet with specified gameserver spec 391 func fleetWithGameServerSpec(gsSpec *agonesv1.GameServerSpec, namespace string) *agonesv1.Fleet { 392 return &agonesv1.Fleet{ 393 ObjectMeta: metav1.ObjectMeta{GenerateName: "simple-fleet-1.0", Namespace: namespace}, 394 Spec: agonesv1.FleetSpec{ 395 Replicas: replicasCount, 396 Template: agonesv1.GameServerTemplateSpec{ 397 Spec: *gsSpec, 398 }, 399 }, 400 } 401 }