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 }