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 }