From 112f9685f7bec8c53e179edbac52450e7c669c14 Mon Sep 17 00:00:00 2001 From: Christopher J Schaefer Date: Tue, 23 Jul 2024 09:40:23 -0500 Subject: [PATCH] VPC: Add v2 support for VPC reconcile (#1886) Add support to the v2 path to reconcile a VPC. Includes adding GlobalTagging support, and extending ResourceManager support. --- cloud/scope/vpc_cluster.go | 321 +++++++++++++++++- controllers/ibmvpccluster_controller.go | 25 +- pkg/cloud/services/globaltagging/doc.go | 19 ++ .../services/globaltagging/globaltagging.go | 30 ++ pkg/cloud/services/globaltagging/service.go | 97 ++++++ .../resourcemanager/resourcemanager.go | 2 + pkg/cloud/services/resourcemanager/service.go | 25 ++ pkg/endpoints/endpoints.go | 4 +- 8 files changed, 514 insertions(+), 9 deletions(-) create mode 100644 pkg/cloud/services/globaltagging/doc.go create mode 100644 pkg/cloud/services/globaltagging/globaltagging.go create mode 100644 pkg/cloud/services/globaltagging/service.go diff --git a/cloud/scope/vpc_cluster.go b/cloud/scope/vpc_cluster.go index 37d6f9b38..aafe578a6 100644 --- a/cloud/scope/vpc_cluster.go +++ b/cloud/scope/vpc_cluster.go @@ -24,9 +24,13 @@ import ( "github.com/go-logr/logr" "github.com/IBM/go-sdk-core/v5/core" + "github.com/IBM/platform-services-go-sdk/globaltaggingv1" "github.com/IBM/platform-services-go-sdk/resourcecontrollerv2" + "github.com/IBM/platform-services-go-sdk/resourcemanagerv2" + "github.com/IBM/vpc-go-sdk/vpcv1" "k8s.io/klog/v2/textlogger" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -36,6 +40,7 @@ import ( infrav1beta2 "sigs.k8s.io/cluster-api-provider-ibmcloud/api/v1beta2" "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/authenticator" "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/cos" + "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/globaltagging" "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/resourcecontroller" "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/resourcemanager" "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/vpc" @@ -65,6 +70,7 @@ type VPCClusterScope struct { patchHelper *patch.Helper COSClient cos.Cos + GlobalTaggingClient globaltagging.GlobalTagging ResourceControllerClient resourcecontroller.ResourceController ResourceManagerClient resourcemanager.ResourceManager VPCClient vpc.Vpc @@ -117,7 +123,20 @@ func NewVPCClusterScope(params VPCClusterScopeParams) (*VPCClusterScope, error) } // Create Global Tagging client. - // TODO(cjschaef): need service support. + gtOptions := globaltagging.ServiceOptions{ + GlobalTaggingV1Options: &globaltaggingv1.GlobalTaggingV1Options{ + Authenticator: auth, + }, + } + // Override the global tagging endpoint if provided. + if gtEndpoint := endpoints.FetchEndpoints(string(endpoints.GlobalTagging), params.ServiceEndpoint); gtEndpoint != "" { + gtOptions.URL = gtEndpoint + params.Logger.V(3).Info("Overriding the default global tagging endpoint", "GlobaTaggingEndpoint", gtEndpoint) + } + globalTaggingClient, err := globaltagging.NewService(gtOptions) + if err != nil { + return nil, fmt.Errorf("failed to create global tagging client: %w", err) + } // Create Resource Controller client. rcOptions := resourcecontroller.ServiceOptions{ @@ -125,19 +144,29 @@ func NewVPCClusterScope(params VPCClusterScopeParams) (*VPCClusterScope, error) Authenticator: auth, }, } - // Fetch the resource controller endpoint. - rcEndpoint := endpoints.FetchEndpoints(string(endpoints.RC), params.ServiceEndpoint) - if rcEndpoint != "" { + // Override the resource controller endpoint if provided. + if rcEndpoint := endpoints.FetchEndpoints(string(endpoints.RC), params.ServiceEndpoint); rcEndpoint != "" { rcOptions.URL = rcEndpoint params.Logger.V(3).Info("Overriding the default resource controller endpoint", "ResourceControllerEndpoint", rcEndpoint) } resourceControllerClient, err := resourcecontroller.NewService(rcOptions) if err != nil { - return nil, fmt.Errorf("error failed to create resource controller client: %w", err) + return nil, fmt.Errorf("failed to create resource controller client: %w", err) } // Create Resource Manager client. - // TODO(cjschaef): Need to extend ResourceManager service and endpoint support to add properly. + rmOptions := &resourcemanagerv2.ResourceManagerV2Options{ + Authenticator: auth, + } + // Override the ResourceManager endpoint if provided. + if rmEndpoint := endpoints.FetchEndpoints(string(endpoints.RM), params.ServiceEndpoint); rmEndpoint != "" { + rmOptions.URL = rmEndpoint + params.Logger.V(3).Info("Overriding the default resource manager endpoint", "ResourceManagerEndpoint", rmEndpoint) + } + resourceManagerClient, err := resourcemanager.NewService(rmOptions) + if err != nil { + return nil, fmt.Errorf("failed to create resource manager client: %w", err) + } clusterScope := &VPCClusterScope{ Logger: params.Logger, @@ -146,7 +175,9 @@ func NewVPCClusterScope(params VPCClusterScopeParams) (*VPCClusterScope, error) Cluster: params.Cluster, IBMVPCCluster: params.IBMVPCCluster, ServiceEndpoint: params.ServiceEndpoint, + GlobalTaggingClient: globalTaggingClient, ResourceControllerClient: resourceControllerClient, + ResourceManagerClient: resourceManagerClient, VPCClient: vpcClient, } return clusterScope, nil @@ -166,3 +197,281 @@ func (s *VPCClusterScope) Close() error { func (s *VPCClusterScope) Name() string { return s.Cluster.Name } + +// NetworkSpec returns the VPCClusterScope's Network spec. +func (s *VPCClusterScope) NetworkSpec() *infrav1beta2.VPCNetworkSpec { + return s.IBMVPCCluster.Spec.Network +} + +// NetworkStatus returns the VPCClusterScope's Network status. +func (s *VPCClusterScope) NetworkStatus() *infrav1beta2.VPCNetworkStatus { + return s.IBMVPCCluster.Status.Network +} + +// CheckTagExists checks whether a user tag already exists. +func (s *VPCClusterScope) CheckTagExists(tagName string) (bool, error) { + exists, err := s.GlobalTaggingClient.GetTagByName(tagName) + if err != nil { + return false, fmt.Errorf("failed checking for tag: %w", err) + } + return exists != nil, nil +} + +// GetNetworkResourceGroupID returns the Resource Group ID for the Network Resources if it is present. Otherwise, it defaults to the cluster's Resource Group ID. +func (s *VPCClusterScope) GetNetworkResourceGroupID() (string, error) { + // Check if the ID is available from Status first. + if s.NetworkStatus() != nil && s.NetworkStatus().ResourceGroup != nil && s.NetworkStatus().ResourceGroup.ID != "" { + return s.NetworkStatus().ResourceGroup.ID, nil + } + + // If there is no Network Resource Group defined, use the cluster's Resource Group. + if s.NetworkSpec() == nil || s.NetworkSpec().ResourceGroup == nil { + return s.GetResourceGroupID() + } + + // Otherwise, Collect the Network's Resource Group Id. + // Retrieve the Resource Group based on the name. + resourceGroup, err := s.ResourceManagerClient.GetResourceGroupByName(*s.NetworkSpec().ResourceGroup) + if err != nil { + return "", fmt.Errorf("failed to retrieve network resource group id by name: %w", err) + } else if resourceGroup == nil || resourceGroup.ID == nil { + return "", fmt.Errorf("error retrieving network resource group by name: %s", *s.NetworkSpec().ResourceGroup) + } + + // Populate the Network Status' Resource Group to shortcut future lookups. + s.SetResourceStatus(infrav1beta2.ResourceTypeResourceGroup, &infrav1beta2.ResourceStatus{ + ID: *resourceGroup.ID, + Name: s.NetworkSpec().ResourceGroup, + Ready: true, + }) + + return *resourceGroup.ID, nil +} + +// GetResourceGroupID returns the Resource Group ID for the cluster. +func (s *VPCClusterScope) GetResourceGroupID() (string, error) { + // Check if the Resource Group ID is available from Status first. + if s.IBMVPCCluster.Status.ResourceGroup != nil && s.IBMVPCCluster.Status.ResourceGroup.ID != "" { + return s.IBMVPCCluster.Status.ResourceGroup.ID, nil + } + + // If the Resource Group is not defined in Spec, we generate the name based on the cluster name. + resourceGroupName := s.IBMVPCCluster.Spec.ResourceGroup + if resourceGroupName == "" { + resourceGroupName = s.IBMVPCCluster.Name + } + + // Retrieve the Resource Group based on the name. + resourceGroup, err := s.ResourceManagerClient.GetResourceGroupByName(resourceGroupName) + if err != nil { + return "", fmt.Errorf("failed to retrieve resource group by name: %w", err) + } else if resourceGroup == nil || resourceGroup.ID == nil { + return "", fmt.Errorf("failed to find resource group by name: %s", resourceGroupName) + } + + // Populate the Stauts Resource Group to shortcut future lookups. + s.SetResourceStatus(infrav1beta2.ResourceTypeResourceGroup, &infrav1beta2.ResourceStatus{ + ID: *resourceGroup.ID, + Name: ptr.To(resourceGroupName), + Ready: true, + }) + + return *resourceGroup.ID, nil +} + +// GetServiceName returns the name of a given service type from Spec or generates a name for it. +func (s *VPCClusterScope) GetServiceName(resourceType infrav1beta2.ResourceType) *string { + switch resourceType { + case infrav1beta2.ResourceTypeVPC: + // Generate a name based off cluster name if no VPC defined in Spec, or no VPC name nor ID. + if s.NetworkSpec().VPC == nil || (s.NetworkSpec().VPC.Name == nil && s.NetworkSpec().VPC.ID == nil) { + return ptr.To(fmt.Sprintf("%s-vpc", s.Name())) + } + if s.NetworkSpec().VPC.Name != nil { + return s.NetworkSpec().VPC.Name + } + default: + s.V(3).Info("unsupported resource type", "resourceType", resourceType) + } + return nil +} + +// GetVPCID returns the VPC id, if available. +func (s *VPCClusterScope) GetVPCID() (*string, error) { + // Check if the VPC ID is available from Status first. + if s.NetworkStatus() != nil && s.NetworkStatus().VPC != nil { + return ptr.To(s.NetworkStatus().VPC.ID), nil + } + + if s.NetworkSpec() != nil && s.NetworkSpec().VPC != nil { + if s.NetworkSpec().VPC.ID != nil { + return s.NetworkSpec().VPC.ID, nil + } else if s.NetworkSpec().VPC.Name != nil { + vpcDetails, err := s.VPCClient.GetVPCByName(*s.NetworkSpec().VPC.Name) + if err != nil { + return nil, fmt.Errorf("failed vpc id lookup: %w", err) + } + + // Check if the VPC was found and has an ID + if vpcDetails != nil && vpcDetails.ID != nil { + // Set VPC ID in Status to shortcut future lookups + s.SetResourceStatus(infrav1beta2.ResourceTypeVPC, &infrav1beta2.ResourceStatus{ + ID: *vpcDetails.ID, + Name: s.NetworkSpec().VPC.Name, + Ready: true, + }) + } + } + } + return nil, nil +} + +// SetResourceStatus sets the status for the provided ResourceType. +func (s *VPCClusterScope) SetResourceStatus(resourceType infrav1beta2.ResourceType, resource *infrav1beta2.ResourceStatus) { + // Ignore attempts to set status without resource. + if resource == nil { + return + } + s.V(3).Info("Setting status", "resourceType", resourceType, "resource", resource) + switch resourceType { + case infrav1beta2.ResourceTypeResourceGroup: + if s.IBMVPCCluster.Status.ResourceGroup == nil { + s.IBMVPCCluster.Status.ResourceGroup = resource + return + } + s.IBMVPCCluster.Status.ResourceGroup.Set(*resource) + case infrav1beta2.ResourceTypeVPC: + if s.NetworkStatus() == nil { + s.IBMVPCCluster.Status.Network = &infrav1beta2.VPCNetworkStatus{ + VPC: resource, + } + return + } else if s.NetworkStatus().VPC == nil { + s.IBMVPCCluster.Status.Network.VPC = resource + } + s.NetworkStatus().VPC.Set(*resource) + default: + s.V(3).Info("unsupported resource type", "resourceType", resourceType) + } +} + +// TagResource will attach a user Tag to a resource. +func (s *VPCClusterScope) TagResource(tagName string, resourceCRN string) error { + // Verify the Tag we wish to use exists, otherwise create it. + exists, err := s.CheckTagExists(tagName) + if err != nil { + return fmt.Errorf("failure checking if tag exists: %w", err) + } + + // Create tag if it doesn't exist. + if !exists { + createOptions := &globaltaggingv1.CreateTagOptions{} + createOptions.SetTagNames([]string{tagName}) + if _, _, err := s.GlobalTaggingClient.CreateTag(createOptions); err != nil { + return fmt.Errorf("failure creating tag: %w", err) + } + } + + // Finally, tag resource. + tagOptions := &globaltaggingv1.AttachTagOptions{} + tagOptions.SetResources([]globaltaggingv1.Resource{ + { + ResourceID: ptr.To(resourceCRN), + }, + }) + tagOptions.SetTagName(tagName) + tagOptions.SetTagType(globaltaggingv1.AttachTagOptionsTagTypeUserConst) + + if _, _, err = s.GlobalTaggingClient.AttachTag(tagOptions); err != nil { + return fmt.Errorf("failure tagging resource: %w", err) + } + + return nil +} + +// ReconcileVPC reconciles the cluster's VPC. +func (s *VPCClusterScope) ReconcileVPC() (bool, error) { + // If VPC id is set, that indicates the VPC already exists. + vpcID, err := s.GetVPCID() + if err != nil { + return false, fmt.Errorf("failed to retrieve vpc id: %w", err) + } + if vpcID != nil { + s.V(3).Info("VPC id is set", "id", vpcID) + vpcDetails, _, err := s.VPCClient.GetVPC(&vpcv1.GetVPCOptions{ + ID: vpcID, + }) + if err != nil { + return false, fmt.Errorf("failed to retrieve vpc by id: %w", err) + } else if vpcDetails == nil { + return false, fmt.Errorf("failed to retrieve vpc with id: %s", *vpcID) + } + s.V(3).Info("Found VPC with provided id", "id", vpcID) + + requeue := true + if vpcDetails.Status != nil && *vpcDetails.Status == string(vpcv1.VPCStatusAvailableConst) { + requeue = false + } + s.SetResourceStatus(infrav1beta2.ResourceTypeVPC, &infrav1beta2.ResourceStatus{ + ID: *vpcID, + Name: vpcDetails.Name, + // Ready status will be invert of the need to requeue. + Ready: !requeue, + }) + + // After updating the Status of VPC, return with requeue or return as reconcile complete. + return requeue, nil + } + + // If no VPC id was found, we need to create a new VPC. + s.V(3).Info("Creating a VPC") + vpcDetails, err := s.createVPC() + if err != nil { + return false, fmt.Errorf("failed to create vpc: %w", err) + } + + s.V(3).Info("Successfully created VPC") + var vpcName *string + if vpcDetails != nil { + vpcName = vpcDetails.Name + } + s.SetResourceStatus(infrav1beta2.ResourceTypeVPC, &infrav1beta2.ResourceStatus{ + ID: *vpcDetails.ID, + Name: vpcName, + Ready: false, + }) + return true, nil +} + +func (s *VPCClusterScope) createVPC() (*vpcv1.VPC, error) { + // We use the cluster's Resource Group ID, as we expect to create all resources in that Resource Group. + resourceGroupID, err := s.GetResourceGroupID() + if err != nil { + return nil, fmt.Errorf("failed retreiving resource group id during vpc creation: %w", err) + } else if resourceGroupID == "" { + return nil, fmt.Errorf("resource group id is empty cannot create vpc") + } + vpcName := s.GetServiceName(infrav1beta2.ResourceTypeVPC) + if s.NetworkSpec() != nil && s.NetworkSpec().VPC != nil && s.NetworkSpec().VPC.Name != nil { + vpcName = s.NetworkSpec().VPC.Name + } + + // TODO(cjschaef): Look at adding support to specify prefix management + addressPrefixManagement := "auto" + vpcOptions := &vpcv1.CreateVPCOptions{ + AddressPrefixManagement: &addressPrefixManagement, + Name: vpcName, + ResourceGroup: &vpcv1.ResourceGroupIdentity{ID: &resourceGroupID}, + } + vpcDetails, _, err := s.VPCClient.CreateVPC(vpcOptions) + if err != nil { + return nil, fmt.Errorf("error creating vpc: %w", err) + } else if vpcDetails == nil { + return nil, fmt.Errorf("no vpc details after creation") + } + if err = s.TagResource(s.IBMVPCCluster.Name, *vpcDetails.CRN); err != nil { + return nil, fmt.Errorf("error tagging vpc: %w", err) + } + + return vpcDetails, nil +} diff --git a/controllers/ibmvpccluster_controller.go b/controllers/ibmvpccluster_controller.go index fe72ba047..50f8abc92 100644 --- a/controllers/ibmvpccluster_controller.go +++ b/controllers/ibmvpccluster_controller.go @@ -229,8 +229,29 @@ func (r *IBMVPCClusterReconciler) reconcile(clusterScope *scope.ClusterScope) (c return ctrl.Result{}, nil } -func (r *IBMVPCClusterReconciler) reconcileCluster(_ *scope.VPCClusterScope) (ctrl.Result, error) { - return ctrl.Result{}, fmt.Errorf("not implemented") +func (r *IBMVPCClusterReconciler) reconcileCluster(clusterScope *scope.VPCClusterScope) (ctrl.Result, error) { + // If the IBMVPCCluster doesn't have our finalizer, add it. + if controllerutil.AddFinalizer(clusterScope.IBMVPCCluster, infrav1beta2.ClusterFinalizer) { + return ctrl.Result{}, nil + } + + // Reconcile the cluster's VPC. + clusterScope.Info("Reconciling VPC") + if requeue, err := clusterScope.ReconcileVPC(); err != nil { + clusterScope.Error(err, "failed to reconcile VPC") + conditions.MarkFalse(clusterScope.IBMVPCCluster, infrav1beta2.VPCReadyCondition, infrav1beta2.VPCReconciliationFailedReason, capiv1beta1.ConditionSeverityError, err.Error()) + return reconcile.Result{}, err + } else if requeue { + clusterScope.Info("VPC creation is pending, requeuing") + return reconcile.Result{RequeueAfter: 15 * time.Second}, nil + } + conditions.MarkTrue(clusterScope.IBMVPCCluster, infrav1beta2.VPCReadyCondition) + + // TODO(cjschaef): add remaining resource reconciliation. + + // Mark cluster as ready. + clusterScope.IBMVPCCluster.Status.Ready = true + return ctrl.Result{}, nil } func (r *IBMVPCClusterReconciler) reconcileDelete(clusterScope *scope.ClusterScope) (ctrl.Result, error) { diff --git a/pkg/cloud/services/globaltagging/doc.go b/pkg/cloud/services/globaltagging/doc.go new file mode 100644 index 000000000..9144fc5ec --- /dev/null +++ b/pkg/cloud/services/globaltagging/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2024 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 globaltagging implements globaltagging code. +// Manage tags for cloud resources using Global Tagging APIs. +package globaltagging diff --git a/pkg/cloud/services/globaltagging/globaltagging.go b/pkg/cloud/services/globaltagging/globaltagging.go new file mode 100644 index 000000000..8ed76fcbd --- /dev/null +++ b/pkg/cloud/services/globaltagging/globaltagging.go @@ -0,0 +1,30 @@ +/* +Copyright 2024 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 globaltagging + +import ( + "github.com/IBM/go-sdk-core/v5/core" + "github.com/IBM/platform-services-go-sdk/globaltaggingv1" +) + +// GlobalTagging interface defines a method that a IBMCLOUD service object should implement in order to +// use the manage tags with the Global Tagging APIs. +type GlobalTagging interface { + CreateTag(*globaltaggingv1.CreateTagOptions) (*globaltaggingv1.CreateTagResults, *core.DetailedResponse, error) + AttachTag(*globaltaggingv1.AttachTagOptions) (*globaltaggingv1.TagResults, *core.DetailedResponse, error) + GetTagByName(string) (*globaltaggingv1.Tag, error) +} diff --git a/pkg/cloud/services/globaltagging/service.go b/pkg/cloud/services/globaltagging/service.go new file mode 100644 index 000000000..9ae455057 --- /dev/null +++ b/pkg/cloud/services/globaltagging/service.go @@ -0,0 +1,97 @@ +/* +Copyright 2024 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 globaltagging + +import ( + "fmt" + "net/http" + + "github.com/IBM/go-sdk-core/v5/core" + "github.com/IBM/platform-services-go-sdk/globaltaggingv1" + + "k8s.io/utils/ptr" + + "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/authenticator" + "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/utils" +) + +// Service holds the IBM Cloud Global Tagging Service specific information. +type Service struct { + client *globaltaggingv1.GlobalTaggingV1 +} + +// ServiceOptions holds the IBM Cloud Global Tagging Service Options specific information. +type ServiceOptions struct { + *globaltaggingv1.GlobalTaggingV1Options +} + +// CreateTag creates a new Tag. +func (s *Service) CreateTag(options *globaltaggingv1.CreateTagOptions) (*globaltaggingv1.CreateTagResults, *core.DetailedResponse, error) { + return s.client.CreateTag(options) +} + +// AttachTag will add tag(s) to resource(s). +func (s *Service) AttachTag(options *globaltaggingv1.AttachTagOptions) (*globaltaggingv1.TagResults, *core.DetailedResponse, error) { + return s.client.AttachTag(options) +} + +// GetTagByName returns the Tag with the provided name, if found. +func (s *Service) GetTagByName(tagName string) (*globaltaggingv1.Tag, error) { + accountID, err := utils.GetAccountID() + if err != nil { + return nil, err + } + + listOptions := s.client.NewListTagsOptions() + listOptions.SetTagType(globaltaggingv1.AttachTagOptionsTagTypeUserConst) + listOptions.SetAccountID(accountID) + + result, response, err := s.client.ListTags(listOptions) + if err != nil { + return nil, fmt.Errorf("failed listing user tags: %w", err) + } + if result == nil || (response != nil && response.StatusCode == http.StatusNotFound) { + return nil, fmt.Errorf("failed to list tags") + } + for _, tag := range result.Items { + if tag.Name != nil && *tag.Name == tagName { + return ptr.To(tag), nil + } + } + return nil, nil +} + +// NewService returns a new service for the IBM Cloud Global Tagging api client. +func NewService(options ServiceOptions) (*Service, error) { + if options.GlobalTaggingV1Options == nil { + options.GlobalTaggingV1Options = &globaltaggingv1.GlobalTaggingV1Options{} + } + if options.Authenticator == nil { + auth, err := authenticator.GetAuthenticator() + if err != nil { + return nil, err + } + options.Authenticator = auth + } + service, err := globaltaggingv1.NewGlobalTaggingV1(options.GlobalTaggingV1Options) + if err != nil { + return nil, err + } + return &Service{ + client: service, + }, nil +} diff --git a/pkg/cloud/services/resourcemanager/resourcemanager.go b/pkg/cloud/services/resourcemanager/resourcemanager.go index e03b8e9cd..4b260805c 100644 --- a/pkg/cloud/services/resourcemanager/resourcemanager.go +++ b/pkg/cloud/services/resourcemanager/resourcemanager.go @@ -25,4 +25,6 @@ import ( // use the manage lifecycle of cloud resource groups using Resource Manager APIs. type ResourceManager interface { ListResourceGroups(*resourcemanagerv2.ListResourceGroupsOptions) (*resourcemanagerv2.ResourceGroupList, *core.DetailedResponse, error) + + GetResourceGroupByName(string) (*resourcemanagerv2.ResourceGroup, error) } diff --git a/pkg/cloud/services/resourcemanager/service.go b/pkg/cloud/services/resourcemanager/service.go index 6bc7ddc89..027277358 100644 --- a/pkg/cloud/services/resourcemanager/service.go +++ b/pkg/cloud/services/resourcemanager/service.go @@ -17,10 +17,14 @@ limitations under the License. package resourcemanager import ( + "fmt" + "net/http" + "github.com/IBM/go-sdk-core/v5/core" "github.com/IBM/platform-services-go-sdk/resourcemanagerv2" "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/authenticator" + "sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/utils" ) // Service holds the IBM Cloud Resource Manager Service specific information. @@ -53,3 +57,24 @@ func NewService(options *resourcemanagerv2.ResourceManagerV2Options) (ResourceMa func (s *Service) ListResourceGroups(listResourceGroupsOptions *resourcemanagerv2.ListResourceGroupsOptions) (result *resourcemanagerv2.ResourceGroupList, response *core.DetailedResponse, err error) { return s.client.ListResourceGroups(listResourceGroupsOptions) } + +// GetResourceGroupByName returns the Resource Group with the provided name, if found. +func (s *Service) GetResourceGroupByName(rgName string) (*resourcemanagerv2.ResourceGroup, error) { + accountID, err := utils.GetAccountID() + if err != nil { + return nil, fmt.Errorf("failed getting account id for resource group lookup: %w", err) + } + + listOptions := s.client.NewListResourceGroupsOptions() + listOptions.SetAccountID(accountID) + listOptions.SetName(rgName) + + result, response, err := s.ListResourceGroups(listOptions) + if err != nil { + return nil, fmt.Errorf("failed listing Resource Groups: %w", err) + } + if result == nil || result.Resources == nil || len(result.Resources) != 1 || (response != nil && response.StatusCode == http.StatusNotFound) { + return nil, fmt.Errorf("failed to find Resource Group") + } + return &result.Resources[0], nil +} diff --git a/pkg/endpoints/endpoints.go b/pkg/endpoints/endpoints.go index 2a833a5e6..470585671 100644 --- a/pkg/endpoints/endpoints.go +++ b/pkg/endpoints/endpoints.go @@ -39,11 +39,13 @@ const ( COS serviceID = "cos" // RM used to identify Resource-Manager service. RM serviceID = "rm" + // GlobalTagging used to identify the Global Tagging service. + GlobalTagging serviceID = "globaltagging" ) type serviceID string -var serviceIDs = []serviceID{VPC, PowerVS, RC, TransitGateway, COS} +var serviceIDs = []serviceID{VPC, PowerVS, RC, TransitGateway, COS, RM, GlobalTagging} // ServiceEndpoint holds the Service endpoint specific information. type ServiceEndpoint struct {