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{}