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

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package async
    18  
    19  import (
    20  	"context"
    21  	"encoding/base64"
    22  	"errors"
    23  	"io"
    24  	"net/http"
    25  	"net/url"
    26  	"reflect"
    27  	"strings"
    28  	"testing"
    29  
    30  	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
    31  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
    32  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
    33  	. "github.com/onsi/gomega"
    34  	"go.uber.org/mock/gomock"
    35  	infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
    36  	"sigs.k8s.io/cluster-api-provider-azure/azure"
    37  	"sigs.k8s.io/cluster-api-provider-azure/azure/mock_azure"
    38  	"sigs.k8s.io/cluster-api-provider-azure/azure/services/async/mock_async"
    39  	gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock"
    40  	"sigs.k8s.io/cluster-api-provider-azure/util/reconciler"
    41  )
    42  
    43  func TestServiceCreateOrUpdateResource(t *testing.T) {
    44  	testcases := []struct {
    45  		name           string
    46  		serviceName    string
    47  		expectedError  string
    48  		expectedResult interface{}
    49  		expect         func(g *WithT, s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder[MockCreator], r *mock_azure.MockResourceSpecGetterMockRecorder)
    50  	}{
    51  		{
    52  			name:          "invalid future",
    53  			serviceName:   serviceName,
    54  			expectedError: "could not decode future data, resetting long-running operation state",
    55  			expect: func(g *WithT, s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder[MockCreator], r *mock_azure.MockResourceSpecGetterMockRecorder) {
    56  				gomock.InOrder(
    57  					r.ResourceName().Return(resourceName),
    58  					r.ResourceGroupName().Return(resourceGroupName),
    59  					s.GetLongRunningOperationState(resourceName, serviceName, infrav1.PutFuture).Return(invalidPutFuture),
    60  					s.DeleteLongRunningOperationState(resourceName, serviceName, infrav1.PutFuture),
    61  				)
    62  			},
    63  		},
    64  		{
    65  			name:           "operation completed",
    66  			serviceName:    serviceName,
    67  			expectedError:  "",
    68  			expectedResult: fakeResource,
    69  			expect: func(g *WithT, s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder[MockCreator], r *mock_azure.MockResourceSpecGetterMockRecorder) {
    70  				gomock.InOrder(
    71  					r.ResourceName().Return(resourceName),
    72  					r.ResourceGroupName().Return(resourceGroupName),
    73  					s.GetLongRunningOperationState(resourceName, serviceName, infrav1.PutFuture).Return(validPutFuture),
    74  					c.CreateOrUpdateAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(azureResourceGetterType), resumeToken, gomock.Any()).Return(fakeResource, nil, nil),
    75  					s.DeleteLongRunningOperationState(resourceName, serviceName, infrav1.PutFuture),
    76  				)
    77  			},
    78  		},
    79  		{
    80  			name:          "operation in progress",
    81  			serviceName:   serviceName,
    82  			expectedError: "operation type PUT on Azure resource mock-resourcegroup/mock-resource is not done. Object will be requeued after 15s",
    83  			expect: func(g *WithT, s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder[MockCreator], r *mock_azure.MockResourceSpecGetterMockRecorder) {
    84  				gomock.InOrder(
    85  					r.ResourceName().Return(resourceName),
    86  					r.ResourceGroupName().Return(resourceGroupName),
    87  					s.GetLongRunningOperationState(resourceName, serviceName, infrav1.PutFuture).Return(validPutFuture),
    88  					c.CreateOrUpdateAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(azureResourceGetterType), resumeToken, gomock.Any()).Return(nil, fakePoller[MockCreator](g, http.StatusAccepted), context.DeadlineExceeded),
    89  					s.SetLongRunningOperationState(gomock.AssignableToTypeOf(&infrav1.Future{})),
    90  					s.DefaultedReconcilerRequeue().Return(reconciler.DefaultReconcilerRequeue),
    91  				)
    92  			},
    93  		},
    94  		{
    95  			name:          "operation failed",
    96  			serviceName:   serviceName,
    97  			expectedError: "failed to create or update resource mock-resourcegroup/mock-resource (service: mock-service): foo",
    98  			expect: func(g *WithT, s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder[MockCreator], r *mock_azure.MockResourceSpecGetterMockRecorder) {
    99  				gomock.InOrder(
   100  					r.ResourceName().Return(resourceName),
   101  					r.ResourceGroupName().Return(resourceGroupName),
   102  					s.GetLongRunningOperationState(resourceName, serviceName, infrav1.PutFuture).Return(validPutFuture),
   103  					c.CreateOrUpdateAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(azureResourceGetterType), resumeToken, gomock.Any()).Return(nil, fakePoller[MockCreator](g, http.StatusAccepted), errors.New("foo")),
   104  					s.DeleteLongRunningOperationState(resourceName, serviceName, infrav1.PutFuture),
   105  				)
   106  			},
   107  		},
   108  		{
   109  			name:          "get returns resource not found error",
   110  			serviceName:   serviceName,
   111  			expectedError: "operation type PUT on Azure resource mock-resourcegroup/mock-resource is not done. Object will be requeued after 15s",
   112  			expect: func(g *WithT, s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder[MockCreator], r *mock_azure.MockResourceSpecGetterMockRecorder) {
   113  				gomock.InOrder(
   114  					r.ResourceName().Return(resourceName),
   115  					r.ResourceGroupName().Return(resourceGroupName),
   116  					s.GetLongRunningOperationState(resourceName, serviceName, infrav1.PutFuture).Return(nil),
   117  					c.Get(gomockinternal.AContext(), gomock.AssignableToTypeOf(azureResourceGetterType)).Return(nil, &azcore.ResponseError{StatusCode: http.StatusNotFound}),
   118  					r.Parameters(gomockinternal.AContext(), nil).Return(fakeParameters, nil),
   119  					c.CreateOrUpdateAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(azureResourceGetterType), "", gomock.Any()).Return(nil, fakePoller[MockCreator](g, http.StatusAccepted), context.DeadlineExceeded),
   120  					s.SetLongRunningOperationState(gomock.AssignableToTypeOf(&infrav1.Future{})),
   121  					s.DefaultedReconcilerRequeue().Return(reconciler.DefaultReconcilerRequeue),
   122  				)
   123  			},
   124  		},
   125  		{
   126  			name:          "get returns unexpected error",
   127  			serviceName:   serviceName,
   128  			expectedError: "failed to get existing resource mock-resourcegroup/mock-resource (service: mock-service): foo. Object will be requeued after 15s",
   129  			expect: func(g *WithT, s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder[MockCreator], r *mock_azure.MockResourceSpecGetterMockRecorder) {
   130  				gomock.InOrder(
   131  					r.ResourceName().Return(resourceName),
   132  					r.ResourceGroupName().Return(resourceGroupName),
   133  					s.GetLongRunningOperationState(resourceName, serviceName, infrav1.PutFuture).Return(nil),
   134  					c.Get(gomockinternal.AContext(), gomock.AssignableToTypeOf(azureResourceGetterType)).Return(nil, errors.New("foo")),
   135  				)
   136  			},
   137  		},
   138  		{
   139  			name:           "parameters are nil: up to date",
   140  			serviceName:    serviceName,
   141  			expectedError:  "",
   142  			expectedResult: fakeResource,
   143  			expect: func(g *WithT, s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder[MockCreator], r *mock_azure.MockResourceSpecGetterMockRecorder) {
   144  				gomock.InOrder(
   145  					r.ResourceName().Return(resourceName),
   146  					r.ResourceGroupName().Return(resourceGroupName),
   147  					s.GetLongRunningOperationState(resourceName, serviceName, infrav1.PutFuture).Return(nil),
   148  					c.Get(gomockinternal.AContext(), gomock.AssignableToTypeOf(azureResourceGetterType)).Return(fakeResource, nil),
   149  					r.Parameters(gomockinternal.AContext(), fakeResource).Return(nil, nil),
   150  				)
   151  			},
   152  		},
   153  		{
   154  			name:           "parameters returns error",
   155  			serviceName:    serviceName,
   156  			expectedError:  "failed to get desired parameters for resource mock-resourcegroup/mock-resource (service: mock-service): foo",
   157  			expectedResult: nil,
   158  			expect: func(g *WithT, s *mock_async.MockFutureScopeMockRecorder, c *mock_async.MockCreatorMockRecorder[MockCreator], r *mock_azure.MockResourceSpecGetterMockRecorder) {
   159  				gomock.InOrder(
   160  					r.ResourceName().Return(resourceName),
   161  					r.ResourceGroupName().Return(resourceGroupName),
   162  					s.GetLongRunningOperationState(resourceName, serviceName, infrav1.PutFuture).Return(nil),
   163  					c.Get(gomockinternal.AContext(), gomock.AssignableToTypeOf(azureResourceGetterType)).Return(fakeResource, nil),
   164  					r.Parameters(gomockinternal.AContext(), fakeResource).Return(nil, errors.New("foo")),
   165  				)
   166  			},
   167  		},
   168  	}
   169  
   170  	for _, tc := range testcases {
   171  		tc := tc
   172  		t.Run(tc.name, func(t *testing.T) {
   173  			g := NewWithT(t)
   174  
   175  			t.Parallel()
   176  			mockCtrl := gomock.NewController(t)
   177  			defer mockCtrl.Finish()
   178  			scopeMock := mock_async.NewMockFutureScope(mockCtrl)
   179  			creatorMock := mock_async.NewMockCreator[MockCreator](mockCtrl)
   180  			svc := New[MockCreator, MockDeleter](scopeMock, creatorMock, nil)
   181  			specMock := mock_azure.NewMockResourceSpecGetter(mockCtrl)
   182  
   183  			tc.expect(g, scopeMock.EXPECT(), creatorMock.EXPECT(), specMock.EXPECT())
   184  
   185  			result, err := svc.CreateOrUpdateResource(context.TODO(), specMock, serviceName)
   186  			if tc.expectedError != "" {
   187  				g.Expect(err).To(HaveOccurred())
   188  				g.Expect(err.Error()).To(ContainSubstring(tc.expectedError))
   189  			} else {
   190  				g.Expect(err).NotTo(HaveOccurred())
   191  				if tc.expectedResult != nil {
   192  					g.Expect(result).To(Equal(tc.expectedResult))
   193  				} else {
   194  					g.Expect(result).To(BeNil())
   195  				}
   196  			}
   197  		})
   198  	}
   199  }
   200  
   201  func TestServiceDeleteResource(t *testing.T) {
   202  	testcases := []struct {
   203  		name           string
   204  		serviceName    string
   205  		expectedError  string
   206  		expectedResult interface{}
   207  		expect         func(g *GomegaWithT, s *mock_async.MockFutureScopeMockRecorder, d *mock_async.MockDeleterMockRecorder[MockDeleter], r *mock_azure.MockResourceSpecGetterMockRecorder)
   208  	}{
   209  		{
   210  			name:          "invalid future",
   211  			serviceName:   serviceName,
   212  			expectedError: "could not decode future data",
   213  			expect: func(_ *GomegaWithT, s *mock_async.MockFutureScopeMockRecorder, _ *mock_async.MockDeleterMockRecorder[MockDeleter], r *mock_azure.MockResourceSpecGetterMockRecorder) {
   214  				gomock.InOrder(
   215  					r.ResourceName().Return(resourceName),
   216  					r.ResourceGroupName().Return(resourceGroupName),
   217  					s.GetLongRunningOperationState(resourceName, serviceName, infrav1.DeleteFuture).Return(invalidDeleteFuture),
   218  					s.DeleteLongRunningOperationState(resourceName, serviceName, infrav1.DeleteFuture),
   219  				)
   220  			},
   221  		},
   222  		{
   223  			name:          "operation in progress",
   224  			serviceName:   serviceName,
   225  			expectedError: "operation type DELETE on Azure resource mock-resourcegroup/mock-resource is not done. Object will be requeued after 15s",
   226  			expect: func(g *GomegaWithT, s *mock_async.MockFutureScopeMockRecorder, d *mock_async.MockDeleterMockRecorder[MockDeleter], r *mock_azure.MockResourceSpecGetterMockRecorder) {
   227  				gomock.InOrder(
   228  					r.ResourceName().Return(resourceName),
   229  					r.ResourceGroupName().Return(resourceGroupName),
   230  					s.GetLongRunningOperationState(resourceName, serviceName, infrav1.DeleteFuture).Return(validDeleteFuture),
   231  					d.DeleteAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(azureResourceGetterType), gomock.Any()).Return(fakePoller[MockDeleter](g, http.StatusAccepted), context.DeadlineExceeded),
   232  					s.SetLongRunningOperationState(gomock.AssignableToTypeOf(&infrav1.Future{})),
   233  					s.DefaultedReconcilerRequeue().Return(reconciler.DefaultReconcilerRequeue),
   234  				)
   235  			},
   236  		},
   237  		{
   238  			name:          "operation succeeds",
   239  			serviceName:   serviceName,
   240  			expectedError: "",
   241  			expect: func(_ *GomegaWithT, s *mock_async.MockFutureScopeMockRecorder, d *mock_async.MockDeleterMockRecorder[MockDeleter], r *mock_azure.MockResourceSpecGetterMockRecorder) {
   242  				gomock.InOrder(
   243  					r.ResourceName().Return(resourceName),
   244  					r.ResourceGroupName().Return(resourceGroupName),
   245  					s.GetLongRunningOperationState(resourceName, serviceName, infrav1.DeleteFuture).Return(validDeleteFuture),
   246  					d.DeleteAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(azureResourceGetterType), gomock.Any()).Return(nil, nil),
   247  					s.DeleteLongRunningOperationState(resourceName, serviceName, infrav1.DeleteFuture),
   248  				)
   249  			},
   250  		},
   251  		{
   252  			name:          "operation fails",
   253  			serviceName:   serviceName,
   254  			expectedError: "failed to delete resource mock-resourcegroup/mock-resource (service: mock-service): foo",
   255  			expect: func(g *GomegaWithT, s *mock_async.MockFutureScopeMockRecorder, d *mock_async.MockDeleterMockRecorder[MockDeleter], r *mock_azure.MockResourceSpecGetterMockRecorder) {
   256  				gomock.InOrder(
   257  					r.ResourceName().Return(resourceName),
   258  					r.ResourceGroupName().Return(resourceGroupName),
   259  					s.GetLongRunningOperationState(resourceName, serviceName, infrav1.DeleteFuture).Return(validDeleteFuture),
   260  					d.DeleteAsync(gomockinternal.AContext(), gomock.AssignableToTypeOf(azureResourceGetterType), gomock.Any()).Return(fakePoller[MockDeleter](g, http.StatusAccepted), errors.New("foo")),
   261  					s.DeleteLongRunningOperationState(resourceName, serviceName, infrav1.DeleteFuture),
   262  				)
   263  			},
   264  		},
   265  	}
   266  
   267  	for _, tc := range testcases {
   268  		tc := tc
   269  		t.Run(tc.name, func(t *testing.T) {
   270  			g := NewWithT(t)
   271  
   272  			t.Parallel()
   273  			mockCtrl := gomock.NewController(t)
   274  			defer mockCtrl.Finish()
   275  			scopeMock := mock_async.NewMockFutureScope(mockCtrl)
   276  			deleterMock := mock_async.NewMockDeleter[MockDeleter](mockCtrl)
   277  			svc := New[MockCreator, MockDeleter](scopeMock, nil, deleterMock)
   278  			specMock := mock_azure.NewMockResourceSpecGetter(mockCtrl)
   279  
   280  			tc.expect(g, scopeMock.EXPECT(), deleterMock.EXPECT(), specMock.EXPECT())
   281  
   282  			err := svc.DeleteResource(context.TODO(), specMock, tc.serviceName)
   283  			if tc.expectedError != "" {
   284  				g.Expect(err).To(HaveOccurred())
   285  				g.Expect(err.Error()).To(ContainSubstring(tc.expectedError))
   286  			} else {
   287  				g.Expect(err).NotTo(HaveOccurred())
   288  			}
   289  		})
   290  	}
   291  }
   292  
   293  const (
   294  	resourceGroupName  = "mock-resourcegroup"
   295  	resourceName       = "mock-resource"
   296  	serviceName        = "mock-service"
   297  	resumeToken        = "mock-resume-token"
   298  	invalidResumeToken = "!invalid-resume-token"
   299  )
   300  
   301  var (
   302  	validPutFuture = &infrav1.Future{
   303  		Type:          infrav1.PutFuture,
   304  		ServiceName:   serviceName,
   305  		Name:          resourceName,
   306  		ResourceGroup: resourceGroupName,
   307  		Data:          base64.URLEncoding.EncodeToString([]byte(resumeToken)),
   308  	}
   309  	invalidPutFuture = &infrav1.Future{
   310  		Type:          infrav1.PutFuture,
   311  		ServiceName:   serviceName,
   312  		Name:          resourceName,
   313  		ResourceGroup: resourceGroupName,
   314  		Data:          invalidResumeToken,
   315  	}
   316  	validDeleteFuture = &infrav1.Future{
   317  		Type:          infrav1.DeleteFuture,
   318  		ServiceName:   serviceName,
   319  		Name:          resourceName,
   320  		ResourceGroup: resourceGroupName,
   321  		Data:          base64.URLEncoding.EncodeToString([]byte(resumeToken)),
   322  	}
   323  	invalidDeleteFuture = &infrav1.Future{
   324  		Type:          infrav1.DeleteFuture,
   325  		ServiceName:   serviceName,
   326  		Name:          resourceName,
   327  		ResourceGroup: resourceGroupName,
   328  		Data:          invalidResumeToken,
   329  	}
   330  	fakeResource            = armresources.GenericResource{}
   331  	fakeParameters          = armresources.GenericResource{}
   332  	azureResourceGetterType = reflect.TypeOf((*azure.ResourceSpecGetter)(nil)).Elem()
   333  )
   334  
   335  func fakePoller[T any](g *GomegaWithT, statusCode int) *runtime.Poller[T] {
   336  	response := &http.Response{
   337  		Body: io.NopCloser(strings.NewReader("")),
   338  		Request: &http.Request{
   339  			Method: http.MethodPut,
   340  			URL:    &url.URL{Path: "/"},
   341  		},
   342  		StatusCode: statusCode,
   343  	}
   344  	pipeline := runtime.NewPipeline("testmodule", "v0.1.0", runtime.PipelineOptions{}, nil)
   345  	poller, err := runtime.NewPoller[T](response, pipeline, nil)
   346  	g.Expect(err).NotTo(HaveOccurred())
   347  	return poller
   348  }
   349  
   350  type MockCreator struct{}
   351  type MockDeleter struct{}