From a426dda32c935be9ae73ed39ec51f87aea300399 Mon Sep 17 00:00:00 2001 From: Jon Huhn Date: Mon, 10 Apr 2023 15:29:29 -0500 Subject: [PATCH] add asogroups --- .golangci.yml | 3 + azure/scope/cluster.go | 19 ++ azure/scope/managedcontrolplane.go | 18 ++ azure/services/asogroups/groups.go | 128 +++++++++ azure/services/asogroups/groups_test.go | 252 ++++++++++++++++++ .../services/asogroups/mock_asogroups/doc.go | 21 ++ .../asogroups/mock_asogroups/groups_mock.go | 156 +++++++++++ azure/services/asogroups/spec.go | 70 +++++ azure/services/asogroups/spec_test.go | 88 ++++++ 9 files changed, 755 insertions(+) create mode 100644 azure/services/asogroups/groups.go create mode 100644 azure/services/asogroups/groups_test.go create mode 100644 azure/services/asogroups/mock_asogroups/doc.go create mode 100644 azure/services/asogroups/mock_asogroups/groups_mock.go create mode 100644 azure/services/asogroups/spec.go create mode 100644 azure/services/asogroups/spec_test.go diff --git a/.golangci.yml b/.golangci.yml index edcbdf3aa16..81246ee054d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -90,6 +90,9 @@ linters-settings: # Azure - pkg: github.com/Azure/go-autorest/autorest/azure alias: azureautorest + # ASO + - pkg: github.com/Azure/azure-service-operator/v2/api/resources/v1api20200601 + alias: asoresourcesv1 # Deprecated - pkg: github.com/Azure/go-autorest/autorest/to alias: deprecated-use-k8s.io-utils-pointer diff --git a/azure/scope/cluster.go b/azure/scope/cluster.go index a19053dc388..0de2fdf214a 100644 --- a/azure/scope/cluster.go +++ b/azure/scope/cluster.go @@ -27,10 +27,12 @@ import ( "github.com/Azure/go-autorest/autorest" "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/net" "k8s.io/utils/pointer" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/asogroups" "sigs.k8s.io/cluster-api-provider-azure/azure/services/bastionhosts" "sigs.k8s.io/cluster-api-provider-azure/azure/services/groups" "sigs.k8s.io/cluster-api-provider-azure/azure/services/loadbalancers" @@ -134,6 +136,11 @@ func (s *ClusterScope) Authorizer() autorest.Authorizer { return s.AzureClients.Authorizer } +// GetClient returns the controller-runtime client. +func (s *ClusterScope) GetClient() client.Client { + return s.Client +} + // PublicIPSpecs returns the public IP specs. func (s *ClusterScope) PublicIPSpecs() []azure.ResourceSpecGetter { var publicIPSpecs []azure.ResourceSpecGetter @@ -420,6 +427,18 @@ func (s *ClusterScope) GroupSpec() azure.ResourceSpecGetter { } } +// ASOGroupSpec returns the resource group spec. +func (s *ClusterScope) ASOGroupSpec() azure.ASOResourceSpecGetter { + return &asogroups.GroupSpec{ + Name: s.ResourceGroup(), + Namespace: s.Namespace(), + Location: s.Location(), + ClusterName: s.ClusterName(), + AdditionalTags: s.AdditionalTags(), + Owner: *metav1.NewControllerRef(s.AzureCluster, infrav1.GroupVersion.WithKind("AzureCluster")), + } +} + // VnetPeeringSpecs returns the virtual network peering specs. func (s *ClusterScope) VnetPeeringSpecs() []azure.ResourceSpecGetter { peeringSpecs := make([]azure.ResourceSpecGetter, 2*len(s.Vnet().Peerings)) diff --git a/azure/scope/managedcontrolplane.go b/azure/scope/managedcontrolplane.go index a48baa86636..3d656f83355 100644 --- a/azure/scope/managedcontrolplane.go +++ b/azure/scope/managedcontrolplane.go @@ -30,6 +30,7 @@ import ( "k8s.io/utils/pointer" infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/asogroups" "sigs.k8s.io/cluster-api-provider-azure/azure/services/groups" "sigs.k8s.io/cluster-api-provider-azure/azure/services/managedclusters" "sigs.k8s.io/cluster-api-provider-azure/azure/services/privateendpoints" @@ -125,6 +126,11 @@ type ManagedControlPlaneCache struct { isVnetManaged *bool } +// GetClient returns the controller-runtime client. +func (s *ManagedControlPlaneScope) GetClient() client.Client { + return s.Client +} + // ResourceGroup returns the managed control plane's resource group. func (s *ManagedControlPlaneScope) ResourceGroup() string { if s.ControlPlane == nil { @@ -248,6 +254,18 @@ func (s *ManagedControlPlaneScope) GroupSpec() azure.ResourceSpecGetter { } } +// ASOGroupSpec returns the resource group spec. +func (s *ManagedControlPlaneScope) ASOGroupSpec() azure.ASOResourceSpecGetter { + return &asogroups.GroupSpec{ + Name: s.ResourceGroup(), + Namespace: s.Cluster.Namespace, + Location: s.Location(), + ClusterName: s.ClusterName(), + AdditionalTags: s.AdditionalTags(), + Owner: *metav1.NewControllerRef(s.ControlPlane, infrav1.GroupVersion.WithKind("AzureManagedControlPlane")), + } +} + // VNetSpec returns the virtual network spec. func (s *ManagedControlPlaneScope) VNetSpec() azure.ResourceSpecGetter { return &virtualnetworks.VNetSpec{ diff --git a/azure/services/asogroups/groups.go b/azure/services/asogroups/groups.go new file mode 100644 index 00000000000..5e403c4a47c --- /dev/null +++ b/azure/services/asogroups/groups.go @@ -0,0 +1,128 @@ +/* +Copyright 2023 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 asogroups + +import ( + "context" + + asoresourcesv1 "github.com/Azure/azure-service-operator/v2/api/resources/v1api20200601" + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-azure/azure" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/aso" + "sigs.k8s.io/cluster-api-provider-azure/util/reconciler" + "sigs.k8s.io/cluster-api-provider-azure/util/tele" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// ServiceName is the name of this service. +const ServiceName = "group" + +// Service provides operations on Azure resources. +type Service struct { + Scope GroupScope + aso.Reconciler +} + +// GroupScope defines the scope interface for a group service. +type GroupScope interface { + azure.AsyncStatusUpdater + ASOGroupSpec() azure.ASOResourceSpecGetter + GetClient() client.Client +} + +// New creates a new service. +func New(scope GroupScope) *Service { + return &Service{ + Scope: scope, + Reconciler: aso.New(scope.GetClient()), + } +} + +// Name returns the service name. +func (s *Service) Name() string { + return ServiceName +} + +// Reconcile idempotently creates or updates a resource group. +func (s *Service) Reconcile(ctx context.Context) error { + ctx, _, done := tele.StartSpanWithLogger(ctx, "groups.Service.Reconcile") + defer done() + + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureServiceReconcileTimeout) + defer cancel() + + groupSpec := s.Scope.ASOGroupSpec() + if groupSpec == nil { + return nil + } + + _, err := s.CreateOrUpdateResource(ctx, groupSpec, ServiceName) + s.Scope.UpdatePutStatus(infrav1.ResourceGroupReadyCondition, ServiceName, err) + return err +} + +// Delete deletes the resource group if it is managed by capz. +func (s *Service) Delete(ctx context.Context) error { + ctx, log, done := tele.StartSpanWithLogger(ctx, "groups.Service.Delete") + defer done() + + ctx, cancel := context.WithTimeout(ctx, reconciler.DefaultAzureServiceReconcileTimeout) + defer cancel() + + groupSpec := s.Scope.ASOGroupSpec() + if groupSpec == nil { + return nil + } + + // check that the resource group is not BYO. + managed, err := s.IsManaged(ctx) + if err != nil { + if apierrors.IsNotFound(err) { + // already deleted or doesn't exist, cleanup status and return. + s.Scope.UpdateDeleteStatus(infrav1.ResourceGroupReadyCondition, ServiceName, nil) + return nil + } + return errors.Wrap(err, "could not get resource group management state") + } + if !managed { + log.V(2).Info("Skipping resource group deletion in unmanaged mode") + return nil + } + + err = s.DeleteResource(ctx, groupSpec, ServiceName) + s.Scope.UpdateDeleteStatus(infrav1.ResourceGroupReadyCondition, ServiceName, err) + return err +} + +// IsManaged returns true if the resource group has an owned tag with the cluster name as value, +// meaning that the resource group's lifecycle is managed. +func (s *Service) IsManaged(ctx context.Context) (bool, error) { + ctx, log, done := tele.StartSpanWithLogger(ctx, "groups.Service.IsManaged") + defer done() + + spec := s.Scope.ASOGroupSpec() + group := &asoresourcesv1.ResourceGroup{} + err := s.Scope.GetClient().Get(ctx, client.ObjectKeyFromObject(spec.ResourceRef()), group) + if err != nil { + log.Error(err, "error getting resource group") + return false, err + } + + return true, nil // not yet implemented +} diff --git a/azure/services/asogroups/groups_test.go b/azure/services/asogroups/groups_test.go new file mode 100644 index 00000000000..a7a834bc024 --- /dev/null +++ b/azure/services/asogroups/groups_test.go @@ -0,0 +1,252 @@ +/* +Copyright 2023 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 asogroups + +import ( + "context" + "errors" + "testing" + + asoresourcesv1 "github.com/Azure/azure-service-operator/v2/api/resources/v1api20200601" + "github.com/golang/mock/gomock" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/pointer" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/aso/mock_aso" + "sigs.k8s.io/cluster-api-provider-azure/azure/services/asogroups/mock_asogroups" + gomockinternal "sigs.k8s.io/cluster-api-provider-azure/internal/test/matchers/gomock" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var ( + fakeGroupSpec = GroupSpec{ + Name: "test-group", + Namespace: "test-group-ns", + Location: "test-location", + ClusterName: "test-cluster", + AdditionalTags: map[string]string{"foo": "bar"}, + } + errInternal = errors.New("internal error") + sampleManagedGroup = &asoresourcesv1.ResourceGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-group", + Namespace: "test-group-ns", + }, + Spec: asoresourcesv1.ResourceGroup_Spec{ + Location: pointer.String("test-location"), + }, + } +) + +func TestReconcileGroups(t *testing.T) { + testcases := []struct { + name string + expectedError string + expect func(s *mock_asogroups.MockGroupScopeMockRecorder, r *mock_aso.MockReconcilerMockRecorder) + }{ + { + name: "noop if no group spec is found", + expectedError: "", + expect: func(s *mock_asogroups.MockGroupScopeMockRecorder, _ *mock_aso.MockReconcilerMockRecorder) { + s.ASOGroupSpec().Return(nil) + }, + }, + { + name: "create group succeeds", + expectedError: "", + expect: func(s *mock_asogroups.MockGroupScopeMockRecorder, r *mock_aso.MockReconcilerMockRecorder) { + s.ASOGroupSpec().Return(&fakeGroupSpec) + r.CreateOrUpdateResource(gomockinternal.AContext(), &fakeGroupSpec, ServiceName).Return(nil, nil) + s.UpdatePutStatus(infrav1.ResourceGroupReadyCondition, ServiceName, nil) + }, + }, + { + name: "create resource group fails", + expectedError: "internal error", + expect: func(s *mock_asogroups.MockGroupScopeMockRecorder, r *mock_aso.MockReconcilerMockRecorder) { + s.ASOGroupSpec().Return(&fakeGroupSpec) + r.CreateOrUpdateResource(gomockinternal.AContext(), &fakeGroupSpec, ServiceName).Return(nil, errInternal) + s.UpdatePutStatus(infrav1.ResourceGroupReadyCondition, ServiceName, errInternal) + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + scopeMock := mock_asogroups.NewMockGroupScope(mockCtrl) + asoMock := mock_aso.NewMockReconciler(mockCtrl) + + tc.expect(scopeMock.EXPECT(), asoMock.EXPECT()) + + s := &Service{ + Scope: scopeMock, + Reconciler: asoMock, + } + + err := s.Reconcile(context.TODO()) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err).To(MatchError(ContainSubstring(tc.expectedError))) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} + +type ErroringGetClient struct { + client.Client + err error +} + +func (e *ErroringGetClient) Get(_ context.Context, _ client.ObjectKey, _ client.Object, _ ...client.GetOption) error { + return e.err +} + +type ErroringDeleteClient struct { + client.Client + err error +} + +func (e *ErroringDeleteClient) Delete(_ context.Context, _ client.Object, _ ...client.DeleteOption) error { + return e.err +} + +func TestDeleteGroups(t *testing.T) { + testcases := []struct { + name string + clientBuilder func(g Gomega) client.Client + expectedError string + expect func(s *mock_asogroups.MockGroupScopeMockRecorder, r *mock_aso.MockReconcilerMockRecorder) + }{ + { + name: "noop if no group spec is found", + expectedError: "", + expect: func(s *mock_asogroups.MockGroupScopeMockRecorder, _ *mock_aso.MockReconcilerMockRecorder) { + s.ASOGroupSpec().Return(nil) + }, + }, + { + name: "delete operation is successful for managed resource group", + expectedError: "", + clientBuilder: func(g Gomega) client.Client { + scheme := runtime.NewScheme() + g.Expect(asoresourcesv1.AddToScheme(scheme)).To(Succeed()) + return fakeclient.NewClientBuilder(). + WithScheme(scheme). + WithObjects(sampleManagedGroup.DeepCopy()). + Build() + }, + expect: func(s *mock_asogroups.MockGroupScopeMockRecorder, r *mock_aso.MockReconcilerMockRecorder) { + s.ASOGroupSpec().AnyTimes().Return(&fakeGroupSpec) + r.DeleteResource(gomockinternal.AContext(), &fakeGroupSpec, ServiceName).Return(nil) + s.UpdateDeleteStatus(infrav1.ResourceGroupReadyCondition, ServiceName, nil) + }, + }, + { + name: "fail to check if resource group is managed", + clientBuilder: func(g Gomega) client.Client { + scheme := runtime.NewScheme() + g.Expect(asoresourcesv1.AddToScheme(scheme)).To(Succeed()) + c := fakeclient.NewClientBuilder(). + WithScheme(scheme). + Build() + return &ErroringGetClient{Client: c, err: errInternal} + }, + expectedError: "could not get resource group management state", + expect: func(s *mock_asogroups.MockGroupScopeMockRecorder, _ *mock_aso.MockReconcilerMockRecorder) { + s.ASOGroupSpec().AnyTimes().Return(&fakeGroupSpec) + }, + }, + { + name: "resource group doesn't exist", + clientBuilder: func(g Gomega) client.Client { + scheme := runtime.NewScheme() + g.Expect(asoresourcesv1.AddToScheme(scheme)).To(Succeed()) + c := fakeclient.NewClientBuilder(). + WithScheme(scheme). + Build() + return &ErroringDeleteClient{Client: c, err: errInternal} + }, + expectedError: "", + expect: func(s *mock_asogroups.MockGroupScopeMockRecorder, _ *mock_aso.MockReconcilerMockRecorder) { + s.ASOGroupSpec().AnyTimes().Return(&fakeGroupSpec) + s.UpdateDeleteStatus(infrav1.ResourceGroupReadyCondition, ServiceName, nil) + }, + }, + { + name: "error occurs when deleting resource group", + clientBuilder: func(g Gomega) client.Client { + scheme := runtime.NewScheme() + g.Expect(asoresourcesv1.AddToScheme(scheme)).To(Succeed()) + c := fakeclient.NewClientBuilder(). + WithScheme(scheme). + WithObjects(sampleManagedGroup.DeepCopy()). + Build() + return &ErroringDeleteClient{Client: c, err: errInternal} + }, + expectedError: "internal error", + expect: func(s *mock_asogroups.MockGroupScopeMockRecorder, r *mock_aso.MockReconcilerMockRecorder) { + s.ASOGroupSpec().AnyTimes().Return(&fakeGroupSpec) + r.DeleteResource(gomockinternal.AContext(), &fakeGroupSpec, ServiceName).Return(errInternal) + s.UpdateDeleteStatus(infrav1.ResourceGroupReadyCondition, ServiceName, gomockinternal.ErrStrEq("internal error")) + }, + }, + } + + for _, tc := range testcases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + t.Parallel() + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + scopeMock := mock_asogroups.NewMockGroupScope(mockCtrl) + asyncMock := mock_aso.NewMockReconciler(mockCtrl) + + var ctrlClient client.Client + if tc.clientBuilder != nil { + ctrlClient = tc.clientBuilder(g) + } + + scopeMock.EXPECT().GetClient().Return(ctrlClient).AnyTimes() + tc.expect(scopeMock.EXPECT(), asyncMock.EXPECT()) + + s := &Service{ + Scope: scopeMock, + Reconciler: asyncMock, + } + + err := s.Delete(context.TODO()) + if tc.expectedError != "" { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring(tc.expectedError)) + } else { + g.Expect(err).NotTo(HaveOccurred()) + } + }) + } +} diff --git a/azure/services/asogroups/mock_asogroups/doc.go b/azure/services/asogroups/mock_asogroups/doc.go new file mode 100644 index 00000000000..25af7f377ac --- /dev/null +++ b/azure/services/asogroups/mock_asogroups/doc.go @@ -0,0 +1,21 @@ +/* +Copyright 2023 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. +*/ + +// Run go generate to regenerate this mock. +// +//go:generate ../../../../hack/tools/bin/mockgen -destination groups_mock.go -package mock_asogroups -source ../groups.go GroupScope +//go:generate /usr/bin/env bash -c "cat ../../../../hack/boilerplate/boilerplate.generatego.txt groups_mock.go > _groups_mock.go && mv _groups_mock.go groups_mock.go" +package mock_asogroups diff --git a/azure/services/asogroups/mock_asogroups/groups_mock.go b/azure/services/asogroups/mock_asogroups/groups_mock.go new file mode 100644 index 00000000000..4e04c28f7e5 --- /dev/null +++ b/azure/services/asogroups/mock_asogroups/groups_mock.go @@ -0,0 +1,156 @@ +/* +Copyright 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. +*/ + +// Code generated by MockGen. DO NOT EDIT. +// Source: ../groups.go + +// Package mock_asogroups is a generated GoMock package. +package mock_asogroups + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + v1beta1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + azure "sigs.k8s.io/cluster-api-provider-azure/azure" + v1beta10 "sigs.k8s.io/cluster-api/api/v1beta1" + client "sigs.k8s.io/controller-runtime/pkg/client" +) + +// MockGroupScope is a mock of GroupScope interface. +type MockGroupScope struct { + ctrl *gomock.Controller + recorder *MockGroupScopeMockRecorder +} + +// MockGroupScopeMockRecorder is the mock recorder for MockGroupScope. +type MockGroupScopeMockRecorder struct { + mock *MockGroupScope +} + +// NewMockGroupScope creates a new mock instance. +func NewMockGroupScope(ctrl *gomock.Controller) *MockGroupScope { + mock := &MockGroupScope{ctrl: ctrl} + mock.recorder = &MockGroupScopeMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGroupScope) EXPECT() *MockGroupScopeMockRecorder { + return m.recorder +} + +// ASOGroupSpec mocks base method. +func (m *MockGroupScope) ASOGroupSpec() azure.ASOResourceSpecGetter { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ASOGroupSpec") + ret0, _ := ret[0].(azure.ASOResourceSpecGetter) + return ret0 +} + +// ASOGroupSpec indicates an expected call of ASOGroupSpec. +func (mr *MockGroupScopeMockRecorder) ASOGroupSpec() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ASOGroupSpec", reflect.TypeOf((*MockGroupScope)(nil).ASOGroupSpec)) +} + +// DeleteLongRunningOperationState mocks base method. +func (m *MockGroupScope) DeleteLongRunningOperationState(arg0, arg1, arg2 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "DeleteLongRunningOperationState", arg0, arg1, arg2) +} + +// DeleteLongRunningOperationState indicates an expected call of DeleteLongRunningOperationState. +func (mr *MockGroupScopeMockRecorder) DeleteLongRunningOperationState(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLongRunningOperationState", reflect.TypeOf((*MockGroupScope)(nil).DeleteLongRunningOperationState), arg0, arg1, arg2) +} + +// GetClient mocks base method. +func (m *MockGroupScope) GetClient() client.Client { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetClient") + ret0, _ := ret[0].(client.Client) + return ret0 +} + +// GetClient indicates an expected call of GetClient. +func (mr *MockGroupScopeMockRecorder) GetClient() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockGroupScope)(nil).GetClient)) +} + +// GetLongRunningOperationState mocks base method. +func (m *MockGroupScope) GetLongRunningOperationState(arg0, arg1, arg2 string) *v1beta1.Future { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLongRunningOperationState", arg0, arg1, arg2) + ret0, _ := ret[0].(*v1beta1.Future) + return ret0 +} + +// GetLongRunningOperationState indicates an expected call of GetLongRunningOperationState. +func (mr *MockGroupScopeMockRecorder) GetLongRunningOperationState(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLongRunningOperationState", reflect.TypeOf((*MockGroupScope)(nil).GetLongRunningOperationState), arg0, arg1, arg2) +} + +// SetLongRunningOperationState mocks base method. +func (m *MockGroupScope) SetLongRunningOperationState(arg0 *v1beta1.Future) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetLongRunningOperationState", arg0) +} + +// 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, "SetLongRunningOperationState", reflect.TypeOf((*MockGroupScope)(nil).SetLongRunningOperationState), arg0) +} + +// UpdateDeleteStatus mocks base method. +func (m *MockGroupScope) UpdateDeleteStatus(arg0 v1beta10.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 v1beta10.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 v1beta10.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) +} diff --git a/azure/services/asogroups/spec.go b/azure/services/asogroups/spec.go new file mode 100644 index 00000000000..6a4fed33da4 --- /dev/null +++ b/azure/services/asogroups/spec.go @@ -0,0 +1,70 @@ +/* +Copyright 2023 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 asogroups + +import ( + "context" + + asoresourcesv1 "github.com/Azure/azure-service-operator/v2/api/resources/v1api20200601" + "github.com/Azure/azure-service-operator/v2/pkg/genruntime" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" +) + +// GroupSpec defines the specification for a Resource Group. +type GroupSpec struct { + Name string + Namespace string + Location string + ClusterName string + AdditionalTags infrav1.Tags + Owner metav1.OwnerReference +} + +// ResourceRef implements aso.ResourceSpecGetter. +func (s *GroupSpec) ResourceRef() genruntime.MetaObject { + return &asoresourcesv1.ResourceGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.Name, + Namespace: s.Namespace, + }, + } +} + +// Parameters implements aso.ResourceSpecGetter. +func (s *GroupSpec) Parameters(ctx context.Context, object genruntime.MetaObject) (genruntime.MetaObject, error) { + if object != nil { + return nil, nil + } + + return &asoresourcesv1.ResourceGroup{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{s.Owner}, + }, + Spec: asoresourcesv1.ResourceGroup_Spec{ + Location: pointer.String(s.Location), + Tags: infrav1.Build(infrav1.BuildParams{ + ClusterName: s.ClusterName, + Lifecycle: infrav1.ResourceLifecycleOwned, + Name: pointer.String(s.Name), + Role: pointer.String(infrav1.CommonRole), + Additional: s.AdditionalTags, + }), + }, + }, nil +} diff --git a/azure/services/asogroups/spec_test.go b/azure/services/asogroups/spec_test.go new file mode 100644 index 00000000000..60da82a5154 --- /dev/null +++ b/azure/services/asogroups/spec_test.go @@ -0,0 +1,88 @@ +/* +Copyright 2023 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 asogroups + +import ( + "context" + "testing" + + asoresourcesv1 "github.com/Azure/azure-service-operator/v2/api/resources/v1api20200601" + "github.com/Azure/azure-service-operator/v2/pkg/genruntime" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" +) + +func TestParameters(t *testing.T) { + tests := []struct { + name string + spec *GroupSpec + existing genruntime.MetaObject + expected genruntime.MetaObject + }{ + { + name: "no existing group", + spec: &GroupSpec{ + Name: "name", + Location: "location", + ClusterName: "cluster", + AdditionalTags: infrav1.Tags{"some": "tags"}, + Namespace: "namespace", + Owner: metav1.OwnerReference{ + Kind: "kind", + }, + }, + existing: nil, + expected: &asoresourcesv1.ResourceGroup{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{ + { + Kind: "kind", + }, + }, + }, + Spec: asoresourcesv1.ResourceGroup_Spec{ + Location: pointer.String("location"), + Tags: map[string]string{ + "some": "tags", + "sigs.k8s.io_cluster-api-provider-azure_cluster_cluster": "owned", + "sigs.k8s.io_cluster-api-provider-azure_role": "common", + "Name": "name", + }, + }, + }, + }, + { + name: "existing group", + existing: &asoresourcesv1.ResourceGroup{}, + expected: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + actual, err := test.spec.Parameters(context.Background(), test.existing) + g.Expect(err).NotTo(HaveOccurred()) + if test.expected == nil { + g.Expect(actual).To(BeNil()) + } else { + g.Expect(actual).To(Equal(test.expected)) + } + }) + } +}