sigs.k8s.io/cluster-api-provider-azure@v1.14.3/azure/services/aso/aso_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 aso 18 19 import ( 20 "context" 21 "errors" 22 "testing" 23 24 asoresourcesv1 "github.com/Azure/azure-service-operator/v2/api/resources/v1api20200601" 25 asoannotations "github.com/Azure/azure-service-operator/v2/pkg/common/annotations" 26 "github.com/Azure/azure-service-operator/v2/pkg/genruntime/conditions" 27 . "github.com/onsi/gomega" 28 "go.uber.org/mock/gomock" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/types" 32 "k8s.io/utils/ptr" 33 infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" 34 "sigs.k8s.io/cluster-api-provider-azure/azure" 35 "sigs.k8s.io/cluster-api-provider-azure/azure/mock_azure" 36 "sigs.k8s.io/cluster-api-provider-azure/azure/services/aso/mock_aso" 37 gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock" 38 "sigs.k8s.io/controller-runtime/pkg/client" 39 "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 40 fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" 41 ) 42 43 const clusterName = "cluster" 44 45 type ErroringGetClient struct { 46 client.Client 47 err error 48 } 49 50 func (e ErroringGetClient) Get(_ context.Context, _ client.ObjectKey, _ client.Object, _ ...client.GetOption) error { 51 return e.err 52 } 53 54 type ErroringPatchClient struct { 55 client.Client 56 err error 57 } 58 59 func (e ErroringPatchClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { 60 return e.err 61 } 62 63 type ErroringDeleteClient struct { 64 client.Client 65 err error 66 } 67 68 func (e ErroringDeleteClient) Delete(_ context.Context, _ client.Object, _ ...client.DeleteOption) error { 69 return e.err 70 } 71 72 func newOwner() *asoresourcesv1.ResourceGroup { 73 return &asoresourcesv1.ResourceGroup{ 74 ObjectMeta: metav1.ObjectMeta{ 75 Namespace: "namespace", 76 }, 77 } 78 } 79 80 func ownerRefs() []metav1.OwnerReference { 81 s := runtime.NewScheme() 82 if err := asoresourcesv1.AddToScheme(s); err != nil { 83 panic(err) 84 } 85 gvk, err := apiutil.GVKForObject(&asoresourcesv1.ResourceGroup{}, s) 86 if err != nil { 87 panic(err) 88 } 89 return []metav1.OwnerReference{ 90 { 91 APIVersion: gvk.GroupVersion().String(), 92 Kind: gvk.Kind, 93 Controller: ptr.To(true), 94 BlockOwnerDeletion: ptr.To(true), 95 }, 96 } 97 } 98 99 // TestCreateOrUpdateResource tests the CreateOrUpdateResource function. 100 func TestCreateOrUpdateResource(t *testing.T) { 101 t.Run("ready status unknown", func(t *testing.T) { 102 g := NewGomegaWithT(t) 103 104 sch := runtime.NewScheme() 105 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 106 c := fakeclient.NewClientBuilder(). 107 WithScheme(sch). 108 Build() 109 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 110 111 mockCtrl := gomock.NewController(t) 112 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 113 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 114 ObjectMeta: metav1.ObjectMeta{ 115 Name: "name", 116 }, 117 }) 118 119 ctx := context.Background() 120 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 121 ObjectMeta: metav1.ObjectMeta{ 122 Name: "name", 123 Namespace: "namespace", 124 OwnerReferences: ownerRefs(), 125 }, 126 Status: asoresourcesv1.ResourceGroup_STATUS{}, 127 })).To(Succeed()) 128 129 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 130 g.Expect(result).To(BeNil()) 131 g.Expect(err).To(HaveOccurred()) 132 g.Expect(err.Error()).To(ContainSubstring("ready status unknown")) 133 }) 134 135 t.Run("create resource that doesn't already exist", func(t *testing.T) { 136 g := NewGomegaWithT(t) 137 138 sch := runtime.NewScheme() 139 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 140 c := fakeclient.NewClientBuilder(). 141 WithScheme(sch). 142 Build() 143 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 144 145 mockCtrl := gomock.NewController(t) 146 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 147 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 148 ObjectMeta: metav1.ObjectMeta{ 149 Name: "name", 150 }, 151 }) 152 specMock.EXPECT().Parameters(gomockinternal.AContext(), gomock.Nil()).Return(&asoresourcesv1.ResourceGroup{ 153 Spec: asoresourcesv1.ResourceGroup_Spec{ 154 Location: ptr.To("location"), 155 }, 156 }, nil) 157 158 ctx := context.Background() 159 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 160 g.Expect(result).To(BeNil()) 161 g.Expect(err).To(HaveOccurred()) 162 g.Expect(azure.IsOperationNotDoneError(err)).To(BeTrue()) 163 var recerr azure.ReconcileError 164 g.Expect(errors.As(err, &recerr)).To(BeTrue()) 165 g.Expect(recerr.IsTransient()).To(BeTrue()) 166 167 created := &asoresourcesv1.ResourceGroup{} 168 g.Expect(c.Get(ctx, types.NamespacedName{Name: "name", Namespace: "namespace"}, created)).To(Succeed()) 169 g.Expect(created.Name).To(Equal("name")) 170 g.Expect(created.Namespace).To(Equal("namespace")) 171 g.Expect(created.OwnerReferences).To(Equal(ownerRefs())) 172 g.Expect(created.Annotations).To(Equal(map[string]string{ 173 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicySkip), 174 asoannotations.PerResourceSecret: "cluster-aso-secret", 175 })) 176 g.Expect(created.Spec).To(Equal(asoresourcesv1.ResourceGroup_Spec{ 177 Location: ptr.To("location"), 178 })) 179 }) 180 181 t.Run("resource is not ready in non-terminal state", func(t *testing.T) { 182 g := NewGomegaWithT(t) 183 184 sch := runtime.NewScheme() 185 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 186 c := fakeclient.NewClientBuilder(). 187 WithScheme(sch). 188 Build() 189 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 190 191 mockCtrl := gomock.NewController(t) 192 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 193 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 194 ObjectMeta: metav1.ObjectMeta{ 195 Name: "name", 196 }, 197 }) 198 specMock.EXPECT().Parameters(gomockinternal.AContext(), gomock.Not(gomock.Nil())).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 199 return group, nil 200 }) 201 specMock.EXPECT().WasManaged(gomock.Any()).Return(false) 202 203 ctx := context.Background() 204 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 205 ObjectMeta: metav1.ObjectMeta{ 206 Name: "name", 207 Namespace: "namespace", 208 OwnerReferences: ownerRefs(), 209 Annotations: map[string]string{ 210 asoannotations.PerResourceSecret: "cluster-aso-secret", 211 }, 212 }, 213 Status: asoresourcesv1.ResourceGroup_STATUS{ 214 Conditions: []conditions.Condition{ 215 { 216 Type: conditions.ConditionTypeReady, 217 Status: metav1.ConditionFalse, 218 Severity: conditions.ConditionSeverityInfo, 219 }, 220 }, 221 }, 222 })).To(Succeed()) 223 224 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 225 g.Expect(result).To(BeNil()) 226 g.Expect(err).To(HaveOccurred()) 227 g.Expect(err.Error()).To(ContainSubstring("resource is not Ready")) 228 var recerr azure.ReconcileError 229 g.Expect(errors.As(err, &recerr)).To(BeTrue()) 230 g.Expect(recerr.IsTransient()).To(BeTrue()) 231 g.Expect(recerr.IsTerminal()).To(BeFalse()) 232 }) 233 234 t.Run("resource is not ready in reconciling state", func(t *testing.T) { 235 g := NewGomegaWithT(t) 236 237 sch := runtime.NewScheme() 238 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 239 c := fakeclient.NewClientBuilder(). 240 WithScheme(sch). 241 Build() 242 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 243 244 mockCtrl := gomock.NewController(t) 245 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 246 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 247 ObjectMeta: metav1.ObjectMeta{ 248 Name: "name", 249 }, 250 }) 251 specMock.EXPECT().Parameters(gomockinternal.AContext(), gomock.Not(gomock.Nil())).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 252 return group, nil 253 }) 254 specMock.EXPECT().WasManaged(gomock.Any()).Return(false) 255 256 ctx := context.Background() 257 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 258 ObjectMeta: metav1.ObjectMeta{ 259 Name: "name", 260 Namespace: "namespace", 261 OwnerReferences: ownerRefs(), 262 Annotations: map[string]string{ 263 asoannotations.PerResourceSecret: "cluster-aso-secret", 264 }, 265 }, 266 Status: asoresourcesv1.ResourceGroup_STATUS{ 267 Conditions: []conditions.Condition{ 268 { 269 Type: conditions.ConditionTypeReady, 270 Status: metav1.ConditionFalse, 271 Severity: conditions.ConditionSeverityInfo, 272 Reason: conditions.ReasonReconciling.Name, 273 }, 274 }, 275 }, 276 })).To(Succeed()) 277 278 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 279 g.Expect(result).To(BeNil()) 280 g.Expect(azure.IsOperationNotDoneError(err)).To(BeTrue()) 281 }) 282 283 t.Run("resource is not ready in terminal state", func(t *testing.T) { 284 g := NewGomegaWithT(t) 285 286 sch := runtime.NewScheme() 287 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 288 c := fakeclient.NewClientBuilder(). 289 WithScheme(sch). 290 Build() 291 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 292 293 mockCtrl := gomock.NewController(t) 294 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 295 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 296 ObjectMeta: metav1.ObjectMeta{ 297 Name: "name", 298 }, 299 }) 300 specMock.EXPECT().Parameters(gomockinternal.AContext(), gomock.Not(gomock.Nil())).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 301 return group, nil 302 }) 303 specMock.EXPECT().WasManaged(gomock.Any()).Return(false) 304 305 ctx := context.Background() 306 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 307 ObjectMeta: metav1.ObjectMeta{ 308 Name: "name", 309 Namespace: "namespace", 310 OwnerReferences: ownerRefs(), 311 Annotations: map[string]string{ 312 asoannotations.PerResourceSecret: "cluster-aso-secret", 313 }, 314 }, 315 Status: asoresourcesv1.ResourceGroup_STATUS{ 316 Conditions: []conditions.Condition{ 317 { 318 Type: conditions.ConditionTypeReady, 319 Status: metav1.ConditionFalse, 320 Severity: conditions.ConditionSeverityError, 321 }, 322 }, 323 }, 324 })).To(Succeed()) 325 326 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 327 g.Expect(result).To(BeNil()) 328 g.Expect(err).To(HaveOccurred()) 329 g.Expect(err.Error()).To(ContainSubstring("resource is not Ready")) 330 var recerr azure.ReconcileError 331 g.Expect(errors.As(err, &recerr)).To(BeTrue()) 332 g.Expect(recerr.IsTerminal()).To(BeTrue()) 333 g.Expect(recerr.IsTransient()).To(BeFalse()) 334 }) 335 336 t.Run("error getting existing resource", func(t *testing.T) { 337 g := NewGomegaWithT(t) 338 339 sch := runtime.NewScheme() 340 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 341 c := fakeclient.NewClientBuilder(). 342 WithScheme(sch). 343 Build() 344 s := New[*asoresourcesv1.ResourceGroup](ErroringGetClient{Client: c, err: errors.New("an error")}, clusterName, newOwner()) 345 346 mockCtrl := gomock.NewController(t) 347 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 348 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 349 ObjectMeta: metav1.ObjectMeta{ 350 Name: "name", 351 }, 352 }) 353 354 ctx := context.Background() 355 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 356 g.Expect(result).To(BeNil()) 357 g.Expect(err).To(HaveOccurred()) 358 g.Expect(err.Error()).To(ContainSubstring("failed to get existing resource")) 359 }) 360 361 t.Run("begin an update", func(t *testing.T) { 362 g := NewGomegaWithT(t) 363 364 sch := runtime.NewScheme() 365 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 366 c := fakeclient.NewClientBuilder(). 367 WithScheme(sch). 368 Build() 369 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 370 371 mockCtrl := gomock.NewController(t) 372 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 373 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 374 ObjectMeta: metav1.ObjectMeta{ 375 Name: "name", 376 }, 377 }) 378 specMock.EXPECT().Parameters(gomockinternal.AContext(), gomock.Not(gomock.Nil())).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 379 group.Spec.Location = ptr.To("location") 380 return group, nil 381 }) 382 specMock.EXPECT().WasManaged(gomock.Any()).Return(false) 383 384 ctx := context.Background() 385 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 386 ObjectMeta: metav1.ObjectMeta{ 387 Name: "name", 388 Namespace: "namespace", 389 OwnerReferences: ownerRefs(), 390 }, 391 Status: asoresourcesv1.ResourceGroup_STATUS{ 392 Conditions: []conditions.Condition{ 393 { 394 Type: conditions.ConditionTypeReady, 395 Status: metav1.ConditionTrue, 396 }, 397 }, 398 }, 399 })).To(Succeed()) 400 401 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 402 g.Expect(result).To(BeNil()) 403 g.Expect(err).To(HaveOccurred()) 404 }) 405 406 t.Run("adopt managed resource in not found state", func(t *testing.T) { 407 g := NewGomegaWithT(t) 408 409 sch := runtime.NewScheme() 410 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 411 c := fakeclient.NewClientBuilder(). 412 WithScheme(sch). 413 Build() 414 clusterName := "cluster" 415 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 416 417 mockCtrl := gomock.NewController(t) 418 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 419 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 420 ObjectMeta: metav1.ObjectMeta{ 421 Name: "name", 422 }, 423 }) 424 specMock.EXPECT().Parameters(gomockinternal.AContext(), gomock.Not(gomock.Nil())).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 425 return group, nil 426 }) 427 428 ctx := context.Background() 429 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 430 ObjectMeta: metav1.ObjectMeta{ 431 Name: "name", 432 Namespace: "namespace", 433 OwnerReferences: ownerRefs(), 434 Annotations: map[string]string{ 435 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicySkip), 436 }, 437 }, 438 Status: asoresourcesv1.ResourceGroup_STATUS{ 439 Conditions: []conditions.Condition{ 440 { 441 Type: conditions.ConditionTypeReady, 442 Status: metav1.ConditionFalse, 443 Reason: conditions.ReasonAzureResourceNotFound.Name, 444 }, 445 }, 446 }, 447 })).To(Succeed()) 448 449 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 450 g.Expect(result).To(BeNil()) 451 g.Expect(err).To(HaveOccurred()) 452 453 updated := &asoresourcesv1.ResourceGroup{} 454 g.Expect(c.Get(ctx, types.NamespacedName{Name: "name", Namespace: "namespace"}, updated)).To(Succeed()) 455 g.Expect(updated.Annotations).To(Equal(map[string]string{ 456 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicyManage), 457 asoannotations.PerResourceSecret: "cluster-aso-secret", 458 })) 459 }) 460 461 t.Run("adopt previously managed resource", func(t *testing.T) { 462 g := NewGomegaWithT(t) 463 464 sch := runtime.NewScheme() 465 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 466 c := fakeclient.NewClientBuilder(). 467 WithScheme(sch). 468 Build() 469 clusterName := "cluster" 470 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 471 472 mockCtrl := gomock.NewController(t) 473 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 474 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 475 ObjectMeta: metav1.ObjectMeta{ 476 Name: "name", 477 }, 478 }) 479 specMock.EXPECT().Parameters(gomockinternal.AContext(), gomock.Not(gomock.Nil())).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 480 return group, nil 481 }) 482 specMock.EXPECT().WasManaged(gomock.Any()).Return(true) 483 484 ctx := context.Background() 485 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 486 ObjectMeta: metav1.ObjectMeta{ 487 Name: "name", 488 Namespace: "namespace", 489 OwnerReferences: ownerRefs(), 490 Annotations: map[string]string{ 491 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicySkip), 492 }, 493 }, 494 Status: asoresourcesv1.ResourceGroup_STATUS{ 495 Conditions: []conditions.Condition{ 496 { 497 Type: conditions.ConditionTypeReady, 498 Status: metav1.ConditionTrue, 499 }, 500 }, 501 }, 502 })).To(Succeed()) 503 504 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 505 g.Expect(result).To(BeNil()) 506 g.Expect(err).To(HaveOccurred()) 507 508 updated := &asoresourcesv1.ResourceGroup{} 509 g.Expect(c.Get(ctx, types.NamespacedName{Name: "name", Namespace: "namespace"}, updated)).To(Succeed()) 510 g.Expect(updated.Annotations).To(Equal(map[string]string{ 511 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicyManage), 512 asoannotations.PerResourceSecret: "cluster-aso-secret", 513 })) 514 }) 515 516 t.Run("adopt previously managed resource with label", func(t *testing.T) { 517 g := NewGomegaWithT(t) 518 519 sch := runtime.NewScheme() 520 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 521 c := fakeclient.NewClientBuilder(). 522 WithScheme(sch). 523 Build() 524 clusterName := "cluster" 525 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 526 527 mockCtrl := gomock.NewController(t) 528 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 529 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 530 ObjectMeta: metav1.ObjectMeta{ 531 Name: "name", 532 }, 533 }) 534 specMock.EXPECT().Parameters(gomockinternal.AContext(), gomock.Not(gomock.Nil())).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 535 return group, nil 536 }) 537 specMock.EXPECT().WasManaged(gomock.Any()).Return(true) 538 539 ctx := context.Background() 540 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 541 ObjectMeta: metav1.ObjectMeta{ 542 Name: "name", 543 Namespace: "namespace", 544 Labels: map[string]string{ 545 //nolint:staticcheck // Referencing this deprecated value is required for backwards compatibility. 546 infrav1.OwnedByClusterLabelKey: clusterName, 547 }, 548 Annotations: map[string]string{ 549 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicySkip), 550 }, 551 }, 552 Status: asoresourcesv1.ResourceGroup_STATUS{ 553 Conditions: []conditions.Condition{ 554 { 555 Type: conditions.ConditionTypeReady, 556 Status: metav1.ConditionTrue, 557 }, 558 }, 559 }, 560 })).To(Succeed()) 561 562 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 563 g.Expect(result).To(BeNil()) 564 g.Expect(err).To(HaveOccurred()) 565 566 updated := &asoresourcesv1.ResourceGroup{} 567 g.Expect(c.Get(ctx, types.NamespacedName{Name: "name", Namespace: "namespace"}, updated)).To(Succeed()) 568 g.Expect(updated.Annotations).To(Equal(map[string]string{ 569 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicyManage), 570 asoannotations.PerResourceSecret: "cluster-aso-secret", 571 })) 572 g.Expect(updated.OwnerReferences).To(Equal(ownerRefs())) 573 }) 574 575 t.Run("Parameters error", func(t *testing.T) { 576 g := NewGomegaWithT(t) 577 578 sch := runtime.NewScheme() 579 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 580 c := fakeclient.NewClientBuilder(). 581 WithScheme(sch). 582 Build() 583 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 584 585 mockCtrl := gomock.NewController(t) 586 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 587 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 588 ObjectMeta: metav1.ObjectMeta{ 589 Name: "name", 590 }, 591 }) 592 specMock.EXPECT().Parameters(gomockinternal.AContext(), gomock.Not(gomock.Nil())).Return(nil, errors.New("parameters error")) 593 594 ctx := context.Background() 595 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 596 ObjectMeta: metav1.ObjectMeta{ 597 Name: "name", 598 Namespace: "namespace", 599 OwnerReferences: ownerRefs(), 600 }, 601 Status: asoresourcesv1.ResourceGroup_STATUS{ 602 Conditions: []conditions.Condition{ 603 { 604 Type: conditions.ConditionTypeReady, 605 Status: metav1.ConditionTrue, 606 }, 607 }, 608 }, 609 })).To(Succeed()) 610 611 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 612 g.Expect(result).To(BeNil()) 613 g.Expect(err).To(HaveOccurred()) 614 g.Expect(err.Error()).To(ContainSubstring("parameters error")) 615 }) 616 617 t.Run("skip update for unmanaged resource", func(t *testing.T) { 618 g := NewGomegaWithT(t) 619 620 sch := runtime.NewScheme() 621 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 622 c := fakeclient.NewClientBuilder(). 623 WithScheme(sch). 624 Build() 625 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 626 627 mockCtrl := gomock.NewController(t) 628 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 629 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 630 ObjectMeta: metav1.ObjectMeta{ 631 Name: "name", 632 }, 633 }) 634 635 ctx := context.Background() 636 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 637 ObjectMeta: metav1.ObjectMeta{ 638 Name: "name", 639 Namespace: "namespace", 640 }, 641 Status: asoresourcesv1.ResourceGroup_STATUS{ 642 Conditions: []conditions.Condition{ 643 { 644 Type: conditions.ConditionTypeReady, 645 Status: metav1.ConditionTrue, 646 }, 647 }, 648 }, 649 })).To(Succeed()) 650 651 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 652 g.Expect(result).NotTo(BeNil()) 653 g.Expect(err).NotTo(HaveOccurred()) 654 }) 655 656 t.Run("resource up to date", func(t *testing.T) { 657 g := NewGomegaWithT(t) 658 659 sch := runtime.NewScheme() 660 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 661 c := fakeclient.NewClientBuilder(). 662 WithScheme(sch). 663 Build() 664 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 665 666 mockCtrl := gomock.NewController(t) 667 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 668 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 669 ObjectMeta: metav1.ObjectMeta{ 670 Name: "name", 671 }, 672 }) 673 specMock.EXPECT().Parameters(gomockinternal.AContext(), gomock.Any()).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 674 return group, nil 675 }) 676 specMock.EXPECT().WasManaged(gomock.Any()).Return(false) 677 678 ctx := context.Background() 679 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 680 ObjectMeta: metav1.ObjectMeta{ 681 Name: "name", 682 Namespace: "namespace", 683 OwnerReferences: ownerRefs(), 684 Annotations: map[string]string{ 685 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicyManage), 686 asoannotations.PerResourceSecret: "cluster-aso-secret", 687 }, 688 }, 689 Spec: asoresourcesv1.ResourceGroup_Spec{ 690 Location: ptr.To("location"), 691 }, 692 Status: asoresourcesv1.ResourceGroup_STATUS{ 693 Conditions: []conditions.Condition{ 694 { 695 Type: conditions.ConditionTypeReady, 696 Status: metav1.ConditionTrue, 697 }, 698 }, 699 }, 700 })).To(Succeed()) 701 702 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 703 g.Expect(result).NotTo(BeNil()) 704 g.Expect(err).NotTo(HaveOccurred()) 705 706 g.Expect(result.GetName()).To(Equal("name")) 707 g.Expect(result.GetNamespace()).To(Equal("namespace")) 708 g.Expect(result.Spec.Location).To(Equal(ptr.To("location"))) 709 }) 710 711 t.Run("error updating", func(t *testing.T) { 712 g := NewGomegaWithT(t) 713 714 sch := runtime.NewScheme() 715 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 716 c := fakeclient.NewClientBuilder(). 717 WithScheme(sch). 718 Build() 719 s := New[*asoresourcesv1.ResourceGroup](ErroringPatchClient{Client: c, err: errors.New("an error")}, clusterName, newOwner()) 720 721 mockCtrl := gomock.NewController(t) 722 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 723 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 724 ObjectMeta: metav1.ObjectMeta{ 725 Name: "name", 726 }, 727 }) 728 specMock.EXPECT().Parameters(gomockinternal.AContext(), gomock.Any()).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 729 group.Spec.Location = ptr.To("location") 730 return group, nil 731 }) 732 specMock.EXPECT().WasManaged(gomock.Any()).Return(false) 733 734 ctx := context.Background() 735 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 736 ObjectMeta: metav1.ObjectMeta{ 737 Name: "name", 738 Namespace: "namespace", 739 OwnerReferences: ownerRefs(), 740 }, 741 Status: asoresourcesv1.ResourceGroup_STATUS{ 742 Conditions: []conditions.Condition{ 743 { 744 Type: conditions.ConditionTypeReady, 745 Status: metav1.ConditionTrue, 746 }, 747 }, 748 }, 749 })).To(Succeed()) 750 751 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 752 g.Expect(result).To(BeNil()) 753 g.Expect(err).To(HaveOccurred()) 754 g.Expect(err.Error()).To(ContainSubstring("failed to update resource")) 755 }) 756 757 t.Run("with tags success", func(t *testing.T) { 758 g := NewGomegaWithT(t) 759 760 sch := runtime.NewScheme() 761 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 762 c := fakeclient.NewClientBuilder(). 763 WithScheme(sch). 764 Build() 765 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 766 767 mockCtrl := gomock.NewController(t) 768 specMock := struct { 769 *mock_azure.MockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup] 770 *mock_aso.MockTagsGetterSetter[*asoresourcesv1.ResourceGroup] 771 }{ 772 MockASOResourceSpecGetter: mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl), 773 MockTagsGetterSetter: mock_aso.NewMockTagsGetterSetter[*asoresourcesv1.ResourceGroup](mockCtrl), 774 } 775 specMock.MockASOResourceSpecGetter.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 776 ObjectMeta: metav1.ObjectMeta{ 777 Name: "name", 778 }, 779 }) 780 specMock.MockASOResourceSpecGetter.EXPECT().Parameters(gomockinternal.AContext(), gomock.Any()).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 781 return group, nil 782 }) 783 specMock.MockASOResourceSpecGetter.EXPECT().WasManaged(gomock.Any()).Return(false) 784 785 specMock.MockTagsGetterSetter.EXPECT().GetAdditionalTags().Return(nil) 786 specMock.MockTagsGetterSetter.EXPECT().GetDesiredTags(gomock.Any()).Return(nil).Times(2) 787 specMock.MockTagsGetterSetter.EXPECT().SetTags(gomock.Any(), gomock.Any()) 788 789 ctx := context.Background() 790 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 791 ObjectMeta: metav1.ObjectMeta{ 792 Name: "name", 793 Namespace: "namespace", 794 OwnerReferences: ownerRefs(), 795 Annotations: map[string]string{ 796 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicyManage), 797 }, 798 }, 799 Status: asoresourcesv1.ResourceGroup_STATUS{ 800 Conditions: []conditions.Condition{ 801 { 802 Type: conditions.ConditionTypeReady, 803 Status: metav1.ConditionTrue, 804 }, 805 }, 806 }, 807 })).To(Succeed()) 808 809 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 810 g.Expect(result).To(BeNil()) 811 g.Expect(azure.IsOperationNotDoneError(err)).To(BeTrue()) 812 813 updated := &asoresourcesv1.ResourceGroup{} 814 g.Expect(c.Get(ctx, types.NamespacedName{Name: "name", Namespace: "namespace"}, updated)).To(Succeed()) 815 g.Expect(updated.Annotations).To(HaveKey(tagsLastAppliedAnnotation)) 816 }) 817 818 t.Run("with tags failure", func(t *testing.T) { 819 g := NewGomegaWithT(t) 820 821 sch := runtime.NewScheme() 822 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 823 c := fakeclient.NewClientBuilder(). 824 WithScheme(sch). 825 Build() 826 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 827 828 mockCtrl := gomock.NewController(t) 829 specMock := struct { 830 *mock_azure.MockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup] 831 *mock_aso.MockTagsGetterSetter[*asoresourcesv1.ResourceGroup] 832 }{ 833 MockASOResourceSpecGetter: mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl), 834 MockTagsGetterSetter: mock_aso.NewMockTagsGetterSetter[*asoresourcesv1.ResourceGroup](mockCtrl), 835 } 836 specMock.MockASOResourceSpecGetter.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 837 ObjectMeta: metav1.ObjectMeta{ 838 Name: "name", 839 }, 840 }) 841 specMock.MockASOResourceSpecGetter.EXPECT().Parameters(gomockinternal.AContext(), gomock.Any()).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 842 return group, nil 843 }) 844 845 ctx := context.Background() 846 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 847 ObjectMeta: metav1.ObjectMeta{ 848 Name: "name", 849 Namespace: "namespace", 850 OwnerReferences: ownerRefs(), 851 Annotations: map[string]string{ 852 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicyManage), 853 tagsLastAppliedAnnotation: "{", 854 }, 855 }, 856 Status: asoresourcesv1.ResourceGroup_STATUS{ 857 Conditions: []conditions.Condition{ 858 { 859 Type: conditions.ConditionTypeReady, 860 Status: metav1.ConditionTrue, 861 }, 862 }, 863 }, 864 })).To(Succeed()) 865 866 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 867 g.Expect(result).To(BeNil()) 868 g.Expect(err.Error()).To(ContainSubstring("failed to reconcile tags")) 869 }) 870 871 t.Run("reconcile policy annotation resets after un-pause", func(t *testing.T) { 872 g := NewGomegaWithT(t) 873 874 sch := runtime.NewScheme() 875 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 876 c := fakeclient.NewClientBuilder(). 877 WithScheme(sch). 878 Build() 879 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 880 881 mockCtrl := gomock.NewController(t) 882 specMock := mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl) 883 specMock.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 884 ObjectMeta: metav1.ObjectMeta{ 885 Name: "name", 886 }, 887 }) 888 specMock.EXPECT().Parameters(gomockinternal.AContext(), gomock.Any()).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 889 return group, nil 890 }) 891 specMock.EXPECT().WasManaged(gomock.Any()).Return(false) 892 893 ctx := context.Background() 894 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 895 ObjectMeta: metav1.ObjectMeta{ 896 Name: "name", 897 Namespace: "namespace", 898 OwnerReferences: ownerRefs(), 899 Annotations: map[string]string{ 900 prePauseReconcilePolicyAnnotation: string(asoannotations.ReconcilePolicyManage), 901 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicySkip), 902 }, 903 }, 904 Spec: asoresourcesv1.ResourceGroup_Spec{ 905 Location: ptr.To("location"), 906 }, 907 Status: asoresourcesv1.ResourceGroup_STATUS{ 908 Conditions: []conditions.Condition{ 909 { 910 Type: conditions.ConditionTypeReady, 911 Status: metav1.ConditionTrue, 912 }, 913 }, 914 }, 915 })).To(Succeed()) 916 917 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 918 g.Expect(result).To(BeNil()) 919 g.Expect(azure.IsOperationNotDoneError(err)).To(BeTrue()) 920 921 updated := &asoresourcesv1.ResourceGroup{} 922 g.Expect(c.Get(ctx, types.NamespacedName{Name: "name", Namespace: "namespace"}, updated)).To(Succeed()) 923 g.Expect(updated.Annotations).NotTo(HaveKey(prePauseReconcilePolicyAnnotation)) 924 g.Expect(updated.Annotations).To(HaveKeyWithValue(asoannotations.ReconcilePolicy, string(asoannotations.ReconcilePolicyManage))) 925 }) 926 927 t.Run("patches applied on create", func(t *testing.T) { 928 g := NewGomegaWithT(t) 929 930 sch := runtime.NewScheme() 931 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 932 c := fakeclient.NewClientBuilder(). 933 WithScheme(sch). 934 Build() 935 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 936 937 mockCtrl := gomock.NewController(t) 938 specMock := struct { 939 *mock_azure.MockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup] 940 *mock_aso.MockPatcher 941 }{ 942 mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl), 943 mock_aso.NewMockPatcher(mockCtrl), 944 } 945 specMock.MockASOResourceSpecGetter.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 946 ObjectMeta: metav1.ObjectMeta{ 947 Name: "name", 948 }, 949 }) 950 specMock.MockASOResourceSpecGetter.EXPECT().Parameters(gomockinternal.AContext(), gomock.Any()).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 951 return &asoresourcesv1.ResourceGroup{ 952 Spec: asoresourcesv1.ResourceGroup_Spec{ 953 Location: ptr.To("location-from-parameters"), 954 }, 955 }, nil 956 }) 957 958 specMock.MockPatcher.EXPECT().ExtraPatches().Return([]string{ 959 `{"metadata": {"labels": {"extra-patch": "not-this-value"}}}`, 960 `{"metadata": {"labels": {"extra-patch": "this-value"}}}`, 961 `{"metadata": {"labels": {"another": "label"}}}`, 962 }) 963 964 ctx := context.Background() 965 966 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 967 g.Expect(result).To(BeNil()) 968 g.Expect(azure.IsOperationNotDoneError(err)).To(BeTrue(), "expected not done error, got %v", err) 969 970 updated := &asoresourcesv1.ResourceGroup{} 971 g.Expect(c.Get(ctx, types.NamespacedName{Name: "name", Namespace: "namespace"}, updated)).To(Succeed()) 972 g.Expect(updated.Labels).To(HaveKeyWithValue("extra-patch", "this-value")) 973 g.Expect(updated.Labels).To(HaveKeyWithValue("another", "label")) 974 g.Expect(*updated.Spec.Location).To(Equal("location-from-parameters")) 975 }) 976 977 t.Run("patches applied on update", func(t *testing.T) { 978 g := NewGomegaWithT(t) 979 980 sch := runtime.NewScheme() 981 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 982 c := fakeclient.NewClientBuilder(). 983 WithScheme(sch). 984 Build() 985 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 986 987 mockCtrl := gomock.NewController(t) 988 specMock := struct { 989 *mock_azure.MockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup] 990 *mock_aso.MockPatcher 991 }{ 992 mock_azure.NewMockASOResourceSpecGetter[*asoresourcesv1.ResourceGroup](mockCtrl), 993 mock_aso.NewMockPatcher(mockCtrl), 994 } 995 specMock.MockASOResourceSpecGetter.EXPECT().ResourceRef().Return(&asoresourcesv1.ResourceGroup{ 996 ObjectMeta: metav1.ObjectMeta{ 997 Name: "name", 998 }, 999 }) 1000 specMock.MockASOResourceSpecGetter.EXPECT().Parameters(gomockinternal.AContext(), gomock.Any()).DoAndReturn(func(_ context.Context, group *asoresourcesv1.ResourceGroup) (*asoresourcesv1.ResourceGroup, error) { 1001 group.Spec.Location = ptr.To("location-from-parameters") 1002 return group, nil 1003 }) 1004 specMock.MockASOResourceSpecGetter.EXPECT().WasManaged(gomock.Any()).Return(false) 1005 1006 specMock.MockPatcher.EXPECT().ExtraPatches().Return([]string{ 1007 `{"metadata": {"labels": {"extra-patch": "not-this-value"}}}`, 1008 `{"metadata": {"labels": {"extra-patch": "this-value"}}}`, 1009 `{"metadata": {"labels": {"another": "label"}}}`, 1010 }) 1011 1012 ctx := context.Background() 1013 g.Expect(c.Create(ctx, &asoresourcesv1.ResourceGroup{ 1014 ObjectMeta: metav1.ObjectMeta{ 1015 Name: "name", 1016 Namespace: "namespace", 1017 OwnerReferences: ownerRefs(), 1018 Annotations: map[string]string{ 1019 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicyManage), 1020 }, 1021 }, 1022 Spec: asoresourcesv1.ResourceGroup_Spec{ 1023 Location: ptr.To("location"), 1024 }, 1025 Status: asoresourcesv1.ResourceGroup_STATUS{ 1026 Conditions: []conditions.Condition{ 1027 { 1028 Type: conditions.ConditionTypeReady, 1029 Status: metav1.ConditionTrue, 1030 }, 1031 }, 1032 }, 1033 })).To(Succeed()) 1034 1035 result, err := s.CreateOrUpdateResource(ctx, specMock, "service") 1036 g.Expect(result).To(BeNil()) 1037 g.Expect(azure.IsOperationNotDoneError(err)).To(BeTrue(), "expected not done error, got %v", err) 1038 1039 updated := &asoresourcesv1.ResourceGroup{} 1040 g.Expect(c.Get(ctx, types.NamespacedName{Name: "name", Namespace: "namespace"}, updated)).To(Succeed()) 1041 g.Expect(updated.Labels).To(HaveKeyWithValue("extra-patch", "this-value")) 1042 g.Expect(updated.Labels).To(HaveKeyWithValue("another", "label")) 1043 g.Expect(*updated.Spec.Location).To(Equal("location-from-parameters")) 1044 }) 1045 } 1046 1047 // TestDeleteResource tests the DeleteResource function. 1048 func TestDeleteResource(t *testing.T) { 1049 t.Run("successful delete", func(t *testing.T) { 1050 g := NewGomegaWithT(t) 1051 1052 sch := runtime.NewScheme() 1053 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 1054 c := fakeclient.NewClientBuilder(). 1055 WithScheme(sch). 1056 Build() 1057 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 1058 1059 resource := &asoresourcesv1.ResourceGroup{ 1060 ObjectMeta: metav1.ObjectMeta{ 1061 Name: "name", 1062 }, 1063 } 1064 1065 ctx := context.Background() 1066 g.Expect(s.DeleteResource(ctx, resource, "service")).To(Succeed()) 1067 }) 1068 1069 t.Run("delete in progress", func(t *testing.T) { 1070 g := NewGomegaWithT(t) 1071 1072 sch := runtime.NewScheme() 1073 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 1074 c := fakeclient.NewClientBuilder(). 1075 WithScheme(sch). 1076 Build() 1077 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 1078 1079 ctx := context.Background() 1080 resource := &asoresourcesv1.ResourceGroup{ 1081 ObjectMeta: metav1.ObjectMeta{ 1082 Name: "name", 1083 Namespace: "namespace", 1084 OwnerReferences: ownerRefs(), 1085 }, 1086 } 1087 g.Expect(c.Create(ctx, resource)).To(Succeed()) 1088 1089 err := s.DeleteResource(ctx, resource, "service") 1090 g.Expect(err).To(HaveOccurred()) 1091 g.Expect(azure.IsOperationNotDoneError(err)).To(BeTrue()) 1092 var recerr azure.ReconcileError 1093 g.Expect(errors.As(err, &recerr)).To(BeTrue()) 1094 g.Expect(recerr.IsTransient()).To(BeTrue()) 1095 }) 1096 1097 t.Run("skip delete for unmanaged resource", func(t *testing.T) { 1098 g := NewGomegaWithT(t) 1099 1100 sch := runtime.NewScheme() 1101 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 1102 c := fakeclient.NewClientBuilder(). 1103 WithScheme(sch). 1104 Build() 1105 s := New[*asoresourcesv1.ResourceGroup](c, clusterName, newOwner()) 1106 1107 resource := &asoresourcesv1.ResourceGroup{ 1108 ObjectMeta: metav1.ObjectMeta{ 1109 Name: "name", 1110 Namespace: "namespace", 1111 }, 1112 } 1113 1114 ctx := context.Background() 1115 g.Expect(c.Create(ctx, resource)).To(Succeed()) 1116 1117 g.Expect(s.DeleteResource(ctx, resource, "service")).To(Succeed()) 1118 }) 1119 1120 t.Run("error checking if resource is managed", func(t *testing.T) { 1121 g := NewGomegaWithT(t) 1122 1123 sch := runtime.NewScheme() 1124 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 1125 c := fakeclient.NewClientBuilder(). 1126 WithScheme(sch). 1127 Build() 1128 s := New[*asoresourcesv1.ResourceGroup](ErroringGetClient{Client: c, err: errors.New("a get error")}, clusterName, newOwner()) 1129 1130 resource := &asoresourcesv1.ResourceGroup{ 1131 ObjectMeta: metav1.ObjectMeta{ 1132 Name: "name", 1133 Namespace: "namespace", 1134 }, 1135 } 1136 1137 ctx := context.Background() 1138 g.Expect(c.Create(ctx, resource)).To(Succeed()) 1139 1140 err := s.DeleteResource(ctx, resource, "service") 1141 g.Expect(err).To(MatchError(ContainSubstring("a get error"))) 1142 }) 1143 1144 t.Run("error deleting", func(t *testing.T) { 1145 g := NewGomegaWithT(t) 1146 1147 sch := runtime.NewScheme() 1148 g.Expect(asoresourcesv1.AddToScheme(sch)).To(Succeed()) 1149 c := fakeclient.NewClientBuilder(). 1150 WithScheme(sch). 1151 Build() 1152 s := New[*asoresourcesv1.ResourceGroup](ErroringDeleteClient{Client: c, err: errors.New("an error")}, clusterName, newOwner()) 1153 1154 resource := &asoresourcesv1.ResourceGroup{ 1155 ObjectMeta: metav1.ObjectMeta{ 1156 Name: "name", 1157 Namespace: "namespace", 1158 OwnerReferences: ownerRefs(), 1159 }, 1160 } 1161 1162 ctx := context.Background() 1163 g.Expect(c.Create(ctx, resource)).To(Succeed()) 1164 1165 err := s.DeleteResource(ctx, resource, "service") 1166 g.Expect(err).To(HaveOccurred()) 1167 g.Expect(err.Error()).To(ContainSubstring("failed to delete resource")) 1168 }) 1169 } 1170 1171 func TestPauseResource(t *testing.T) { 1172 tests := []struct { 1173 name string 1174 resource *asoresourcesv1.ResourceGroup 1175 clientBuilder func(g Gomega) client.Client 1176 expectedErr string 1177 verify func(g Gomega, ctrlClient client.Client, resource *asoresourcesv1.ResourceGroup) 1178 }{ 1179 { 1180 name: "success, not already paused", 1181 resource: &asoresourcesv1.ResourceGroup{ 1182 ObjectMeta: metav1.ObjectMeta{ 1183 Name: "name", 1184 }, 1185 }, 1186 clientBuilder: func(g Gomega) client.Client { 1187 scheme := runtime.NewScheme() 1188 g.Expect(asoresourcesv1.AddToScheme(scheme)).To(Succeed()) 1189 return fakeclient.NewClientBuilder(). 1190 WithScheme(scheme). 1191 WithObjects(&asoresourcesv1.ResourceGroup{ 1192 ObjectMeta: metav1.ObjectMeta{ 1193 Name: "name", 1194 Namespace: "namespace", 1195 Annotations: map[string]string{ 1196 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicyManage), 1197 }, 1198 OwnerReferences: ownerRefs(), 1199 }, 1200 }). 1201 Build() 1202 }, 1203 verify: func(g Gomega, ctrlClient client.Client, resource *asoresourcesv1.ResourceGroup) { 1204 ctx := context.Background() 1205 actual := &asoresourcesv1.ResourceGroup{} 1206 g.Expect(ctrlClient.Get(ctx, client.ObjectKeyFromObject(resource), actual)).To(Succeed()) 1207 g.Expect(actual.Annotations).To(HaveKeyWithValue(prePauseReconcilePolicyAnnotation, string(asoannotations.ReconcilePolicyManage))) 1208 g.Expect(actual.Annotations).To(HaveKeyWithValue(asoannotations.ReconcilePolicy, string(asoannotations.ReconcilePolicySkip))) 1209 }, 1210 }, 1211 { 1212 name: "success, already paused", 1213 resource: &asoresourcesv1.ResourceGroup{ 1214 ObjectMeta: metav1.ObjectMeta{ 1215 Name: "name", 1216 }, 1217 }, 1218 clientBuilder: func(g Gomega) client.Client { 1219 scheme := runtime.NewScheme() 1220 g.Expect(asoresourcesv1.AddToScheme(scheme)).To(Succeed()) 1221 return fakeclient.NewClientBuilder(). 1222 WithScheme(scheme). 1223 WithObjects(&asoresourcesv1.ResourceGroup{ 1224 ObjectMeta: metav1.ObjectMeta{ 1225 Name: "name", 1226 Namespace: "namespace", 1227 Annotations: map[string]string{ 1228 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicySkip), 1229 }, 1230 OwnerReferences: ownerRefs(), 1231 }, 1232 }). 1233 Build() 1234 }, 1235 verify: func(g Gomega, ctrlClient client.Client, resource *asoresourcesv1.ResourceGroup) { 1236 ctx := context.Background() 1237 actual := &asoresourcesv1.ResourceGroup{} 1238 g.Expect(ctrlClient.Get(ctx, client.ObjectKeyFromObject(resource), actual)).To(Succeed()) 1239 g.Expect(actual.Annotations).To(HaveKeyWithValue(prePauseReconcilePolicyAnnotation, string(asoannotations.ReconcilePolicySkip))) 1240 g.Expect(actual.Annotations).To(HaveKeyWithValue(asoannotations.ReconcilePolicy, string(asoannotations.ReconcilePolicySkip))) 1241 }, 1242 }, 1243 { 1244 name: "success, no patch needed", 1245 resource: &asoresourcesv1.ResourceGroup{ 1246 ObjectMeta: metav1.ObjectMeta{ 1247 Name: "name", 1248 }, 1249 }, 1250 clientBuilder: func(g Gomega) client.Client { 1251 scheme := runtime.NewScheme() 1252 g.Expect(asoresourcesv1.AddToScheme(scheme)).To(Succeed()) 1253 c := fakeclient.NewClientBuilder(). 1254 WithScheme(scheme). 1255 WithObjects(&asoresourcesv1.ResourceGroup{ 1256 ObjectMeta: metav1.ObjectMeta{ 1257 Name: "name", 1258 Namespace: "namespace", 1259 Annotations: map[string]string{ 1260 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicySkip), 1261 prePauseReconcilePolicyAnnotation: string(asoannotations.ReconcilePolicyManage), 1262 }, 1263 OwnerReferences: ownerRefs(), 1264 }, 1265 }). 1266 Build() 1267 return ErroringPatchClient{Client: c, err: errors.New("patch shouldn't be called")} 1268 }, 1269 expectedErr: "", 1270 }, 1271 { 1272 name: "failure getting existing resource", 1273 resource: &asoresourcesv1.ResourceGroup{ 1274 ObjectMeta: metav1.ObjectMeta{ 1275 Name: "name", 1276 }, 1277 }, 1278 clientBuilder: func(g Gomega) client.Client { 1279 scheme := runtime.NewScheme() 1280 g.Expect(asoresourcesv1.AddToScheme(scheme)).To(Succeed()) 1281 return fakeclient.NewClientBuilder(). 1282 WithScheme(scheme). 1283 Build() 1284 }, 1285 expectedErr: "not found", 1286 }, 1287 { 1288 name: "failure patching resource", 1289 resource: &asoresourcesv1.ResourceGroup{ 1290 ObjectMeta: metav1.ObjectMeta{ 1291 Name: "name", 1292 }, 1293 }, 1294 clientBuilder: func(g Gomega) client.Client { 1295 scheme := runtime.NewScheme() 1296 g.Expect(asoresourcesv1.AddToScheme(scheme)).To(Succeed()) 1297 c := fakeclient.NewClientBuilder(). 1298 WithScheme(scheme). 1299 WithObjects(&asoresourcesv1.ResourceGroup{ 1300 ObjectMeta: metav1.ObjectMeta{ 1301 Name: "name", 1302 Namespace: "namespace", 1303 Annotations: map[string]string{ 1304 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicySkip), 1305 }, 1306 OwnerReferences: ownerRefs(), 1307 }, 1308 }). 1309 Build() 1310 return ErroringPatchClient{Client: c, err: errors.New("test patch error")} 1311 }, 1312 expectedErr: "test patch error", 1313 }, 1314 { 1315 name: "success, unmanaged resource", 1316 resource: &asoresourcesv1.ResourceGroup{ 1317 ObjectMeta: metav1.ObjectMeta{ 1318 Name: "name", 1319 }, 1320 }, 1321 clientBuilder: func(g Gomega) client.Client { 1322 scheme := runtime.NewScheme() 1323 g.Expect(asoresourcesv1.AddToScheme(scheme)).To(Succeed()) 1324 return fakeclient.NewClientBuilder(). 1325 WithScheme(scheme). 1326 WithObjects(&asoresourcesv1.ResourceGroup{ 1327 ObjectMeta: metav1.ObjectMeta{ 1328 Name: "name", 1329 Namespace: "namespace", 1330 Annotations: map[string]string{ 1331 asoannotations.ReconcilePolicy: string(asoannotations.ReconcilePolicyManage), 1332 }, 1333 OwnerReferences: []metav1.OwnerReference{{Name: "other-owner"}}, 1334 }, 1335 }). 1336 Build() 1337 }, 1338 verify: func(g Gomega, ctrlClient client.Client, resource *asoresourcesv1.ResourceGroup) { 1339 ctx := context.Background() 1340 actual := &asoresourcesv1.ResourceGroup{} 1341 g.Expect(ctrlClient.Get(ctx, client.ObjectKeyFromObject(resource), actual)).To(Succeed()) 1342 g.Expect(actual.Annotations).NotTo(HaveKey(prePauseReconcilePolicyAnnotation)) 1343 g.Expect(actual.Annotations).To(HaveKeyWithValue(asoannotations.ReconcilePolicy, string(asoannotations.ReconcilePolicyManage))) 1344 }, 1345 }, 1346 } 1347 1348 for _, test := range tests { 1349 t.Run(test.name, func(t *testing.T) { 1350 g := NewWithT(t) 1351 1352 ctx := context.Background() 1353 svcName := "service" 1354 1355 ctrlClient := test.clientBuilder(g) 1356 1357 s := New[*asoresourcesv1.ResourceGroup](ctrlClient, clusterName, newOwner()) 1358 1359 err := s.PauseResource(ctx, test.resource, svcName) 1360 if test.expectedErr != "" { 1361 g.Expect(err.Error()).To(ContainSubstring(test.expectedErr)) 1362 } else { 1363 g.Expect(err).NotTo(HaveOccurred()) 1364 } 1365 if test.verify != nil { 1366 test.verify(g, ctrlClient, test.resource) 1367 } 1368 }) 1369 } 1370 }