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  }