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  }