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  }