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  }