sigs.k8s.io/cluster-api-provider-azure@v1.17.0/test/e2e/azure_selfhosted.go (about)

     1  //go:build e2e
     2  // +build e2e
     3  
     4  /*
     5  Copyright 2021 The Kubernetes Authors.
     6  
     7  Licensed under the Apache License, Version 2.0 (the "License");
     8  you may not use this file except in compliance with the License.
     9  You may obtain a copy of the License at
    10  
    11      http://www.apache.org/licenses/LICENSE-2.0
    12  
    13  Unless required by applicable law or agreed to in writing, software
    14  distributed under the License is distributed on an "AS IS" BASIS,
    15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  See the License for the specific language governing permissions and
    17  limitations under the License.
    18  */
    19  
    20  package e2e
    21  
    22  import (
    23  	"context"
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  
    28  	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
    29  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi"
    30  	. "github.com/onsi/ginkgo/v2"
    31  	. "github.com/onsi/gomega"
    32  	corev1 "k8s.io/api/core/v1"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    35  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    36  	clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
    37  	capi_e2e "sigs.k8s.io/cluster-api/test/e2e"
    38  	"sigs.k8s.io/cluster-api/test/framework"
    39  	"sigs.k8s.io/cluster-api/test/framework/clusterctl"
    40  	"sigs.k8s.io/cluster-api/util"
    41  	"sigs.k8s.io/controller-runtime/pkg/client"
    42  )
    43  
    44  // SelfHostedSpecInput is the input for SelfHostedSpec.
    45  type SelfHostedSpecInput struct {
    46  	E2EConfig             *clusterctl.E2EConfig
    47  	ClusterctlConfigPath  string
    48  	BootstrapClusterProxy framework.ClusterProxy
    49  	ArtifactFolder        string
    50  	SkipCleanup           bool
    51  	ControlPlaneWaiters   clusterctl.ControlPlaneWaiters
    52  }
    53  
    54  // SelfHostedSpec implements a test that verifies Cluster API creating a cluster, pivoting to a self-hosted cluster.
    55  func SelfHostedSpec(ctx context.Context, inputGetter func() SelfHostedSpecInput) {
    56  	var (
    57  		specName         = "self-hosted"
    58  		input            SelfHostedSpecInput
    59  		namespace        *corev1.Namespace
    60  		cancelWatches    context.CancelFunc
    61  		clusterResources *clusterctl.ApplyClusterTemplateAndWaitResult
    62  
    63  		selfHostedClusterProxy  framework.ClusterProxy
    64  		selfHostedNamespace     *corev1.Namespace
    65  		selfHostedCancelWatches context.CancelFunc
    66  		selfHostedCluster       *clusterv1.Cluster
    67  	)
    68  
    69  	BeforeEach(func() {
    70  		Expect(ctx).NotTo(BeNil(), "ctx is required for %s spec", specName)
    71  		input = inputGetter()
    72  		Expect(input.E2EConfig).NotTo(BeNil(), "Invalid argument. input.E2EConfig can't be nil when calling %s spec", specName)
    73  		Expect(input.ClusterctlConfigPath).To(BeAnExistingFile(), "Invalid argument. input.ClusterctlConfigPath must be an existing file when calling %s spec", specName)
    74  		Expect(input.BootstrapClusterProxy).NotTo(BeNil(), "Invalid argument. input.BootstrapClusterProxy can't be nil when calling %s spec", specName)
    75  		Expect(os.MkdirAll(input.ArtifactFolder, 0o750)).To(Succeed(), "Invalid argument. input.ArtifactFolder can't be created for %s spec", specName)
    76  		Expect(input.E2EConfig.Variables).To(HaveKey(capi_e2e.KubernetesVersion))
    77  
    78  		// Setup a Namespace where to host objects for this spec and create a watcher for the namespace events.
    79  		var err error
    80  		namespace, cancelWatches, err = setupSpecNamespace(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder)
    81  		Expect(err).NotTo(HaveOccurred())
    82  		clusterResources = new(clusterctl.ApplyClusterTemplateAndWaitResult)
    83  
    84  		identityName := input.E2EConfig.GetVariable(ClusterIdentityName)
    85  		Expect(os.Setenv(ClusterIdentityName, identityName)).To(Succeed())
    86  		Expect(os.Setenv(ClusterIdentityNamespace, namespace.Name)).To(Succeed())
    87  	})
    88  
    89  	// Management clusters do not support Windows nodes because of cert manager
    90  	// We are using the capi specs located in test/e2e/data/infrastructure-azure/v1beta1 that only have linux nodes
    91  	// to act as the management cluster until Windows nodes are supported for management nodes
    92  	// Tracking support for cert manager: https://github.com/jetstack/cert-manager/issues/3606
    93  	It("Should pivot the bootstrap cluster to a self-hosted cluster", func() {
    94  		By("Creating a workload cluster")
    95  		clusterctl.ApplyClusterTemplateAndWait(ctx, createApplyClusterTemplateInput(
    96  			specName,
    97  			withFlavor("management"),
    98  			withNamespace(namespace.Name),
    99  			withClusterName(fmt.Sprintf("%s-%s", specName, util.RandomString(6))),
   100  			withControlPlaneMachineCount(1),
   101  			withWorkerMachineCount(1),
   102  			withControlPlaneWaiters(input.ControlPlaneWaiters),
   103  		), clusterResources)
   104  
   105  		By("Turning the workload cluster into a management cluster")
   106  		cluster := clusterResources.Cluster
   107  		// Get a ClusterBroker so we can interact with the workload cluster
   108  		selfHostedClusterProxy = input.BootstrapClusterProxy.GetWorkloadCluster(ctx, cluster.Namespace, cluster.Name)
   109  
   110  		Byf("Creating a namespace for hosting the %s test spec", specName)
   111  		selfHostedNamespace, selfHostedCancelWatches = framework.CreateNamespaceAndWatchEvents(ctx, framework.CreateNamespaceAndWatchEventsInput{
   112  			Creator:   selfHostedClusterProxy.GetClient(),
   113  			ClientSet: selfHostedClusterProxy.GetClientSet(),
   114  			Name:      namespace.Name,
   115  			LogFolder: filepath.Join(input.ArtifactFolder, "clusters", "bootstrap"),
   116  		})
   117  
   118  		By("Initializing the workload cluster")
   119  		clusterctl.InitManagementClusterAndWatchControllerLogs(ctx, clusterctl.InitManagementClusterAndWatchControllerLogsInput{
   120  			ClusterProxy:            selfHostedClusterProxy,
   121  			ClusterctlConfigPath:    input.ClusterctlConfigPath,
   122  			InfrastructureProviders: input.E2EConfig.InfrastructureProviders(),
   123  			AddonProviders:          input.E2EConfig.AddonProviders(),
   124  			LogFolder:               filepath.Join(input.ArtifactFolder, "clusters", cluster.Name),
   125  		}, input.E2EConfig.GetIntervals(specName, "wait-controllers")...)
   126  
   127  		By("Ensure API servers are stable before doing move")
   128  		// Nb. This check was introduced to prevent doing move to self-hosted in an aggressive way and thus avoid flakes.
   129  		// More specifically, we were observing the test failing to get objects from the API server during move, so we
   130  		// are now testing the API servers are stable before starting move.
   131  		Consistently(func() error {
   132  			ns := &corev1.Namespace{}
   133  			return input.BootstrapClusterProxy.GetClient().Get(ctx, client.ObjectKey{Name: kubesystem}, ns)
   134  		}, "5s", "100ms").Should(BeNil(), "Failed to assert bootstrap API server stability")
   135  		Consistently(func() error {
   136  			ns := &corev1.Namespace{}
   137  			return selfHostedClusterProxy.GetClient().Get(ctx, client.ObjectKey{Name: kubesystem}, ns)
   138  		}, "5s", "100ms").Should(BeNil(), "Failed to assert self-hosted API server stability")
   139  
   140  		By("Moving the cluster to self hosted")
   141  		clusterctl.Move(ctx, clusterctl.MoveInput{
   142  			LogFolder:            filepath.Join(input.ArtifactFolder, "clusters", "bootstrap"),
   143  			ClusterctlConfigPath: input.ClusterctlConfigPath,
   144  			FromKubeconfigPath:   input.BootstrapClusterProxy.GetKubeconfigPath(),
   145  			ToKubeconfigPath:     selfHostedClusterProxy.GetKubeconfigPath(),
   146  			Namespace:            namespace.Name,
   147  		})
   148  
   149  		// The workload cluster is not set up for workload identity. Use UserAssignedMSI there instead.
   150  		err := selfHostedClusterProxy.GetClient().Delete(ctx, &infrav1.AzureClusterIdentity{
   151  			ObjectMeta: metav1.ObjectMeta{
   152  				Namespace: cluster.Namespace,
   153  				Name:      e2eConfig.GetVariable(ClusterIdentityName),
   154  			},
   155  		})
   156  		Expect(err).NotTo(HaveOccurred())
   157  		cred, err := azidentity.NewDefaultAzureCredential(nil)
   158  		Expect(err).NotTo(HaveOccurred())
   159  		identityClient, err := armmsi.NewUserAssignedIdentitiesClient(getSubscriptionID(Default), cred, nil)
   160  		Expect(err).NotTo(HaveOccurred())
   161  		identityRG := e2eConfig.GetVariable(AzureIdentityResourceGroup)
   162  		identityName := e2eConfig.GetVariable(AzureUserIdentity)
   163  		identity, err := identityClient.Get(ctx, identityRG, identityName, nil)
   164  		Expect(err).NotTo(HaveOccurred())
   165  		err = selfHostedClusterProxy.GetClient().Create(ctx, &infrav1.AzureClusterIdentity{
   166  			ObjectMeta: metav1.ObjectMeta{
   167  				Namespace: cluster.Namespace,
   168  				Name:      e2eConfig.GetVariable(ClusterIdentityName),
   169  				Labels: map[string]string{
   170  					clusterctlv1.ClusterctlMoveHierarchyLabel: "true",
   171  				},
   172  			},
   173  			Spec: infrav1.AzureClusterIdentitySpec{
   174  				AllowedNamespaces: &infrav1.AllowedNamespaces{},
   175  				ClientID:          *identity.Properties.ClientID,
   176  				ResourceID:        *identity.ID,
   177  				TenantID:          e2eConfig.GetVariable(AzureTenantID),
   178  				Type:              infrav1.UserAssignedMSI,
   179  			},
   180  		})
   181  		Expect(err).NotTo(HaveOccurred())
   182  
   183  		Log("Waiting for the cluster to be reconciled after moving to self hosted")
   184  		selfHostedCluster = framework.DiscoveryAndWaitForCluster(ctx, framework.DiscoveryAndWaitForClusterInput{
   185  			Getter:    selfHostedClusterProxy.GetClient(),
   186  			Namespace: selfHostedNamespace.Name,
   187  			Name:      cluster.Name,
   188  		}, input.E2EConfig.GetIntervals(specName, "wait-cluster")...)
   189  
   190  		controlPlane := framework.GetKubeadmControlPlaneByCluster(ctx, framework.GetKubeadmControlPlaneByClusterInput{
   191  			Lister:      selfHostedClusterProxy.GetClient(),
   192  			ClusterName: selfHostedCluster.Name,
   193  			Namespace:   selfHostedCluster.Namespace,
   194  		})
   195  		Expect(controlPlane).NotTo(BeNil())
   196  
   197  		By("PASSED!")
   198  	})
   199  
   200  	AfterEach(func() {
   201  		if input.SkipCleanup {
   202  			return
   203  		}
   204  		if selfHostedNamespace != nil {
   205  			// Dump all Cluster API related resources to artifacts before pivoting back.
   206  			framework.DumpAllResources(ctx, framework.DumpAllResourcesInput{
   207  				Lister:    selfHostedClusterProxy.GetClient(),
   208  				Namespace: namespace.Name,
   209  				LogPath:   filepath.Join(input.ArtifactFolder, "clusters", clusterResources.Cluster.Name, "resources"),
   210  			})
   211  		}
   212  		if selfHostedCluster != nil {
   213  			By("Ensure API servers are stable before doing move")
   214  			// Nb. This check was introduced to prevent doing move back to bootstrap in an aggressive way and thus avoid flakes.
   215  			// More specifically, we were observing the test failing to get objects from the API server during move, so we
   216  			// are now testing the API servers are stable before starting move.
   217  			Consistently(func() error {
   218  				ns := &corev1.Namespace{}
   219  				return input.BootstrapClusterProxy.GetClient().Get(ctx, client.ObjectKey{Name: kubesystem}, ns)
   220  			}, "5s", "100ms").Should(BeNil(), "Failed to assert bootstrap API server stability")
   221  			Consistently(func() error {
   222  				ns := &corev1.Namespace{}
   223  				return selfHostedClusterProxy.GetClient().Get(ctx, client.ObjectKey{Name: kubesystem}, ns)
   224  			}, "5s", "100ms").Should(BeNil(), "Failed to assert self-hosted API server stability")
   225  
   226  			By("Moving the cluster back to bootstrap")
   227  			clusterctl.Move(ctx, clusterctl.MoveInput{
   228  				LogFolder:            filepath.Join(input.ArtifactFolder, "clusters", clusterResources.Cluster.Name),
   229  				ClusterctlConfigPath: input.ClusterctlConfigPath,
   230  				FromKubeconfigPath:   selfHostedClusterProxy.GetKubeconfigPath(),
   231  				ToKubeconfigPath:     input.BootstrapClusterProxy.GetKubeconfigPath(),
   232  				Namespace:            selfHostedNamespace.Name,
   233  			})
   234  
   235  			// Restore the workload identity AzureClusterIdentity
   236  			err := input.BootstrapClusterProxy.GetClient().Delete(ctx, &infrav1.AzureClusterIdentity{
   237  				ObjectMeta: metav1.ObjectMeta{
   238  					Namespace: namespace.Name,
   239  					Name:      e2eConfig.GetVariable(ClusterIdentityName),
   240  				},
   241  			})
   242  			Expect(err).NotTo(HaveOccurred())
   243  			err = input.BootstrapClusterProxy.GetClient().Create(ctx, &infrav1.AzureClusterIdentity{
   244  				ObjectMeta: metav1.ObjectMeta{
   245  					Namespace: namespace.Name,
   246  					Name:      e2eConfig.GetVariable(ClusterIdentityName),
   247  					Labels: map[string]string{
   248  						clusterctlv1.ClusterctlMoveHierarchyLabel: "true",
   249  					},
   250  				},
   251  				Spec: infrav1.AzureClusterIdentitySpec{
   252  					AllowedNamespaces: &infrav1.AllowedNamespaces{},
   253  					ClientID:          e2eConfig.GetVariable(AzureClientIDUserAssignedIdentity),
   254  					TenantID:          e2eConfig.GetVariable(AzureTenantID),
   255  					Type:              infrav1.WorkloadIdentity,
   256  				},
   257  			})
   258  			Expect(err).NotTo(HaveOccurred())
   259  
   260  			Log("Waiting for the cluster to be reconciled after moving back to booststrap")
   261  			clusterResources.Cluster = framework.DiscoveryAndWaitForCluster(ctx, framework.DiscoveryAndWaitForClusterInput{
   262  				Getter:    input.BootstrapClusterProxy.GetClient(),
   263  				Namespace: namespace.Name,
   264  				Name:      clusterResources.Cluster.Name,
   265  			}, input.E2EConfig.GetIntervals(specName, "wait-cluster")...)
   266  		}
   267  		if selfHostedCancelWatches != nil {
   268  			selfHostedCancelWatches()
   269  		}
   270  
   271  		cleanInput := cleanupInput{
   272  			SpecName:          specName,
   273  			Cluster:           clusterResources.Cluster,
   274  			ClusterProxy:      input.BootstrapClusterProxy,
   275  			Namespace:         namespace,
   276  			CancelWatches:     cancelWatches,
   277  			IntervalsGetter:   input.E2EConfig.GetIntervals,
   278  			SkipCleanup:       input.SkipCleanup,
   279  			SkipLogCollection: skipLogCollection,
   280  			ArtifactFolder:    input.ArtifactFolder,
   281  		}
   282  		// Dumps all the resources in the spec namespace, then cleanups the cluster object and the spec namespace itself.
   283  		dumpSpecResourcesAndCleanup(ctx, cleanInput)
   284  	})
   285  }