From 5828b2a625f46eed901d9d53dd89515f8cb814c8 Mon Sep 17 00:00:00 2001 From: cjschaef Date: Tue, 17 Sep 2024 14:56:30 -0500 Subject: [PATCH] VPC: Add subnet reconciliation Add support to reoncile VPC subnets for the new v2 VPC Infrastructure reoncile logic. Related: https://github.com/kubernetes-sigs/cluster-api-provider-ibmcloud/issues/1896 --- api/v1beta2/types.go | 4 +- cloud/scope/vpc_cluster.go | 377 +++++++++++++++++++ controllers/ibmvpccluster_controller.go | 13 + pkg/cloud/services/vpc/mock/vpc_generated.go | 30 ++ pkg/cloud/services/vpc/service.go | 53 +++ pkg/cloud/services/vpc/vpc.go | 2 + 6 files changed, 477 insertions(+), 2 deletions(-) diff --git a/api/v1beta2/types.go b/api/v1beta2/types.go index c82ec61e7..e6d9efd7d 100644 --- a/api/v1beta2/types.go +++ b/api/v1beta2/types.go @@ -258,10 +258,10 @@ var ( ResourceTypeVPC = ResourceType("vpc") // ResourceTypeSubnet is VPC subnet resource. ResourceTypeSubnet = ResourceType("subnet") - // ResourceTypeComputeSubnet is a VPC subnet resource designated for the Compute (Data) Plane. - ResourceTypeComputeSubnet = ResourceType("computeSubnet") // ResourceTypeControlPlaneSubnet is a VPC subnet resource designated for the Control Plane. ResourceTypeControlPlaneSubnet = ResourceType("controlPlaneSubnet") + // ResourceTypeWorkerSubnet is a VPC subnet resource designated for the Worker (Data) Plane. + ResourceTypeWorkerSubnet = ResourceType("workerSubnet") // ResourceTypeSecurityGroup is a VPC Security Group resource. ResourceTypeSecurityGroup = ResourceType("securityGroup") // ResourceTypeCOSInstance is IBM COS instance resource. diff --git a/cloud/scope/vpc_cluster.go b/cloud/scope/vpc_cluster.go index 71311e6b2..ca54e9228 100644 --- a/cloud/scope/vpc_cluster.go +++ b/cloud/scope/vpc_cluster.go @@ -307,12 +307,44 @@ func (s *VPCClusterScope) GetServiceName(resourceType infrav1beta2.ResourceType) if s.NetworkSpec().VPC.Name != nil { return s.NetworkSpec().VPC.Name } + case infrav1beta2.ResourceTypeSubnet: + // Generate a generic subnet name based off the cluster name, which can be extended as necessary (for Zones). + return ptr.To(fmt.Sprintf("%s-subnet", s.IBMVPCCluster.Name)) + case infrav1beta2.ResourceTypePublicGateway: + // Generate a generic public gateway name based off the cluster name, which can be extedned as necessary (for Zone). + return ptr.To(fmt.Sprintf("%s-pgateway", s.IBMVPCCluster.Name)) default: s.V(3).Info("unsupported resource type", "resourceType", resourceType) } return nil } +// GetSubnetID returns the ID of a subnet, provided the name. +func (s *VPCClusterScope) GetSubnetID(name string) (*string, error) { + // Check Status first + if s.NetworkStatus() != nil { + if s.NetworkStatus().ControlPlaneSubnets != nil { + if subnet, ok := s.NetworkStatus().ControlPlaneSubnets[name]; ok { + return &subnet.ID, nil + } + } + if s.NetworkStatus().WorkerSubnets != nil { + if subnet, ok := s.NetworkStatus().WorkerSubnets[name]; ok { + return &subnet.ID, nil + } + } + } + // Otherwise, if no Status, or not found, attempt to look it up via IBM Cloud API. + subnet, err := s.VPCClient.GetVPCSubnetByName(name) + if err != nil { + return nil, err + } + if subnet == nil { + return nil, nil + } + return subnet.ID, nil +} + // GetVPCID returns the VPC id, if available. func (s *VPCClusterScope) GetVPCID() (*string, error) { // Check if the VPC ID is available from Status first. @@ -377,6 +409,30 @@ func (s *VPCClusterScope) SetResourceStatus(resourceType infrav1beta2.ResourceTy return } s.IBMVPCCluster.Status.Image.Set(*resource) + case infrav1beta2.ResourceTypeControlPlaneSubnet: + if s.NetworkStatus() == nil { + s.IBMVPCCluster.Status.Network = &infrav1beta2.VPCNetworkStatus{} + } + if s.NetworkStatus().ControlPlaneSubnets == nil { + s.IBMVPCCluster.Status.Network.ControlPlaneSubnets = make(map[string]*infrav1beta2.ResourceStatus) + } + if subnet, ok := s.NetworkStatus().ControlPlaneSubnets[*resource.Name]; ok { + subnet.Set(*resource) + } else { + s.IBMVPCCluster.Status.Network.ControlPlaneSubnets[*resource.Name] = resource + } + case infrav1beta2.ResourceTypeWorkerSubnet: + if s.NetworkStatus() == nil { + s.IBMVPCCluster.Status.Network = &infrav1beta2.VPCNetworkStatus{} + } + if s.NetworkStatus().WorkerSubnets == nil { + s.IBMVPCCluster.Status.Network.WorkerSubnets = make(map[string]*infrav1beta2.ResourceStatus) + } + if subnet, ok := s.NetworkStatus().WorkerSubnets[*resource.Name]; ok { + subnet.Set(*resource) + } else { + s.IBMVPCCluster.Status.Network.WorkerSubnets[*resource.Name] = resource + } default: s.V(3).Info("unsupported resource type", "resourceType", resourceType) } @@ -686,3 +742,324 @@ func (s *VPCClusterScope) buildCOSObjectHRef() (*string, error) { s.V(3).Info("building image ref", "href", href) return ptr.To(href), nil } + +// ReconcileSubnets reconciles the VPC Subnet(s). +// For Subnets, we collect all of the required subnets, for each Plane, and reconcile them individually. Requeing if one is missing or just created. Reconciliation is attempted on all subnets each loop, to prevent single subnet creation per reconciliation loop. +func (s *VPCClusterScope) ReconcileSubnets() (bool, error) { + var subnets []infrav1beta2.Subnet + var err error + // If no ControlPlane Subnets were supplied, we default to create one in each availability zone of the region. + if s.IBMVPCCluster.Spec.Network.ControlPlaneSubnets == nil || len(s.IBMVPCCluster.Spec.Network.ControlPlaneSubnets) == 0 { + subnets, err = s.buildSubnetsForZones() + if err != nil { + return false, fmt.Errorf("error failed building control plane subnets: %w", err) + } + } else { + subnets = s.IBMVPCCluster.Spec.Network.ControlPlaneSubnets + } + + // Reconcile Control Plane subnets. + requeue := false + for _, subnet := range subnets { + if requiresRequeue, err := s.reconcileSubnet(subnet, true); err != nil { + return false, fmt.Errorf("error failed reconciling control plane subnet: %w", err) + } else if requiresRequeue { + // If the reconcile of the subnet requires further reconciliation, plan to requeue entire ReconcileSubnets call, but attempt to further reconcile additional Subnets (attempt all subnet reconciliation). + requeue = true + } + } + + // If no Worker subnets were supplied, attempt to create one in each zone. + if s.IBMVPCCluster.Spec.Network.WorkerSubnets == nil || len(s.IBMVPCCluster.Spec.Network.WorkerSubnets) == 0 { + // Build subnets for Workers if none were provided, but only if Control Plane subnets were. + // Otherwise, if neither Control Plane nor Worker subnets were supplied, we rely on both Planes using the same subnet per zone, and we will re-reconcile those subnets below, for IBMVPCCluster Status updates. + if len(s.IBMVPCCluster.Spec.Network.ControlPlaneSubnets) != 0 { + subnets, err = s.buildSubnetsForZones() + if err != nil { + return false, fmt.Errorf("error failed building worker subnets: %w", err) + } + } + } else { + subnets = s.IBMVPCCluster.Spec.Network.WorkerSubnets + } + + // Reconcile Worker subnets. + for _, subnet := range subnets { + if requiresRequeue, err := s.reconcileSubnet(subnet, false); err != nil { + return false, fmt.Errorf("error failed reconciling worker subnet: %w", err) + } else if requiresRequeue { + // If the reconcile of the subnet requires further reconciliation, plan to requeue entire ReconcileSubnets call, but attempt to further reconcile additional Subnets (attempt all subnet reconciliation). + requeue = true + } + } + + // Return whether or not one or more subnets required further reconciling after attempting to process all Control Plane and Worker subnets. + return requeue, nil +} + +// reconcileSubnet will attempt to find the existing subnet, or create it if necessary. +// The logic can handle either Control Plane or Worker subnets, but must distinguish between them for Status updates. +func (s *VPCClusterScope) reconcileSubnet(subnet infrav1beta2.Subnet, isControlPlane bool) (bool, error) { //nolint: gocyclo + // If subnet already has a Name defined, use that for lookup. + if subnet.Name != nil { + // If we have the subnet name, we can easily check Network Stauts on the subnet's status. + if s.NetworkStatus() != nil { + if isControlPlane { + // If we find the subnet in Control Plane subnet status and that it is ready, we can return, with no requeue required. + if subnet, ok := s.NetworkStatus().ControlPlaneSubnets[*subnet.Name]; ok && subnet.Ready { + s.V(3).Info("found subnet is ready", "subnetName", subnet.Name) + return false, nil + } + } else { + // If we find the subnet in Worker subnet status and that it is ready, we can return, with no requeue required. + if subnet, ok := s.NetworkStatus().WorkerSubnets[*subnet.Name]; ok && subnet.Ready { + s.V(3).Info("found subnet is ready", "subnetName", subnet.Name) + return false, nil + } + } + } + + // If the subnet was not found in Network Status, attempt to lookup via IBM Cloud API. + subnetDetails, err := s.VPCClient.GetVPCSubnetByName(*subnet.Name) + if err != nil { + return false, fmt.Errorf("error trying to find subnet by name: %s", *subnet.Name) + } + // If the subnet was found, check and update the subnet status. + if subnetDetails != nil { + return s.updateSubnetStatus(subnetDetails, isControlPlane) + } + // If we didn't find the subnet by name in Network Status, or via IBM Cloud API, we assume the subnet doesn't exist yet, and must be created. + } else if subnet.ID != nil { + // Check Network Status for the subnet, if ID is available. + if s.NetworkStatus() != nil { + // Try to find the subnet status by ID in Network Status. + if isControlPlane && s.NetworkStatus().ControlPlaneSubnets != nil { + for _, cpSubnet := range s.NetworkStatus().ControlPlaneSubnets { + if *subnet.ID == cpSubnet.ID && cpSubnet.Ready { + s.V(3).Info("found subnet is ready", "subnetID", subnet.ID) + return false, nil + } + } + } else if !isControlPlane && s.NetworkStatus().WorkerSubnets != nil { + for _, wSubnet := range s.NetworkStatus().WorkerSubnets { + if *subnet.ID == wSubnet.ID && wSubnet.Ready { + s.V(3).Info("found subnet is ready", "subnetID", subnet.ID) + return false, nil + } + } + } + } + + // Otherwise, if we have a subnet ID, attempt lookup to confirm it exists + options := &vpcv1.GetSubnetOptions{ + ID: subnet.ID, + } + subnetDetails, _, err := s.VPCClient.GetSubnet(options) + if err != nil { + return false, fmt.Errorf("error trying to find subnet by id: %s", *subnet.ID) + } + // If a subnet ID is provided, we assume it must exist, we will not attempt to create a new subnet. + if subnetDetails == nil { + return false, fmt.Errorf("error failed to find subnet with id %s", *subnet.ID) + } + s.V(3).Info("Found Subnet with provided id", "subnetID", subnet.ID) + + // Check and update the subnet Network Status based on returned details. + return s.updateSubnetStatus(subnetDetails, isControlPlane) + } else { + // TODO(cjschaef): No name or ID should results in an error currently. We may wish to revisit cases where Subnets already exist, and we don't care to reconcile them (verify they exist). + return false, fmt.Errorf("error no subnet name or id defined") + } + + // If we have reached this point, we assume the sunbet does not exist yet and we need to create it. + s.V(3).Info("creating subnet", "subnetName", subnet.Name) + err := s.createSubnet(subnet, isControlPlane) + if err != nil { + return false, err + } + s.V(3).Info("Successfully created subnet", "subnetName", subnet.Name) + + // Recommend we requeue reconciliation after subnet was successfully created + return true, nil +} + +// buildSubnetsForZones will create a set of Subnets, using default names, for each availability zone within a Region. This is typically used when no subnets were provided, so a set of default subnets gets created. +func (s *VPCClusterScope) buildSubnetsForZones() ([]infrav1beta2.Subnet, error) { + subnets := make([]infrav1beta2.Subnet, 0) + zones, err := s.VPCClient.GetVPCZonesByRegion(s.IBMVPCCluster.Spec.Region) + if err != nil { + return subnets, fmt.Errorf("error unknown failure retrieving zones for region %s: %w", s.IBMVPCCluster.Spec.Region, err) + } + if len(zones) == 0 { + return subnets, fmt.Errorf("error retrieving subnet zones, no zones found in %s", s.IBMVPCCluster.Spec.Region) + } + for _, zone := range zones { + name := fmt.Sprintf("%s-%s", *s.GetServiceName(infrav1beta2.ResourceTypeSubnet), zone) + subnets = append(subnets, infrav1beta2.Subnet{ + Name: ptr.To(name), + Zone: ptr.To(zone), + }) + } + return subnets, nil +} + +// updateSubnetStatus will check the status of a IBM Cloud Subnet and update the Network Status. +func (s *VPCClusterScope) updateSubnetStatus(subnetDetails *vpcv1.Subnet, isControlPlane bool) (bool, error) { + requeue := true + if subnetDetails.Status != nil && *subnetDetails.Status == string(vpcv1.SubnetStatusAvailableConst) { + requeue = false + } + + resourceStatus := &infrav1beta2.ResourceStatus{ + ID: *subnetDetails.ID, + Name: subnetDetails.Name, + // Ready status will be invert of the need to requeue + Ready: !requeue, + } + if isControlPlane { + s.SetResourceStatus(infrav1beta2.ResourceTypeControlPlaneSubnet, resourceStatus) + } else { + s.SetResourceStatus(infrav1beta2.ResourceTypeWorkerSubnet, resourceStatus) + } + return requeue, nil +} + +// createSubnet creates a new VPC subnet. +func (s *VPCClusterScope) createSubnet(subnet infrav1beta2.Subnet, isControlPlane bool) error { + // Created resources should be placed in the cluster Resource Group (not Network, if it exists). + resourceGroupID, err := s.GetResourceGroupID() + if err != nil { + return fmt.Errorf("error retrieving resource group id for subnet creation: %w", err) + } else if resourceGroupID == "" { + return fmt.Errorf("error retrieving resource group id for resource group %s", s.IBMVPCCluster.Spec.ResourceGroup) + } + + vpcID, err := s.GetVPCID() + if err != nil { + return fmt.Errorf("error retrieving vpc id for subnet creation: %w", err) + } + + // TODO(cjschaef): Move to webhook validation. + if subnet.Zone == nil { + return fmt.Errorf("error subnet zone must be defined for subnet %s", *subnet.Name) + } + + // NOTE(cjschaef): We likely will want to add support to use custom Address Prefixes + // For now, we rely on the API to assign us prefixes, as we request via IP count + var ipCount int64 = 256 + // We currnetly only support IPv4 + ipVersion := "ipv4" + + // Find or create a Public Gateway in this zone for the subnet, only one Public Gateway is required for each zone, for this cluster. + // NOTE(cjschaef): We may need to add support to not attach Public Gateways to subnets. + publicGateway, err := s.findOrCreatePublicGateway(*subnet.Zone) + if err != nil { + return fmt.Errorf("error failed to find or create public gateway for subnet %s: %w", *subnet.Name, err) + } + + options := &vpcv1.CreateSubnetOptions{} + options.SetSubnetPrototype(&vpcv1.SubnetPrototype{ + IPVersion: ptr.To(ipVersion), + TotalIpv4AddressCount: ptr.To(ipCount), + Name: subnet.Name, + VPC: &vpcv1.VPCIdentity{ + ID: vpcID, + }, + Zone: &vpcv1.ZoneIdentity{ + Name: subnet.Zone, + }, + ResourceGroup: &vpcv1.ResourceGroupIdentity{ + ID: ptr.To(resourceGroupID), + }, + PublicGateway: &vpcv1.PublicGatewayIdentity{ + ID: publicGateway.ID, + }, + }) + + // Create subnet. + subnetDetails, _, err := s.VPCClient.CreateSubnet(options) + if err != nil { + return fmt.Errorf("error unknown failure creating vpc subnet: %w", err) + } + if subnetDetails == nil || subnetDetails.ID == nil || subnetDetails.CRN == nil { + return fmt.Errorf("error failed creating subnet: %s", *subnet.Name) + } + + // Initially populate subnet's status. + resourceStatus := &infrav1beta2.ResourceStatus{ + ID: *subnetDetails.ID, + Name: subnetDetails.Name, + Ready: false, + } + if isControlPlane { + s.SetResourceStatus(infrav1beta2.ResourceTypeControlPlaneSubnet, resourceStatus) + } else { + s.SetResourceStatus(infrav1beta2.ResourceTypeWorkerSubnet, resourceStatus) + } + + // Add a tag to the subnet for the cluster. + err = s.TagResource(s.IBMVPCCluster.Name, *subnetDetails.CRN) + if err != nil { + return fmt.Errorf("error failed to tag subnet %s: %w", *subnetDetails.Name, err) + } + + return nil +} + +// findOrCreatePublicGateway will attempt to find if there is an existing Public Gateway for a specific zone, for the cluster (in cluster's Resource Group and VPC), or create a new one. Only one Public Gateway is required in each zone, for any subnets in that zone. +func (s *VPCClusterScope) findOrCreatePublicGateway(zone string) (*vpcv1.PublicGateway, error) { + publicGatewayName := fmt.Sprintf("%s-%s", *s.GetServiceName(infrav1beta2.ResourceTypePublicGateway), zone) + // We will use the cluster Resource Group ID, as we expect to create all resources (Public Gateways and Subnets) in that Resource Group. + resourceGroupID, err := s.GetResourceGroupID() + if err != nil { + return nil, fmt.Errorf("error unknown failure retrieving resource group id for public gateway: %w", err) + } + publicGateway, err := s.VPCClient.GetVPCPublicGatewayByName(publicGatewayName, resourceGroupID) + if err != nil { + return nil, fmt.Errorf("error unknown failure retrieving public gateway for zone %s: %w", zone, err) + } + + // If we found the Public Gateway, with an ID, for the zone, return it. + // NOTE(cjschaef): We may wish to confirm the PublicGateway, by checking Tags (Global Tagging), but this might be sufficient, as we don't expect to have duplicate PG's or existing PG's, as we wouldn't create subnets and PG's for existing Network Infrastructure. + if publicGateway != nil && publicGateway.ID != nil { + return publicGateway, nil + } + + // Otherwise, create a new Public Gateway for the zone. + vpcID, err := s.GetVPCID() + if err != nil { + return nil, fmt.Errorf("error failed retrieving vpc id for public gateway creation: %w", err) + } + if vpcID == nil { + return nil, fmt.Errorf("error failed to retrieve vpc id for public gateway creation") + } + + publicGatewayDetails, _, err := s.VPCClient.CreatePublicGateway(&vpcv1.CreatePublicGatewayOptions{ + Name: ptr.To(publicGatewayName), + ResourceGroup: &vpcv1.ResourceGroupIdentity{ + ID: ptr.To(resourceGroupID), + }, + VPC: &vpcv1.VPCIdentity{ + ID: vpcID, + }, + Zone: &vpcv1.ZoneIdentity{ + Name: ptr.To(zone), + }, + }) + if err != nil { + return nil, fmt.Errorf("error unknown failure creating public gateway: %w", err) + } + if publicGatewayDetails == nil || publicGatewayDetails.ID == nil || publicGatewayDetails.CRN == nil { + return nil, fmt.Errorf("error failed creating public gateway for zone %s", zone) + } + + s.V(3).Info("created public gateway", "id", publicGatewayDetails.ID) + + // Add a tag to the public gateway for the cluster + err = s.TagResource(s.IBMVPCCluster.Name, *publicGatewayDetails.CRN) + if err != nil { + return nil, fmt.Errorf("error failed to tag public gateway %s: %w", *publicGatewayDetails.Name, err) + } + + return publicGatewayDetails, nil +} diff --git a/controllers/ibmvpccluster_controller.go b/controllers/ibmvpccluster_controller.go index 33741878f..762ac8b06 100644 --- a/controllers/ibmvpccluster_controller.go +++ b/controllers/ibmvpccluster_controller.go @@ -261,6 +261,19 @@ func (r *IBMVPCClusterReconciler) reconcileCluster(clusterScope *scope.VPCCluste clusterScope.Info("Reconciliation of VPC Custom Image complete") conditions.MarkTrue(clusterScope.IBMVPCCluster, infrav1beta2.ImageReadyCondition) + // Reconcile the cluster's VPC Subnets. + clusterScope.Info("Reconciling VPC Subnets") + if requeue, err := clusterScope.ReconcileSubnets(); err != nil { + clusterScope.Error(err, "failed to reconcile VPC Subnets") + conditions.MarkFalse(clusterScope.IBMVPCCluster, infrav1beta2.VPCSubnetReadyCondition, infrav1beta2.VPCSubnetReconciliationFailedReason, capiv1beta1.ConditionSeverityError, err.Error()) + return reconcile.Result{}, err + } else if requeue { + clusterScope.Info("VPC Subnets creation is pending, requeueing") + return reconcile.Result{RequeueAfter: 15 * time.Second}, nil + } + clusterScope.Info("Reconciliation of VPC Subnets complete") + conditions.MarkTrue(clusterScope.IBMVPCCluster, infrav1beta2.VPCSubnetReadyCondition) + // TODO(cjschaef): add remaining resource reconciliation. // Mark cluster as ready. diff --git a/pkg/cloud/services/vpc/mock/vpc_generated.go b/pkg/cloud/services/vpc/mock/vpc_generated.go index d7cd6191c..815d6d29c 100644 --- a/pkg/cloud/services/vpc/mock/vpc_generated.go +++ b/pkg/cloud/services/vpc/mock/vpc_generated.go @@ -523,6 +523,21 @@ func (mr *MockVpcMockRecorder) GetVPCByName(vpcName any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVPCByName", reflect.TypeOf((*MockVpc)(nil).GetVPCByName), vpcName) } +// GetVPCPublicGatewayByName mocks base method. +func (m *MockVpc) GetVPCPublicGatewayByName(publicGatewayName, resourceGroupID string) (*vpcv1.PublicGateway, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVPCPublicGatewayByName", publicGatewayName, resourceGroupID) + ret0, _ := ret[0].(*vpcv1.PublicGateway) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetVPCPublicGatewayByName indicates an expected call of GetVPCPublicGatewayByName. +func (mr *MockVpcMockRecorder) GetVPCPublicGatewayByName(publicGatewayName, resourceGroupID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVPCPublicGatewayByName", reflect.TypeOf((*MockVpc)(nil).GetVPCPublicGatewayByName), publicGatewayName, resourceGroupID) +} + // GetVPCSubnetByName mocks base method. func (m *MockVpc) GetVPCSubnetByName(subnetName string) (*vpcv1.Subnet, error) { m.ctrl.T.Helper() @@ -538,6 +553,21 @@ func (mr *MockVpcMockRecorder) GetVPCSubnetByName(subnetName any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVPCSubnetByName", reflect.TypeOf((*MockVpc)(nil).GetVPCSubnetByName), subnetName) } +// GetVPCZonesByRegion mocks base method. +func (m *MockVpc) GetVPCZonesByRegion(region string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVPCZonesByRegion", region) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetVPCZonesByRegion indicates an expected call of GetVPCZonesByRegion. +func (mr *MockVpcMockRecorder) GetVPCZonesByRegion(region any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVPCZonesByRegion", reflect.TypeOf((*MockVpc)(nil).GetVPCZonesByRegion), region) +} + // ListImages mocks base method. func (m *MockVpc) ListImages(options *vpcv1.ListImagesOptions) (*vpcv1.ImageCollection, *core.DetailedResponse, error) { m.ctrl.T.Helper() diff --git a/pkg/cloud/services/vpc/service.go b/pkg/cloud/services/vpc/service.go index 25b0cdcdf..7bcb4971e 100644 --- a/pkg/cloud/services/vpc/service.go +++ b/pkg/cloud/services/vpc/service.go @@ -262,6 +262,45 @@ func (s *Service) GetImageByName(imageName string) (*vpcv1.Image, error) { return image, nil } +// GetVPCPublicGatewayByName returns the VPC Public Gateway with given name. If not found, returns nil. +func (s *Service) GetVPCPublicGatewayByName(publicGatewayName string, resourceGroupID string) (*vpcv1.PublicGateway, error) { + var publicGateway *vpcv1.PublicGateway + f := func(start string) (bool, string, error) { + // check for existing public gateways + listPublicGatewaysOptions := s.vpcService.NewListPublicGatewaysOptions().SetResourceGroupID(resourceGroupID) + if start != "" { + listPublicGatewaysOptions.Start = &start + } + + publicGatewaysList, _, err := s.vpcService.ListPublicGateways(listPublicGatewaysOptions) + if err != nil { + return false, "", err + } + + if publicGatewaysList == nil { + return false, "", fmt.Errorf("public gateways list returned is nil") + } + + for index, pg := range publicGatewaysList.PublicGateways { + if *pg.Name == publicGatewayName { + publicGateway = &publicGatewaysList.PublicGateways[index] + return true, "", nil + } + } + + if publicGatewaysList.Next != nil && *publicGatewaysList.Next.Href != "" { + return false, *publicGatewaysList.Next.Href, nil + } + return true, "", nil + } + + if err := utils.PagingHelper(f); err != nil { + return nil, err + } + + return publicGateway, nil +} + // GetSubnet return subnet. func (s *Service) GetSubnet(options *vpcv1.GetSubnetOptions) (*vpcv1.Subnet, *core.DetailedResponse, error) { return s.vpcService.GetSubnet(options) @@ -441,6 +480,20 @@ func (s *Service) GetSecurityGroupRule(options *vpcv1.GetSecurityGroupRuleOption return s.vpcService.GetSecurityGroupRule(options) } +// GetVPCZonesByRegion gets the VPC availability zones for a specific IBM Cloud region. +func (s *Service) GetVPCZonesByRegion(region string) ([]string, error) { + zones := make([]string, 0) + options := s.vpcService.NewListRegionZonesOptions(region) + result, _, err := s.vpcService.ListRegionZones(options) + if err != nil { + return zones, err + } + for _, zone := range result.Zones { + zones = append(zones, *zone.Name) + } + return zones, nil +} + // NewService returns a new VPC Service. func NewService(svcEndpoint string) (Vpc, error) { service := &Service{} diff --git a/pkg/cloud/services/vpc/vpc.go b/pkg/cloud/services/vpc/vpc.go index 69afc89eb..bd0b5820b 100644 --- a/pkg/cloud/services/vpc/vpc.go +++ b/pkg/cloud/services/vpc/vpc.go @@ -58,6 +58,7 @@ type Vpc interface { GetVPC(*vpcv1.GetVPCOptions) (*vpcv1.VPC, *core.DetailedResponse, error) GetVPCByName(vpcName string) (*vpcv1.VPC, error) GetImageByName(imageName string) (*vpcv1.Image, error) + GetVPCPublicGatewayByName(publicGatewayName string, resourceGroupID string) (*vpcv1.PublicGateway, error) GetSubnet(*vpcv1.GetSubnetOptions) (*vpcv1.Subnet, *core.DetailedResponse, error) GetVPCSubnetByName(subnetName string) (*vpcv1.Subnet, error) GetLoadBalancerByName(loadBalancerName string) (*vpcv1.LoadBalancer, error) @@ -68,4 +69,5 @@ type Vpc interface { GetSecurityGroup(options *vpcv1.GetSecurityGroupOptions) (*vpcv1.SecurityGroup, *core.DetailedResponse, error) GetSecurityGroupByName(name string) (*vpcv1.SecurityGroup, error) GetSecurityGroupRule(options *vpcv1.GetSecurityGroupRuleOptions) (vpcv1.SecurityGroupRuleIntf, *core.DetailedResponse, error) + GetVPCZonesByRegion(region string) ([]string, error) }