diff --git a/api/v1alpha4/conditions_consts.go b/api/v1alpha4/conditions_consts.go index bdcdf1eecc7..d6f94b9ce8b 100644 --- a/api/v1alpha4/conditions_consts.go +++ b/api/v1alpha4/conditions_consts.go @@ -20,6 +20,8 @@ import clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha4" // AzureCluster Conditions and Reasons. const ( + // NetworkInfrastructureReadyCondition reports of current status of cluster infrastructure. + NetworkInfrastructureReadyCondition clusterv1.ConditionType = "NetworkInfrastructureReady" // NamespaceNotAllowedByIdentity used to indicate cluster in a namespace not allowed by identity. NamespaceNotAllowedByIdentity = "NamespaceNotAllowedByIdentity" ) diff --git a/azure/scope/cluster.go b/azure/scope/cluster.go index 56b6fce782a..1685f26e275 100644 --- a/azure/scope/cluster.go +++ b/azure/scope/cluster.go @@ -36,6 +36,7 @@ import ( infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/groups" "sigs.k8s.io/cluster-api-provider-azure/util/futures" ) @@ -286,6 +287,16 @@ func (s *ClusterScope) SubnetSpecs() []azure.SubnetSpec { return subnetSpecs } +// GroupSpec returns the resource group spec. +func (s *ClusterScope) GroupSpec() azure.ResourceSpecGetter { + return &groups.GroupSpec{ + Name: s.ResourceGroup(), + Location: s.Location(), + ClusterName: s.ClusterName(), + AdditionalTags: s.AdditionalTags(), + } +} + // VNetSpec returns the virtual network spec. func (s *ClusterScope) VNetSpec() azure.VNetSpec { return azure.VNetSpec{ @@ -539,13 +550,20 @@ func (s *ClusterScope) ListOptionsLabelSelector() client.ListOption { // PatchObject persists the cluster configuration and status. func (s *ClusterScope) PatchObject(ctx context.Context) error { - conditions.SetSummary(s.AzureCluster) + conditions.SetSummary(s.AzureCluster, + conditions.WithConditions( + infrav1.ResourceGroupReadyCondition, + infrav1.NetworkInfrastructureReadyCondition, + ), + ) return s.patchHelper.Patch( ctx, s.AzureCluster, patch.WithOwnedConditions{Conditions: []clusterv1.ConditionType{ clusterv1.ReadyCondition, + infrav1.ResourceGroupReadyCondition, + infrav1.NetworkInfrastructureReadyCondition, }}) } diff --git a/azure/scope/managedcontrolplane.go b/azure/scope/managedcontrolplane.go index 5cd98970fb6..93114f6ec37 100644 --- a/azure/scope/managedcontrolplane.go +++ b/azure/scope/managedcontrolplane.go @@ -38,6 +38,7 @@ import ( infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/groups" infrav1exp "sigs.k8s.io/cluster-api-provider-azure/exp/api/v1alpha4" "sigs.k8s.io/cluster-api-provider-azure/util/futures" ) @@ -197,6 +198,16 @@ func (s *ManagedControlPlaneScope) Vnet() *infrav1.VnetSpec { } } +// GroupSpec returns the resource group spec. +func (s *ManagedControlPlaneScope) GroupSpec() azure.ResourceSpecGetter { + return &groups.GroupSpec{ + Name: s.ResourceGroup(), + Location: s.Location(), + ClusterName: s.ClusterName(), + AdditionalTags: s.AdditionalTags(), + } +} + // VNetSpec returns the virtual network spec. func (s *ManagedControlPlaneScope) VNetSpec() azure.VNetSpec { return azure.VNetSpec{ diff --git a/azure/services/groups/client.go b/azure/services/groups/client.go index ab648f60e9c..94e81e66c5c 100644 --- a/azure/services/groups/client.go +++ b/azure/services/groups/client.go @@ -21,16 +21,20 @@ import ( "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-05-01/resources" "github.com/Azure/go-autorest/autorest" + azureautorest "github.com/Azure/go-autorest/autorest/azure" + "github.com/pkg/errors" "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/util/reconciler" "sigs.k8s.io/cluster-api-provider-azure/util/tele" ) // client wraps go-sdk. type client interface { Get(context.Context, string) (resources.Group, error) - CreateOrUpdate(context.Context, string, resources.Group) (resources.Group, error) - Delete(context.Context, string) error + CreateOrUpdateAsync(context.Context, azure.ResourceSpecGetter) (azureautorest.FutureAPI, error) + DeleteAsync(context.Context, azure.ResourceSpecGetter) (azureautorest.FutureAPI, error) + IsDone(context.Context, azureautorest.FutureAPI) (bool, error) } // azureClient contains the Azure go-sdk Client. @@ -63,27 +67,97 @@ func (ac *azureClient) Get(ctx context.Context, name string) (resources.Group, e return ac.groups.Get(ctx, name) } -// CreateOrUpdate creates or updates a resource group. -func (ac *azureClient) CreateOrUpdate(ctx context.Context, name string, group resources.Group) (resources.Group, error) { +// CreateOrUpdateAsync creates or updates a resource group. +// Creating a resource group is not a long running operation, so we don't ever return a future. +func (ac *azureClient) CreateOrUpdateAsync(ctx context.Context, spec azure.ResourceSpecGetter) (azureautorest.FutureAPI, error) { ctx, span := tele.Tracer().Start(ctx, "groups.AzureClient.CreateOrUpdate") defer span.End() - return ac.groups.CreateOrUpdate(ctx, name, group) + group, err := ac.resourceGroupParams(ctx, spec) + if err != nil { + return nil, errors.Wrapf(err, "failed to get desired parameters for group %s", spec.ResourceName()) + } else if group == nil { + // nothing to do here + return nil, nil + } + + _, err = ac.groups.CreateOrUpdate(ctx, spec.ResourceName(), *group) + return nil, err } -// Delete deletes a resource group. When you delete a resource group, all of its resources are also deleted. -func (ac *azureClient) Delete(ctx context.Context, name string) error { - ctx, span := tele.Tracer().Start(ctx, "groups.AzureClient.Delete") +// DeleteAsync deletes a resource group asynchronously. DeleteAsync sends a DELETE +// request to Azure and if accepted without error, the func will return a Future which can be used to track the ongoing +// progress of the operation. +// +// NOTE: When you delete a resource group, all of its resources are also deleted. +func (ac *azureClient) DeleteAsync(ctx context.Context, spec azure.ResourceSpecGetter) (azureautorest.FutureAPI, error) { + ctx, span := tele.Tracer().Start(ctx, "groups.AzureClient.DeleteAsync") defer span.End() - future, err := ac.groups.Delete(ctx, name) + future, err := ac.groups.Delete(ctx, spec.ResourceName()) if err != nil { - return err + return nil, err } + + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureCallTimeout) + defer cancel() + err = future.WaitForCompletionRef(ctx, ac.groups.Client) if err != nil { - return err + // if an error occurs, return the future. + // this means the long-running operation didn't finish in the specified timeout. + return &future, err } _, err = future.Result(ac.groups) - return err + // if the operation completed, return a nil future. + return nil, err +} + +// IsDone returns true if the long-running operation has completed. +func (ac *azureClient) IsDone(ctx context.Context, future azureautorest.FutureAPI) (bool, error) { + ctx, span := tele.Tracer().Start(ctx, "groups.AzureClient.IsDone") + defer span.End() + + done, err := future.DoneWithContext(ctx, ac.groups) + if err != nil { + return false, errors.Wrap(err, "failed checking if the operation was complete") + } + + return done, nil +} + +// resourceGroupParams returns the desired resource group parameters from the given spec. +func (ac *azureClient) resourceGroupParams(ctx context.Context, spec azure.ResourceSpecGetter) (*resources.Group, error) { + ctx, span := tele.Tracer().Start(ctx, "groups.AzureClient.resourceGroupParams") + defer span.End() + + var params interface{} + + existingRG, err := ac.Get(ctx, spec.ResourceName()) + if azure.ResourceNotFound(err) { + // rg doesn't exist, create it from scratch. + params, err = spec.Parameters(nil) + if err != nil { + return nil, err + } + } else if err != nil { + return nil, errors.Wrapf(err, "failed to get RG %s", spec.ResourceName()) + } else { + // rg already exists + params, err = spec.Parameters(existingRG) + if err != nil { + return nil, err + } + } + + rg, ok := params.(resources.Group) + if !ok { + if params == nil { + // nothing to do here. + return nil, nil + } + return nil, errors.Errorf("%T is not a resources.Group", params) + } + + return &rg, nil } diff --git a/azure/services/groups/groups.go b/azure/services/groups/groups.go index 9386be22cda..aaba4805761 100644 --- a/azure/services/groups/groups.go +++ b/azure/services/groups/groups.go @@ -19,17 +19,19 @@ package groups import ( "context" - "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-05-01/resources" - "github.com/Azure/go-autorest/autorest/to" "github.com/go-logr/logr" "github.com/pkg/errors" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" "sigs.k8s.io/cluster-api-provider-azure/azure" "sigs.k8s.io/cluster-api-provider-azure/azure/converters" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/async" + "sigs.k8s.io/cluster-api-provider-azure/util/reconciler" "sigs.k8s.io/cluster-api-provider-azure/util/tele" ) +const serviceName = "group" + // Service provides operations on Azure resources. type Service struct { Scope GroupScope @@ -39,7 +41,10 @@ type Service struct { // GroupScope defines the scope interface for a group service. type GroupScope interface { logr.Logger - azure.ClusterDescriber + azure.Authorizer + azure.AsyncStatusUpdater + GroupSpec() azure.ResourceSpecGetter + ClusterName() string } // New creates a new service. @@ -55,65 +60,45 @@ func (s *Service) Reconcile(ctx context.Context) error { ctx, span := tele.Tracer().Start(ctx, "groups.Service.Reconcile") defer span.End() - if _, err := s.client.Get(ctx, s.Scope.ResourceGroup()); err == nil { - // resource group already exists, skip creation - return nil - } else if !azure.ResourceNotFound(err) { - return errors.Wrapf(err, "failed to get resource group %s", s.Scope.ResourceGroup()) - } + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureServiceReconcileTimeout) + defer cancel() - s.Scope.V(2).Info("creating resource group", "resource group", s.Scope.ResourceGroup()) - group := resources.Group{ - Location: to.StringPtr(s.Scope.Location()), - Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{ - ClusterName: s.Scope.ClusterName(), - Lifecycle: infrav1.ResourceLifecycleOwned, - Name: to.StringPtr(s.Scope.ResourceGroup()), - Role: to.StringPtr(infrav1.CommonRole), - Additional: s.Scope.AdditionalTags(), - })), - } + groupSpec := s.Scope.GroupSpec() - _, err := s.client.CreateOrUpdate(ctx, s.Scope.ResourceGroup(), group) - if err != nil { - return errors.Wrapf(err, "failed to create resource group %s", s.Scope.ResourceGroup()) - } - - s.Scope.V(2).Info("successfully created resource group", "resource group", s.Scope.ResourceGroup()) - return nil + err := async.CreateResource(ctx, s.Scope, s.client, groupSpec, serviceName) + s.Scope.UpdatePutStatus(infrav1.ResourceGroupReadyCondition, serviceName, err) + return err } -// Delete deletes the resource group with the provided name. +// Delete deletes the resource group if it is managed by capz. func (s *Service) Delete(ctx context.Context) error { ctx, span := tele.Tracer().Start(ctx, "groups.Service.Delete") defer span.End() + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureServiceReconcileTimeout) + defer cancel() + + groupSpec := s.Scope.GroupSpec() + + // check that the resource group is not BYO. managed, err := s.IsGroupManaged(ctx) - if err != nil && azure.ResourceNotFound(err) { - // already deleted or doesn't exist - return nil - } if err != nil { + if azure.ResourceNotFound(err) { + // already deleted or doesn't exist, cleanup status and return. + s.Scope.DeleteLongRunningOperationState(groupSpec.ResourceName(), serviceName) + s.Scope.UpdateDeleteStatus(infrav1.ResourceGroupReadyCondition, serviceName, nil) + return nil + } return errors.Wrap(err, "could not get resource group management state") } - if !managed { s.Scope.V(2).Info("Should not delete resource group in unmanaged mode") return azure.ErrNotOwned } - s.Scope.V(2).Info("deleting resource group", "resource group", s.Scope.ResourceGroup()) - err = s.client.Delete(ctx, s.Scope.ResourceGroup()) - if err != nil && azure.ResourceNotFound(err) { - // already deleted - return nil - } - if err != nil { - return errors.Wrapf(err, "failed to delete resource group %s", s.Scope.ResourceGroup()) - } - - s.Scope.V(2).Info("successfully deleted resource group", "resource group", s.Scope.ResourceGroup()) - return nil + err = async.DeleteResource(ctx, s.Scope, s.client, groupSpec, serviceName) + s.Scope.UpdateDeleteStatus(infrav1.ResourceGroupReadyCondition, serviceName, err) + return err } // IsGroupManaged returns true if the resource group has an owned tag with the cluster name as value, @@ -122,7 +107,8 @@ func (s *Service) IsGroupManaged(ctx context.Context) (bool, error) { ctx, span := tele.Tracer().Start(ctx, "groups.Service.IsGroupManaged") defer span.End() - group, err := s.client.Get(ctx, s.Scope.ResourceGroup()) + groupSpec := s.Scope.GroupSpec() + group, err := s.client.Get(ctx, groupSpec.ResourceName()) if err != nil { return false, err } diff --git a/azure/services/groups/groups_test.go b/azure/services/groups/groups_test.go index 4bddbc0743a..fb6654dec35 100644 --- a/azure/services/groups/groups_test.go +++ b/azure/services/groups/groups_test.go @@ -18,22 +18,56 @@ package groups import ( "context" + "errors" + "fmt" "net/http" "testing" "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-05-01/resources" "github.com/Azure/go-autorest/autorest" + azureautorest "github.com/Azure/go-autorest/autorest/azure" + "github.com/Azure/go-autorest/autorest/to" "github.com/golang/mock/gomock" . "github.com/onsi/gomega" "k8s.io/klog/v2/klogr" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" "sigs.k8s.io/cluster-api-provider-azure/azure" - "sigs.k8s.io/cluster-api-provider-azure/azure/converters" "sigs.k8s.io/cluster-api-provider-azure/azure/services/groups/mock_groups" gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock" ) +var ( + fakeGroupSpec = GroupSpec{ + Name: "test-group", + Location: "test-location", + ClusterName: "test-cluster", + AdditionalTags: map[string]string{"foo": "bar"}, + } + internalError = autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error") + notFoundError = autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not Found") + errCtxExceeded = errors.New("ctx exceeded") + fakeFuture = infrav1.Future{ + Type: infrav1.DeleteFuture, + ServiceName: serviceName, + Name: "test-group", + ResourceGroup: "test-group", + Data: "eyJtZXRob2QiOiJERUxFVEUiLCJwb2xsaW5nTWV0aG9kIjoiTG9jYXRpb24iLCJscm9TdGF0ZSI6IkluUHJvZ3Jlc3MifQ==", + } + sampleManagedGroup = resources.Group{ + Name: to.StringPtr("test-group"), + Location: to.StringPtr("test-location"), + Properties: &resources.GroupProperties{}, + Tags: map[string]*string{"sigs.k8s.io_cluster-api-provider-azure_cluster_test-cluster": to.StringPtr("owned")}, + } + sampleBYOGroup = resources.Group{ + Name: to.StringPtr("test-group"), + Location: to.StringPtr("test-location"), + Properties: &resources.GroupProperties{}, + Tags: map[string]*string{"foo": to.StringPtr("bar")}, + } +) + func TestReconcileGroups(t *testing.T) { testcases := []struct { name string @@ -41,50 +75,25 @@ func TestReconcileGroups(t *testing.T) { expect func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) }{ { - name: "resource group already exist", + name: "create group succeeds", expectedError: "", expect: func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) - s.ResourceGroup().Return("my-rg") - m.Get(gomockinternal.AContext(), "my-rg").Return(resources.Group{}, nil) - }, - }, - { - name: "return error when querying a resource group", - expectedError: "failed to get resource group my-rg: #: Internal Server Error: StatusCode=500", - expect: func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) { - s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) - s.ResourceGroup().AnyTimes().Return("my-rg") - s.Location().AnyTimes().Return("fake-location") - s.ClusterName().AnyTimes().Return("fake-cluster") - m.Get(gomockinternal.AContext(), "my-rg").Return(resources.Group{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) + s.GroupSpec().Return(&fakeGroupSpec) + s.GetLongRunningOperationState("test-group", serviceName) + m.CreateOrUpdateAsync(gomockinternal.AContext(), &fakeGroupSpec).Return(nil, nil) + s.UpdatePutStatus(infrav1.ResourceGroupReadyCondition, serviceName, nil) }, }, - { - name: "create a resource group", - expectedError: "", + name: "create resource group fails", + expectedError: "failed to create resource test-group/test-group (service: group): #: Internal Server Error: StatusCode=500", expect: func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) - s.ResourceGroup().AnyTimes().Return("my-rg") - s.Location().AnyTimes().Return("fake-location") - s.ClusterName().AnyTimes().Return("fake-cluster") - s.AdditionalTags().AnyTimes().Return(infrav1.Tags{}) - m.Get(gomockinternal.AContext(), "my-rg").Return(resources.Group{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) - m.CreateOrUpdate(gomockinternal.AContext(), "my-rg", gomock.AssignableToTypeOf(resources.Group{})).Return(resources.Group{}, nil) - }, - }, - { - name: "return error when creating a resource group", - expectedError: "failed to create resource group my-rg: #: Internal Server Error: StatusCode=500", - expect: func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) { - s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) - s.ResourceGroup().AnyTimes().Return("my-rg") - s.Location().AnyTimes().Return("fake-location") - s.ClusterName().AnyTimes().Return("fake-cluster") - s.AdditionalTags().AnyTimes().Return(infrav1.Tags{}) - m.Get(gomockinternal.AContext(), "my-rg").Return(resources.Group{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not found")) - m.CreateOrUpdate(gomockinternal.AContext(), "my-rg", gomock.AssignableToTypeOf(resources.Group{})).Return(resources.Group{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) + s.GroupSpec().Return(&fakeGroupSpec) + s.GetLongRunningOperationState("test-group", serviceName) + m.CreateOrUpdateAsync(gomockinternal.AContext(), &fakeGroupSpec).Return(nil, internalError) + s.UpdatePutStatus(infrav1.ResourceGroupReadyCondition, serviceName, gomockinternal.ErrStrEq(fmt.Sprintf("failed to create resource test-group/test-group (service: group): %s", internalError.Error()))) }, }, } @@ -125,89 +134,100 @@ func TestDeleteGroups(t *testing.T) { expect func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) }{ { - name: "error getting the resource group management state", - expectedError: "could not get resource group management state: #: Internal Server Error: StatusCode=500", + name: "long running delete operation is done", + expectedError: "", + expect: func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.GroupSpec().AnyTimes().Return(&fakeGroupSpec) + m.Get(gomockinternal.AContext(), "test-group").Return(sampleManagedGroup, nil) + s.ClusterName().Return("test-cluster") + s.GetLongRunningOperationState("test-group", serviceName).Times(2).Return(&fakeFuture) + m.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(true, nil) + s.DeleteLongRunningOperationState("test-group", serviceName) + s.UpdateDeleteStatus(infrav1.ResourceGroupReadyCondition, serviceName, nil) + }, + }, + { + name: "long running delete operation is not done", + expectedError: "transient reconcile error occurred: operation type DELETE on Azure resource test-group/test-group is not done. Object will be requeued after 15s", expect: func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) - s.ResourceGroup().AnyTimes().Return("my-rg") - m.Get(gomockinternal.AContext(), "my-rg").Return(resources.Group{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")) + s.GroupSpec().AnyTimes().Return(&fakeGroupSpec) + m.Get(gomockinternal.AContext(), "test-group").Return(sampleManagedGroup, nil) + s.ClusterName().Return("test-cluster") + s.GetLongRunningOperationState("test-group", serviceName).Times(2).Return(&fakeFuture) + m.IsDone(gomockinternal.AContext(), gomock.AssignableToTypeOf(&azureautorest.Future{})).Return(false, nil) + s.UpdateDeleteStatus(infrav1.ResourceGroupReadyCondition, serviceName, gomockinternal.ErrStrEq("transient reconcile error occurred: operation type DELETE on Azure resource test-group/test-group is not done. Object will be requeued after 15s")) }, }, { - name: "skip deletion in unmanaged mode", + name: "resource group is not managed by capz", expectedError: azure.ErrNotOwned.Error(), expect: func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) - s.ResourceGroup().AnyTimes().Return("my-rg") - s.ClusterName().AnyTimes().Return("fake-cluster") - m.Get(gomockinternal.AContext(), "my-rg").Return(resources.Group{}, nil) + s.GroupSpec().AnyTimes().Return(&fakeGroupSpec) + m.Get(gomockinternal.AContext(), "test-group").Return(sampleBYOGroup, nil) + s.ClusterName().Return("test-cluster") }, }, { - name: "resource group already deleted", - expectedError: "", + name: "fail to check if resource group is managed", + expectedError: "could not get resource group management state", expect: func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) - s.ResourceGroup().AnyTimes().Return("my-rg") - s.ClusterName().AnyTimes().Return("fake-cluster") - gomock.InOrder( - m.Get(gomockinternal.AContext(), "my-rg").Return(resources.Group{ - Tags: converters.TagsToMap(infrav1.Tags{ - "Name": "my-rg", - "sigs.k8s.io_cluster-api-provider-azure_cluster_fake-cluster": "owned", - "sigs.k8s.io_cluster-api-provider-azure_role": "common", - }), - }, nil), - m.Delete(gomockinternal.AContext(), "my-rg").Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not Found")), - ) + s.GroupSpec().AnyTimes().Return(&fakeGroupSpec) + m.Get(gomockinternal.AContext(), "test-group").Return(resources.Group{}, internalError) }, }, { - name: "resource group get returns error", + name: "resource group doesn't exist", expectedError: "", expect: func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) - s.ResourceGroup().AnyTimes().Return("my-rg") - s.ClusterName().AnyTimes().Return("fake-cluster") - m.Get(gomockinternal.AContext(), "my-rg").Return(resources.Group{}, autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 404}, "Not Found")) + s.GroupSpec().AnyTimes().Return(&fakeGroupSpec) + m.Get(gomockinternal.AContext(), "test-group").Return(resources.Group{}, notFoundError) + s.DeleteLongRunningOperationState("test-group", serviceName) + s.UpdateDeleteStatus(infrav1.ResourceGroupReadyCondition, serviceName, nil) }, }, { - name: "resource group deletion fails", - expectedError: "failed to delete resource group my-rg: #: Internal Server Error: StatusCode=500", + name: "error occurs when deleting resource group", + expectedError: "failed to delete resource test-group/test-group (service: group): #: Internal Server Error: StatusCode=500", expect: func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) - s.ResourceGroup().AnyTimes().Return("my-rg") - s.ClusterName().AnyTimes().Return("fake-cluster") - gomock.InOrder( - m.Get(gomockinternal.AContext(), "my-rg").Return(resources.Group{ - Tags: converters.TagsToMap(infrav1.Tags{ - "Name": "my-rg", - "sigs.k8s.io_cluster-api-provider-azure_cluster_fake-cluster": "owned", - "sigs.k8s.io_cluster-api-provider-azure_role": "common", - }), - }, nil), - m.Delete(gomockinternal.AContext(), "my-rg").Return(autorest.NewErrorWithResponse("", "", &http.Response{StatusCode: 500}, "Internal Server Error")), - ) + s.GroupSpec().AnyTimes().Return(&fakeGroupSpec) + s.GetLongRunningOperationState("test-group", serviceName).Return(nil) + m.Get(gomockinternal.AContext(), "test-group").Return(sampleManagedGroup, nil) + s.ClusterName().Return("test-cluster") + m.DeleteAsync(gomockinternal.AContext(), &fakeGroupSpec).Return(nil, internalError) + s.UpdateDeleteStatus(infrav1.ResourceGroupReadyCondition, serviceName, gomockinternal.ErrStrEq("failed to delete resource test-group/test-group (service: group): #: Internal Server Error: StatusCode=500")) }, }, { - name: "resource group deletion successfully", + name: "context deadline exceeded while deleting resource group", + expectedError: "transient reconcile error occurred: operation type DELETE on Azure resource test-group/test-group is not done. Object will be requeued after 15s", + expect: func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) { + s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) + s.GroupSpec().AnyTimes().Return(&fakeGroupSpec) + s.GetLongRunningOperationState("test-group", serviceName).Return(nil) + m.Get(gomockinternal.AContext(), "test-group").Return(sampleManagedGroup, nil) + s.ClusterName().Return("test-cluster") + m.DeleteAsync(gomockinternal.AContext(), &fakeGroupSpec).Return(&azureautorest.Future{}, errCtxExceeded) + s.SetLongRunningOperationState(gomock.AssignableToTypeOf(&infrav1.Future{})) + s.UpdateDeleteStatus(infrav1.ResourceGroupReadyCondition, serviceName, gomockinternal.ErrStrEq("transient reconcile error occurred: operation type DELETE on Azure resource test-group/test-group is not done. Object will be requeued after 15s")) + }, + }, + { + name: "delete the resource group successfully", expectedError: "", expect: func(s *mock_groups.MockGroupScopeMockRecorder, m *mock_groups.MockclientMockRecorder) { s.V(gomock.AssignableToTypeOf(2)).AnyTimes().Return(klogr.New()) - s.ResourceGroup().AnyTimes().Return("my-rg") - s.ClusterName().AnyTimes().Return("fake-cluster") - gomock.InOrder( - m.Get(gomockinternal.AContext(), "my-rg").Return(resources.Group{ - Tags: converters.TagsToMap(infrav1.Tags{ - "Name": "my-rg", - "sigs.k8s.io_cluster-api-provider-azure_cluster_fake-cluster": "owned", - "sigs.k8s.io_cluster-api-provider-azure_role": "common", - }), - }, nil), - m.Delete(gomockinternal.AContext(), "my-rg").Return(nil), - ) + s.GroupSpec().AnyTimes().Return(&fakeGroupSpec) + s.GetLongRunningOperationState("test-group", serviceName).Return(nil) + m.Get(gomockinternal.AContext(), "test-group").Return(sampleManagedGroup, nil) + s.ClusterName().Return("test-cluster") + m.DeleteAsync(gomockinternal.AContext(), &fakeGroupSpec).Return(nil, nil) + s.UpdateDeleteStatus(infrav1.ResourceGroupReadyCondition, serviceName, nil) }, }, } @@ -233,7 +253,7 @@ func TestDeleteGroups(t *testing.T) { err := s.Delete(context.TODO()) if tc.expectedError != "" { g.Expect(err).To(HaveOccurred()) - g.Expect(err).To(MatchError(tc.expectedError)) + g.Expect(err.Error()).To(ContainSubstring(tc.expectedError)) } else { g.Expect(err).NotTo(HaveOccurred()) } diff --git a/azure/services/groups/mock_groups/client_mock.go b/azure/services/groups/mock_groups/client_mock.go index b0ff7fe680c..e16d83a17df 100644 --- a/azure/services/groups/mock_groups/client_mock.go +++ b/azure/services/groups/mock_groups/client_mock.go @@ -25,7 +25,9 @@ import ( reflect "reflect" resources "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-05-01/resources" + azure "github.com/Azure/go-autorest/autorest/azure" gomock "github.com/golang/mock/gomock" + azure0 "sigs.k8s.io/cluster-api-provider-azure/azure" ) // Mockclient is a mock of client interface. @@ -51,33 +53,34 @@ func (m *Mockclient) EXPECT() *MockclientMockRecorder { return m.recorder } -// CreateOrUpdate mocks base method. -func (m *Mockclient) CreateOrUpdate(arg0 context.Context, arg1 string, arg2 resources.Group) (resources.Group, error) { +// CreateOrUpdateAsync mocks base method. +func (m *Mockclient) CreateOrUpdateAsync(arg0 context.Context, arg1 azure0.ResourceSpecGetter) (azure.FutureAPI, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateOrUpdate", arg0, arg1, arg2) - ret0, _ := ret[0].(resources.Group) + ret := m.ctrl.Call(m, "CreateOrUpdateAsync", arg0, arg1) + ret0, _ := ret[0].(azure.FutureAPI) ret1, _ := ret[1].(error) return ret0, ret1 } -// CreateOrUpdate indicates an expected call of CreateOrUpdate. -func (mr *MockclientMockRecorder) CreateOrUpdate(arg0, arg1, arg2 interface{}) *gomock.Call { +// CreateOrUpdateAsync indicates an expected call of CreateOrUpdateAsync. +func (mr *MockclientMockRecorder) CreateOrUpdateAsync(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdate", reflect.TypeOf((*Mockclient)(nil).CreateOrUpdate), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateAsync", reflect.TypeOf((*Mockclient)(nil).CreateOrUpdateAsync), arg0, arg1) } -// Delete mocks base method. -func (m *Mockclient) Delete(arg0 context.Context, arg1 string) error { +// DeleteAsync mocks base method. +func (m *Mockclient) DeleteAsync(arg0 context.Context, arg1 azure0.ResourceSpecGetter) (azure.FutureAPI, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 + ret := m.ctrl.Call(m, "DeleteAsync", arg0, arg1) + ret0, _ := ret[0].(azure.FutureAPI) + ret1, _ := ret[1].(error) + return ret0, ret1 } -// Delete indicates an expected call of Delete. -func (mr *MockclientMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { +// DeleteAsync indicates an expected call of DeleteAsync. +func (mr *MockclientMockRecorder) DeleteAsync(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*Mockclient)(nil).Delete), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAsync", reflect.TypeOf((*Mockclient)(nil).DeleteAsync), arg0, arg1) } // Get mocks base method. @@ -94,3 +97,18 @@ func (mr *MockclientMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*Mockclient)(nil).Get), arg0, arg1) } + +// IsDone mocks base method. +func (m *Mockclient) IsDone(arg0 context.Context, arg1 azure.FutureAPI) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsDone", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsDone indicates an expected call of IsDone. +func (mr *MockclientMockRecorder) IsDone(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDone", reflect.TypeOf((*Mockclient)(nil).IsDone), arg0, arg1) +} diff --git a/azure/services/groups/mock_groups/groups_mock.go b/azure/services/groups/mock_groups/groups_mock.go index 4879cd69dde..58229d9597f 100644 --- a/azure/services/groups/mock_groups/groups_mock.go +++ b/azure/services/groups/mock_groups/groups_mock.go @@ -27,6 +27,8 @@ import ( logr "github.com/go-logr/logr" gomock "github.com/golang/mock/gomock" v1alpha4 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" + azure "sigs.k8s.io/cluster-api-provider-azure/azure" + v1alpha40 "sigs.k8s.io/cluster-api/api/v1alpha4" ) // MockGroupScope is a mock of GroupScope interface. @@ -52,20 +54,6 @@ func (m *MockGroupScope) EXPECT() *MockGroupScopeMockRecorder { return m.recorder } -// AdditionalTags mocks base method. -func (m *MockGroupScope) AdditionalTags() v1alpha4.Tags { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AdditionalTags") - ret0, _ := ret[0].(v1alpha4.Tags) - return ret0 -} - -// AdditionalTags indicates an expected call of AdditionalTags. -func (mr *MockGroupScopeMockRecorder) AdditionalTags() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdditionalTags", reflect.TypeOf((*MockGroupScope)(nil).AdditionalTags)) -} - // Authorizer mocks base method. func (m *MockGroupScope) Authorizer() autorest.Authorizer { m.ctrl.T.Helper() @@ -80,20 +68,6 @@ func (mr *MockGroupScopeMockRecorder) Authorizer() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authorizer", reflect.TypeOf((*MockGroupScope)(nil).Authorizer)) } -// AvailabilitySetEnabled mocks base method. -func (m *MockGroupScope) AvailabilitySetEnabled() bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AvailabilitySetEnabled") - ret0, _ := ret[0].(bool) - return ret0 -} - -// AvailabilitySetEnabled indicates an expected call of AvailabilitySetEnabled. -func (mr *MockGroupScopeMockRecorder) AvailabilitySetEnabled() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AvailabilitySetEnabled", reflect.TypeOf((*MockGroupScope)(nil).AvailabilitySetEnabled)) -} - // BaseURI mocks base method. func (m *MockGroupScope) BaseURI() string { m.ctrl.T.Helper() @@ -150,20 +124,6 @@ func (mr *MockGroupScopeMockRecorder) CloudEnvironment() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloudEnvironment", reflect.TypeOf((*MockGroupScope)(nil).CloudEnvironment)) } -// CloudProviderConfigOverrides mocks base method. -func (m *MockGroupScope) CloudProviderConfigOverrides() *v1alpha4.CloudProviderConfigOverrides { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CloudProviderConfigOverrides") - ret0, _ := ret[0].(*v1alpha4.CloudProviderConfigOverrides) - return ret0 -} - -// CloudProviderConfigOverrides indicates an expected call of CloudProviderConfigOverrides. -func (mr *MockGroupScopeMockRecorder) CloudProviderConfigOverrides() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloudProviderConfigOverrides", reflect.TypeOf((*MockGroupScope)(nil).CloudProviderConfigOverrides)) -} - // ClusterName mocks base method. func (m *MockGroupScope) ClusterName() string { m.ctrl.T.Helper() @@ -178,6 +138,18 @@ func (mr *MockGroupScopeMockRecorder) ClusterName() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterName", reflect.TypeOf((*MockGroupScope)(nil).ClusterName)) } +// DeleteLongRunningOperationState mocks base method. +func (m *MockGroupScope) DeleteLongRunningOperationState(arg0, arg1 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DeleteLongRunningOperationState", arg0, arg1) +} + +// DeleteLongRunningOperationState indicates an expected call of DeleteLongRunningOperationState. +func (mr *MockGroupScopeMockRecorder) DeleteLongRunningOperationState(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLongRunningOperationState", reflect.TypeOf((*MockGroupScope)(nil).DeleteLongRunningOperationState), arg0, arg1) +} + // Enabled mocks base method. func (m *MockGroupScope) Enabled() bool { m.ctrl.T.Helper() @@ -209,6 +181,34 @@ func (mr *MockGroupScopeMockRecorder) Error(err, msg interface{}, keysAndValues return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockGroupScope)(nil).Error), varargs...) } +// GetLongRunningOperationState mocks base method. +func (m *MockGroupScope) GetLongRunningOperationState(arg0, arg1 string) *v1alpha4.Future { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLongRunningOperationState", arg0, arg1) + ret0, _ := ret[0].(*v1alpha4.Future) + return ret0 +} + +// GetLongRunningOperationState indicates an expected call of GetLongRunningOperationState. +func (mr *MockGroupScopeMockRecorder) GetLongRunningOperationState(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLongRunningOperationState", reflect.TypeOf((*MockGroupScope)(nil).GetLongRunningOperationState), arg0, arg1) +} + +// GroupSpec mocks base method. +func (m *MockGroupScope) GroupSpec() azure.ResourceSpecGetter { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GroupSpec") + ret0, _ := ret[0].(azure.ResourceSpecGetter) + return ret0 +} + +// GroupSpec indicates an expected call of GroupSpec. +func (mr *MockGroupScopeMockRecorder) GroupSpec() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GroupSpec", reflect.TypeOf((*MockGroupScope)(nil).GroupSpec)) +} + // HashKey mocks base method. func (m *MockGroupScope) HashKey() string { m.ctrl.T.Helper() @@ -240,32 +240,16 @@ func (mr *MockGroupScopeMockRecorder) Info(msg interface{}, keysAndValues ...int return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockGroupScope)(nil).Info), varargs...) } -// Location mocks base method. -func (m *MockGroupScope) Location() string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Location") - ret0, _ := ret[0].(string) - return ret0 -} - -// Location indicates an expected call of Location. -func (mr *MockGroupScopeMockRecorder) Location() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Location", reflect.TypeOf((*MockGroupScope)(nil).Location)) -} - -// ResourceGroup mocks base method. -func (m *MockGroupScope) ResourceGroup() string { +// SetLongRunningOperationState mocks base method. +func (m *MockGroupScope) SetLongRunningOperationState(arg0 *v1alpha4.Future) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ResourceGroup") - ret0, _ := ret[0].(string) - return ret0 + m.ctrl.Call(m, "SetLongRunningOperationState", arg0) } -// ResourceGroup indicates an expected call of ResourceGroup. -func (mr *MockGroupScopeMockRecorder) ResourceGroup() *gomock.Call { +// SetLongRunningOperationState indicates an expected call of SetLongRunningOperationState. +func (mr *MockGroupScopeMockRecorder) SetLongRunningOperationState(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResourceGroup", reflect.TypeOf((*MockGroupScope)(nil).ResourceGroup)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLongRunningOperationState", reflect.TypeOf((*MockGroupScope)(nil).SetLongRunningOperationState), arg0) } // SubscriptionID mocks base method. @@ -296,6 +280,42 @@ func (mr *MockGroupScopeMockRecorder) TenantID() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TenantID", reflect.TypeOf((*MockGroupScope)(nil).TenantID)) } +// UpdateDeleteStatus mocks base method. +func (m *MockGroupScope) UpdateDeleteStatus(arg0 v1alpha40.ConditionType, arg1 string, arg2 error) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdateDeleteStatus", arg0, arg1, arg2) +} + +// UpdateDeleteStatus indicates an expected call of UpdateDeleteStatus. +func (mr *MockGroupScopeMockRecorder) UpdateDeleteStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDeleteStatus", reflect.TypeOf((*MockGroupScope)(nil).UpdateDeleteStatus), arg0, arg1, arg2) +} + +// UpdatePatchStatus mocks base method. +func (m *MockGroupScope) UpdatePatchStatus(arg0 v1alpha40.ConditionType, arg1 string, arg2 error) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdatePatchStatus", arg0, arg1, arg2) +} + +// UpdatePatchStatus indicates an expected call of UpdatePatchStatus. +func (mr *MockGroupScopeMockRecorder) UpdatePatchStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePatchStatus", reflect.TypeOf((*MockGroupScope)(nil).UpdatePatchStatus), arg0, arg1, arg2) +} + +// UpdatePutStatus mocks base method. +func (m *MockGroupScope) UpdatePutStatus(arg0 v1alpha40.ConditionType, arg1 string, arg2 error) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "UpdatePutStatus", arg0, arg1, arg2) +} + +// UpdatePutStatus indicates an expected call of UpdatePutStatus. +func (mr *MockGroupScopeMockRecorder) UpdatePutStatus(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePutStatus", reflect.TypeOf((*MockGroupScope)(nil).UpdatePutStatus), arg0, arg1, arg2) +} + // V mocks base method. func (m *MockGroupScope) V(level int) logr.Logger { m.ctrl.T.Helper() diff --git a/azure/services/groups/spec.go b/azure/services/groups/spec.go new file mode 100644 index 00000000000..b61a3d97639 --- /dev/null +++ b/azure/services/groups/spec.go @@ -0,0 +1,66 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package groups + +import ( + "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-05-01/resources" + "github.com/Azure/go-autorest/autorest/to" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha4" + "sigs.k8s.io/cluster-api-provider-azure/azure/converters" +) + +// GroupSpec defines the specification for a Resource Group. +type GroupSpec struct { + Name string + Location string + ClusterName string + AdditionalTags infrav1.Tags +} + +// ResourceName returns the name of the group. +func (s *GroupSpec) ResourceName() string { + return s.Name +} + +// ResourceGroupName returns the name of the group. +// Note that it is the same as the resource name in this case. +func (s *GroupSpec) ResourceGroupName() string { + return s.Name +} + +// OwnerResourceName is a no-op for groups. +func (s *GroupSpec) OwnerResourceName() string { + return "" // not applicable +} + +// Parameters returns the parameters for the group. +func (s *GroupSpec) Parameters(existing interface{}) (interface{}, error) { + if existing != nil { + // rg already exists, nothing to update. + return nil, nil + } + return resources.Group{ + Location: to.StringPtr(s.Location), + Tags: converters.TagsToMap(infrav1.Build(infrav1.BuildParams{ + ClusterName: s.ClusterName, + Lifecycle: infrav1.ResourceLifecycleOwned, + Name: to.StringPtr(s.Name), + Role: to.StringPtr(infrav1.CommonRole), + Additional: s.AdditionalTags, + })), + }, nil +} diff --git a/controllers/azurecluster_controller.go b/controllers/azurecluster_controller.go index 0dbbc40f282..7614ff2890f 100644 --- a/controllers/azurecluster_controller.go +++ b/controllers/azurecluster_controller.go @@ -159,7 +159,7 @@ func (r *AzureClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request return reconcile.Result{}, err } if !scope.IsClusterNamespaceAllowed(ctx, r.Client, identity.Spec.AllowedNamespaces, azureCluster.Namespace) { - conditions.MarkFalse(azureCluster, clusterv1.ReadyCondition, infrav1.NamespaceNotAllowedByIdentity, clusterv1.ConditionSeverityError, "") + conditions.MarkFalse(azureCluster, infrav1.NetworkInfrastructureReadyCondition, infrav1.NamespaceNotAllowedByIdentity, clusterv1.ConditionSeverityError, "") return reconcile.Result{}, errors.New("AzureClusterIdentity list of allowed namespaces doesn't include current cluster namespace") } } else { @@ -229,6 +229,7 @@ func (r *AzureClusterReconciler) reconcileNormal(ctx context.Context, clusterSco // No errors, so mark us ready so the Cluster API Cluster Controller can pull it azureCluster.Status.Ready = true + conditions.MarkTrue(azureCluster, infrav1.NetworkInfrastructureReadyCondition) return reconcile.Result{}, nil } @@ -240,7 +241,7 @@ func (r *AzureClusterReconciler) reconcileDelete(ctx context.Context, clusterSco clusterScope.Info("Reconciling AzureCluster delete") azureCluster := clusterScope.AzureCluster - conditions.MarkFalse(azureCluster, clusterv1.ReadyCondition, clusterv1.DeletedReason, clusterv1.ConditionSeverityInfo, "") + conditions.MarkFalse(azureCluster, infrav1.NetworkInfrastructureReadyCondition, clusterv1.DeletedReason, clusterv1.ConditionSeverityInfo, "") if err := clusterScope.PatchObject(ctx); err != nil { return reconcile.Result{}, err } @@ -253,7 +254,7 @@ func (r *AzureClusterReconciler) reconcileDelete(ctx context.Context, clusterSco if err := acr.Delete(ctx); err != nil { wrappedErr := errors.Wrapf(err, "error deleting AzureCluster %s/%s", azureCluster.Namespace, azureCluster.Name) r.Recorder.Eventf(azureCluster, corev1.EventTypeWarning, "ClusterReconcilerDeleteFailed", wrappedErr.Error()) - conditions.MarkFalse(azureCluster, clusterv1.ReadyCondition, clusterv1.DeletionFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) + conditions.MarkFalse(azureCluster, infrav1.NetworkInfrastructureReadyCondition, clusterv1.DeletionFailedReason, clusterv1.ConditionSeverityWarning, err.Error()) return reconcile.Result{}, wrappedErr }