sigs.k8s.io/cluster-api-provider-azure@v1.17.0/controllers/resource_reconciler.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  	"github.com/Azure/azure-service-operator/v2/pkg/genruntime/conditions"
    25  	"github.com/go-logr/logr"
    26  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/apimachinery/pkg/runtime/schema"
    30  	"k8s.io/klog/v2"
    31  	infrav1alpha "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha1"
    32  	"sigs.k8s.io/cluster-api-provider-azure/pkg/mutators"
    33  	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
    34  	"sigs.k8s.io/controller-runtime/pkg/client"
    35  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    36  	"sigs.k8s.io/controller-runtime/pkg/handler"
    37  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    38  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    39  )
    40  
    41  // ResourceReconciler reconciles a set of arbitrary ASO resources.
    42  type ResourceReconciler struct {
    43  	client.Client
    44  	resources []*unstructured.Unstructured
    45  	owner     resourceStatusObject
    46  	watcher   watcher
    47  }
    48  
    49  type watcher interface {
    50  	Watch(log logr.Logger, obj client.Object, handler handler.EventHandler, p ...predicate.Predicate) error
    51  }
    52  
    53  type resourceStatusObject interface {
    54  	client.Object
    55  	GetResourceStatuses() []infrav1alpha.ResourceStatus
    56  	SetResourceStatuses([]infrav1alpha.ResourceStatus)
    57  }
    58  
    59  // Reconcile creates or updates the specified resources.
    60  func (r *ResourceReconciler) Reconcile(ctx context.Context) error {
    61  	ctx, log, done := tele.StartSpanWithLogger(ctx, "controllers.ResourceReconciler.Reconcile")
    62  	defer done()
    63  	log.V(4).Info("reconciling resources")
    64  
    65  	var newResourceStatuses []infrav1alpha.ResourceStatus
    66  
    67  	for _, spec := range r.resources {
    68  		gvk := spec.GroupVersionKind()
    69  		spec.SetNamespace(r.owner.GetNamespace())
    70  
    71  		log := log.WithValues("resource", klog.KObj(spec), "resourceVersion", gvk.GroupVersion(), "resourceKind", gvk.Kind)
    72  
    73  		if err := controllerutil.SetControllerReference(r.owner, spec, r.Scheme()); err != nil {
    74  			return fmt.Errorf("failed to set owner reference: %w", err)
    75  		}
    76  
    77  		if err := r.watcher.Watch(log, spec, handler.EnqueueRequestForOwner(r.Client.Scheme(), r.Client.RESTMapper(), r.owner)); err != nil {
    78  			return fmt.Errorf("failed to watch resource: %w", err)
    79  		}
    80  
    81  		log.V(4).Info("applying resource")
    82  		err := r.Patch(ctx, spec, client.Apply, client.FieldOwner("capz-manager"), client.ForceOwnership)
    83  		if err != nil {
    84  			return fmt.Errorf("failed to apply resource: %w", err)
    85  		}
    86  
    87  		ready, err := readyStatus(ctx, spec)
    88  		if err != nil {
    89  			return fmt.Errorf("failed to get ready status: %w", err)
    90  		}
    91  		newResourceStatuses = append(newResourceStatuses, infrav1alpha.ResourceStatus{
    92  			Resource: infrav1alpha.StatusResource{
    93  				Group:   gvk.Group,
    94  				Version: gvk.Version,
    95  				Kind:    gvk.Kind,
    96  				Name:    spec.GetName(),
    97  			},
    98  			Ready: ready,
    99  		})
   100  	}
   101  
   102  	for _, oldStatus := range r.owner.GetResourceStatuses() {
   103  		needsDelete := true
   104  		for _, newStatus := range newResourceStatuses {
   105  			if oldStatus.Resource.Group == newStatus.Resource.Group &&
   106  				oldStatus.Resource.Kind == newStatus.Resource.Kind &&
   107  				oldStatus.Resource.Name == newStatus.Resource.Name {
   108  				needsDelete = false
   109  				break
   110  			}
   111  		}
   112  
   113  		if needsDelete {
   114  			updatedStatus, err := r.deleteResource(ctx, oldStatus.Resource)
   115  			if err != nil {
   116  				return err
   117  			}
   118  			if updatedStatus != nil {
   119  				newResourceStatuses = append(newResourceStatuses, *updatedStatus)
   120  			}
   121  		}
   122  	}
   123  
   124  	r.owner.SetResourceStatuses(newResourceStatuses)
   125  
   126  	return nil
   127  }
   128  
   129  // Pause pauses reconciliation of the specified resources.
   130  func (r *ResourceReconciler) Pause(ctx context.Context) error {
   131  	ctx, log, done := tele.StartSpanWithLogger(ctx, "controllers.ResourceReconciler.Pause")
   132  	defer done()
   133  	log.V(4).Info("pausing resources")
   134  
   135  	err := mutators.Pause(ctx, r.resources)
   136  	if err != nil {
   137  		if errors.As(err, &mutators.Incompatible{}) {
   138  			err = reconcile.TerminalError(err)
   139  		}
   140  		return err
   141  	}
   142  
   143  	for _, spec := range r.resources {
   144  		gvk := spec.GroupVersionKind()
   145  		spec.SetNamespace(r.owner.GetNamespace())
   146  
   147  		log := log.WithValues("resource", klog.KObj(spec), "resourceVersion", gvk.GroupVersion(), "resourceKind", gvk.Kind)
   148  
   149  		log.V(4).Info("pausing resource")
   150  		err := r.Patch(ctx, spec, client.Apply, client.FieldOwner("capz-manager"))
   151  		if err != nil {
   152  			return fmt.Errorf("failed to patch resource: %w", err)
   153  		}
   154  	}
   155  
   156  	return nil
   157  }
   158  
   159  // Delete deletes the specified resources.
   160  func (r *ResourceReconciler) Delete(ctx context.Context) error {
   161  	ctx, log, done := tele.StartSpanWithLogger(ctx, "controllers.ResourceReconciler.Delete")
   162  	defer done()
   163  	log.V(4).Info("deleting resources")
   164  
   165  	var newResourceStatuses []infrav1alpha.ResourceStatus
   166  
   167  	for _, spec := range r.owner.GetResourceStatuses() {
   168  		newStatus, err := r.deleteResource(ctx, spec.Resource)
   169  		if err != nil {
   170  			return err
   171  		}
   172  		if newStatus != nil {
   173  			newResourceStatuses = append(newResourceStatuses, *newStatus)
   174  		}
   175  	}
   176  
   177  	r.owner.SetResourceStatuses(newResourceStatuses)
   178  
   179  	return nil
   180  }
   181  
   182  func (r *ResourceReconciler) deleteResource(ctx context.Context, resource infrav1alpha.StatusResource) (*infrav1alpha.ResourceStatus, error) {
   183  	ctx, log, done := tele.StartSpanWithLogger(ctx, "controllers.ResourceReconciler.deleteResource")
   184  	defer done()
   185  
   186  	spec := &unstructured.Unstructured{}
   187  	spec.SetGroupVersionKind(schema.GroupVersionKind{Group: resource.Group, Version: resource.Version, Kind: resource.Kind})
   188  	spec.SetNamespace(r.owner.GetNamespace())
   189  	spec.SetName(resource.Name)
   190  
   191  	log = log.WithValues("resource", klog.KObj(spec), "resourceVersion", spec.GroupVersionKind().GroupVersion(), "resourceKind", spec.GetKind())
   192  
   193  	log.V(4).Info("deleting resource")
   194  	err := r.Client.Delete(ctx, spec)
   195  	if apierrors.IsNotFound(err) {
   196  		log.V(4).Info("resource has been deleted")
   197  		return nil, nil
   198  	}
   199  	if err != nil {
   200  		return nil, fmt.Errorf("failed to delete resource: %w", err)
   201  	}
   202  
   203  	err = r.Client.Get(ctx, client.ObjectKeyFromObject(spec), spec)
   204  	if apierrors.IsNotFound(err) {
   205  		log.V(4).Info("resource has been deleted")
   206  		return nil, nil
   207  	}
   208  	if err != nil {
   209  		return nil, fmt.Errorf("failed to get resource: %w", err)
   210  	}
   211  	ready, err := readyStatus(ctx, spec)
   212  	if err != nil {
   213  		return nil, fmt.Errorf("failed to get ready status: %w", err)
   214  	}
   215  
   216  	return &infrav1alpha.ResourceStatus{
   217  		Resource: resource,
   218  		Ready:    ready,
   219  	}, nil
   220  }
   221  
   222  func readyStatus(ctx context.Context, u *unstructured.Unstructured) (bool, error) {
   223  	_, log, done := tele.StartSpanWithLogger(ctx, "controllers.ResourceReconciler.readyStatus")
   224  	defer done()
   225  
   226  	statusConditions, found, err := unstructured.NestedSlice(u.Object, "status", "conditions")
   227  	if err != nil {
   228  		return false, err
   229  	}
   230  	if !found {
   231  		return false, nil
   232  	}
   233  
   234  	for _, el := range statusConditions {
   235  		condition, ok := el.(map[string]interface{})
   236  		if !ok {
   237  			continue
   238  		}
   239  		condType, found, err := unstructured.NestedString(condition, "type")
   240  		if !found || err != nil || condType != conditions.ConditionTypeReady {
   241  			continue
   242  		}
   243  
   244  		observedGen, _, err := unstructured.NestedInt64(condition, "observedGeneration")
   245  		if err != nil {
   246  			return false, err
   247  		}
   248  		if observedGen < u.GetGeneration() {
   249  			log.V(4).Info("waiting for ASO to reconcile the resource")
   250  			return false, nil
   251  		}
   252  
   253  		readyStatus, _, err := unstructured.NestedString(condition, "status")
   254  		if err != nil {
   255  			return false, err
   256  		}
   257  		return readyStatus == string(metav1.ConditionTrue), nil
   258  	}
   259  
   260  	// no ready condition is set
   261  	return false, nil
   262  }