sigs.k8s.io/cluster-api-provider-azure@v1.17.0/controllers/azureasomanagedcluster_controller.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 controllers
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    26  	infrav1alpha "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha1"
    27  	"sigs.k8s.io/cluster-api-provider-azure/pkg/mutators"
    28  	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
    29  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    30  	"sigs.k8s.io/cluster-api/controllers/external"
    31  	"sigs.k8s.io/cluster-api/util"
    32  	"sigs.k8s.io/cluster-api/util/annotations"
    33  	"sigs.k8s.io/cluster-api/util/patch"
    34  	"sigs.k8s.io/cluster-api/util/predicates"
    35  	ctrl "sigs.k8s.io/controller-runtime"
    36  	"sigs.k8s.io/controller-runtime/pkg/builder"
    37  	"sigs.k8s.io/controller-runtime/pkg/client"
    38  	"sigs.k8s.io/controller-runtime/pkg/controller"
    39  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    40  	"sigs.k8s.io/controller-runtime/pkg/event"
    41  	"sigs.k8s.io/controller-runtime/pkg/handler"
    42  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    43  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    44  )
    45  
    46  var errInvalidControlPlaneKind = errors.New("AzureASOManagedCluster cannot be used without AzureASOManagedControlPlane")
    47  
    48  // AzureASOManagedClusterReconciler reconciles a AzureASOManagedCluster object.
    49  type AzureASOManagedClusterReconciler struct {
    50  	client.Client
    51  	WatchFilterValue string
    52  
    53  	newResourceReconciler func(*infrav1alpha.AzureASOManagedCluster, []*unstructured.Unstructured) resourceReconciler
    54  }
    55  
    56  type resourceReconciler interface {
    57  	// Reconcile reconciles resources defined by this object and updates this object's status to reflect the
    58  	// state of the specified resources.
    59  	Reconcile(context.Context) error
    60  
    61  	// Pause stops ASO from continuously reconciling the specified resources.
    62  	Pause(context.Context) error
    63  
    64  	// Delete begins deleting the specified resources and updates the object's status to reflect the state of
    65  	// the specified resources.
    66  	Delete(context.Context) error
    67  }
    68  
    69  // SetupWithManager sets up the controller with the Manager.
    70  func (r *AzureASOManagedClusterReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
    71  	ctx, log, done := tele.StartSpanWithLogger(ctx,
    72  		"controllers.AzureASOManagedClusterReconciler.SetupWithManager",
    73  		tele.KVP("controller", infrav1alpha.AzureASOManagedClusterKind),
    74  	)
    75  	defer done()
    76  
    77  	c, err := ctrl.NewControllerManagedBy(mgr).
    78  		WithOptions(options).
    79  		For(&infrav1alpha.AzureASOManagedCluster{}).
    80  		WithEventFilter(predicates.ResourceHasFilterLabel(log, r.WatchFilterValue)).
    81  		WithEventFilter(predicates.ResourceIsNotExternallyManaged(log)).
    82  		// Watch clusters for pause/unpause notifications
    83  		Watches(
    84  			&clusterv1.Cluster{},
    85  			handler.EnqueueRequestsFromMapFunc(
    86  				util.ClusterToInfrastructureMapFunc(ctx, infrav1alpha.GroupVersion.WithKind(infrav1alpha.AzureASOManagedClusterKind), mgr.GetClient(), &infrav1alpha.AzureASOManagedCluster{}),
    87  			),
    88  			builder.WithPredicates(
    89  				predicates.ResourceHasFilterLabel(log, r.WatchFilterValue),
    90  				ClusterUpdatePauseChange(log),
    91  			),
    92  		).
    93  		Watches(
    94  			&infrav1alpha.AzureASOManagedControlPlane{},
    95  			handler.EnqueueRequestsFromMapFunc(asoManagedControlPlaneToManagedClusterMap(r.Client)),
    96  			builder.WithPredicates(
    97  				predicates.ResourceHasFilterLabel(log, r.WatchFilterValue),
    98  				predicate.Funcs{
    99  					CreateFunc: func(ev event.CreateEvent) bool {
   100  						controlPlane := ev.Object.(*infrav1alpha.AzureASOManagedControlPlane)
   101  						return !controlPlane.Status.ControlPlaneEndpoint.IsZero()
   102  					},
   103  					UpdateFunc: func(ev event.UpdateEvent) bool {
   104  						oldControlPlane := ev.ObjectOld.(*infrav1alpha.AzureASOManagedControlPlane)
   105  						newControlPlane := ev.ObjectNew.(*infrav1alpha.AzureASOManagedControlPlane)
   106  						return oldControlPlane.Status.ControlPlaneEndpoint !=
   107  							newControlPlane.Status.ControlPlaneEndpoint
   108  					},
   109  				},
   110  			),
   111  		).
   112  		Build(r)
   113  	if err != nil {
   114  		return err
   115  	}
   116  
   117  	externalTracker := &external.ObjectTracker{
   118  		Cache:      mgr.GetCache(),
   119  		Controller: c,
   120  	}
   121  
   122  	r.newResourceReconciler = func(asoManagedCluster *infrav1alpha.AzureASOManagedCluster, resources []*unstructured.Unstructured) resourceReconciler {
   123  		return &ResourceReconciler{
   124  			Client:    r.Client,
   125  			resources: resources,
   126  			owner:     asoManagedCluster,
   127  			watcher:   externalTracker,
   128  		}
   129  	}
   130  
   131  	return nil
   132  }
   133  
   134  func asoManagedControlPlaneToManagedClusterMap(c client.Client) handler.MapFunc {
   135  	return func(ctx context.Context, o client.Object) []reconcile.Request {
   136  		asoManagedControlPlane := o.(*infrav1alpha.AzureASOManagedControlPlane)
   137  
   138  		cluster, err := util.GetOwnerCluster(ctx, c, asoManagedControlPlane.ObjectMeta)
   139  		if err != nil {
   140  			return nil
   141  		}
   142  
   143  		if cluster == nil ||
   144  			cluster.Spec.InfrastructureRef == nil ||
   145  			cluster.Spec.InfrastructureRef.APIVersion != infrav1alpha.GroupVersion.Identifier() ||
   146  			cluster.Spec.InfrastructureRef.Kind != infrav1alpha.AzureASOManagedClusterKind {
   147  			return nil
   148  		}
   149  
   150  		return []reconcile.Request{
   151  			{
   152  				NamespacedName: client.ObjectKey{
   153  					Namespace: cluster.Spec.InfrastructureRef.Namespace,
   154  					Name:      cluster.Spec.InfrastructureRef.Name,
   155  				},
   156  			},
   157  		}
   158  	}
   159  }
   160  
   161  //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=azureasomanagedclusters,verbs=get;list;watch;create;update;patch;delete
   162  //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=azureasomanagedclusters/status,verbs=get;update;patch
   163  //+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=azureasomanagedclusters/finalizers,verbs=update
   164  
   165  // Reconcile reconciles an AzureASOManagedCluster.
   166  func (r *AzureASOManagedClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, resultErr error) {
   167  	ctx, _, done := tele.StartSpanWithLogger(ctx,
   168  		"controllers.AzureASOManagedClusterReconciler.Reconcile",
   169  		tele.KVP("namespace", req.Namespace),
   170  		tele.KVP("name", req.Name),
   171  		tele.KVP("kind", infrav1alpha.AzureASOManagedClusterKind),
   172  	)
   173  	defer done()
   174  
   175  	asoManagedCluster := &infrav1alpha.AzureASOManagedCluster{}
   176  	err := r.Get(ctx, req.NamespacedName, asoManagedCluster)
   177  	if err != nil {
   178  		return ctrl.Result{}, client.IgnoreNotFound(err)
   179  	}
   180  
   181  	patchHelper, err := patch.NewHelper(asoManagedCluster, r.Client)
   182  	if err != nil {
   183  		return ctrl.Result{}, fmt.Errorf("failed to create patch helper: %w", err)
   184  	}
   185  	defer func() {
   186  		err := patchHelper.Patch(ctx, asoManagedCluster)
   187  		if err != nil && resultErr == nil {
   188  			resultErr = err
   189  			result = ctrl.Result{}
   190  		}
   191  	}()
   192  
   193  	asoManagedCluster.Status.Ready = false
   194  
   195  	cluster, err := util.GetOwnerCluster(ctx, r.Client, asoManagedCluster.ObjectMeta)
   196  	if err != nil {
   197  		return ctrl.Result{}, err
   198  	}
   199  
   200  	if cluster != nil && cluster.Spec.Paused ||
   201  		annotations.HasPaused(asoManagedCluster) {
   202  		return r.reconcilePaused(ctx, asoManagedCluster)
   203  	}
   204  
   205  	if !asoManagedCluster.GetDeletionTimestamp().IsZero() {
   206  		return r.reconcileDelete(ctx, asoManagedCluster)
   207  	}
   208  
   209  	return r.reconcileNormal(ctx, asoManagedCluster, cluster)
   210  }
   211  
   212  func (r *AzureASOManagedClusterReconciler) reconcileNormal(ctx context.Context, asoManagedCluster *infrav1alpha.AzureASOManagedCluster, cluster *clusterv1.Cluster) (ctrl.Result, error) {
   213  	ctx, log, done := tele.StartSpanWithLogger(ctx,
   214  		"controllers.AzureASOManagedClusterReconciler.reconcileNormal",
   215  	)
   216  	defer done()
   217  	log.V(4).Info("reconciling normally")
   218  
   219  	if cluster == nil {
   220  		log.V(4).Info("Cluster Controller has not yet set OwnerRef")
   221  		return ctrl.Result{}, nil
   222  	}
   223  	if cluster.Spec.ControlPlaneRef == nil ||
   224  		cluster.Spec.ControlPlaneRef.APIVersion != infrav1alpha.GroupVersion.Identifier() ||
   225  		cluster.Spec.ControlPlaneRef.Kind != infrav1alpha.AzureASOManagedControlPlaneKind {
   226  		return ctrl.Result{}, reconcile.TerminalError(errInvalidControlPlaneKind)
   227  	}
   228  
   229  	needsPatch := controllerutil.AddFinalizer(asoManagedCluster, clusterv1.ClusterFinalizer)
   230  	needsPatch = AddBlockMoveAnnotation(asoManagedCluster) || needsPatch
   231  	if needsPatch {
   232  		return ctrl.Result{Requeue: true}, nil
   233  	}
   234  
   235  	resources, err := mutators.ToUnstructured(ctx, asoManagedCluster.Spec.Resources)
   236  	if err != nil {
   237  		return ctrl.Result{}, err
   238  	}
   239  	resourceReconciler := r.newResourceReconciler(asoManagedCluster, resources)
   240  	err = resourceReconciler.Reconcile(ctx)
   241  	if err != nil {
   242  		return ctrl.Result{}, fmt.Errorf("failed to reconcile resources: %w", err)
   243  	}
   244  	for _, status := range asoManagedCluster.Status.Resources {
   245  		if !status.Ready {
   246  			return ctrl.Result{}, nil
   247  		}
   248  	}
   249  
   250  	asoManagedControlPlane := &infrav1alpha.AzureASOManagedControlPlane{
   251  		ObjectMeta: metav1.ObjectMeta{
   252  			Namespace: cluster.Spec.ControlPlaneRef.Namespace,
   253  			Name:      cluster.Spec.ControlPlaneRef.Name,
   254  		},
   255  	}
   256  	err = r.Get(ctx, client.ObjectKeyFromObject(asoManagedControlPlane), asoManagedControlPlane)
   257  	if client.IgnoreNotFound(err) != nil {
   258  		return ctrl.Result{}, fmt.Errorf("failed to get AzureASOManagedControlPlane %s/%s: %w", asoManagedControlPlane.Namespace, asoManagedControlPlane.Name, err)
   259  	}
   260  	asoManagedCluster.Spec.ControlPlaneEndpoint = asoManagedControlPlane.Status.ControlPlaneEndpoint
   261  
   262  	asoManagedCluster.Status.Ready = !asoManagedCluster.Spec.ControlPlaneEndpoint.IsZero()
   263  
   264  	return ctrl.Result{}, nil
   265  }
   266  
   267  //nolint:unparam // an empty ctrl.Result is always returned here, leaving it as-is to avoid churn in refactoring later if that changes.
   268  func (r *AzureASOManagedClusterReconciler) reconcilePaused(ctx context.Context, asoManagedCluster *infrav1alpha.AzureASOManagedCluster) (ctrl.Result, error) {
   269  	ctx, log, done := tele.StartSpanWithLogger(ctx, "controllers.AzureASOManagedClusterReconciler.reconcilePaused")
   270  	defer done()
   271  	log.V(4).Info("reconciling pause")
   272  
   273  	resources, err := mutators.ToUnstructured(ctx, asoManagedCluster.Spec.Resources)
   274  	if err != nil {
   275  		return ctrl.Result{}, err
   276  	}
   277  	resourceReconciler := r.newResourceReconciler(asoManagedCluster, resources)
   278  	err = resourceReconciler.Pause(ctx)
   279  	if err != nil {
   280  		return ctrl.Result{}, fmt.Errorf("failed to pause resources: %w", err)
   281  	}
   282  
   283  	RemoveBlockMoveAnnotation(asoManagedCluster)
   284  
   285  	return ctrl.Result{}, nil
   286  }
   287  
   288  //nolint:unparam // an empty ctrl.Result is always returned here, leaving it as-is to avoid churn in refactoring later if that changes.
   289  func (r *AzureASOManagedClusterReconciler) reconcileDelete(ctx context.Context, asoManagedCluster *infrav1alpha.AzureASOManagedCluster) (ctrl.Result, error) {
   290  	ctx, log, done := tele.StartSpanWithLogger(ctx,
   291  		"controllers.AzureASOManagedClusterReconciler.reconcileDelete",
   292  	)
   293  	defer done()
   294  	log.V(4).Info("reconciling delete")
   295  
   296  	resources, err := mutators.ToUnstructured(ctx, asoManagedCluster.Spec.Resources)
   297  	if err != nil {
   298  		return ctrl.Result{}, err
   299  	}
   300  	resourceReconciler := r.newResourceReconciler(asoManagedCluster, resources)
   301  	err = resourceReconciler.Delete(ctx)
   302  	if err != nil {
   303  		return ctrl.Result{}, fmt.Errorf("failed to reconcile resources: %w", err)
   304  	}
   305  	if len(asoManagedCluster.Status.Resources) > 0 {
   306  		return ctrl.Result{}, nil
   307  	}
   308  
   309  	controllerutil.RemoveFinalizer(asoManagedCluster, clusterv1.ClusterFinalizer)
   310  	return ctrl.Result{}, nil
   311  }