sigs.k8s.io/cluster-api-provider-azure@v1.14.3/exp/controllers/helpers.go (about)

     1  /*
     2  Copyright 2020 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 controllers
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  
    23  	"github.com/go-logr/logr"
    24  	"github.com/google/go-cmp/cmp"
    25  	"github.com/pkg/errors"
    26  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    31  	"sigs.k8s.io/cluster-api-provider-azure/controllers"
    32  	infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1beta1"
    33  	"sigs.k8s.io/cluster-api-provider-azure/util/reconciler"
    34  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    35  	kubeadmv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
    36  	expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1"
    37  	"sigs.k8s.io/cluster-api/util"
    38  	ctrl "sigs.k8s.io/controller-runtime"
    39  	"sigs.k8s.io/controller-runtime/pkg/client"
    40  	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
    41  	"sigs.k8s.io/controller-runtime/pkg/event"
    42  	"sigs.k8s.io/controller-runtime/pkg/handler"
    43  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    44  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    45  )
    46  
    47  // AzureClusterToAzureMachinePoolsMapper creates a mapping handler to transform AzureClusters into AzureMachinePools. The transform
    48  // requires AzureCluster to map to the owning Cluster, then from the Cluster, collect the MachinePools belonging to the cluster,
    49  // then finally projecting the infrastructure reference to the AzureMachinePool.
    50  func AzureClusterToAzureMachinePoolsMapper(ctx context.Context, c client.Client, scheme *runtime.Scheme, log logr.Logger) (handler.MapFunc, error) {
    51  	gvk, err := apiutil.GVKForObject(new(infrav1exp.AzureMachinePool), scheme)
    52  	if err != nil {
    53  		return nil, errors.Wrap(err, "failed to find GVK for AzureMachinePool")
    54  	}
    55  
    56  	return func(ctx context.Context, o client.Object) []ctrl.Request {
    57  		ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultMappingTimeout)
    58  		defer cancel()
    59  
    60  		azCluster, ok := o.(*infrav1.AzureCluster)
    61  		if !ok {
    62  			log.Error(errors.Errorf("expected an AzureCluster, got %T instead", o), "failed to map AzureCluster")
    63  			return nil
    64  		}
    65  
    66  		log := log.WithValues("AzureCluster", azCluster.Name, "Namespace", azCluster.Namespace)
    67  
    68  		// Don't handle deleted AzureClusters
    69  		if !azCluster.ObjectMeta.DeletionTimestamp.IsZero() {
    70  			log.V(4).Info("AzureCluster has a deletion timestamp, skipping mapping.")
    71  			return nil
    72  		}
    73  
    74  		clusterName, ok := controllers.GetOwnerClusterName(azCluster.ObjectMeta)
    75  		if !ok {
    76  			log.V(4).Info("unable to get the owner cluster")
    77  			return nil
    78  		}
    79  
    80  		machineList := &expv1.MachinePoolList{}
    81  		machineList.SetGroupVersionKind(gvk)
    82  		// list all of the requested objects within the cluster namespace with the cluster name label
    83  		if err := c.List(ctx, machineList, client.InNamespace(azCluster.Namespace), client.MatchingLabels{clusterv1.ClusterNameLabel: clusterName}); err != nil {
    84  			log.V(4).Info(fmt.Sprintf("unable to list machine pools in cluster %s", clusterName))
    85  			return nil
    86  		}
    87  
    88  		mapFunc := MachinePoolToInfrastructureMapFunc(gvk, log)
    89  		var results []ctrl.Request
    90  		for _, machine := range machineList.Items {
    91  			m := machine
    92  			azureMachines := mapFunc(ctx, &m)
    93  			results = append(results, azureMachines...)
    94  		}
    95  
    96  		return results
    97  	}, nil
    98  }
    99  
   100  // AzureManagedControlPlaneToAzureMachinePoolsMapper creates a mapping handler to transform AzureManagedControlPlanes into AzureMachinePools. The transform
   101  // requires AzureManagedControlPlane to map to the owning Cluster, then from the Cluster, collect the MachinePools belonging to the cluster,
   102  // then finally projecting the infrastructure reference to the AzureMachinePool.
   103  func AzureManagedControlPlaneToAzureMachinePoolsMapper(ctx context.Context, c client.Client, scheme *runtime.Scheme, log logr.Logger) (handler.MapFunc, error) {
   104  	gvk, err := apiutil.GVKForObject(new(infrav1exp.AzureMachinePool), scheme)
   105  	if err != nil {
   106  		return nil, errors.Wrap(err, "failed to find GVK for AzureMachinePool")
   107  	}
   108  
   109  	return func(ctx context.Context, o client.Object) []ctrl.Request {
   110  		ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultMappingTimeout)
   111  		defer cancel()
   112  
   113  		azControlPlane, ok := o.(*infrav1.AzureManagedControlPlane)
   114  		if !ok {
   115  			log.Error(errors.Errorf("expected an AzureManagedControlPlane, got %T instead", o), "failed to map AzureManagedControlPlane")
   116  			return nil
   117  		}
   118  
   119  		log := log.WithValues("AzureManagedControlPlane", azControlPlane.Name, "Namespace", azControlPlane.Namespace)
   120  
   121  		// Don't handle deleted AzureManagedControlPlane
   122  		if !azControlPlane.ObjectMeta.DeletionTimestamp.IsZero() {
   123  			log.V(4).Info("AzureManagedControlPlane has a deletion timestamp, skipping mapping.")
   124  			return nil
   125  		}
   126  
   127  		clusterName, ok := controllers.GetOwnerClusterName(azControlPlane.ObjectMeta)
   128  		if !ok {
   129  			log.V(4).Info("unable to get the owner cluster")
   130  			return nil
   131  		}
   132  
   133  		machineList := &expv1.MachinePoolList{}
   134  		machineList.SetGroupVersionKind(gvk)
   135  		// list all of the requested objects within the cluster namespace with the cluster name label
   136  		if err := c.List(ctx, machineList, client.InNamespace(azControlPlane.Namespace), client.MatchingLabels{clusterv1.ClusterNameLabel: clusterName}); err != nil {
   137  			log.V(4).Info(fmt.Sprintf("unable to list machine pools in cluster %s", clusterName))
   138  			return nil
   139  		}
   140  
   141  		mapFunc := MachinePoolToInfrastructureMapFunc(gvk, log)
   142  		var results []ctrl.Request
   143  		for _, machine := range machineList.Items {
   144  			m := machine
   145  			azureMachines := mapFunc(ctx, &m)
   146  			results = append(results, azureMachines...)
   147  		}
   148  
   149  		return results
   150  	}, nil
   151  }
   152  
   153  // AzureMachinePoolMachineMapper creates a mapping handler to transform AzureMachinePoolMachine to AzureMachinePools.
   154  func AzureMachinePoolMachineMapper(scheme *runtime.Scheme, log logr.Logger) handler.MapFunc {
   155  	return func(ctx context.Context, o client.Object) []ctrl.Request {
   156  		gvk, err := apiutil.GVKForObject(new(infrav1exp.AzureMachinePool), scheme)
   157  		if err != nil {
   158  			log.Error(errors.WithStack(err), "failed to find GVK for AzureMachinePool")
   159  			return nil
   160  		}
   161  
   162  		azureMachinePoolMachine, ok := o.(*infrav1exp.AzureMachinePoolMachine)
   163  		if !ok {
   164  			log.Error(errors.Errorf("expected an AzureCluster, got %T instead", o), "failed to map AzureMachinePoolMachine")
   165  			return nil
   166  		}
   167  
   168  		log := log.WithValues("AzureMachinePoolMachine", azureMachinePoolMachine.Name, "Namespace", azureMachinePoolMachine.Namespace)
   169  		for _, ref := range azureMachinePoolMachine.OwnerReferences {
   170  			if ref.Kind != gvk.Kind {
   171  				continue
   172  			}
   173  
   174  			gv, err := schema.ParseGroupVersion(ref.APIVersion)
   175  			if err != nil {
   176  				log.Error(errors.WithStack(err), "unable to parse group version", "APIVersion", ref.APIVersion)
   177  				return nil
   178  			}
   179  
   180  			if gv.Group == gvk.Group {
   181  				return []ctrl.Request{
   182  					{
   183  						NamespacedName: types.NamespacedName{
   184  							Name:      ref.Name,
   185  							Namespace: azureMachinePoolMachine.Namespace,
   186  						},
   187  					},
   188  				}
   189  			}
   190  		}
   191  
   192  		return nil
   193  	}
   194  }
   195  
   196  // MachinePoolToInfrastructureMapFunc returns a handler.MapFunc that watches for
   197  // MachinePool events and returns reconciliation requests for an infrastructure provider object.
   198  func MachinePoolToInfrastructureMapFunc(gvk schema.GroupVersionKind, log logr.Logger) handler.MapFunc {
   199  	return func(ctx context.Context, o client.Object) []reconcile.Request {
   200  		m, ok := o.(*expv1.MachinePool)
   201  		if !ok {
   202  			log.V(4).Info("attempt to map incorrect type", "type", fmt.Sprintf("%T", o))
   203  			return nil
   204  		}
   205  
   206  		gk := gvk.GroupKind()
   207  		ref := m.Spec.Template.Spec.InfrastructureRef
   208  		// Return early if the GroupKind doesn't match what we expect.
   209  		infraGK := ref.GroupVersionKind().GroupKind()
   210  		if gk != infraGK {
   211  			log.V(4).Info("gk does not match", "gk", gk, "infraGK", infraGK)
   212  			return nil
   213  		}
   214  
   215  		return []reconcile.Request{
   216  			{
   217  				NamespacedName: client.ObjectKey{
   218  					Namespace: m.Namespace,
   219  					Name:      ref.Name,
   220  				},
   221  			},
   222  		}
   223  	}
   224  }
   225  
   226  // AzureClusterToAzureMachinePoolsFunc is a handler.MapFunc to be used to enqueue
   227  // requests for reconciliation of AzureMachinePools.
   228  func AzureClusterToAzureMachinePoolsFunc(ctx context.Context, c client.Client, log logr.Logger) handler.MapFunc {
   229  	return func(ctx context.Context, o client.Object) []reconcile.Request {
   230  		ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultMappingTimeout)
   231  		defer cancel()
   232  
   233  		ac, ok := o.(*infrav1.AzureCluster)
   234  		if !ok {
   235  			log.Error(errors.Errorf("expected a AzureCluster but got a %T", o), "failed to get AzureCluster")
   236  			return nil
   237  		}
   238  		logWithValues := log.WithValues("AzureCluster", ac.Name, "Namespace", ac.Namespace)
   239  
   240  		cluster, err := util.GetOwnerCluster(ctx, c, ac.ObjectMeta)
   241  		switch {
   242  		case apierrors.IsNotFound(err) || cluster == nil:
   243  			logWithValues.V(4).Info("owning cluster not found")
   244  			return nil
   245  		case err != nil:
   246  			logWithValues.Error(err, "failed to get owning cluster")
   247  			return nil
   248  		}
   249  
   250  		labels := map[string]string{clusterv1.ClusterNameLabel: cluster.Name}
   251  		ampl := &infrav1exp.AzureMachinePoolList{}
   252  		if err := c.List(ctx, ampl, client.InNamespace(ac.Namespace), client.MatchingLabels(labels)); err != nil {
   253  			logWithValues.Error(err, "failed to list AzureMachinePools")
   254  			return nil
   255  		}
   256  
   257  		var result []reconcile.Request
   258  		for _, m := range ampl.Items {
   259  			result = append(result, reconcile.Request{
   260  				NamespacedName: client.ObjectKey{
   261  					Namespace: m.Namespace,
   262  					Name:      m.Name,
   263  				},
   264  			})
   265  		}
   266  
   267  		return result
   268  	}
   269  }
   270  
   271  // AzureMachinePoolToAzureMachinePoolMachines maps an AzureMachinePool to its child AzureMachinePoolMachines through
   272  // Cluster and MachinePool labels.
   273  func AzureMachinePoolToAzureMachinePoolMachines(ctx context.Context, c client.Client, log logr.Logger) handler.MapFunc {
   274  	return func(ctx context.Context, o client.Object) []reconcile.Request {
   275  		ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultMappingTimeout)
   276  		defer cancel()
   277  
   278  		amp, ok := o.(*infrav1exp.AzureMachinePool)
   279  		if !ok {
   280  			log.Error(errors.Errorf("expected a AzureMachinePool but got a %T", o), "failed to get AzureMachinePool")
   281  			return nil
   282  		}
   283  		logWithValues := log.WithValues("AzureMachinePool", amp.Name, "Namespace", amp.Namespace)
   284  
   285  		labels := map[string]string{
   286  			clusterv1.ClusterNameLabel:      amp.Labels[clusterv1.ClusterNameLabel],
   287  			infrav1exp.MachinePoolNameLabel: amp.Name,
   288  		}
   289  		ampml := &infrav1exp.AzureMachinePoolMachineList{}
   290  		if err := c.List(ctx, ampml, client.InNamespace(amp.Namespace), client.MatchingLabels(labels)); err != nil {
   291  			logWithValues.Error(err, "failed to list AzureMachinePoolMachines")
   292  			return nil
   293  		}
   294  
   295  		logWithValues.Info("mapping from AzureMachinePool", "count", len(ampml.Items))
   296  		var result []reconcile.Request
   297  		for _, m := range ampml.Items {
   298  			result = append(result, reconcile.Request{
   299  				NamespacedName: client.ObjectKey{
   300  					Namespace: m.Namespace,
   301  					Name:      m.Name,
   302  				},
   303  			})
   304  		}
   305  
   306  		return result
   307  	}
   308  }
   309  
   310  // MachinePoolModelHasChanged predicates any events based on changes to the AzureMachinePool model.
   311  func MachinePoolModelHasChanged(logger logr.Logger) predicate.Funcs {
   312  	return predicate.Funcs{
   313  		UpdateFunc: func(e event.UpdateEvent) bool {
   314  			log := logger.WithValues("predicate", "MachinePoolModelHasChanged", "eventType", "update")
   315  
   316  			oldAmp, ok := e.ObjectOld.(*infrav1exp.AzureMachinePool)
   317  			if !ok {
   318  				log.V(4).Info("Expected AzureMachinePool", "type", e.ObjectOld.GetObjectKind().GroupVersionKind().String())
   319  				return false
   320  			}
   321  			log = log.WithValues("namespace", oldAmp.Namespace, "azureMachinePool", oldAmp.Name)
   322  
   323  			newAmp := e.ObjectNew.(*infrav1exp.AzureMachinePool)
   324  
   325  			// if any of these are not equal, run the update
   326  			shouldUpdate := !cmp.Equal(oldAmp.Spec.Identity, newAmp.Spec.Identity) ||
   327  				!cmp.Equal(oldAmp.Spec.Template, newAmp.Spec.Template) ||
   328  				!cmp.Equal(oldAmp.Spec.UserAssignedIdentities, newAmp.Spec.UserAssignedIdentities) ||
   329  				!cmp.Equal(oldAmp.Status.ProvisioningState, newAmp.Status.ProvisioningState)
   330  
   331  			// if shouldUpdate {
   332  			log.Info("machine pool predicate", "shouldUpdate", shouldUpdate)
   333  			//}
   334  			return shouldUpdate
   335  		},
   336  		CreateFunc:  func(e event.CreateEvent) bool { return false },
   337  		DeleteFunc:  func(e event.DeleteEvent) bool { return false },
   338  		GenericFunc: func(e event.GenericEvent) bool { return false },
   339  	}
   340  }
   341  
   342  // MachinePoolMachineHasStateOrVersionChange predicates any events based on changes to the AzureMachinePoolMachine status
   343  // relevant for the AzureMachinePool controller.
   344  func MachinePoolMachineHasStateOrVersionChange(logger logr.Logger) predicate.Funcs {
   345  	return predicate.Funcs{
   346  		UpdateFunc: func(e event.UpdateEvent) bool {
   347  			log := logger.WithValues("predicate", "MachinePoolModelHasChanged", "eventType", "update")
   348  
   349  			oldAmp, ok := e.ObjectOld.(*infrav1exp.AzureMachinePoolMachine)
   350  			if !ok {
   351  				log.V(4).Info("Expected AzureMachinePoolMachine", "type", e.ObjectOld.GetObjectKind().GroupVersionKind().String())
   352  				return false
   353  			}
   354  			log = log.WithValues("namespace", oldAmp.Namespace, "machinePoolMachine", oldAmp.Name)
   355  
   356  			newAmp := e.ObjectNew.(*infrav1exp.AzureMachinePoolMachine)
   357  
   358  			// if any of these are not equal, run the update
   359  			shouldUpdate := oldAmp.Status.LatestModelApplied != newAmp.Status.LatestModelApplied ||
   360  				oldAmp.Status.Version != newAmp.Status.Version ||
   361  				oldAmp.Status.ProvisioningState != newAmp.Status.ProvisioningState ||
   362  				oldAmp.Status.Ready != newAmp.Status.Ready
   363  
   364  			if shouldUpdate {
   365  				log.Info("machine pool machine predicate", "shouldUpdate", shouldUpdate)
   366  			}
   367  			return shouldUpdate
   368  		},
   369  		CreateFunc:  func(e event.CreateEvent) bool { return false },
   370  		DeleteFunc:  func(e event.DeleteEvent) bool { return false },
   371  		GenericFunc: func(e event.GenericEvent) bool { return false },
   372  	}
   373  }
   374  
   375  // KubeadmConfigToInfrastructureMapFunc returns a handler.ToRequestsFunc that watches for KubeadmConfig events and returns.
   376  func KubeadmConfigToInfrastructureMapFunc(ctx context.Context, c client.Client, log logr.Logger) handler.MapFunc {
   377  	return func(ctx context.Context, o client.Object) []reconcile.Request {
   378  		ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultMappingTimeout)
   379  		defer cancel()
   380  
   381  		kc, ok := o.(*kubeadmv1.KubeadmConfig)
   382  		if !ok {
   383  			log.V(4).Info("attempt to map incorrect type", "type", fmt.Sprintf("%T", o))
   384  			return nil
   385  		}
   386  
   387  		mpKey := client.ObjectKey{
   388  			Namespace: kc.Namespace,
   389  			Name:      kc.Name,
   390  		}
   391  
   392  		// fetch MachinePool to get reference
   393  		mp := &expv1.MachinePool{}
   394  		if err := c.Get(ctx, mpKey, mp); err != nil {
   395  			if !apierrors.IsNotFound(err) {
   396  				log.Error(err, "failed to fetch MachinePool for KubeadmConfig")
   397  			}
   398  			return []reconcile.Request{}
   399  		}
   400  
   401  		ref := mp.Spec.Template.Spec.Bootstrap.ConfigRef
   402  		if ref == nil {
   403  			log.V(4).Info("fetched MachinePool has no Bootstrap.ConfigRef")
   404  			return []reconcile.Request{}
   405  		}
   406  		sameKind := ref.Kind != o.GetObjectKind().GroupVersionKind().Kind
   407  		sameName := ref.Name == kc.Name
   408  		sameNamespace := ref.Namespace == kc.Namespace
   409  		if !sameKind || !sameName || !sameNamespace {
   410  			log.V(4).Info("Bootstrap.ConfigRef does not match",
   411  				"sameKind", sameKind,
   412  				"ref kind", ref.Kind,
   413  				"other kind", o.GetObjectKind().GroupVersionKind().Kind,
   414  				"sameName", sameName,
   415  				"sameNamespace", sameNamespace,
   416  			)
   417  			return []reconcile.Request{}
   418  		}
   419  
   420  		key := client.ObjectKey{
   421  			Namespace: kc.Namespace,
   422  			Name:      kc.Name,
   423  		}
   424  		log.V(4).Info("adding KubeadmConfig to watch", "key", key)
   425  
   426  		return []reconcile.Request{
   427  			{
   428  				NamespacedName: key,
   429  			},
   430  		}
   431  	}
   432  }