sigs.k8s.io/cluster-api-provider-azure@v1.17.0/azure/services/async/async.go (about)

     1  /*
     2  Copyright 2023 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 async
    18  
    19  import (
    20  	"context"
    21  	"net/http"
    22  	"strconv"
    23  	"time"
    24  
    25  	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
    26  	"github.com/pkg/errors"
    27  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    28  	"sigs.k8s.io/cluster-api-provider-azure/azure"
    29  	"sigs.k8s.io/cluster-api-provider-azure/azure/converters"
    30  	"sigs.k8s.io/cluster-api-provider-azure/util/reconciler"
    31  	"sigs.k8s.io/cluster-api-provider-azure/util/tele"
    32  )
    33  
    34  const (
    35  	// DefaultPollerFrequency is how often a poller should check for completion, in seconds.
    36  	DefaultPollerFrequency = 1 * time.Second
    37  )
    38  
    39  // Service handles asynchronous creation and deletion of resources. It implements the Reconciler interface.
    40  type Service[C, D any] struct {
    41  	Scope FutureScope
    42  	Creator[C]
    43  	Deleter[D]
    44  }
    45  
    46  // New creates an async Service.
    47  func New[C, D any](scope FutureScope, createClient Creator[C], deleteClient Deleter[D]) *Service[C, D] {
    48  	return &Service[C, D]{
    49  		Scope:   scope,
    50  		Creator: createClient,
    51  		Deleter: deleteClient,
    52  	}
    53  }
    54  
    55  // CreateOrUpdateResource creates a new resource or updates an existing one asynchronously.
    56  func (s *Service[C, D]) CreateOrUpdateResource(ctx context.Context, spec azure.ResourceSpecGetter, serviceName string) (result interface{}, err error) {
    57  	ctx, log, done := tele.StartSpanWithLogger(ctx, "async.Service.CreateOrUpdateResource")
    58  	defer done()
    59  
    60  	resourceName := spec.ResourceName()
    61  	rgName := spec.ResourceGroupName()
    62  	futureType := infrav1.PutFuture
    63  
    64  	// Check if there is an ongoing long-running operation.
    65  	resumeToken := ""
    66  	if future := s.Scope.GetLongRunningOperationState(resourceName, serviceName, futureType); future != nil {
    67  		t, err := converters.FutureToResumeToken(*future)
    68  		if err != nil {
    69  			s.Scope.DeleteLongRunningOperationState(resourceName, serviceName, futureType)
    70  			return "", errors.Wrap(err, "could not decode future data, resetting long-running operation state")
    71  		}
    72  		resumeToken = t
    73  	}
    74  
    75  	// Only when no long running operation is currently in progress do we need to get the parameters.
    76  	// The polling implemented by the SDK does not use parameters when a resume token exists.
    77  	var parameters interface{}
    78  	if resumeToken == "" {
    79  		// Get the resource if it already exists, and use it to construct the desired resource parameters.
    80  		var existingResource interface{}
    81  		if existing, err := s.Creator.Get(ctx, spec); err != nil && !azure.ResourceNotFound(err) {
    82  			errWrapped := errors.Wrapf(err, "failed to get existing resource %s/%s (service: %s)", rgName, resourceName, serviceName)
    83  			return nil, azure.WithTransientError(errWrapped, getRetryAfterFromError(err))
    84  		} else if err == nil {
    85  			existingResource = existing
    86  			log.V(2).Info("successfully got existing resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName)
    87  		}
    88  
    89  		// Construct parameters using the resource spec and information from the existing resource, if there is one.
    90  		parameters, err = spec.Parameters(ctx, existingResource)
    91  		if err != nil {
    92  			return nil, errors.Wrapf(err, "failed to get desired parameters for resource %s/%s (service: %s)", rgName, resourceName, serviceName)
    93  		} else if parameters == nil {
    94  			// Nothing to do, don't create or update the resource and return the existing resource.
    95  			log.V(2).Info("resource up to date", "service", serviceName, "resource", resourceName, "resourceGroup", rgName)
    96  			return existingResource, nil
    97  		}
    98  
    99  		// Create or update the resource with the desired parameters.
   100  		if existingResource != nil {
   101  			log.V(2).Info("updating resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName)
   102  		} else {
   103  			log.V(2).Info("creating resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName)
   104  		}
   105  	}
   106  
   107  	result, poller, err := s.Creator.CreateOrUpdateAsync(ctx, spec, resumeToken, parameters)
   108  	errWrapped := errors.Wrapf(err, "failed to create or update resource %s/%s (service: %s)", rgName, resourceName, serviceName)
   109  	if poller != nil && azure.IsContextDeadlineExceededOrCanceledError(err) {
   110  		future, err := converters.PollerToFuture(poller, infrav1.PutFuture, serviceName, resourceName, rgName)
   111  		if err != nil {
   112  			return nil, errWrapped
   113  		}
   114  		s.Scope.SetLongRunningOperationState(future)
   115  		return nil, azure.WithTransientError(azure.NewOperationNotDoneError(future), requeueTime(s.Scope))
   116  	}
   117  
   118  	// Once the operation is done, delete the long-running operation state. Even if the operation ended with
   119  	// an error, clear out any lingering state to try the operation again.
   120  	s.Scope.DeleteLongRunningOperationState(resourceName, serviceName, futureType)
   121  
   122  	if err != nil {
   123  		return nil, errWrapped
   124  	}
   125  
   126  	log.V(2).Info("successfully created or updated resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName)
   127  	return result, nil
   128  }
   129  
   130  // DeleteResource deletes a resource asynchronously.
   131  func (s *Service[C, D]) DeleteResource(ctx context.Context, spec azure.ResourceSpecGetter, serviceName string) (err error) {
   132  	ctx, log, done := tele.StartSpanWithLogger(ctx, "async.Service.DeleteResource")
   133  	defer done()
   134  
   135  	resourceName := spec.ResourceName()
   136  	rgName := spec.ResourceGroupName()
   137  	futureType := infrav1.DeleteFuture
   138  
   139  	// Check for an ongoing long-running operation.
   140  	resumeToken := ""
   141  	if future := s.Scope.GetLongRunningOperationState(resourceName, serviceName, futureType); future != nil {
   142  		t, err := converters.FutureToResumeToken(*future)
   143  		if err != nil {
   144  			s.Scope.DeleteLongRunningOperationState(resourceName, serviceName, futureType)
   145  			return errors.Wrap(err, "could not decode future data, resetting long-running operation state")
   146  		}
   147  		resumeToken = t
   148  	}
   149  
   150  	// Delete the resource.
   151  	log.V(2).Info("deleting resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName)
   152  	poller, err := s.Deleter.DeleteAsync(ctx, spec, resumeToken)
   153  	if poller != nil && azure.IsContextDeadlineExceededOrCanceledError(err) {
   154  		future, err := converters.PollerToFuture(poller, infrav1.DeleteFuture, serviceName, resourceName, rgName)
   155  		if err != nil {
   156  			return errors.Wrap(err, "failed to convert poller to future")
   157  		}
   158  		s.Scope.SetLongRunningOperationState(future)
   159  		return azure.WithTransientError(azure.NewOperationNotDoneError(future), requeueTime(s.Scope))
   160  	}
   161  
   162  	// Once the operation is done, delete the long-running operation state. Even if the operation ended with
   163  	// an error, clear out any lingering state to try the operation again.
   164  	s.Scope.DeleteLongRunningOperationState(resourceName, serviceName, futureType)
   165  
   166  	if err != nil && !azure.ResourceNotFound(err) {
   167  		return errors.Wrapf(err, "failed to delete resource %s/%s (service: %s)", rgName, resourceName, serviceName)
   168  	}
   169  
   170  	log.V(2).Info("successfully deleted resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName)
   171  	return nil
   172  }
   173  
   174  // requeueTime returns the time to wait before requeuing a reconciliation.
   175  // It would be ideal to use the "retry-after" header from the API response, but
   176  // that is not readily accessible in the SDK v2 Poller framework.
   177  func requeueTime(timeouts azure.AsyncReconciler) time.Duration {
   178  	return timeouts.DefaultedReconcilerRequeue()
   179  }
   180  
   181  // getRetryAfterFromError returns the time.Duration from the http.Response in the azcore.ResponseError.
   182  // If there is no Response object, or if there is no meaningful Retry-After header data, it returns a default.
   183  func getRetryAfterFromError(err error) time.Duration {
   184  	// In case we aren't able to introspect Retry-After from the error type, we'll return this default
   185  	ret := reconciler.DefaultReconcilerRequeue
   186  	var responseError *azcore.ResponseError
   187  	// if we have a strongly typed azcore.ResponseError then we can introspect the HTTP response data
   188  	if errors.As(err, &responseError) && responseError.RawResponse != nil {
   189  		// If we have Retry-After HTTP header data for any reason, prefer it
   190  		if retryAfter := responseError.RawResponse.Header.Get("Retry-After"); retryAfter != "" {
   191  			// This handles the case where Retry-After data is in the form of units of seconds
   192  			if rai, err := strconv.Atoi(retryAfter); err == nil {
   193  				ret = time.Duration(rai) * time.Second
   194  				// This handles the case where Retry-After data is in the form of absolute time
   195  			} else if t, err := time.Parse(time.RFC1123, retryAfter); err == nil {
   196  				ret = time.Until(t)
   197  			}
   198  			// If we didn't find Retry-After HTTP header data but the response type is 429,
   199  			// we'll have to come up with our sane default.
   200  		} else if responseError.RawResponse.StatusCode == http.StatusTooManyRequests {
   201  			ret = reconciler.DefaultHTTP429RetryAfter
   202  		}
   203  	}
   204  	return ret
   205  }