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