From ae498daab47160bace882293000a021c55deeb95 Mon Sep 17 00:00:00 2001 From: Matt Boersma Date: Tue, 21 Feb 2023 12:32:22 -0700 Subject: [PATCH 1/7] Add asyncpoller framework for azure-sdk-for-go v2 --- azure/converters/futures.go | 32 ++ azure/errors.go | 22 +- azure/services/asyncpoller/asyncpoller.go | 230 ++++++++ .../services/asyncpoller/asyncpoller_test.go | 522 ++++++++++++++++++ azure/services/asyncpoller/interfaces.go | 67 +++ .../mock_asyncpoller/asyncpoller_mock.go | 463 ++++++++++++++++ .../asyncpoller/mock_asyncpoller/doc.go | 21 + 7 files changed, 1346 insertions(+), 11 deletions(-) create mode 100644 azure/services/asyncpoller/asyncpoller.go create mode 100644 azure/services/asyncpoller/asyncpoller_test.go create mode 100644 azure/services/asyncpoller/interfaces.go create mode 100644 azure/services/asyncpoller/mock_asyncpoller/asyncpoller_mock.go create mode 100644 azure/services/asyncpoller/mock_asyncpoller/doc.go diff --git a/azure/converters/futures.go b/azure/converters/futures.go index 91926416c5e..16d91540a81 100644 --- a/azure/converters/futures.go +++ b/azure/converters/futures.go @@ -19,6 +19,8 @@ package converters import ( "encoding/base64" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" azureautorest "github.com/Azure/go-autorest/autorest/azure" "github.com/pkg/errors" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" @@ -52,3 +54,33 @@ func FutureToSDK(future infrav1.Future) (azureautorest.FutureAPI, error) { } return &genericFuture, nil } + +// PollerToFuture converts an SDK poller to an infrav1.Future. +func PollerToFuture[T any](poller *runtime.Poller[T], futureType, service, resourceName, rgName string) (*infrav1.Future, error) { + token, err := poller.ResumeToken() + if err != nil { + return nil, errors.Wrap(err, "failed to get resume token") + } + return &infrav1.Future{ + Type: futureType, + ResourceGroup: rgName, + ServiceName: service, + Name: resourceName, + Data: base64.URLEncoding.EncodeToString([]byte(token)), + }, nil +} + +// FutureToPoller converts an infrav1.Future to an SDK poller. +func FutureToPoller[T any](future infrav1.Future) (*runtime.Poller[T], error) { + token, err := base64.URLEncoding.DecodeString(future.Data) + if err != nil { + return nil, errors.Wrap(err, "failed to base64-decode poller resume token") + } + pl := runtime.NewPipeline("", "", runtime.PipelineOptions{}, &policy.ClientOptions{}) + opts := runtime.NewPollerFromResumeTokenOptions[T]{} + poller, err := runtime.NewPollerFromResumeToken(string(token), pl, &opts) + if err != nil { + return nil, errors.Wrap(err, "failed to create poller from resume token") + } + return poller, nil +} diff --git a/azure/errors.go b/azure/errors.go index 18925094755..5ace0e09c69 100644 --- a/azure/errors.go +++ b/azure/errors.go @@ -22,24 +22,24 @@ import ( "fmt" "time" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/go-autorest/autorest" - azureautorest "github.com/Azure/go-autorest/autorest/azure" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" ) -const codeResourceGroupNotFound = "ResourceGroupNotFound" - -// ResourceGroupNotFound parses the error to check if it's a resource group not found error. -func ResourceGroupNotFound(err error) bool { - derr := autorest.DetailedError{} - serr := &azureautorest.ServiceError{} - return errors.As(err, &derr) && errors.As(derr.Original, &serr) && serr.Code == codeResourceGroupNotFound +// ResourceNotFound parses the error to check if it's a resource not found error (404). +func ResourceNotFound(err error) bool { + return hasStatusCode(err, 404) } -// ResourceNotFound parses the error to check if it's a resource not found error. -func ResourceNotFound(err error) bool { +// hasStatusCode checks if the error is a ResponseError or Detailed error and if the status code matches. +func hasStatusCode(err error, statusCode int) bool { + var rerr *azcore.ResponseError + if errors.As(err, &rerr) { + return rerr.RawResponse.StatusCode == statusCode + } derr := autorest.DetailedError{} - return errors.As(err, &derr) && derr.StatusCode == 404 + return errors.As(err, &derr) && derr.StatusCode == statusCode } // ResourceConflict parses the error to check if it's a resource conflict error (409). diff --git a/azure/services/asyncpoller/asyncpoller.go b/azure/services/asyncpoller/asyncpoller.go new file mode 100644 index 00000000000..9da4a6e89f9 --- /dev/null +++ b/azure/services/asyncpoller/asyncpoller.go @@ -0,0 +1,230 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package asyncpoller + +import ( + "context" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/go-autorest/autorest" + "github.com/pkg/errors" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/azure/converters" + "sigs.k8s.io/cluster-api-provider-azure/util/reconciler" + "sigs.k8s.io/cluster-api-provider-azure/util/tele" +) + +// Service is an implementation of the Reconciler interface. It handles asynchronous creation and deletion of resources. +type Service[C, D any] struct { + Scope FutureScope + Creator[C] + Deleter[D] +} + +// New creates a new async service. +func New[C, D any](scope FutureScope, createClient Creator[C], deleteClient Deleter[D]) *Service[C, D] { + return &Service[C, D]{ + Scope: scope, + Creator: createClient, + Deleter: deleteClient, + } +} + +// processOngoingOperation is a helper function that will process an ongoing operation to check if it is done. +// If it is not done, it will return a transient error. +func processOngoingOperation[T any](ctx context.Context, scope FutureScope, client FutureHandler, resourceName string, serviceName string, futureType string) (result interface{}, err error) { + ctx, log, done := tele.StartSpanWithLogger(ctx, "asyncpoller.processOngoingOperation") + defer done() + + future := scope.GetLongRunningOperationState(resourceName, serviceName, futureType) + if future == nil { + log.V(2).Info("no long-running operation found", "service", serviceName, "resource", resourceName) + return nil, nil + } + poller, err := converters.FutureToPoller[T](*future) + if err != nil { + // Reset the future data to avoid getting stuck in a bad loop. + // In theory, this should never happen, but if for some reason the future that is already stored in Status isn't properly formatted + // and we don't reset it we would be stuck in an infinite loop trying to parse it. + scope.DeleteLongRunningOperationState(resourceName, serviceName, futureType) + return nil, errors.Wrap(err, "could not decode future data, resetting long-running operation state") + } + + isDone, err := client.IsDone(ctx, poller) + // Assume that if isDone is true, then we successfully checked that the + // operation was complete even if err is non-nil. Assume the error in that + // case is unrelated and will be captured in Result below. + if !isDone { + if err != nil { + return nil, errors.Wrap(err, "failed checking if the operation was complete") + } + + // Operation is still in progress, update conditions and requeue. + log.V(2).Info("long-running operation is still ongoing", "service", serviceName, "resource", resourceName) + return nil, azure.WithTransientError(azure.NewOperationNotDoneError(future), getRequeueAfterFromPoller(poller)) + } + if err != nil { + log.V(2).Error(err, "error checking long-running operation status after it finished") + } + + // Once the operation is done, we can delete the long-running operation state. + // If the operation failed, this will allow it to be retried during the next reconciliation. + // If the resource is not found, we also reset the long-running operation state so we can attempt to create it again. + // This can happen if the resource was deleted by another process before we could get the result. + scope.DeleteLongRunningOperationState(resourceName, serviceName, futureType) + + // Resource has been created/deleted/updated. + log.V(2).Info("long-running operation has completed", "service", serviceName, "resource", resourceName) + return client.Result(ctx, &poller) +} + +// CreateOrUpdateResource implements the logic for creating a new, or updating an existing, resource Asynchronously. +func (s *Service[C, D]) CreateOrUpdateResource(ctx context.Context, spec azure.ResourceSpecGetter, serviceName string) (result interface{}, err error) { + ctx, log, done := tele.StartSpanWithLogger(ctx, "asyncpoller.Service.CreateOrUpdateResource") + defer done() + + resourceName := spec.ResourceName() + rgName := spec.ResourceGroupName() + futureType := infrav1.PutFuture + + // Check if there is an ongoing long-running operation. + future := s.Scope.GetLongRunningOperationState(resourceName, serviceName, futureType) + if future != nil { + return processOngoingOperation[C](ctx, s.Scope, s.Creator, resourceName, serviceName, futureType) + } + + // Get the resource if it already exists, and use it to construct the desired resource parameters. + var existingResource interface{} + if existing, err := s.Creator.Get(ctx, spec); err != nil && !azure.ResourceNotFound(err) { + errWrapped := errors.Wrapf(err, "failed to get existing resource %s/%s (service: %s)", rgName, resourceName, serviceName) + return nil, azure.WithTransientError(errWrapped, getRetryAfterFromError(err)) + } else if err == nil { + existingResource = existing + log.V(2).Info("successfully got existing resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName) + } + + // Construct parameters using the resource spec and information from the existing resource, if there is one. + parameters, err := spec.Parameters(ctx, existingResource) + if err != nil { + return nil, errors.Wrapf(err, "failed to get desired parameters for resource %s/%s (service: %s)", rgName, resourceName, serviceName) + } else if parameters == nil { + // Nothing to do, don't create or update the resource and return the existing resource. + log.V(2).Info("resource up to date", "service", serviceName, "resource", resourceName, "resourceGroup", rgName) + return existingResource, nil + } + + // Create or update the resource with the desired parameters. + logMessageVerbPrefix := "creat" + if existingResource != nil { + logMessageVerbPrefix = "updat" + } + log.V(2).Info(fmt.Sprintf("%sing resource", logMessageVerbPrefix), "service", serviceName, "resource", resourceName, "resourceGroup", rgName) + result, poller, err := s.Creator.CreateOrUpdateAsync(ctx, spec, parameters) + errWrapped := errors.Wrapf(err, fmt.Sprintf("failed to %se resource %s/%s (service: %s)", logMessageVerbPrefix, rgName, resourceName, serviceName)) + if poller != nil { + future, err := converters.PollerToFuture(poller, infrav1.PutFuture, serviceName, resourceName, rgName) + if err != nil { + return nil, errWrapped + } + s.Scope.SetLongRunningOperationState(future) + return nil, azure.WithTransientError(azure.NewOperationNotDoneError(future), getRequeueAfterFromPoller(poller)) + } else if err != nil { + return nil, errWrapped + } + + log.V(2).Info(fmt.Sprintf("successfully %sed resource", logMessageVerbPrefix), "service", serviceName, "resource", resourceName, "resourceGroup", rgName) + return result, nil +} + +// DeleteResource implements the logic for deleting a resource Asynchronously. +func (s *Service[C, D]) DeleteResource(ctx context.Context, spec azure.ResourceSpecGetter, serviceName string) (err error) { + ctx, log, done := tele.StartSpanWithLogger(ctx, "asyncpoller.Service.DeleteResource") + defer done() + + resourceName := spec.ResourceName() + rgName := spec.ResourceGroupName() + futureType := infrav1.DeleteFuture + + // Check if there is an ongoing long-running operation. + future := s.Scope.GetLongRunningOperationState(resourceName, serviceName, futureType) + if future != nil { + _, err := processOngoingOperation[D](ctx, s.Scope, s.Deleter, resourceName, serviceName, futureType) + return err + } + + // No long-running operation is active, so delete the resource. + log.V(2).Info("deleting resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName) + poller, err := s.Deleter.DeleteAsync(ctx, spec) + if poller != nil { + future, err := converters.PollerToFuture(poller, infrav1.DeleteFuture, serviceName, resourceName, rgName) + if err != nil { + return errors.Wrapf(err, "failed to delete resource %s/%s (service: %s)", rgName, resourceName, serviceName) + } + s.Scope.SetLongRunningOperationState(future) + return azure.WithTransientError(azure.NewOperationNotDoneError(future), getRequeueAfterFromPoller(poller)) + } else if err != nil { + if azure.ResourceNotFound(err) { + // already deleted + return nil + } + return errors.Wrapf(err, "failed to delete resource %s/%s (service: %s)", rgName, resourceName, serviceName) + } + + log.V(2).Info("successfully deleted resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName) + return nil +} + +// getRequeueAfterFromPoller returns the max between the `RETRY-AFTER` header and the default requeue time. +// This ensures we respect the retry-after header if it is set and avoid retrying too often during an API throttling event. +func getRequeueAfterFromPoller[T any](poller *runtime.Poller[T]) time.Duration { + // TODO: there doesn't seem to be a replacement for sdkFuture.GetPollingDelay() in the new poller. + return reconciler.DefaultReconcilerRequeue +} + +// getRetryAfterFromError returns the time.Duration from the http.Response in the autorest.DetailedError. +// If there is no Response object, or if there is no meaningful Retry-After header data, we return a default. +func getRetryAfterFromError(err error) time.Duration { + // TODO: need to refactor autorest out of this codebase entirely. + // In case we aren't able to introspect Retry-After from the error type, we'll return this default + ret := reconciler.DefaultReconcilerRequeue + var detailedError autorest.DetailedError + // if we have a strongly typed autorest.DetailedError then we can introspect the HTTP response data + if errors.As(err, &detailedError) { + if detailedError.Response != nil { + // If we have Retry-After HTTP header data for any reason, prefer it + if retryAfter := detailedError.Response.Header.Get("Retry-After"); retryAfter != "" { + // This handles the case where Retry-After data is in the form of units of seconds + if rai, err := strconv.Atoi(retryAfter); err == nil { + ret = time.Duration(rai) * time.Second + // This handles the case where Retry-After data is in the form of absolute time + } else if t, err := time.Parse(time.RFC1123, retryAfter); err == nil { + ret = time.Until(t) + } + // If we didn't find Retry-After HTTP header data but the response type is 429, + // we'll have to come up with our sane default. + } else if detailedError.Response.StatusCode == http.StatusTooManyRequests { + ret = reconciler.DefaultHTTP429RetryAfter + } + } + } + return ret +} diff --git a/azure/services/asyncpoller/asyncpoller_test.go b/azure/services/asyncpoller/asyncpoller_test.go new file mode 100644 index 00000000000..c8195a50900 --- /dev/null +++ b/azure/services/asyncpoller/asyncpoller_test.go @@ -0,0 +1,522 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package asyncpoller + +/* +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-05-01/resources" + "github.com/Azure/go-autorest/autorest" + azureautorest "github.com/Azure/go-autorest/autorest/azure" + "github.com/golang/mock/gomock" + . "github.com/onsi/gomega" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/asyncpoller/mock_asyncpoller" + gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock" +) + +var ( + validCreateFuture = infrav1.Future{ + Type: infrav1.PutFuture, + ServiceName: "test-service", + Name: "test-resource", + ResourceGroup: "test-group", + Data: "eyJtZXRob2QiOiJQVVQiLCJwb2xsaW5nTWV0aG9kIjoiTG9jYXRpb24iLCJscm9TdGF0ZSI6IkluUHJvZ3Jlc3MifQ==", + } + validDeleteFuture = infrav1.Future{ + Type: infrav1.DeleteFuture, + ServiceName: "test-service", + Name: "test-resource", + ResourceGroup: "test-group", + Data: "eyJtZXRob2QiOiJERUxFVEUiLCJwb2xsaW5nTWV0aG9kIjoiTG9jYXRpb24iLCJscm9TdGF0ZSI6IkluUHJvZ3Jlc3MifQ==", + } + invalidFuture = infrav1.Future{ + Type: infrav1.DeleteFuture, + ServiceName: "test-service", + Name: "test-resource", + ResourceGroup: "test-group", + Data: "ZmFrZSBiNjQgZnV0dXJlIGRhdGEK", + } + fakeExistingResource = resources.GenericResource{} + fakeResourceParameters = resources.GenericResource{} + fakeInternalError = autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: http.StatusInternalServerError}, "Internal Server Error") + fakeNotFoundError = autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: http.StatusNotFound}, "Not Found") + errCtxExceeded = errors.New("ctx exceeded") +) + +// TestProcessOngoingOperation tests the processOngoingOperation function. +func TestProcessOngoingOperation(t *testing.T) { + testcases := []struct { + name string + resourceName string + serviceName string + futureType string + expectedError string + expectedResult interface{} + expect func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockFutureHandlerMockRecorder) + }{ + { + name: "no future data stored in status", + expectedError: "", + resourceName: "test-resource", + serviceName: "test-service", + futureType: infrav1.DeleteFuture, + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockFutureHandlerMockRecorder) { + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture).Return(nil) + }, + }, + { + name: "future data is not valid", + expectedError: "could not decode future data, resetting long-running operation state", + resourceName: "test-resource", + serviceName: "test-service", + futureType: infrav1.DeleteFuture, + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockFutureHandlerMockRecorder) { + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture).Return(&invalidFuture) + s.DeleteLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture) + }, + }, + { + name: "fail to check if ongoing operation is done", + expectedError: "failed checking if the operation was complete", + resourceName: "test-resource", + serviceName: "test-service", + futureType: infrav1.DeleteFuture, + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockFutureHandlerMockRecorder) { + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture).Return(&validDeleteFuture) + c.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(false, fakeInternalError) + }, + }, + { + name: "ongoing operation is not done", + expectedError: "operation type DELETE on Azure resource test-group/test-resource is not done", + resourceName: "test-resource", + serviceName: "test-service", + futureType: infrav1.DeleteFuture, + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockFutureHandlerMockRecorder) { + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture).Return(&validDeleteFuture) + c.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(false, nil) + }, + }, + { + name: "operation is done", + expectedError: "", + expectedResult: &fakeExistingResource, + resourceName: "test-resource", + serviceName: "test-service", + futureType: infrav1.DeleteFuture, + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockFutureHandlerMockRecorder) { + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture).Return(&validDeleteFuture) + c.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(true, nil) + c.Result(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(&fakeExistingResource, nil) + s.DeleteLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture) + }, + }, + { + name: "resource was deleted by an external process", + expectedError: fakeNotFoundError.Error(), + expectedResult: nil, + resourceName: "test-resource", + serviceName: "test-service", + futureType: infrav1.DeleteFuture, + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockFutureHandlerMockRecorder) { + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture).Return(&validDeleteFuture) + c.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(true, nil) + c.Result(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(nil, fakeNotFoundError) + s.DeleteLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture) + }, + }, + { + name: "failed to get resulting resource", + expectedError: fakeInternalError.Error(), + expectedResult: nil, + resourceName: "test-resource", + serviceName: "test-service", + futureType: infrav1.DeleteFuture, + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockFutureHandlerMockRecorder) { + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture).Return(&validDeleteFuture) + c.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(true, nil) + c.Result(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(nil, fakeInternalError) + s.DeleteLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture) + }, + }, + { + name: "terminal failure with IsDone error", + expectedError: fakeInternalError.Error(), + expectedResult: nil, + resourceName: "test-resource", + serviceName: "test-service", + futureType: infrav1.DeleteFuture, + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockFutureHandlerMockRecorder) { + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture).Return(&validDeleteFuture) + c.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(true, errors.New("IsDone error")) + c.Result(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(nil, fakeInternalError) + s.DeleteLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture) + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + t.Parallel() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + scopeMock := mock_asyncpoller.NewMockFutureScope(mockCtrl) + clientMock := mock_asyncpoller.NewMockFutureHandler(mockCtrl) + + tc.expect(scopeMock.EXPECT(), clientMock.EXPECT()) + + result, err := processOngoingOperation[mock_asyncpoller.MockFutureHandler](context.TODO(), scopeMock, clientMock, tc.resourceName, tc.serviceName, tc.futureType) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + if tc.expectedResult != nil { + g.Expect(result).To(Equal(tc.expectedResult)) + } else { + g.Expect(result).To(BeNil()) + } + }) + } +} + +// TestCreateOrUpdateResource tests the CreateOrUpdateResource function. +func TestCreateOrUpdateResource(t *testing.T) { + testcases := []struct { + name string + serviceName string + expectedError string + expectedResult interface{} + expect func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) + }{ + { + name: "create operation is already in progress", + expectedError: "operation type PUT on Azure resource test-group/test-resource is not done. Object will be requeued after 15s", + serviceName: "test-service", + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.PutFuture).Times(2).Return(&validCreateFuture) + c.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(false, nil) + }, + }, + { + name: "create async returns success", + expectedError: "", + expectedResult: "test-resource", + serviceName: "test-service", + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.PutFuture).Return(nil) + c.Get(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(&fakeExistingResource, nil) + r.Parameters(gomockinternal.AContext(), &fakeExistingResource).Return(&fakeResourceParameters, nil) + c.CreateOrUpdateAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{}), &fakeResourceParameters).Return("test-resource", nil, nil) + }, + }, + { + name: "error occurs while running async get", + expectedError: "failed to get existing resource test-group/test-resource (service: test-service)", + serviceName: "test-service", + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.PutFuture).Return(nil) + c.Get(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(nil, fakeInternalError) + }, + }, + { + name: "async get returns not found", + expectedError: "", + serviceName: "test-service", + expectedResult: &fakeExistingResource, + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.PutFuture).Return(nil) + c.Get(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(nil, fakeNotFoundError) + r.Parameters(gomockinternal.AContext(), nil).Return(&fakeResourceParameters, nil) + c.CreateOrUpdateAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{}), &fakeResourceParameters).Return(&fakeExistingResource, nil, nil) + }, + }, + { + name: "error occurs while running async spec parameters", + expectedError: "failed to get desired parameters for resource test-group/test-resource (service: test-service)", + serviceName: "test-service", + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.PutFuture).Return(nil) + c.Get(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(&fakeExistingResource, nil) + r.Parameters(gomockinternal.AContext(), &fakeExistingResource).Return(nil, fakeInternalError) + }, + }, + { + name: "async spec parameters returns nil", + expectedError: "", + serviceName: "test-service", + expectedResult: &fakeExistingResource, + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.PutFuture).Return(nil) + c.Get(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(&fakeExistingResource, nil) + r.Parameters(gomockinternal.AContext(), &fakeExistingResource).Return(nil, nil) + }, + }, + { + name: "error occurs while running async create", + expectedError: "failed to update resource test-group/test-resource (service: test-service)", + serviceName: "test-service", + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.PutFuture).Return(nil) + c.Get(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(&fakeExistingResource, nil) + r.Parameters(gomockinternal.AContext(), &fakeExistingResource).Return(&fakeResourceParameters, nil) + c.CreateOrUpdateAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{}), &fakeResourceParameters).Return(nil, nil, fakeInternalError) + }, + }, + { + name: "create async exits before completing", + expectedError: "operation type PUT on Azure resource test-group/test-resource is not done. Object will be requeued after 15s", + serviceName: "test-service", + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockCreatorMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.PutFuture).Return(nil) + c.Get(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(&fakeExistingResource, nil) + r.Parameters(gomockinternal.AContext(), &fakeExistingResource).Return(&fakeResourceParameters, nil) + c.CreateOrUpdateAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{}), &fakeResourceParameters).Return(nil, &azureautorest.Future{}, errCtxExceeded) + s.SetLongRunningOperationState(gomock.AssignableToTypeOf(&infrav1.Future{})) + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + t.Parallel() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + scopeMock := mock_asyncpoller.NewMockFutureScope(mockCtrl) + creatorMock := mock_asyncpoller.NewMockCreator(mockCtrl) + specMock := mock_azure.NewMockResourceSpecGetter(mockCtrl) + + tc.expect(scopeMock.EXPECT(), creatorMock.EXPECT(), specMock.EXPECT()) + + s := New(scopeMock, creatorMock, nil) + result, err := s.CreateOrUpdateResource(context.TODO(), specMock, tc.serviceName) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(result).To(Equal(tc.expectedResult)) + } + }) + } +} + +// TestDeleteResource tests the DeleteResource function. +func TestDeleteResource(t *testing.T) { + testcases := []struct { + name string + serviceName string + expectedError string + expect func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockDeleterMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) + }{ + { + name: "delete operation is already in progress", + expectedError: "operation type DELETE on Azure resource test-group/test-resource is not done. Object will be requeued after 15s", + serviceName: "test-service", + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockDeleterMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture).Times(2).Return(&validDeleteFuture) + c.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(false, nil) + }, + }, + { + name: "delete async returns success", + expectedError: "", + serviceName: "test-service", + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockDeleterMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture).Return(nil) + c.DeleteAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(nil, nil) + }, + }, + { + name: "delete async returns not found", + expectedError: "", + serviceName: "test-service", + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockDeleterMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture).Return(nil) + c.DeleteAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(nil, fakeNotFoundError) + }, + }, + { + name: "error occurs while running async delete", + expectedError: "failed to delete resource test-group/test-resource (service: test-service)", + serviceName: "test-service", + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockDeleterMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture).Return(nil) + c.DeleteAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(nil, fakeInternalError) + }, + }, + { + name: "delete async exits before completing", + expectedError: "operation type DELETE on Azure resource test-group/test-resource is not done. Object will be requeued after 15s", + serviceName: "test-service", + expect: func(s *mock_asyncpoller.MockFutureScopeMockRecorder, c *mock_asyncpoller.MockDeleterMockRecorder, r *mock_azure.MockResourceSpecGetterMockRecorder) { + r.ResourceName().Return("test-resource") + r.ResourceGroupName().Return("test-group") + s.GetLongRunningOperationState("test-resource", "test-service", infrav1.DeleteFuture).Return(nil) + c.DeleteAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(&mock_azure.MockResourceSpecGetter{})).Return(&azureautorest.Future{}, errCtxExceeded) + s.SetLongRunningOperationState(gomock.AssignableToTypeOf(&infrav1.Future{})) + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + t.Parallel() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + scopeMock := mock_asyncpoller.NewMockFutureScope(mockCtrl) + mockObj := mock_asyncpoller + deleterMock := mock_asyncpoller.NewMockDeleter(mockCtrl) + specMock := mock_azure.NewMockResourceSpecGetter(mockCtrl) + + tc.expect(scopeMock.EXPECT(), deleterMock.EXPECT(), specMock.EXPECT()) + + s := New(scopeMock, nil, deleterMock) + err := s.DeleteResource(context.TODO(), specMock, tc.serviceName) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} + +func TestGetRetryAfterFromError(t *testing.T) { + cases := []struct { + name string + input error + expected time.Duration + expectedRangeTolerance time.Duration + }{ + { + name: "Retry-After header data present in the form of units of seconds", + input: autorest.DetailedError{ + Response: &http.Response{ + Header: http.Header{ + "Retry-After": []string{"2"}, + }, + }, + }, + expected: 2 * time.Second, + }, + { + name: "Retry-After header data present in the form of units of absolute time", + input: autorest.DetailedError{ + Response: &http.Response{ + Header: http.Header{ + "Retry-After": []string{time.Now().Add(1 * time.Hour).Format(time.RFC1123)}, + }, + }, + }, + expected: 1 * time.Hour, + expectedRangeTolerance: 5 * time.Second, + }, + { + name: "Retry-After header data not present", + input: autorest.DetailedError{ + Response: &http.Response{ + Header: http.Header{ + "foo": []string{"bar"}, + }, + }, + }, + expected: reconciler.DefaultReconcilerRequeue, + }, + { + name: "Retry-After header data not present in HTTP 429", + input: autorest.DetailedError{ + Response: &http.Response{ + StatusCode: http.StatusTooManyRequests, + Header: http.Header{ + "foo": []string{"bar"}, + }, + }, + }, + expected: reconciler.DefaultHTTP429RetryAfter, + }, + { + name: "nil http.Response", + input: autorest.DetailedError{ + Response: nil, + }, + expected: reconciler.DefaultReconcilerRequeue, + }, + { + name: "error type is not autorest.DetailedError", + input: errors.New("error"), + expected: reconciler.DefaultReconcilerRequeue, + }, + } + + for _, c := range cases { + c := c + t.Run(c.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + ret := getRetryAfterFromError(c.input) + if c.expectedRangeTolerance > 0 { + g.Expect(ret).To(BeNumerically("<", c.expected)) + g.Expect(ret + c.expectedRangeTolerance).To(BeNumerically(">", c.expected)) + } else { + g.Expect(ret).To(Equal(c.expected)) + } + }) + } +} +. +*/ diff --git a/azure/services/asyncpoller/interfaces.go b/azure/services/asyncpoller/interfaces.go new file mode 100644 index 00000000000..40d9a69ea38 --- /dev/null +++ b/azure/services/asyncpoller/interfaces.go @@ -0,0 +1,67 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package asyncpoller + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-10-01/resources" + "sigs.k8s.io/cluster-api-provider-azure/azure" +) + +// FutureScope is a scope that can perform store futures and conditions in Status. +type FutureScope interface { + azure.AsyncStatusUpdater +} + +// FutureHandler is a client that can check on the progress of a poller. +type FutureHandler interface { + // IsDone returns true if the operation is complete. + IsDone(ctx context.Context, poller interface{}) (isDone bool, err error) + // Result returns the result of the operation. + Result(ctx context.Context, poller interface{}) (result interface{}, err error) +} + +// Getter is an interface that can get a resource. +type Getter interface { + Get(ctx context.Context, spec azure.ResourceSpecGetter) (result interface{}, err error) +} + +// TagsGetter is an interface that can get a tags resource. +type TagsGetter interface { + GetAtScope(ctx context.Context, scope string) (result resources.TagsResource, err error) +} + +// Creator is a client that can create or update a resource asynchronously. +type Creator[T any] interface { + FutureHandler + Getter + CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter, parameters interface{}) (result interface{}, poller *runtime.Poller[T], err error) +} + +// Deleter is a client that can delete a resource asynchronously. +type Deleter[T any] interface { + FutureHandler + DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter) (poller *runtime.Poller[T], err error) +} + +// Reconciler is a generic interface used to perform asynchronous reconciliation of Azure resources. +type Reconciler interface { + CreateOrUpdateResource(ctx context.Context, spec azure.ResourceSpecGetter, serviceName string) (result interface{}, err error) + DeleteResource(ctx context.Context, spec azure.ResourceSpecGetter, serviceName string) (err error) +} diff --git a/azure/services/asyncpoller/mock_asyncpoller/asyncpoller_mock.go b/azure/services/asyncpoller/mock_asyncpoller/asyncpoller_mock.go new file mode 100644 index 00000000000..d8b505d5994 --- /dev/null +++ b/azure/services/asyncpoller/mock_asyncpoller/asyncpoller_mock.go @@ -0,0 +1,463 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by MockGen. DO NOT EDIT. +// Source: ../interfaces.go + +// Package mock_asyncpoller is a generated GoMock package. +package mock_asyncpoller + +import ( + context "context" + reflect "reflect" + + runtime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + resources "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-10-01/resources" + gomock "github.com/golang/mock/gomock" + v1beta1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + azure "sigs.k8s.io/cluster-api-provider-azure/azure" + v1beta10 "sigs.k8s.io/cluster-api/api/v1beta1" +) + +// MockFutureScope is a mock of FutureScope interface. +type MockFutureScope struct { + ctrl *gomock.Controller + recorder *MockFutureScopeMockRecorder +} + +// MockFutureScopeMockRecorder is the mock recorder for MockFutureScope. +type MockFutureScopeMockRecorder struct { + mock *MockFutureScope +} + +// NewMockFutureScope creates a new mock instance. +func NewMockFutureScope(ctrl *gomock.Controller) *MockFutureScope { + mock := &MockFutureScope{ctrl: ctrl} + mock.recorder = &MockFutureScopeMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFutureScope) EXPECT() *MockFutureScopeMockRecorder { + return m.recorder +} + +// DeleteLongRunningOperationState mocks base method. +func (m *MockFutureScope) DeleteLongRunningOperationState(arg0, arg1, arg2 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DeleteLongRunningOperationState", arg0, arg1, arg2) +} + +// DeleteLongRunningOperationState indicates an expected call of DeleteLongRunningOperationState. +func (mr *MockFutureScopeMockRecorder) DeleteLongRunningOperationState(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLongRunningOperationState", reflect.TypeOf((*MockFutureScope)(nil).DeleteLongRunningOperationState), arg0, arg1, arg2) +} + +// GetLongRunningOperationState mocks base method. +func (m *MockFutureScope) GetLongRunningOperationState(arg0, arg1, arg2 string) *v1beta1.Future { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLongRunningOperationState", arg0, arg1, arg2) + ret0, _ := ret[0].(*v1beta1.Future) + return ret0 +} + +// GetLongRunningOperationState indicates an expected call of GetLongRunningOperationState. +func (mr *MockFutureScopeMockRecorder) GetLongRunningOperationState(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLongRunningOperationState", reflect.TypeOf((*MockFutureScope)(nil).GetLongRunningOperationState), arg0, arg1, arg2) +} + +// SetLongRunningOperationState mocks base method. +func (m *MockFutureScope) SetLongRunningOperationState(arg0 *v1beta1.Future) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetLongRunningOperationState", arg0) +} + +// SetLongRunningOperationState indicates an expected call of SetLongRunningOperationState. +func (mr *MockFutureScopeMockRecorder) SetLongRunningOperationState(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLongRunningOperationState", reflect.TypeOf((*MockFutureScope)(nil).SetLongRunningOperationState), arg0) +} + +// UpdateDeleteStatus mocks base method. +func (m *MockFutureScope) UpdateDeleteStatus(arg0 v1beta10.ConditionType, arg1 string, arg2 error) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdateDeleteStatus", arg0, arg1, arg2) +} + +// UpdateDeleteStatus indicates an expected call of UpdateDeleteStatus. +func (mr *MockFutureScopeMockRecorder) UpdateDeleteStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeleteStatus", reflect.TypeOf((*MockFutureScope)(nil).UpdateDeleteStatus), arg0, arg1, arg2) +} + +// UpdatePatchStatus mocks base method. +func (m *MockFutureScope) UpdatePatchStatus(arg0 v1beta10.ConditionType, arg1 string, arg2 error) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdatePatchStatus", arg0, arg1, arg2) +} + +// UpdatePatchStatus indicates an expected call of UpdatePatchStatus. +func (mr *MockFutureScopeMockRecorder) UpdatePatchStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePatchStatus", reflect.TypeOf((*MockFutureScope)(nil).UpdatePatchStatus), arg0, arg1, arg2) +} + +// UpdatePutStatus mocks base method. +func (m *MockFutureScope) UpdatePutStatus(arg0 v1beta10.ConditionType, arg1 string, arg2 error) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdatePutStatus", arg0, arg1, arg2) +} + +// UpdatePutStatus indicates an expected call of UpdatePutStatus. +func (mr *MockFutureScopeMockRecorder) UpdatePutStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePutStatus", reflect.TypeOf((*MockFutureScope)(nil).UpdatePutStatus), arg0, arg1, arg2) +} + +// MockFutureHandler is a mock of FutureHandler interface. +type MockFutureHandler struct { + ctrl *gomock.Controller + recorder *MockFutureHandlerMockRecorder +} + +// MockFutureHandlerMockRecorder is the mock recorder for MockFutureHandler. +type MockFutureHandlerMockRecorder struct { + mock *MockFutureHandler +} + +// NewMockFutureHandler creates a new mock instance. +func NewMockFutureHandler(ctrl *gomock.Controller) *MockFutureHandler { + mock := &MockFutureHandler{ctrl: ctrl} + mock.recorder = &MockFutureHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFutureHandler) EXPECT() *MockFutureHandlerMockRecorder { + return m.recorder +} + +// IsDone mocks base method. +func (m *MockFutureHandler) IsDone(ctx context.Context, poller interface{}) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsDone", ctx, poller) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsDone indicates an expected call of IsDone. +func (mr *MockFutureHandlerMockRecorder) IsDone(ctx, poller interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDone", reflect.TypeOf((*MockFutureHandler)(nil).IsDone), ctx, poller) +} + +// Result mocks base method. +func (m *MockFutureHandler) Result(ctx context.Context, poller interface{}) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Result", ctx, poller) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Result indicates an expected call of Result. +func (mr *MockFutureHandlerMockRecorder) Result(ctx, poller interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Result", reflect.TypeOf((*MockFutureHandler)(nil).Result), ctx, poller) +} + +// MockGetter is a mock of Getter interface. +type MockGetter struct { + ctrl *gomock.Controller + recorder *MockGetterMockRecorder +} + +// MockGetterMockRecorder is the mock recorder for MockGetter. +type MockGetterMockRecorder struct { + mock *MockGetter +} + +// NewMockGetter creates a new mock instance. +func NewMockGetter(ctrl *gomock.Controller) *MockGetter { + mock := &MockGetter{ctrl: ctrl} + mock.recorder = &MockGetterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGetter) EXPECT() *MockGetterMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockGetter) Get(ctx context.Context, spec azure.ResourceSpecGetter) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, spec) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockGetterMockRecorder) Get(ctx, spec interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGetter)(nil).Get), ctx, spec) +} + +// MockTagsGetter is a mock of TagsGetter interface. +type MockTagsGetter struct { + ctrl *gomock.Controller + recorder *MockTagsGetterMockRecorder +} + +// MockTagsGetterMockRecorder is the mock recorder for MockTagsGetter. +type MockTagsGetterMockRecorder struct { + mock *MockTagsGetter +} + +// NewMockTagsGetter creates a new mock instance. +func NewMockTagsGetter(ctrl *gomock.Controller) *MockTagsGetter { + mock := &MockTagsGetter{ctrl: ctrl} + mock.recorder = &MockTagsGetterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTagsGetter) EXPECT() *MockTagsGetterMockRecorder { + return m.recorder +} + +// GetAtScope mocks base method. +func (m *MockTagsGetter) GetAtScope(ctx context.Context, scope string) (resources.TagsResource, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAtScope", ctx, scope) + ret0, _ := ret[0].(resources.TagsResource) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAtScope indicates an expected call of GetAtScope. +func (mr *MockTagsGetterMockRecorder) GetAtScope(ctx, scope interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAtScope", reflect.TypeOf((*MockTagsGetter)(nil).GetAtScope), ctx, scope) +} + +// MockCreator is a mock of Creator interface. +type MockCreator[T any] struct { + ctrl *gomock.Controller + recorder *MockCreatorMockRecorder[T] +} + +// MockCreatorMockRecorder is the mock recorder for MockCreator. +type MockCreatorMockRecorder[T any] struct { + mock *MockCreator[T] +} + +// NewMockCreator creates a new mock instance. +func NewMockCreator[T any](ctrl *gomock.Controller) *MockCreator[T] { + mock := &MockCreator[T]{ctrl: ctrl} + mock.recorder = &MockCreatorMockRecorder[T]{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCreator[T]) EXPECT() *MockCreatorMockRecorder[T] { + return m.recorder +} + +// CreateOrUpdateAsync mocks base method. +func (m *MockCreator[T]) CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter, parameters interface{}) (interface{}, *runtime.Poller[T], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrUpdateAsync", ctx, spec, parameters) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(*runtime.Poller[T]) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// CreateOrUpdateAsync indicates an expected call of CreateOrUpdateAsync. +func (mr *MockCreatorMockRecorder[T]) CreateOrUpdateAsync(ctx, spec, parameters interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateAsync", reflect.TypeOf((*MockCreator[T])(nil).CreateOrUpdateAsync), ctx, spec, parameters) +} + +// Get mocks base method. +func (m *MockCreator[T]) Get(ctx context.Context, spec azure.ResourceSpecGetter) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, spec) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockCreatorMockRecorder[T]) Get(ctx, spec interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCreator[T])(nil).Get), ctx, spec) +} + +// IsDone mocks base method. +func (m *MockCreator[T]) IsDone(ctx context.Context, poller interface{}) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsDone", ctx, poller) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsDone indicates an expected call of IsDone. +func (mr *MockCreatorMockRecorder[T]) IsDone(ctx, poller interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDone", reflect.TypeOf((*MockCreator[T])(nil).IsDone), ctx, poller) +} + +// Result mocks base method. +func (m *MockCreator[T]) Result(ctx context.Context, poller interface{}) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Result", ctx, poller) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Result indicates an expected call of Result. +func (mr *MockCreatorMockRecorder[T]) Result(ctx, poller interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Result", reflect.TypeOf((*MockCreator[T])(nil).Result), ctx, poller) +} + +// MockDeleter is a mock of Deleter interface. +type MockDeleter[T any] struct { + ctrl *gomock.Controller + recorder *MockDeleterMockRecorder[T] +} + +// MockDeleterMockRecorder is the mock recorder for MockDeleter. +type MockDeleterMockRecorder[T any] struct { + mock *MockDeleter[T] +} + +// NewMockDeleter creates a new mock instance. +func NewMockDeleter[T any](ctrl *gomock.Controller) *MockDeleter[T] { + mock := &MockDeleter[T]{ctrl: ctrl} + mock.recorder = &MockDeleterMockRecorder[T]{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDeleter[T]) EXPECT() *MockDeleterMockRecorder[T] { + return m.recorder +} + +// DeleteAsync mocks base method. +func (m *MockDeleter[T]) DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter) (*runtime.Poller[T], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAsync", ctx, spec) + ret0, _ := ret[0].(*runtime.Poller[T]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteAsync indicates an expected call of DeleteAsync. +func (mr *MockDeleterMockRecorder[T]) DeleteAsync(ctx, spec interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAsync", reflect.TypeOf((*MockDeleter[T])(nil).DeleteAsync), ctx, spec) +} + +// IsDone mocks base method. +func (m *MockDeleter[T]) IsDone(ctx context.Context, poller interface{}) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsDone", ctx, poller) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsDone indicates an expected call of IsDone. +func (mr *MockDeleterMockRecorder[T]) IsDone(ctx, poller interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDone", reflect.TypeOf((*MockDeleter[T])(nil).IsDone), ctx, poller) +} + +// Result mocks base method. +func (m *MockDeleter[T]) Result(ctx context.Context, poller interface{}) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Result", ctx, poller) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Result indicates an expected call of Result. +func (mr *MockDeleterMockRecorder[T]) Result(ctx, poller interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Result", reflect.TypeOf((*MockDeleter[T])(nil).Result), ctx, poller) +} + +// MockReconciler is a mock of Reconciler interface. +type MockReconciler struct { + ctrl *gomock.Controller + recorder *MockReconcilerMockRecorder +} + +// MockReconcilerMockRecorder is the mock recorder for MockReconciler. +type MockReconcilerMockRecorder struct { + mock *MockReconciler +} + +// NewMockReconciler creates a new mock instance. +func NewMockReconciler(ctrl *gomock.Controller) *MockReconciler { + mock := &MockReconciler{ctrl: ctrl} + mock.recorder = &MockReconcilerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockReconciler) EXPECT() *MockReconcilerMockRecorder { + return m.recorder +} + +// CreateOrUpdateResource mocks base method. +func (m *MockReconciler) CreateOrUpdateResource(ctx context.Context, spec azure.ResourceSpecGetter, serviceName string) (interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOrUpdateResource", ctx, spec, serviceName) + ret0, _ := ret[0].(interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateOrUpdateResource indicates an expected call of CreateOrUpdateResource. +func (mr *MockReconcilerMockRecorder) CreateOrUpdateResource(ctx, spec, serviceName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateResource", reflect.TypeOf((*MockReconciler)(nil).CreateOrUpdateResource), ctx, spec, serviceName) +} + +// DeleteResource mocks base method. +func (m *MockReconciler) DeleteResource(ctx context.Context, spec azure.ResourceSpecGetter, serviceName string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteResource", ctx, spec, serviceName) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteResource indicates an expected call of DeleteResource. +func (mr *MockReconcilerMockRecorder) DeleteResource(ctx, spec, serviceName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteResource", reflect.TypeOf((*MockReconciler)(nil).DeleteResource), ctx, spec, serviceName) +} diff --git a/azure/services/asyncpoller/mock_asyncpoller/doc.go b/azure/services/asyncpoller/mock_asyncpoller/doc.go new file mode 100644 index 00000000000..d77f85d043f --- /dev/null +++ b/azure/services/asyncpoller/mock_asyncpoller/doc.go @@ -0,0 +1,21 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Run go generate to regenerate this mock. +// +// *DISABLED pending generics support* go:generate ../../../../hack/tools/bin/mockgen -destination asyncpoller_mock.go -package mock_asyncpoller -source ../interfaces.go FutureHandler +// *DISABLED pending generics support* go:generate /usr/bin/env bash -c "cat ../../../../hack/boilerplate/boilerplate.generatego.txt asyncpoller_mock.go > _asyncpoller_mock.go && mv _asyncpoller_mock.go asyncpoller_mock.go". +package mock_asyncpoller From 5ace8bffa8cca0d863ce67e7a4f3d02f0e33ed07 Mon Sep 17 00:00:00 2001 From: Matt Boersma Date: Thu, 6 Apr 2023 14:31:22 -0600 Subject: [PATCH 2/7] Convert natgateways service to asyncpoller framework --- azure/services/natgateways/client.go | 117 +++++++++--------- azure/services/natgateways/natgateways.go | 15 +-- .../services/natgateways/natgateways_test.go | 8 +- azure/services/natgateways/spec.go | 25 ++-- go.mod | 1 + go.sum | 4 + 6 files changed, 87 insertions(+), 83 deletions(-) diff --git a/azure/services/natgateways/client.go b/azure/services/natgateways/client.go index c951c0d9fde..554d5d82710 100644 --- a/azure/services/natgateways/client.go +++ b/azure/services/natgateways/client.go @@ -18,13 +18,12 @@ package natgateways import ( "context" - "encoding/json" - "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-08-01/network" - "github.com/Azure/go-autorest/autorest" - azureautorest "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork" "github.com/pkg/errors" - infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" "sigs.k8s.io/cluster-api-provider-azure/azure" "sigs.k8s.io/cluster-api-provider-azure/util/reconciler" "sigs.k8s.io/cluster-api-provider-azure/util/tele" @@ -32,20 +31,29 @@ import ( // azureClient contains the Azure go-sdk Client. type azureClient struct { - natgateways network.NatGatewaysClient + natgateways armnetwork.NatGatewaysClient } -// newClient creates a new VM client from subscription ID. +// newClient creates a new azureClient from an Authorizer. func newClient(auth azure.Authorizer) *azureClient { - c := netNatGatewaysClient(auth.SubscriptionID(), auth.BaseURI(), auth.Authorizer()) + c, err := newNatGatewaysClient(auth.SubscriptionID()) + if err != nil { + panic(err) // TODO: Handle this properly! + } return &azureClient{c} } -// netNatGatewaysClient creates a new nat gateways client from subscription ID. -func netNatGatewaysClient(subscriptionID string, baseURI string, authorizer autorest.Authorizer) network.NatGatewaysClient { - natGatewaysClient := network.NewNatGatewaysClientWithBaseURI(baseURI, subscriptionID) - azure.SetAutoRestClientDefaults(&natGatewaysClient.Client, authorizer) - return natGatewaysClient +// newNatGatewaysClient creates a new nat gateways client from subscription ID. +func newNatGatewaysClient(subscriptionID string) (armnetwork.NatGatewaysClient, error) { + credential, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return armnetwork.NatGatewaysClient{}, errors.Wrap(err, "failed to create credential") + } + client, err := armnetwork.NewNatGatewaysClient(subscriptionID, credential, &arm.ClientOptions{}) + if err != nil { + return armnetwork.NatGatewaysClient{}, errors.Wrap(err, "cannot create new Resource SKUs client") + } + return *client, nil } // Get gets the specified nat gateway. @@ -53,22 +61,23 @@ func (ac *azureClient) Get(ctx context.Context, spec azure.ResourceSpecGetter) ( ctx, _, done := tele.StartSpanWithLogger(ctx, "natgateways.azureClient.Get") defer done() - return ac.natgateways.Get(ctx, spec.ResourceGroupName(), spec.ResourceName(), "") + return ac.natgateways.Get(ctx, spec.ResourceGroupName(), spec.ResourceName(), &armnetwork.NatGatewaysClientGetOptions{}) } // CreateOrUpdateAsync creates or updates a Nat Gateway asynchronously. // It sends a PUT request to Azure and if accepted without error, the func will return a Future which can be used to track the ongoing // progress of the operation. -func (ac *azureClient) CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter, parameters interface{}) (result interface{}, future azureautorest.FutureAPI, err error) { +func (ac *azureClient) CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter, parameters interface{}) (result interface{}, poller *runtime.Poller[armnetwork.NatGatewaysClientCreateOrUpdateResponse], err error) { ctx, _, done := tele.StartSpanWithLogger(ctx, "natgateways.azureClient.CreateOrUpdateAsync") defer done() - natGateway, ok := parameters.(network.NatGateway) + natGateway, ok := parameters.(armnetwork.NatGateway) if !ok { - return nil, nil, errors.Errorf("%T is not a network.NatGateway", parameters) + return nil, nil, errors.Errorf("%T is not a armnetwork.NatGateway", parameters) } - createFuture, err := ac.natgateways.CreateOrUpdate(ctx, spec.ResourceGroupName(), spec.ResourceName(), natGateway) + opts := &armnetwork.NatGatewaysClientBeginCreateOrUpdateOptions{} + poller, err = ac.natgateways.BeginCreateOrUpdate(ctx, spec.ResourceGroupName(), spec.ResourceName(), natGateway, opts) if err != nil { return nil, nil, err } @@ -76,26 +85,25 @@ func (ac *azureClient) CreateOrUpdateAsync(ctx context.Context, spec azure.Resou ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureCallTimeout) defer cancel() - err = createFuture.WaitForCompletionRef(ctx, ac.natgateways.Client) + result, err = poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{}) if err != nil { - // if an error occurs, return the future. + // if an error occurs, return the poller. // this means the long-running operation didn't finish in the specified timeout. - return nil, &createFuture, err + return nil, poller, err } - result, err = createFuture.Result(ac.natgateways) - // if the operation completed, return a nil future + // if the operation completed, return a nil poller return result, nil, err } // DeleteAsync deletes a Nat Gateway asynchronously. DeleteAsync sends a DELETE // request to Azure and if accepted without error, the func will return a Future which can be used to track the ongoing // progress of the operation. -func (ac *azureClient) DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter) (future azureautorest.FutureAPI, err error) { +func (ac *azureClient) DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter) (poller *runtime.Poller[armnetwork.NatGatewaysClientDeleteResponse], err error) { ctx, _, done := tele.StartSpanWithLogger(ctx, "natgateways.azureClient.DeleteAsync") defer done() - deleteFuture, err := ac.natgateways.Delete(ctx, spec.ResourceGroupName(), spec.ResourceName()) + poller, err = ac.natgateways.BeginDelete(ctx, spec.ResourceGroupName(), spec.ResourceName(), &armnetwork.NatGatewaysClientBeginDeleteOptions{}) if err != nil { return nil, err } @@ -103,54 +111,47 @@ func (ac *azureClient) DeleteAsync(ctx context.Context, spec azure.ResourceSpecG ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureCallTimeout) defer cancel() - err = deleteFuture.WaitForCompletionRef(ctx, ac.natgateways.Client) + _, err = poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{}) if err != nil { - // if an error occurs, return the future. + // if an error occurs, return the poller. // this means the long-running operation didn't finish in the specified timeout. - return &deleteFuture, err + return poller, err } - _, err = deleteFuture.Result(ac.natgateways) - // if the operation completed, return a nil future. + + // if the operation completed, return a nil poller. return nil, err } // IsDone returns true if the long-running operation has completed. -func (ac *azureClient) IsDone(ctx context.Context, future azureautorest.FutureAPI) (isDone bool, err error) { - ctx, _, done := tele.StartSpanWithLogger(ctx, "natgateways.azureClient.IsDone") +func (ac *azureClient) IsDone(ctx context.Context, poller interface{}) (isDone bool, err error) { + _, _, done := tele.StartSpanWithLogger(ctx, "natgateways.azureClient.IsDone") defer done() - return future.DoneWithContext(ctx, ac.natgateways) + switch t := poller.(type) { + case *runtime.Poller[armnetwork.NatGatewaysClientCreateOrUpdateResponse]: + c, _ := poller.(*runtime.Poller[armnetwork.NatGatewaysClientCreateOrUpdateResponse]) + return c.Done(), nil + case *runtime.Poller[armnetwork.NatGatewaysClientDeleteResponse]: + d, _ := poller.(*runtime.Poller[armnetwork.NatGatewaysClientDeleteResponse]) + return d.Done(), nil + default: + return false, errors.Errorf("unexpected poller type %T", t) + } } // Result fetches the result of a long-running operation future. -func (ac *azureClient) Result(ctx context.Context, future azureautorest.FutureAPI, futureType string) (result interface{}, err error) { +func (ac *azureClient) Result(ctx context.Context, poller interface{}) (result interface{}, err error) { _, _, done := tele.StartSpanWithLogger(ctx, "natgateways.azureClient.Result") defer done() - if future == nil { - return nil, errors.Errorf("cannot get result from nil future") - } - - switch futureType { - case infrav1.PutFuture: - // Marshal and Unmarshal the future to put it into the correct future type so we can access the Result function. - // Unfortunately the FutureAPI can't be casted directly to NatGatewaysCreateOrUpdateFuture because it is a azureautorest.Future, which doesn't implement the Result function. See PR #1686 for discussion on alternatives. - // It was converted back to a generic azureautorest.Future from the CAPZ infrav1.Future type stored in Status: https://github.com/kubernetes-sigs/cluster-api-provider-azure/blob/main/azure/converters/futures.go#L49. - var createFuture *network.NatGatewaysCreateOrUpdateFuture - jsonData, err := future.MarshalJSON() - if err != nil { - return nil, errors.Wrap(err, "failed to marshal future") - } - if err := json.Unmarshal(jsonData, &createFuture); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal future data") - } - return createFuture.Result(ac.natgateways) - - case infrav1.DeleteFuture: - // Delete does not return a result NAT gateway - return nil, nil - + switch t := poller.(type) { + case *runtime.Poller[armnetwork.NatGatewaysClientCreateOrUpdateResponse]: + c, _ := poller.(*runtime.Poller[armnetwork.NatGatewaysClientCreateOrUpdateResponse]) + return c.Result(ctx) + case *runtime.Poller[armnetwork.NatGatewaysClientDeleteResponse]: + d, _ := poller.(*runtime.Poller[armnetwork.NatGatewaysClientDeleteResponse]) + return d.Result(ctx) default: - return nil, errors.Errorf("unknown future type %q", futureType) + return false, errors.Errorf("unexpected poller type %T", t) } } diff --git a/azure/services/natgateways/natgateways.go b/azure/services/natgateways/natgateways.go index c15c4600755..64b556a9870 100644 --- a/azure/services/natgateways/natgateways.go +++ b/azure/services/natgateways/natgateways.go @@ -19,11 +19,11 @@ package natgateways import ( "context" - "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-08-01/network" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork" "github.com/pkg/errors" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" "sigs.k8s.io/cluster-api-provider-azure/azure" - "sigs.k8s.io/cluster-api-provider-azure/azure/services/async" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/asyncpoller" "sigs.k8s.io/cluster-api-provider-azure/util/reconciler" "sigs.k8s.io/cluster-api-provider-azure/util/tele" ) @@ -41,15 +41,16 @@ type NatGatewayScope interface { // Service provides operations on azure resources. type Service struct { Scope NatGatewayScope - async.Reconciler + asyncpoller.Reconciler } // New creates a new service. func New(scope NatGatewayScope) *Service { client := newClient(scope) return &Service{ - Scope: scope, - Reconciler: async.New(scope, client, client), + Scope: scope, + Reconciler: asyncpoller.New[armnetwork.NatGatewaysClientCreateOrUpdateResponse, + armnetwork.NatGatewaysClientDeleteResponse](scope, client, client), } } @@ -91,10 +92,10 @@ func (s *Service) Reconcile(ctx context.Context) error { } } if err == nil { - natGateway, ok := result.(network.NatGateway) + natGateway, ok := result.(armnetwork.NatGateway) if !ok { // Return out of loop since this would be an unexpected fatal error - resultingErr = errors.Errorf("created resource %T is not a network.NatGateway", result) + resultingErr = errors.Errorf("created resource %T is not a armnetwork.NatGateway", result) break } diff --git a/azure/services/natgateways/natgateways_test.go b/azure/services/natgateways/natgateways_test.go index d5242999b6a..fb6713dab38 100644 --- a/azure/services/natgateways/natgateways_test.go +++ b/azure/services/natgateways/natgateways_test.go @@ -21,7 +21,7 @@ import ( "net/http" "testing" - "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-08-01/network" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork" "github.com/Azure/go-autorest/autorest" "github.com/golang/mock/gomock" . "github.com/onsi/gomega" @@ -48,7 +48,7 @@ var ( ClusterName: "my-cluster", NatGatewayIP: infrav1.PublicIPSpec{Name: "pip-node-subnet"}, } - natGateway1 = network.NatGateway{ + natGateway1 = armnetwork.NatGateway{ ID: pointer.String("/subscriptions/my-sub/resourceGroups/my-rg/providers/Microsoft.Network/natGateways/my-node-natgateway-1"), } customVNetTags = infrav1.Tags{ @@ -114,12 +114,12 @@ func TestReconcileNatGateways(t *testing.T) { { name: "result is not a NAT gateway", tags: ownedVNetTags, - expectedError: "created resource string is not a network.NatGateway", + expectedError: "created resource string is not a armnetwork.NatGateway", expect: func(s *mock_natgateways.MockNatGatewayScopeMockRecorder, r *mock_async.MockReconcilerMockRecorder) { s.IsVnetManaged().Return(true) s.NatGatewaySpecs().Return([]azure.ResourceSpecGetter{&natGatewaySpec1}) r.CreateOrUpdateResource(gomockinternal.AContext(), &natGatewaySpec1, serviceName).Return("not a nat gateway", nil) - s.UpdatePutStatus(infrav1.NATGatewaysReadyCondition, serviceName, gomockinternal.ErrStrEq("created resource string is not a network.NatGateway")) + s.UpdatePutStatus(infrav1.NATGatewaysReadyCondition, serviceName, gomockinternal.ErrStrEq("created resource string is not a armnetwork.NatGateway")) }, }, } diff --git a/azure/services/natgateways/spec.go b/azure/services/natgateways/spec.go index f45f4341a80..b2c8d593a3d 100644 --- a/azure/services/natgateways/spec.go +++ b/azure/services/natgateways/spec.go @@ -20,7 +20,8 @@ import ( "context" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" - "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-08-01/network" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork" "github.com/pkg/errors" "k8s.io/utils/pointer" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" @@ -57,9 +58,9 @@ func (s *NatGatewaySpec) OwnerResourceName() string { // Parameters returns the parameters for the NAT gateway. func (s *NatGatewaySpec) Parameters(ctx context.Context, existing interface{}) (params interface{}, err error) { if existing != nil { - existingNatGateway, ok := existing.(network.NatGateway) + existingNatGateway, ok := existing.(armnetwork.NatGateway) if !ok { - return nil, errors.Errorf("%T is not a network.NatGateway", existing) + return nil, errors.Errorf("%T is not a armnetwork.NatGateway", existing) } if hasPublicIP(existingNatGateway, s.NatGatewayIP.Name) { @@ -68,12 +69,12 @@ func (s *NatGatewaySpec) Parameters(ctx context.Context, existing interface{}) ( } } - natGatewayToCreate := network.NatGateway{ + natGatewayToCreate := armnetwork.NatGateway{ Name: pointer.String(s.Name), Location: pointer.String(s.Location), - Sku: &network.NatGatewaySku{Name: network.NatGatewaySkuNameStandard}, - NatGatewayPropertiesFormat: &network.NatGatewayPropertiesFormat{ - PublicIPAddresses: &[]network.SubResource{ + SKU: &armnetwork.NatGatewaySKU{Name: to.Ptr(armnetwork.NatGatewaySKUNameStandard)}, + Properties: &armnetwork.NatGatewayPropertiesFormat{ + PublicIPAddresses: []*armnetwork.SubResource{ { ID: pointer.String(azure.PublicIPID(s.SubscriptionID, s.ResourceGroupName(), s.NatGatewayIP.Name)), }, @@ -90,13 +91,8 @@ func (s *NatGatewaySpec) Parameters(ctx context.Context, existing interface{}) ( return natGatewayToCreate, nil } -func hasPublicIP(natGateway network.NatGateway, publicIPName string) bool { - // We must have a non-nil, non-"empty" PublicIPAddresses - if !(natGateway.PublicIPAddresses != nil && len(*natGateway.PublicIPAddresses) > 0) { - return false - } - - for _, publicIP := range *natGateway.PublicIPAddresses { +func hasPublicIP(natGateway armnetwork.NatGateway, publicIPName string) bool { + for _, publicIP := range natGateway.Properties.PublicIPAddresses { resource, err := arm.ParseResourceID(*publicIP.ID) if err != nil { continue @@ -105,5 +101,6 @@ func hasPublicIP(natGateway network.NatGateway, publicIPName string) bool { return true } } + return false } diff --git a/go.mod b/go.mod index 09d5e9717b1..06cecd382c1 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/Azure/azure-sdk-for-go v68.0.0+incompatible github.com/Azure/azure-sdk-for-go/sdk/azcore v1.5.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0 github.com/Azure/go-autorest/autorest v0.11.28 github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 github.com/Azure/go-autorest/tracing v0.6.0 diff --git a/go.sum b/go.sum index d116d73543e..a78101171b9 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,10 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 h1:uqM+VoHjVH6zdlkLF2b6O github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2/go.mod h1:twTKAa1E6hLmSDjLhaCkbTMQKc7p/rNLU40rLxGEOCI= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal v1.0.0 h1:lMW1lD/17LUA5z1XTURo7LcVG2ICBPlyMHjIUrcFZNQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0 h1:QM6sE5k2ZT/vI5BEe0r7mqjsUSnhVBFbOsVkEuaEfiA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0/go.mod h1:243D9iHbcQXoFUtgHJwL7gl2zx1aDuDMjvBZVGr2uW0= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 h1:ECsQtyERDVz3NP3kvDOTLvbQhqWp/x9EsGKtb4ogUr8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= From c4203fa5704fe32210892d72c9ab2191e7fb245c Mon Sep 17 00:00:00 2001 From: Matt Boersma Date: Thu, 6 Apr 2023 16:32:03 -0600 Subject: [PATCH 3/7] Update asyncpoller getRetryAfterFromError --- azure/services/asyncpoller/asyncpoller.go | 34 +++++++++++------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/azure/services/asyncpoller/asyncpoller.go b/azure/services/asyncpoller/asyncpoller.go index 9da4a6e89f9..42d49b9e9f1 100644 --- a/azure/services/asyncpoller/asyncpoller.go +++ b/azure/services/asyncpoller/asyncpoller.go @@ -23,8 +23,8 @@ import ( "strconv" "time" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" - "github.com/Azure/go-autorest/autorest" "github.com/pkg/errors" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" "sigs.k8s.io/cluster-api-provider-azure/azure" @@ -206,24 +206,22 @@ func getRetryAfterFromError(err error) time.Duration { // TODO: need to refactor autorest out of this codebase entirely. // In case we aren't able to introspect Retry-After from the error type, we'll return this default ret := reconciler.DefaultReconcilerRequeue - var detailedError autorest.DetailedError - // if we have a strongly typed autorest.DetailedError then we can introspect the HTTP response data - if errors.As(err, &detailedError) { - if detailedError.Response != nil { - // If we have Retry-After HTTP header data for any reason, prefer it - if retryAfter := detailedError.Response.Header.Get("Retry-After"); retryAfter != "" { - // This handles the case where Retry-After data is in the form of units of seconds - if rai, err := strconv.Atoi(retryAfter); err == nil { - ret = time.Duration(rai) * time.Second - // This handles the case where Retry-After data is in the form of absolute time - } else if t, err := time.Parse(time.RFC1123, retryAfter); err == nil { - ret = time.Until(t) - } - // If we didn't find Retry-After HTTP header data but the response type is 429, - // we'll have to come up with our sane default. - } else if detailedError.Response.StatusCode == http.StatusTooManyRequests { - ret = reconciler.DefaultHTTP429RetryAfter + var responseError azcore.ResponseError + // if we have a strongly typed azcore.ResponseError then we can introspect the HTTP response data + if errors.As(err, &responseError) && responseError.RawResponse != nil { + // If we have Retry-After HTTP header data for any reason, prefer it + if retryAfter := responseError.RawResponse.Header.Get("Retry-After"); retryAfter != "" { + // This handles the case where Retry-After data is in the form of units of seconds + if rai, err := strconv.Atoi(retryAfter); err == nil { + ret = time.Duration(rai) * time.Second + // This handles the case where Retry-After data is in the form of absolute time + } else if t, err := time.Parse(time.RFC1123, retryAfter); err == nil { + ret = time.Until(t) } + // If we didn't find Retry-After HTTP header data but the response type is 429, + // we'll have to come up with our sane default. + } else if responseError.RawResponse.StatusCode == http.StatusTooManyRequests { + ret = reconciler.DefaultHTTP429RetryAfter } } return ret From a98f193e41a236123f43c9068cb969226ad64b4e Mon Sep 17 00:00:00 2001 From: Matt Boersma Date: Fri, 7 Apr 2023 11:13:34 -0600 Subject: [PATCH 4/7] Update asyncpoller to use resumeToken --- azure/converters/futures.go | 15 +--- azure/services/asyncpoller/asyncpoller.go | 89 +++++++---------------- azure/services/asyncpoller/interfaces.go | 4 +- 3 files changed, 32 insertions(+), 76 deletions(-) diff --git a/azure/converters/futures.go b/azure/converters/futures.go index 16d91540a81..8edc0d41b74 100644 --- a/azure/converters/futures.go +++ b/azure/converters/futures.go @@ -19,7 +19,6 @@ package converters import ( "encoding/base64" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" azureautorest "github.com/Azure/go-autorest/autorest/azure" "github.com/pkg/errors" @@ -70,17 +69,11 @@ func PollerToFuture[T any](poller *runtime.Poller[T], futureType, service, resou }, nil } -// FutureToPoller converts an infrav1.Future to an SDK poller. -func FutureToPoller[T any](future infrav1.Future) (*runtime.Poller[T], error) { +// FutureToResumeToken converts an infrav1.Future to an Azure SDK resume token. +func FutureToResumeToken(future infrav1.Future) (string, error) { token, err := base64.URLEncoding.DecodeString(future.Data) if err != nil { - return nil, errors.Wrap(err, "failed to base64-decode poller resume token") + return "", errors.Wrap(err, "failed to base64-decode future data") } - pl := runtime.NewPipeline("", "", runtime.PipelineOptions{}, &policy.ClientOptions{}) - opts := runtime.NewPollerFromResumeTokenOptions[T]{} - poller, err := runtime.NewPollerFromResumeToken(string(token), pl, &opts) - if err != nil { - return nil, errors.Wrap(err, "failed to create poller from resume token") - } - return poller, nil + return string(token), nil } diff --git a/azure/services/asyncpoller/asyncpoller.go b/azure/services/asyncpoller/asyncpoller.go index 42d49b9e9f1..616b1a0a14a 100644 --- a/azure/services/asyncpoller/asyncpoller.go +++ b/azure/services/asyncpoller/asyncpoller.go @@ -49,54 +49,6 @@ func New[C, D any](scope FutureScope, createClient Creator[C], deleteClient Dele } } -// processOngoingOperation is a helper function that will process an ongoing operation to check if it is done. -// If it is not done, it will return a transient error. -func processOngoingOperation[T any](ctx context.Context, scope FutureScope, client FutureHandler, resourceName string, serviceName string, futureType string) (result interface{}, err error) { - ctx, log, done := tele.StartSpanWithLogger(ctx, "asyncpoller.processOngoingOperation") - defer done() - - future := scope.GetLongRunningOperationState(resourceName, serviceName, futureType) - if future == nil { - log.V(2).Info("no long-running operation found", "service", serviceName, "resource", resourceName) - return nil, nil - } - poller, err := converters.FutureToPoller[T](*future) - if err != nil { - // Reset the future data to avoid getting stuck in a bad loop. - // In theory, this should never happen, but if for some reason the future that is already stored in Status isn't properly formatted - // and we don't reset it we would be stuck in an infinite loop trying to parse it. - scope.DeleteLongRunningOperationState(resourceName, serviceName, futureType) - return nil, errors.Wrap(err, "could not decode future data, resetting long-running operation state") - } - - isDone, err := client.IsDone(ctx, poller) - // Assume that if isDone is true, then we successfully checked that the - // operation was complete even if err is non-nil. Assume the error in that - // case is unrelated and will be captured in Result below. - if !isDone { - if err != nil { - return nil, errors.Wrap(err, "failed checking if the operation was complete") - } - - // Operation is still in progress, update conditions and requeue. - log.V(2).Info("long-running operation is still ongoing", "service", serviceName, "resource", resourceName) - return nil, azure.WithTransientError(azure.NewOperationNotDoneError(future), getRequeueAfterFromPoller(poller)) - } - if err != nil { - log.V(2).Error(err, "error checking long-running operation status after it finished") - } - - // Once the operation is done, we can delete the long-running operation state. - // If the operation failed, this will allow it to be retried during the next reconciliation. - // If the resource is not found, we also reset the long-running operation state so we can attempt to create it again. - // This can happen if the resource was deleted by another process before we could get the result. - scope.DeleteLongRunningOperationState(resourceName, serviceName, futureType) - - // Resource has been created/deleted/updated. - log.V(2).Info("long-running operation has completed", "service", serviceName, "resource", resourceName) - return client.Result(ctx, &poller) -} - // CreateOrUpdateResource implements the logic for creating a new, or updating an existing, resource Asynchronously. func (s *Service[C, D]) CreateOrUpdateResource(ctx context.Context, spec azure.ResourceSpecGetter, serviceName string) (result interface{}, err error) { ctx, log, done := tele.StartSpanWithLogger(ctx, "asyncpoller.Service.CreateOrUpdateResource") @@ -107,9 +59,14 @@ func (s *Service[C, D]) CreateOrUpdateResource(ctx context.Context, spec azure.R futureType := infrav1.PutFuture // Check if there is an ongoing long-running operation. - future := s.Scope.GetLongRunningOperationState(resourceName, serviceName, futureType) - if future != nil { - return processOngoingOperation[C](ctx, s.Scope, s.Creator, resourceName, serviceName, futureType) + resumeToken := "" + if future := s.Scope.GetLongRunningOperationState(resourceName, serviceName, futureType); future != nil { + t, err := converters.FutureToResumeToken(*future) + if err != nil { + s.Scope.DeleteLongRunningOperationState(resourceName, serviceName, futureType) + return "", errors.Wrap(err, "could not decode future data, resetting long-running operation state") + } + resumeToken = t } // Get the resource if it already exists, and use it to construct the desired resource parameters. @@ -138,7 +95,7 @@ func (s *Service[C, D]) CreateOrUpdateResource(ctx context.Context, spec azure.R logMessageVerbPrefix = "updat" } log.V(2).Info(fmt.Sprintf("%sing resource", logMessageVerbPrefix), "service", serviceName, "resource", resourceName, "resourceGroup", rgName) - result, poller, err := s.Creator.CreateOrUpdateAsync(ctx, spec, parameters) + result, poller, err := s.Creator.CreateOrUpdateAsync(ctx, spec, resumeToken, parameters) errWrapped := errors.Wrapf(err, fmt.Sprintf("failed to %se resource %s/%s (service: %s)", logMessageVerbPrefix, rgName, resourceName, serviceName)) if poller != nil { future, err := converters.PollerToFuture(poller, infrav1.PutFuture, serviceName, resourceName, rgName) @@ -151,6 +108,9 @@ func (s *Service[C, D]) CreateOrUpdateResource(ctx context.Context, spec azure.R return nil, errWrapped } + // Once the operation is done, we can delete the long-running operation state. + s.Scope.DeleteLongRunningOperationState(resourceName, serviceName, futureType) + log.V(2).Info(fmt.Sprintf("successfully %sed resource", logMessageVerbPrefix), "service", serviceName, "resource", resourceName, "resourceGroup", rgName) return result, nil } @@ -165,15 +125,19 @@ func (s *Service[C, D]) DeleteResource(ctx context.Context, spec azure.ResourceS futureType := infrav1.DeleteFuture // Check if there is an ongoing long-running operation. - future := s.Scope.GetLongRunningOperationState(resourceName, serviceName, futureType) - if future != nil { - _, err := processOngoingOperation[D](ctx, s.Scope, s.Deleter, resourceName, serviceName, futureType) - return err + resumeToken := "" + if future := s.Scope.GetLongRunningOperationState(resourceName, serviceName, futureType); future != nil { + t, err := converters.FutureToResumeToken(*future) + if err != nil { + s.Scope.DeleteLongRunningOperationState(resourceName, serviceName, futureType) + return errors.Wrap(err, "could not decode future data, resetting long-running operation state") + } + resumeToken = t } - // No long-running operation is active, so delete the resource. + // Delete the resource. log.V(2).Info("deleting resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName) - poller, err := s.Deleter.DeleteAsync(ctx, spec) + poller, err := s.Deleter.DeleteAsync(ctx, spec, resumeToken) if poller != nil { future, err := converters.PollerToFuture(poller, infrav1.DeleteFuture, serviceName, resourceName, rgName) if err != nil { @@ -181,14 +145,13 @@ func (s *Service[C, D]) DeleteResource(ctx context.Context, spec azure.ResourceS } s.Scope.SetLongRunningOperationState(future) return azure.WithTransientError(azure.NewOperationNotDoneError(future), getRequeueAfterFromPoller(poller)) - } else if err != nil { - if azure.ResourceNotFound(err) { - // already deleted - return nil - } + } else if err != nil && !azure.ResourceNotFound(err) { return errors.Wrapf(err, "failed to delete resource %s/%s (service: %s)", rgName, resourceName, serviceName) } + // Once the operation is done, delete the long-running operation state. + s.Scope.DeleteLongRunningOperationState(resourceName, serviceName, futureType) + log.V(2).Info("successfully deleted resource", "service", serviceName, "resource", resourceName, "resourceGroup", rgName) return nil } diff --git a/azure/services/asyncpoller/interfaces.go b/azure/services/asyncpoller/interfaces.go index 40d9a69ea38..d1a7909c70b 100644 --- a/azure/services/asyncpoller/interfaces.go +++ b/azure/services/asyncpoller/interfaces.go @@ -51,13 +51,13 @@ type TagsGetter interface { type Creator[T any] interface { FutureHandler Getter - CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter, parameters interface{}) (result interface{}, poller *runtime.Poller[T], err error) + CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter, resumeToken string, parameters interface{}) (result interface{}, poller *runtime.Poller[T], err error) } // Deleter is a client that can delete a resource asynchronously. type Deleter[T any] interface { FutureHandler - DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter) (poller *runtime.Poller[T], err error) + DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter, resumeToken string) (poller *runtime.Poller[T], err error) } // Reconciler is a generic interface used to perform asynchronous reconciliation of Azure resources. From 44fbf1567fc14da6737f63c0e0a3dd9c98a67877 Mon Sep 17 00:00:00 2001 From: Matt Boersma Date: Fri, 7 Apr 2023 11:13:53 -0600 Subject: [PATCH 5/7] Update natgateways for API changes --- azure/services/natgateways/client.go | 31 +++++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/azure/services/natgateways/client.go b/azure/services/natgateways/client.go index 554d5d82710..c32ea1d4bad 100644 --- a/azure/services/natgateways/client.go +++ b/azure/services/natgateways/client.go @@ -61,22 +61,31 @@ func (ac *azureClient) Get(ctx context.Context, spec azure.ResourceSpecGetter) ( ctx, _, done := tele.StartSpanWithLogger(ctx, "natgateways.azureClient.Get") defer done() - return ac.natgateways.Get(ctx, spec.ResourceGroupName(), spec.ResourceName(), &armnetwork.NatGatewaysClientGetOptions{}) + resp, err := ac.natgateways.Get(ctx, spec.ResourceGroupName(), spec.ResourceName(), &armnetwork.NatGatewaysClientGetOptions{}) + if err != nil { + return nil, err + } + return resp.NatGateway, nil } // CreateOrUpdateAsync creates or updates a Nat Gateway asynchronously. // It sends a PUT request to Azure and if accepted without error, the func will return a Future which can be used to track the ongoing // progress of the operation. -func (ac *azureClient) CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter, parameters interface{}) (result interface{}, poller *runtime.Poller[armnetwork.NatGatewaysClientCreateOrUpdateResponse], err error) { - ctx, _, done := tele.StartSpanWithLogger(ctx, "natgateways.azureClient.CreateOrUpdateAsync") +func (ac *azureClient) CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter, resumeToken string, parameters interface{}) (result interface{}, poller *runtime.Poller[armnetwork.NatGatewaysClientCreateOrUpdateResponse], err error) { + ctx, log, done := tele.StartSpanWithLogger(ctx, "natgateways.azureClient.CreateOrUpdateAsync") defer done() - natGateway, ok := parameters.(armnetwork.NatGateway) - if !ok { - return nil, nil, errors.Errorf("%T is not a armnetwork.NatGateway", parameters) + var natGateway armnetwork.NatGateway + if parameters != nil { + ngw, ok := parameters.(armnetwork.NatGateway) + if !ok { + return nil, nil, errors.Errorf("%T is not an armnetwork.NatGateway", parameters) + } + natGateway = ngw } - opts := &armnetwork.NatGatewaysClientBeginCreateOrUpdateOptions{} + opts := &armnetwork.NatGatewaysClientBeginCreateOrUpdateOptions{ResumeToken: resumeToken} + log.Info("CreateOrUpdateAsync: sending request", "resumeToken", resumeToken) poller, err = ac.natgateways.BeginCreateOrUpdate(ctx, spec.ResourceGroupName(), spec.ResourceName(), natGateway, opts) if err != nil { return nil, nil, err @@ -99,11 +108,13 @@ func (ac *azureClient) CreateOrUpdateAsync(ctx context.Context, spec azure.Resou // DeleteAsync deletes a Nat Gateway asynchronously. DeleteAsync sends a DELETE // request to Azure and if accepted without error, the func will return a Future which can be used to track the ongoing // progress of the operation. -func (ac *azureClient) DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter) (poller *runtime.Poller[armnetwork.NatGatewaysClientDeleteResponse], err error) { - ctx, _, done := tele.StartSpanWithLogger(ctx, "natgateways.azureClient.DeleteAsync") +func (ac *azureClient) DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter, resumeToken string) (poller *runtime.Poller[armnetwork.NatGatewaysClientDeleteResponse], err error) { + ctx, log, done := tele.StartSpanWithLogger(ctx, "natgateways.azureClient.DeleteAsync") defer done() - poller, err = ac.natgateways.BeginDelete(ctx, spec.ResourceGroupName(), spec.ResourceName(), &armnetwork.NatGatewaysClientBeginDeleteOptions{}) + opts := &armnetwork.NatGatewaysClientBeginDeleteOptions{ResumeToken: resumeToken} + log.Info("DeleteAsync: sending request", "resumeToken", resumeToken) + poller, err = ac.natgateways.BeginDelete(ctx, spec.ResourceGroupName(), spec.ResourceName(), opts) if err != nil { return nil, err } From ae0e7009240737e09762ebe9007ad9fac7cd3895 Mon Sep 17 00:00:00 2001 From: Matt Boersma Date: Fri, 7 Apr 2023 13:02:53 -0600 Subject: [PATCH 6/7] Fix error typing in asyncpoller --- azure/services/asyncpoller/asyncpoller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure/services/asyncpoller/asyncpoller.go b/azure/services/asyncpoller/asyncpoller.go index 616b1a0a14a..b8e74be41b9 100644 --- a/azure/services/asyncpoller/asyncpoller.go +++ b/azure/services/asyncpoller/asyncpoller.go @@ -169,7 +169,7 @@ func getRetryAfterFromError(err error) time.Duration { // TODO: need to refactor autorest out of this codebase entirely. // In case we aren't able to introspect Retry-After from the error type, we'll return this default ret := reconciler.DefaultReconcilerRequeue - var responseError azcore.ResponseError + var responseError *azcore.ResponseError // if we have a strongly typed azcore.ResponseError then we can introspect the HTTP response data if errors.As(err, &responseError) && responseError.RawResponse != nil { // If we have Retry-After HTTP header data for any reason, prefer it From fb61432ff57bc0d766da4f45e497d5fd5373aaea Mon Sep 17 00:00:00 2001 From: Matt Boersma Date: Fri, 7 Apr 2023 13:34:43 -0600 Subject: [PATCH 7/7] Convert vnetpeerings service to asyncpoller framework --- azure/services/natgateways/client.go | 2 +- azure/services/vnetpeerings/client.go | 135 +++++++++++--------- azure/services/vnetpeerings/spec.go | 16 +-- azure/services/vnetpeerings/vnetpeerings.go | 10 +- 4 files changed, 89 insertions(+), 74 deletions(-) diff --git a/azure/services/natgateways/client.go b/azure/services/natgateways/client.go index c32ea1d4bad..536aa62c356 100644 --- a/azure/services/natgateways/client.go +++ b/azure/services/natgateways/client.go @@ -51,7 +51,7 @@ func newNatGatewaysClient(subscriptionID string) (armnetwork.NatGatewaysClient, } client, err := armnetwork.NewNatGatewaysClient(subscriptionID, credential, &arm.ClientOptions{}) if err != nil { - return armnetwork.NatGatewaysClient{}, errors.Wrap(err, "cannot create new Resource SKUs client") + return armnetwork.NatGatewaysClient{}, errors.Wrap(err, "cannot create new NAT gateways client") } return *client, nil } diff --git a/azure/services/vnetpeerings/client.go b/azure/services/vnetpeerings/client.go index 470eb10d2f7..6b1ab677d74 100644 --- a/azure/services/vnetpeerings/client.go +++ b/azure/services/vnetpeerings/client.go @@ -18,13 +18,12 @@ package vnetpeerings import ( "context" - "encoding/json" - "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-08-01/network" - "github.com/Azure/go-autorest/autorest" - azureautorest "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork" "github.com/pkg/errors" - infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" "sigs.k8s.io/cluster-api-provider-azure/azure" "sigs.k8s.io/cluster-api-provider-azure/util/reconciler" "sigs.k8s.io/cluster-api-provider-azure/util/tele" @@ -32,43 +31,63 @@ import ( // AzureClient contains the Azure go-sdk Client. type AzureClient struct { - peerings network.VirtualNetworkPeeringsClient + peerings armnetwork.VirtualNetworkPeeringsClient } -// NewClient creates a new virtual network peerings client from subscription ID. +// NewClient creates an AzureClient from an Authorizer. func NewClient(auth azure.Authorizer) *AzureClient { - c := newPeeringsClient(auth.SubscriptionID(), auth.BaseURI(), auth.Authorizer()) + c, err := newPeeringsClient(auth.SubscriptionID()) + if err != nil { + panic(err) // TODO: Handle this properly! + } return &AzureClient{c} } // newPeeringsClient creates a new virtual network peerings client from subscription ID. -func newPeeringsClient(subscriptionID string, baseURI string, authorizer autorest.Authorizer) network.VirtualNetworkPeeringsClient { - peeringsClient := network.NewVirtualNetworkPeeringsClientWithBaseURI(baseURI, subscriptionID) - azure.SetAutoRestClientDefaults(&peeringsClient.Client, authorizer) - return peeringsClient +func newPeeringsClient(subscriptionID string) (armnetwork.VirtualNetworkPeeringsClient, error) { + credential, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return armnetwork.VirtualNetworkPeeringsClient{}, errors.Wrap(err, "failed to create credential") + } + client, err := armnetwork.NewVirtualNetworkPeeringsClient(subscriptionID, credential, &arm.ClientOptions{}) + if err != nil { + return armnetwork.VirtualNetworkPeeringsClient{}, errors.Wrap(err, "cannot create new virtual network peerings client") + } + return *client, nil } -// Get gets the specified virtual network peering by the peering name, virtual network, and resource group. +// Get returns a virtual network peering by the specified resource group, virtual network, and peering name. func (ac *AzureClient) Get(ctx context.Context, spec azure.ResourceSpecGetter) (result interface{}, err error) { ctx, _, done := tele.StartSpanWithLogger(ctx, "vnetpeerings.AzureClient.Get") defer done() - return ac.peerings.Get(ctx, spec.ResourceGroupName(), spec.OwnerResourceName(), spec.ResourceName()) + opts := &armnetwork.VirtualNetworkPeeringsClientGetOptions{} + resp, err := ac.peerings.Get(ctx, spec.ResourceGroupName(), spec.OwnerResourceName(), spec.ResourceName(), opts) + if err != nil { + return nil, err + } + return resp.VirtualNetworkPeering, nil } // CreateOrUpdateAsync creates or updates a virtual network peering asynchronously. // It sends a PUT request to Azure and if accepted without error, the func will return a Future which can be used to track the ongoing // progress of the operation. -func (ac *AzureClient) CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter, parameters interface{}) (result interface{}, future azureautorest.FutureAPI, err error) { - ctx, _, done := tele.StartSpanWithLogger(ctx, "vnetpeerings.AzureClient.CreateOrUpdateAsync") +func (ac *AzureClient) CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter, resumeToken string, parameters interface{}) (result interface{}, poller *runtime.Poller[armnetwork.VirtualNetworkPeeringsClientCreateOrUpdateResponse], err error) { + ctx, log, done := tele.StartSpanWithLogger(ctx, "vnetpeerings.AzureClient.CreateOrUpdateAsync") defer done() - peering, ok := parameters.(network.VirtualNetworkPeering) - if !ok { - return nil, nil, errors.Errorf("%T is not a network.VirtualNetworkPeering", parameters) + var peering armnetwork.VirtualNetworkPeering + if parameters != nil { + p, ok := parameters.(armnetwork.VirtualNetworkPeering) + if !ok { + return nil, nil, errors.Errorf("%T is not an armnetwork.VirtualNetworkPeering", parameters) + } + peering = p } - createFuture, err := ac.peerings.CreateOrUpdate(ctx, spec.ResourceGroupName(), spec.OwnerResourceName(), spec.ResourceName(), peering, network.SyncRemoteAddressSpaceTrue) + opts := &armnetwork.VirtualNetworkPeeringsClientBeginCreateOrUpdateOptions{ResumeToken: resumeToken} + log.Info("CreateOrUpdateAsync: sending request", "resumeToken", resumeToken) + poller, err = ac.peerings.BeginCreateOrUpdate(ctx, spec.ResourceGroupName(), spec.OwnerResourceName(), spec.ResourceName(), peering, opts) if err != nil { return nil, nil, err } @@ -76,26 +95,27 @@ func (ac *AzureClient) CreateOrUpdateAsync(ctx context.Context, spec azure.Resou ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureCallTimeout) defer cancel() - err = createFuture.WaitForCompletionRef(ctx, ac.peerings.Client) + result, err = poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{}) if err != nil { - // if an error occurs, return the future. + // if an error occurs, return the poller. // this means the long-running operation didn't finish in the specified timeout. - return nil, &createFuture, err + return nil, poller, err } - result, err = createFuture.Result(ac.peerings) - // if the operation completed, return a nil future + // if the operation completed, return a nil poller return result, nil, err } // DeleteAsync deletes a virtual network peering asynchronously. DeleteAsync sends a DELETE // request to Azure and if accepted without error, the func will return a Future which can be used to track the ongoing // progress of the operation. -func (ac *AzureClient) DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter) (future azureautorest.FutureAPI, err error) { - ctx, _, done := tele.StartSpanWithLogger(ctx, "vnetpeerings.AzureClient.Delete") +func (ac *AzureClient) DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter, resumeToken string) (poller *runtime.Poller[armnetwork.VirtualNetworkPeeringsClientDeleteResponse], err error) { + ctx, log, done := tele.StartSpanWithLogger(ctx, "vnetpeerings.AzureClient.Delete") defer done() - deleteFuture, err := ac.peerings.Delete(ctx, spec.ResourceGroupName(), spec.OwnerResourceName(), spec.ResourceName()) + opts := &armnetwork.VirtualNetworkPeeringsClientBeginDeleteOptions{ResumeToken: resumeToken} + log.Info("DeleteAsync: sending request", "resumeToken", resumeToken) + poller, err = ac.peerings.BeginDelete(ctx, spec.ResourceGroupName(), spec.OwnerResourceName(), spec.ResourceName(), opts) if err != nil { return nil, err } @@ -103,54 +123,47 @@ func (ac *AzureClient) DeleteAsync(ctx context.Context, spec azure.ResourceSpecG ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureCallTimeout) defer cancel() - err = deleteFuture.WaitForCompletionRef(ctx, ac.peerings.Client) + _, err = poller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{}) if err != nil { - // if an error occurs, return the future. + // if an error occurs, return the poller. // this means the long-running operation didn't finish in the specified timeout. - return &deleteFuture, err + return poller, err } - _, err = deleteFuture.Result(ac.peerings) - // if the operation completed, return a nil future. + + // if the operation completed, return a nil poller. return nil, err } // IsDone returns true if the long-running operation has completed. -func (ac *AzureClient) IsDone(ctx context.Context, future azureautorest.FutureAPI) (isDone bool, err error) { - ctx, _, done := tele.StartSpanWithLogger(ctx, "vnetpeerings.AzureClient.IsDone") +func (ac *AzureClient) IsDone(ctx context.Context, poller interface{}) (isDone bool, err error) { + _, _, done := tele.StartSpanWithLogger(ctx, "vnetpeerings.AzureClient.IsDone") defer done() - return future.DoneWithContext(ctx, ac.peerings) + switch t := poller.(type) { + case *runtime.Poller[armnetwork.VirtualNetworkPeeringsClientCreateOrUpdateResponse]: + c, _ := poller.(*runtime.Poller[armnetwork.VirtualNetworkPeeringsClientCreateOrUpdateResponse]) + return c.Done(), nil + case *runtime.Poller[armnetwork.VirtualNetworkPeeringsClientDeleteResponse]: + d, _ := poller.(*runtime.Poller[armnetwork.VirtualNetworkPeeringsClientDeleteResponse]) + return d.Done(), nil + default: + return false, errors.Errorf("unexpected poller type %T", t) + } } // Result fetches the result of a long-running operation future. -func (ac *AzureClient) Result(ctx context.Context, future azureautorest.FutureAPI, futureType string) (result interface{}, err error) { +func (ac *AzureClient) Result(ctx context.Context, poller interface{}) (result interface{}, err error) { _, _, done := tele.StartSpanWithLogger(ctx, "vnetpeerings.AzureClient.Result") defer done() - if future == nil { - return nil, errors.Errorf("cannot get result from nil future") - } - - switch futureType { - case infrav1.PutFuture: - // Marshal and Unmarshal the future to put it into the correct future type so we can access the Result function. - // Unfortunately the FutureAPI can't be casted directly to VirtualNetworkPeeringsCreateOrUpdateFuture because it is a azureautorest.Future, which doesn't implement the Result function. See PR #1686 for discussion on alternatives. - // It was converted back to a generic azureautorest.Future from the CAPZ infrav1.Future type stored in Status: https://github.com/kubernetes-sigs/cluster-api-provider-azure/blob/main/azure/converters/futures.go#L49. - var createFuture *network.VirtualNetworkPeeringsCreateOrUpdateFuture - jsonData, err := future.MarshalJSON() - if err != nil { - return nil, errors.Wrap(err, "failed to marshal future") - } - if err := json.Unmarshal(jsonData, &createFuture); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal future data") - } - return createFuture.Result(ac.peerings) - - case infrav1.DeleteFuture: - // Delete does not return a result virtual network peering - return nil, nil - + switch t := poller.(type) { + case *runtime.Poller[armnetwork.VirtualNetworkPeeringsClientCreateOrUpdateResponse]: + c, _ := poller.(*runtime.Poller[armnetwork.VirtualNetworkPeeringsClientCreateOrUpdateResponse]) + return c.Result(ctx) + case *runtime.Poller[armnetwork.VirtualNetworkPeeringsClientDeleteResponse]: + d, _ := poller.(*runtime.Poller[armnetwork.VirtualNetworkPeeringsClientDeleteResponse]) + return d.Result(ctx) default: - return nil, errors.Errorf("unknown future type %q", futureType) + return false, errors.Errorf("unexpected poller type %T", t) } } diff --git a/azure/services/vnetpeerings/spec.go b/azure/services/vnetpeerings/spec.go index a7ee3ff9f42..af15281b438 100644 --- a/azure/services/vnetpeerings/spec.go +++ b/azure/services/vnetpeerings/spec.go @@ -19,7 +19,7 @@ package vnetpeerings import ( "context" - "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-08-01/network" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork" "github.com/pkg/errors" "k8s.io/utils/pointer" "sigs.k8s.io/cluster-api-provider-azure/azure" @@ -57,15 +57,15 @@ func (s *VnetPeeringSpec) OwnerResourceName() string { // Parameters returns the parameters for the virtual network peering. func (s *VnetPeeringSpec) Parameters(ctx context.Context, existing interface{}) (params interface{}, err error) { if existing != nil { - if _, ok := existing.(network.VirtualNetworkPeering); !ok { - return nil, errors.Errorf("%T is not a network.VnetPeering", existing) + if _, ok := existing.(armnetwork.VirtualNetworkPeering); !ok { + return nil, errors.Errorf("%T is not an armnetwork.VnetPeering", existing) } // virtual network peering already exists return nil, nil } vnetID := azure.VNetID(s.SubscriptionID, s.RemoteResourceGroup, s.RemoteVnetName) - peeringProperties := network.VirtualNetworkPeeringPropertiesFormat{ - RemoteVirtualNetwork: &network.SubResource{ + peeringProperties := armnetwork.VirtualNetworkPeeringPropertiesFormat{ + RemoteVirtualNetwork: &armnetwork.SubResource{ ID: pointer.String(vnetID), }, AllowForwardedTraffic: s.AllowForwardedTraffic, @@ -73,8 +73,8 @@ func (s *VnetPeeringSpec) Parameters(ctx context.Context, existing interface{}) AllowVirtualNetworkAccess: s.AllowVirtualNetworkAccess, UseRemoteGateways: s.UseRemoteGateways, } - return network.VirtualNetworkPeering{ - Name: pointer.String(s.PeeringName), - VirtualNetworkPeeringPropertiesFormat: &peeringProperties, + return armnetwork.VirtualNetworkPeering{ + Name: pointer.String(s.PeeringName), + Properties: &peeringProperties, }, nil } diff --git a/azure/services/vnetpeerings/vnetpeerings.go b/azure/services/vnetpeerings/vnetpeerings.go index fcd2bfa8b6d..4e8b9bd0450 100644 --- a/azure/services/vnetpeerings/vnetpeerings.go +++ b/azure/services/vnetpeerings/vnetpeerings.go @@ -19,9 +19,10 @@ package vnetpeerings import ( "context" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" "sigs.k8s.io/cluster-api-provider-azure/azure" - "sigs.k8s.io/cluster-api-provider-azure/azure/services/async" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/asyncpoller" "sigs.k8s.io/cluster-api-provider-azure/util/reconciler" "sigs.k8s.io/cluster-api-provider-azure/util/tele" ) @@ -39,15 +40,16 @@ type VnetPeeringScope interface { // Service provides operations on Azure resources. type Service struct { Scope VnetPeeringScope - async.Reconciler + asyncpoller.Reconciler } // New creates a new service. func New(scope VnetPeeringScope) *Service { Client := NewClient(scope) return &Service{ - Scope: scope, - Reconciler: async.New(scope, Client, Client), + Scope: scope, + Reconciler: asyncpoller.New[armnetwork.VirtualNetworkPeeringsClientCreateOrUpdateResponse, + armnetwork.VirtualNetworkPeeringsClientDeleteResponse](scope, Client, Client), } }