sigs.k8s.io/cluster-api-provider-azure@v1.17.0/pkg/mutators/azureasomanagedcontrolplane.go (about) 1 /* 2 Copyright 2024 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package mutators 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "strings" 24 25 asocontainerservicev1 "github.com/Azure/azure-service-operator/v2/api/containerservice/v1api20231001" 26 asocontainerservicev1hub "github.com/Azure/azure-service-operator/v2/api/containerservice/v1api20231001/storage" 27 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 28 infrav1alpha "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha1" 29 "sigs.k8s.io/cluster-api-provider-azure/azure" 30 "sigs.k8s.io/cluster-api-provider-azure/util/tele" 31 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 32 exputil "sigs.k8s.io/cluster-api/exp/util" 33 "sigs.k8s.io/cluster-api/util/secret" 34 "sigs.k8s.io/controller-runtime/pkg/client" 35 "sigs.k8s.io/controller-runtime/pkg/conversion" 36 "sigs.k8s.io/controller-runtime/pkg/reconcile" 37 ) 38 39 var ( 40 // ErrNoManagedClusterDefined describes an AzureASOManagedControlPlane without a ManagedCluster. 41 ErrNoManagedClusterDefined = fmt.Errorf("no %s ManagedCluster defined in AzureASOManagedControlPlane spec.resources", asocontainerservicev1hub.GroupVersion.Group) 42 43 // ErrNoAzureASOManagedMachinePools means no AzureASOManagedMachinePools exist for an AzureASOManagedControlPlane. 44 ErrNoAzureASOManagedMachinePools = errors.New("no AzureASOManagedMachinePools found for AzureASOManagedControlPlane") 45 ) 46 47 // SetManagedClusterDefaults propagates values defined by Cluster API to an ASO ManagedCluster. 48 func SetManagedClusterDefaults(ctrlClient client.Client, asoManagedControlPlane *infrav1alpha.AzureASOManagedControlPlane, cluster *clusterv1.Cluster) ResourcesMutator { 49 return func(ctx context.Context, us []*unstructured.Unstructured) error { 50 ctx, _, done := tele.StartSpanWithLogger(ctx, "mutators.SetManagedClusterDefaults") 51 defer done() 52 53 var managedCluster *unstructured.Unstructured 54 var managedClusterPath string 55 for i, u := range us { 56 if u.GroupVersionKind().Group == asocontainerservicev1hub.GroupVersion.Group && 57 u.GroupVersionKind().Kind == "ManagedCluster" { 58 managedCluster = u 59 managedClusterPath = fmt.Sprintf("spec.resources[%d]", i) 60 break 61 } 62 } 63 if managedCluster == nil { 64 return reconcile.TerminalError(ErrNoManagedClusterDefined) 65 } 66 67 if err := setManagedClusterKubernetesVersion(ctx, asoManagedControlPlane, managedClusterPath, managedCluster); err != nil { 68 return err 69 } 70 71 if err := setManagedClusterServiceCIDR(ctx, cluster, managedClusterPath, managedCluster); err != nil { 72 return err 73 } 74 75 if err := setManagedClusterPodCIDR(ctx, cluster, managedClusterPath, managedCluster); err != nil { 76 return err 77 } 78 79 if err := setManagedClusterAgentPoolProfiles(ctx, ctrlClient, asoManagedControlPlane.Namespace, cluster, managedClusterPath, managedCluster); err != nil { 80 return err 81 } 82 83 if err := setManagedClusterCredentials(ctx, cluster, managedClusterPath, managedCluster); err != nil { 84 return err 85 } 86 87 return nil 88 } 89 } 90 91 func setManagedClusterKubernetesVersion(ctx context.Context, asoManagedControlPlane *infrav1alpha.AzureASOManagedControlPlane, managedClusterPath string, managedCluster *unstructured.Unstructured) error { 92 _, log, done := tele.StartSpanWithLogger(ctx, "mutators.setManagedClusterKubernetesVersion") 93 defer done() 94 95 capzK8sVersion := strings.TrimPrefix(asoManagedControlPlane.Spec.Version, "v") 96 if capzK8sVersion == "" { 97 // When the CAPI contract field isn't set, any value for version in the embedded ASO resource may be specified. 98 return nil 99 } 100 101 k8sVersionPath := []string{"spec", "kubernetesVersion"} 102 userK8sVersion, k8sVersionFound, err := unstructured.NestedString(managedCluster.UnstructuredContent(), k8sVersionPath...) 103 if err != nil { 104 return err 105 } 106 setK8sVersion := mutation{ 107 location: managedClusterPath + "." + strings.Join(k8sVersionPath, "."), 108 val: capzK8sVersion, 109 reason: "because spec.version is set to " + asoManagedControlPlane.Spec.Version, 110 } 111 if k8sVersionFound && userK8sVersion != capzK8sVersion { 112 return Incompatible{ 113 mutation: setK8sVersion, 114 userVal: userK8sVersion, 115 } 116 } 117 logMutation(log, setK8sVersion) 118 return unstructured.SetNestedField(managedCluster.UnstructuredContent(), capzK8sVersion, k8sVersionPath...) 119 } 120 121 func setManagedClusterServiceCIDR(ctx context.Context, cluster *clusterv1.Cluster, managedClusterPath string, managedCluster *unstructured.Unstructured) error { 122 _, log, done := tele.StartSpanWithLogger(ctx, "mutators.setManagedClusterServiceCIDR") 123 defer done() 124 125 if cluster.Spec.ClusterNetwork == nil || 126 cluster.Spec.ClusterNetwork.Services == nil || 127 len(cluster.Spec.ClusterNetwork.Services.CIDRBlocks) == 0 { 128 return nil 129 } 130 131 capiCIDR := cluster.Spec.ClusterNetwork.Services.CIDRBlocks[0] 132 133 // ManagedCluster.v1api20210501.containerservice.azure.com does not contain the plural serviceCidrs field. 134 svcCIDRPath := []string{"spec", "networkProfile", "serviceCidr"} 135 userSvcCIDR, found, err := unstructured.NestedString(managedCluster.UnstructuredContent(), svcCIDRPath...) 136 if err != nil { 137 return err 138 } 139 setSvcCIDR := mutation{ 140 location: managedClusterPath + "." + strings.Join(svcCIDRPath, "."), 141 val: capiCIDR, 142 reason: fmt.Sprintf("because spec.clusterNetwork.services.cidrBlocks[0] in Cluster %s/%s is set to %s", cluster.Namespace, cluster.Name, capiCIDR), 143 } 144 if found && userSvcCIDR != capiCIDR { 145 return Incompatible{ 146 mutation: setSvcCIDR, 147 userVal: userSvcCIDR, 148 } 149 } 150 logMutation(log, setSvcCIDR) 151 return unstructured.SetNestedField(managedCluster.UnstructuredContent(), capiCIDR, svcCIDRPath...) 152 } 153 154 func setManagedClusterPodCIDR(ctx context.Context, cluster *clusterv1.Cluster, managedClusterPath string, managedCluster *unstructured.Unstructured) error { 155 _, log, done := tele.StartSpanWithLogger(ctx, "mutators.setManagedClusterPodCIDR") 156 defer done() 157 158 if cluster.Spec.ClusterNetwork == nil || 159 cluster.Spec.ClusterNetwork.Pods == nil || 160 len(cluster.Spec.ClusterNetwork.Pods.CIDRBlocks) == 0 { 161 return nil 162 } 163 164 capiCIDR := cluster.Spec.ClusterNetwork.Pods.CIDRBlocks[0] 165 166 // ManagedCluster.v1api20210501.containerservice.azure.com does not contain the plural podCidrs field. 167 podCIDRPath := []string{"spec", "networkProfile", "podCidr"} 168 userPodCIDR, found, err := unstructured.NestedString(managedCluster.UnstructuredContent(), podCIDRPath...) 169 if err != nil { 170 return err 171 } 172 setPodCIDR := mutation{ 173 location: managedClusterPath + "." + strings.Join(podCIDRPath, "."), 174 val: capiCIDR, 175 reason: fmt.Sprintf("because spec.clusterNetwork.pods.cidrBlocks[0] in Cluster %s/%s is set to %s", cluster.Namespace, cluster.Name, capiCIDR), 176 } 177 if found && userPodCIDR != capiCIDR { 178 return Incompatible{ 179 mutation: setPodCIDR, 180 userVal: userPodCIDR, 181 } 182 } 183 logMutation(log, setPodCIDR) 184 return unstructured.SetNestedField(managedCluster.UnstructuredContent(), capiCIDR, podCIDRPath...) 185 } 186 187 func setManagedClusterAgentPoolProfiles(ctx context.Context, ctrlClient client.Client, namespace string, cluster *clusterv1.Cluster, managedClusterPath string, managedCluster *unstructured.Unstructured) error { 188 ctx, log, done := tele.StartSpanWithLogger(ctx, "mutators.setManagedClusterAgentPoolProfiles") 189 defer done() 190 191 agentPoolProfilesPath := []string{"spec", "agentPoolProfiles"} 192 userAgentPoolProfiles, agentPoolProfilesFound, err := unstructured.NestedSlice(managedCluster.UnstructuredContent(), agentPoolProfilesPath...) 193 if err != nil { 194 return err 195 } 196 setAgentPoolProfiles := mutation{ 197 location: managedClusterPath + "." + strings.Join(agentPoolProfilesPath, "."), 198 val: "nil", 199 reason: "because agent pool definitions must be inherited from AzureASOManagedMachinePools", 200 } 201 if agentPoolProfilesFound { 202 return Incompatible{ 203 mutation: setAgentPoolProfiles, 204 userVal: fmt.Sprintf("<slice of length %d>", len(userAgentPoolProfiles)), 205 } 206 } 207 208 // AKS requires ManagedClusters to be created with agent pools: https://github.com/Azure/azure-service-operator/issues/2791 209 getMC := &asocontainerservicev1.ManagedCluster{} 210 err = ctrlClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: managedCluster.GetName()}, getMC) 211 if client.IgnoreNotFound(err) != nil { 212 return err 213 } 214 if len(getMC.Status.AgentPoolProfiles) != 0 { 215 return nil 216 } 217 218 log.V(4).Info("gathering agent pool profiles to include in ManagedCluster create") 219 agentPools, err := agentPoolsFromManagedMachinePools(ctx, ctrlClient, cluster.Name, namespace) 220 if err != nil { 221 return err 222 } 223 mc, err := ctrlClient.Scheme().New(managedCluster.GroupVersionKind()) 224 if err != nil { 225 return err 226 } 227 err = ctrlClient.Scheme().Convert(managedCluster, mc, nil) 228 if err != nil { 229 return err 230 } 231 setAgentPoolProfiles.val = fmt.Sprintf("<slice of length %d>", len(agentPools)) 232 logMutation(log, setAgentPoolProfiles) 233 err = setAgentPoolProfilesFromAgentPools(mc.(conversion.Convertible), agentPools) 234 if err != nil { 235 return err 236 } 237 err = ctrlClient.Scheme().Convert(mc, managedCluster, nil) 238 if err != nil { 239 return err 240 } 241 242 return nil 243 } 244 245 func agentPoolsFromManagedMachinePools(ctx context.Context, ctrlClient client.Client, clusterName string, namespace string) ([]conversion.Convertible, error) { 246 ctx, log, done := tele.StartSpanWithLogger(ctx, "mutators.agentPoolsFromManagedMachinePools") 247 defer done() 248 249 asoManagedMachinePools := &infrav1alpha.AzureASOManagedMachinePoolList{} 250 err := ctrlClient.List(ctx, asoManagedMachinePools, 251 client.InNamespace(namespace), 252 client.MatchingLabels{ 253 clusterv1.ClusterNameLabel: clusterName, 254 }, 255 ) 256 if err != nil { 257 return nil, fmt.Errorf("failed to list AzureASOManagedMachinePools: %w", err) 258 } 259 260 var agentPools []conversion.Convertible 261 for _, asoManagedMachinePool := range asoManagedMachinePools.Items { 262 machinePool, err := exputil.GetOwnerMachinePool(ctx, ctrlClient, asoManagedMachinePool.ObjectMeta) 263 if err != nil { 264 return nil, err 265 } 266 if machinePool == nil { 267 log.V(2).Info("Waiting for MachinePool Controller to set OwnerRef on AzureASOManagedMachinePool") 268 return nil, nil 269 } 270 271 resources, err := ApplyMutators(ctx, asoManagedMachinePool.Spec.Resources, 272 SetAgentPoolDefaults(ctrlClient, machinePool), 273 ) 274 if err != nil { 275 return nil, err 276 } 277 278 for _, u := range resources { 279 if u.GroupVersionKind().Group != asocontainerservicev1hub.GroupVersion.Group || 280 u.GroupVersionKind().Kind != "ManagedClustersAgentPool" { 281 continue 282 } 283 284 agentPool, err := ctrlClient.Scheme().New(u.GroupVersionKind()) 285 if err != nil { 286 return nil, fmt.Errorf("error creating new %v: %w", u.GroupVersionKind(), err) 287 } 288 err = ctrlClient.Scheme().Convert(u, agentPool, nil) 289 if err != nil { 290 return nil, err 291 } 292 293 agentPools = append(agentPools, agentPool.(conversion.Convertible)) 294 break 295 } 296 } 297 298 return agentPools, nil 299 } 300 301 func setAgentPoolProfilesFromAgentPools(managedCluster conversion.Convertible, agentPools []conversion.Convertible) error { 302 hubMC := &asocontainerservicev1hub.ManagedCluster{} 303 err := managedCluster.ConvertTo(hubMC) 304 if err != nil { 305 return err 306 } 307 hubMC.Spec.AgentPoolProfiles = nil 308 309 for _, agentPool := range agentPools { 310 hubPool := &asocontainerservicev1hub.ManagedClustersAgentPool{} 311 err := agentPool.ConvertTo(hubPool) 312 if err != nil { 313 return err 314 } 315 316 profile := asocontainerservicev1hub.ManagedClusterAgentPoolProfile{ 317 AvailabilityZones: hubPool.Spec.AvailabilityZones, 318 CapacityReservationGroupReference: hubPool.Spec.CapacityReservationGroupReference, 319 Count: hubPool.Spec.Count, 320 CreationData: hubPool.Spec.CreationData, 321 EnableAutoScaling: hubPool.Spec.EnableAutoScaling, 322 EnableEncryptionAtHost: hubPool.Spec.EnableEncryptionAtHost, 323 EnableFIPS: hubPool.Spec.EnableFIPS, 324 EnableNodePublicIP: hubPool.Spec.EnableNodePublicIP, 325 EnableUltraSSD: hubPool.Spec.EnableUltraSSD, 326 GpuInstanceProfile: hubPool.Spec.GpuInstanceProfile, 327 HostGroupReference: hubPool.Spec.HostGroupReference, 328 KubeletConfig: hubPool.Spec.KubeletConfig, 329 KubeletDiskType: hubPool.Spec.KubeletDiskType, 330 LinuxOSConfig: hubPool.Spec.LinuxOSConfig, 331 MaxCount: hubPool.Spec.MaxCount, 332 MaxPods: hubPool.Spec.MaxPods, 333 MinCount: hubPool.Spec.MinCount, 334 Mode: hubPool.Spec.Mode, 335 Name: azure.AliasOrNil[string](&hubPool.Spec.AzureName), 336 NetworkProfile: hubPool.Spec.NetworkProfile, 337 NodeLabels: hubPool.Spec.NodeLabels, 338 NodePublicIPPrefixReference: hubPool.Spec.NodePublicIPPrefixReference, 339 NodeTaints: hubPool.Spec.NodeTaints, 340 OrchestratorVersion: hubPool.Spec.OrchestratorVersion, 341 OsDiskSizeGB: hubPool.Spec.OsDiskSizeGB, 342 OsDiskType: hubPool.Spec.OsDiskType, 343 OsSKU: hubPool.Spec.OsSKU, 344 OsType: hubPool.Spec.OsType, 345 PodSubnetReference: hubPool.Spec.PodSubnetReference, 346 PowerState: hubPool.Spec.PowerState, 347 PropertyBag: hubPool.Spec.PropertyBag, 348 ProximityPlacementGroupReference: hubPool.Spec.ProximityPlacementGroupReference, 349 ScaleDownMode: hubPool.Spec.ScaleDownMode, 350 ScaleSetEvictionPolicy: hubPool.Spec.ScaleSetEvictionPolicy, 351 ScaleSetPriority: hubPool.Spec.ScaleSetPriority, 352 SpotMaxPrice: hubPool.Spec.SpotMaxPrice, 353 Tags: hubPool.Spec.Tags, 354 Type: hubPool.Spec.Type, 355 UpgradeSettings: hubPool.Spec.UpgradeSettings, 356 VmSize: hubPool.Spec.VmSize, 357 VnetSubnetReference: hubPool.Spec.VnetSubnetReference, 358 WorkloadRuntime: hubPool.Spec.WorkloadRuntime, 359 } 360 361 hubMC.Spec.AgentPoolProfiles = append(hubMC.Spec.AgentPoolProfiles, profile) 362 } 363 364 return managedCluster.ConvertFrom(hubMC) 365 } 366 367 func setManagedClusterCredentials(ctx context.Context, cluster *clusterv1.Cluster, managedClusterPath string, managedCluster *unstructured.Unstructured) error { 368 _, log, done := tele.StartSpanWithLogger(ctx, "mutators.setManagedClusterCredentials") 369 defer done() 370 371 // CAPZ only cares that some set of credentials is created by ASO, but not where. CAPZ will propagate 372 // whatever is defined in the ASO resource to the <cluster>-kubeconfig secret as expected by CAPI. 373 374 _, hasUserCreds, err := unstructured.NestedMap(managedCluster.UnstructuredContent(), "spec", "operatorSpec", "secrets", "userCredentials") 375 if err != nil { 376 return err 377 } 378 if hasUserCreds { 379 return nil 380 } 381 382 _, hasAdminCreds, err := unstructured.NestedMap(managedCluster.UnstructuredContent(), "spec", "operatorSpec", "secrets", "adminCredentials") 383 if err != nil { 384 return err 385 } 386 if hasAdminCreds { 387 return nil 388 } 389 390 secrets := map[string]interface{}{ 391 "adminCredentials": map[string]interface{}{ 392 "name": cluster.Name + "-" + string(secret.Kubeconfig), 393 "key": secret.KubeconfigDataName, 394 }, 395 } 396 397 setCreds := mutation{ 398 location: managedClusterPath + ".spec.operatorSpec.secrets", 399 val: secrets, 400 reason: "because no userCredentials or adminCredentials are defined", 401 } 402 logMutation(log, setCreds) 403 return unstructured.SetNestedMap(managedCluster.UnstructuredContent(), secrets, "spec", "operatorSpec", "secrets") 404 }