From 9dcb0667e31411ea96ad8c62fdc44b2f6ba56cfa Mon Sep 17 00:00:00 2001 From: Christopher J Schaefer Date: Mon, 16 Sep 2024 23:57:14 -0500 Subject: [PATCH] VPC: Add Custom Image reconciliation (#1942) Add support to reconcile a VPC Custom Image for the new v2 VPC Infrastructure reconcile logic. Related: https://github.com/kubernetes-sigs/cluster-api-provider-ibmcloud/issues/1896 --- api/v1beta2/conditions_consts.go | 3 + api/v1beta2/ibmvpccluster_types.go | 2 +- cloud/scope/util.go | 56 +++++ cloud/scope/vpc_cluster.go | 192 ++++++++++++++++++ ...cture.cluster.x-k8s.io_ibmvpcclusters.yaml | 2 +- ...uster.x-k8s.io_ibmvpcclustertemplates.yaml | 2 +- controllers/ibmvpccluster_controller.go | 14 ++ pkg/cloud/services/vpc/mock/vpc_generated.go | 47 +++++ pkg/cloud/services/vpc/service.go | 49 +++++ pkg/cloud/services/vpc/vpc.go | 3 + 10 files changed, 367 insertions(+), 3 deletions(-) diff --git a/api/v1beta2/conditions_consts.go b/api/v1beta2/conditions_consts.go index a3cda5597..b03f95a45 100644 --- a/api/v1beta2/conditions_consts.go +++ b/api/v1beta2/conditions_consts.go @@ -59,6 +59,9 @@ const ( // ImageImportFailedReason used when the image import is failed. ImageImportFailedReason = "ImageImportFailed" + + // ImageReconciliationFailedReason used when an error occurs during VPC Custom Image reconciliation. + ImageReconciliationFailedReason = "ImageReconciliationFailed" ) const ( diff --git a/api/v1beta2/ibmvpccluster_types.go b/api/v1beta2/ibmvpccluster_types.go index 3740f4350..b4ed7037b 100644 --- a/api/v1beta2/ibmvpccluster_types.go +++ b/api/v1beta2/ibmvpccluster_types.go @@ -195,7 +195,7 @@ type ImageSpec struct { // name is the name of the desired VPC Custom Image. // +kubebuilder:validation:MinLength:=1 // +kubebuilder:validation:MaxLength:=63 - // +kubebuilder:validation:Pattern='/^([a-z]|[a-z][-a-z0-9]*[a-z0-9])$/' + // +kubebuilder:validation:Pattern=`^([a-z]|[a-z][-a-z0-9]*[a-z0-9])$` // +optional Name *string `json:"name,omitempty"` diff --git a/cloud/scope/util.go b/cloud/scope/util.go index 7559d1d64..6ce84d754 100644 --- a/cloud/scope/util.go +++ b/cloud/scope/util.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "strconv" + "strings" "sigs.k8s.io/controller-runtime/pkg/client" @@ -57,3 +58,58 @@ func CheckCreateInfraAnnotation(cluster infrav1beta2.IBMPowerVSCluster) bool { } return createInfra } + +// CRN is a local duplicate of IBM Cloud CRN for parsing and references. +type CRN struct { + Scheme string + Version string + CName string + CType string + ServiceName string + Region string + ScopeType string + Scope string + ServiceInstance string + ResourceType string + Resource string +} + +// ParseCRN is a local duplicate of IBM Cloud CRN Parse functionality, to convert a string into a CRN, if it is in the correct format. +func ParseCRN(s string) (*CRN, error) { + if s == "" { + return nil, nil + } + + segments := strings.Split(s, ":") + if len(segments) != 10 || segments[0] != "crn" { + return nil, fmt.Errorf("malformed CRN") + } + + crn := &CRN{ + Scheme: segments[0], + Version: segments[1], + CName: segments[2], + CType: segments[3], + ServiceName: segments[4], + Region: segments[5], + ServiceInstance: segments[7], + ResourceType: segments[8], + Resource: segments[9], + } + + // Scope portions require additional parsing. + scopeSegments := segments[6] + if scopeSegments != "" { + if scopeSegments == "global" { + crn.Scope = scopeSegments + } else { + scopeParts := strings.Split(scopeSegments, "/") + if len(scopeParts) != 2 { + return nil, fmt.Errorf("malformed scope in CRN") + } + crn.ScopeType, crn.Scope = scopeParts[0], scopeParts[1] + } + } + + return crn, nil +} diff --git a/cloud/scope/vpc_cluster.go b/cloud/scope/vpc_cluster.go index b65c72448..71311e6b2 100644 --- a/cloud/scope/vpc_cluster.go +++ b/cloud/scope/vpc_cluster.go @@ -367,6 +367,16 @@ func (s *VPCClusterScope) SetResourceStatus(resourceType infrav1beta2.ResourceTy s.IBMVPCCluster.Status.Network.VPC = resource } s.NetworkStatus().VPC.Set(*resource) + case infrav1beta2.ResourceTypeCustomImage: + if s.IBMVPCCluster.Status.Image == nil { + s.IBMVPCCluster.Status.Image = &infrav1beta2.ResourceStatus{ + ID: resource.ID, + Name: resource.Name, + Ready: resource.Ready, + } + return + } + s.IBMVPCCluster.Status.Image.Set(*resource) default: s.V(3).Info("unsupported resource type", "resourceType", resourceType) } @@ -494,3 +504,185 @@ func (s *VPCClusterScope) createVPC() (*vpcv1.VPC, error) { return vpcDetails, nil } + +// ReconcileVPCCustomImage reconciles the VPC Custom Image. +func (s *VPCClusterScope) ReconcileVPCCustomImage() (bool, error) { + // VPC Custom Image reconciliation is based on the following possibilities. + // 1. Check Status for ID or Name, from previous lookup in reconciliation loop. + // 2. If no Image spec is provided, assume the image is managed externally, thus no reconciliation required. + // 3. If Image name is provided, check if an existing VPC Custom Image exists with that name (unfortunately names may not be unique), checking status of the image, updating appropriately. + // 4. If Image CRN is provided, parse the ID from the CRN to perform lookup. CRN may be for another account, causing lookup to fail (permissions), may require better safechecks based on other CRN details. + // 5. If no Image ID has been identified, assume a VPC Custom Image needs to be created, do so. + var imageID *string + // Attempt to collect VPC Custom Image info from Status. + if s.IBMVPCCluster.Status.Image != nil { + if s.IBMVPCCluster.Status.Image.ID != "" { + imageID = ptr.To(s.IBMVPCCluster.Status.Image.ID) + } + } else if s.IBMVPCCluster.Spec.Image == nil { + // If no Image spec was defined, we expect it is maintained externally and continue without reconciling. For example, using a Catalog Offering Custom Image, which may be in another account, which means it cannot be looked up, but can be used when creating Instances. + s.V(3).Info("No VPC Custom Image defined, skipping reconciliation") + return false, nil + } else if s.IBMVPCCluster.Spec.Image.Name != nil { + // Attempt to retrieve the image details via the name, if it already exists + imageDetails, err := s.VPCClient.GetImageByName(*s.IBMVPCCluster.Spec.Image.Name) + if err != nil { + return false, fmt.Errorf("error checking vpc custom image by name: %w", err) + } else if imageDetails != nil && imageDetails.ID != nil { + // Prevent relookup (API request) of VPC Custom Image if we already have the necessary data + requeue := true + if imageDetails.Status != nil && *imageDetails.Status == string(vpcv1.ImageStatusAvailableConst) { + requeue = false + } + s.SetResourceStatus(infrav1beta2.ResourceTypeCustomImage, &infrav1beta2.ResourceStatus{ + ID: *imageDetails.ID, + Name: s.IBMVPCCluster.Spec.Image.Name, + // Ready status will be invert of the need to requeue. + Ready: !requeue, + }) + return requeue, nil + } + } else if s.IBMVPCCluster.Spec.Image.CRN != nil { + // Parse the supplied Image CRN for Id, to perform image lookup. + imageCRN, err := ParseCRN(*s.IBMVPCCluster.Spec.Image.CRN) + if err != nil { + return false, fmt.Errorf("error parsing vpc custom image crn: %w", err) + } + // If the value provided isn't a CRN or is missing the Resource ID, raise an error. + if imageCRN == nil || imageCRN.Resource == "" { + return false, fmt.Errorf("error parsing vpc custom image crn, missing resource id") + } + // If we didn't hit an error during parsing, and Resource was set, set that as the Image ID. + imageID = ptr.To(imageCRN.Resource) + } + + // Check status of VPC Custom Image. + if imageID != nil { + image, _, err := s.VPCClient.GetImage(&vpcv1.GetImageOptions{ + ID: imageID, + }) + if err != nil { + return false, fmt.Errorf("error retrieving vpc custom image by id: %w", err) + } + if image == nil { + return false, fmt.Errorf("error failed to retrieve vpc custom image with id %s", *imageID) + } + s.V(3).Info("Found VPC Custom Image with provided id", "imageID", imageID) + + requeue := true + if image.Status != nil && *image.Status == string(vpcv1.ImageStatusAvailableConst) { + requeue = false + } + s.SetResourceStatus(infrav1beta2.ResourceTypeCustomImage, &infrav1beta2.ResourceStatus{ + ID: *imageID, + Name: image.Name, + // Ready status will be invert of the need to requeue. + Ready: !requeue, + }) + return requeue, nil + } + + // No VPC Custom Image exists or was found, so create the Custom Image. + s.V(3).Info("Creating a VPC Custom Image") + err := s.createCustomImage() + if err != nil { + return false, fmt.Errorf("error failure trying to create vpc custom image: %w", err) + } + + s.V(3).Info("Successfully created VPC Custom Image") + return true, nil +} + +// createCustomImage will create a new VPC Custom Image. +func (s *VPCClusterScope) createCustomImage() error { + // TODO(cjschaef): Remove in favor of webhook validation. + if s.IBMVPCCluster.Spec.Image.OperatingSystem == nil { + return fmt.Errorf("error failed to create vpc custom image due to missing operatingSystem") + } + + // Collect the Resource Group ID. + var resourceGroupID *string + // Check Resource Group in Image spec. + if s.IBMVPCCluster.Spec.Image.ResourceGroup != nil { + if s.IBMVPCCluster.Spec.Image.ResourceGroup.ID != "" { + resourceGroupID = ptr.To(s.IBMVPCCluster.Spec.Image.ResourceGroup.ID) + } else if s.IBMVPCCluster.Spec.Image.ResourceGroup.Name != nil { + id, err := s.ResourceManagerClient.GetResourceGroupByName(*s.IBMVPCCluster.Spec.Image.ResourceGroup.Name) + if err != nil { + return fmt.Errorf("error retrieving resource group by name: %w", err) + } + resourceGroupID = id.ID + } + } else { + // Otherwise, we will use the cluster Resource Group ID, as we expect to create all resources in that Resource Group. + id, err := s.GetResourceGroupID() + if err != nil { + return fmt.Errorf("error retrieving resource group id: %w", err) + } + resourceGroupID = ptr.To(id) + } + + // Build the COS Object URL using the ImageSpec + fileHRef, err := s.buildCOSObjectHRef() + if err != nil { + return fmt.Errorf("error building vpc custom image file href: %w", err) + } + + options := &vpcv1.CreateImageOptions{ + ImagePrototype: &vpcv1.ImagePrototype{ + Name: s.IBMVPCCluster.Spec.Image.Name, + File: &vpcv1.ImageFilePrototype{ + Href: fileHRef, + }, + OperatingSystem: &vpcv1.OperatingSystemIdentity{ + Name: s.IBMVPCCluster.Spec.Image.OperatingSystem, + }, + ResourceGroup: &vpcv1.ResourceGroupIdentity{ + ID: resourceGroupID, + }, + }, + } + + imageDetails, _, err := s.VPCClient.CreateImage(options) + if err != nil { + return fmt.Errorf("error unknown failure creating vpc custom image: %w", err) + } + if imageDetails == nil || imageDetails.ID == nil || imageDetails.Name == nil || imageDetails.CRN == nil { + return fmt.Errorf("error failed creating custom image") + } + + // Initially populate the Image's status. + s.SetResourceStatus(infrav1beta2.ResourceTypeCustomImage, &infrav1beta2.ResourceStatus{ + ID: *imageDetails.ID, + Name: imageDetails.Name, + // We must wait for the image to be ready, on followup reconciliation loops. + Ready: false, + }) + + // NOTE: This tagging is only attempted once. We may wish to refactor in case this single attempt fails. + if err := s.TagResource(s.Name(), *imageDetails.CRN); err != nil { + return fmt.Errorf("error failure tagging vpc custom image: %w", err) + } + return nil +} + +// buildCOSObjectHRef will build the HRef path to a COS Object that can be used for VPC Custom Image creation. +func (s *VPCClusterScope) buildCOSObjectHRef() (*string, error) { + // TODO(cjschaef): Remove in favor of webhook validation. + // We need COS details in order to create the Custom Image from. + if s.IBMVPCCluster.Spec.Image.COSInstance == nil || s.IBMVPCCluster.Spec.Image.COSBucket == nil || s.IBMVPCCluster.Spec.Image.COSObject == nil { + return nil, fmt.Errorf("error failed to build cos object href, cos details missing") + } + + // Get COS Bucket Region, defaulting to cluster Region if not specified. + bucketRegion := s.IBMVPCCluster.Spec.Region + if s.IBMVPCCluster.Spec.Image.COSBucketRegion != nil { + bucketRegion = *s.IBMVPCCluster.Spec.Image.COSBucketRegion + } + + // Expected HRef format: + // cos://// + href := fmt.Sprintf("cos://%s/%s/%s", bucketRegion, *s.IBMVPCCluster.Spec.Image.COSBucket, *s.IBMVPCCluster.Spec.Image.COSObject) + s.V(3).Info("building image ref", "href", href) + return ptr.To(href), nil +} diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclusters.yaml index cc93420a6..311704df9 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclusters.yaml @@ -462,7 +462,7 @@ spec: description: name is the name of the desired VPC Custom Image. maxLength: 63 minLength: 1 - pattern: '''/^([a-z]|[a-z][-a-z0-9]*[a-z0-9])$/''' + pattern: ^([a-z]|[a-z][-a-z0-9]*[a-z0-9])$ type: string operatingSystem: description: operatingSystem is the Custom Image's Operating System diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclustertemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclustertemplates.yaml index 85b26933a..ee2a7c366 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclustertemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclustertemplates.yaml @@ -317,7 +317,7 @@ spec: Image. maxLength: 63 minLength: 1 - pattern: '''/^([a-z]|[a-z][-a-z0-9]*[a-z0-9])$/''' + pattern: ^([a-z]|[a-z][-a-z0-9]*[a-z0-9])$ type: string operatingSystem: description: operatingSystem is the Custom Image's Operating diff --git a/controllers/ibmvpccluster_controller.go b/controllers/ibmvpccluster_controller.go index 50f8abc92..33741878f 100644 --- a/controllers/ibmvpccluster_controller.go +++ b/controllers/ibmvpccluster_controller.go @@ -245,8 +245,22 @@ func (r *IBMVPCClusterReconciler) reconcileCluster(clusterScope *scope.VPCCluste clusterScope.Info("VPC creation is pending, requeuing") return reconcile.Result{RequeueAfter: 15 * time.Second}, nil } + clusterScope.Info("Reconciliation of VPC complete") conditions.MarkTrue(clusterScope.IBMVPCCluster, infrav1beta2.VPCReadyCondition) + // Reconcile the cluster's VPC Custom Image. + clusterScope.Info("Reconciling VPC Custom Image") + if requeue, err := clusterScope.ReconcileVPCCustomImage(); err != nil { + clusterScope.Error(err, "failed to reconcile VPC Custom Image") + conditions.MarkFalse(clusterScope.IBMVPCCluster, infrav1beta2.ImageReadyCondition, infrav1beta2.ImageReconciliationFailedReason, capiv1beta1.ConditionSeverityError, err.Error()) + return reconcile.Result{}, err + } else if requeue { + clusterScope.Info("VPC Custom Image creation is pending, requeueing") + return reconcile.Result{RequeueAfter: 15 * time.Second}, nil + } + clusterScope.Info("Reconciliation of VPC Custom Image complete") + conditions.MarkTrue(clusterScope.IBMVPCCluster, infrav1beta2.ImageReadyCondition) + // 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 7312a76eb..d7cd6191c 100644 --- a/pkg/cloud/services/vpc/mock/vpc_generated.go +++ b/pkg/cloud/services/vpc/mock/vpc_generated.go @@ -55,6 +55,22 @@ func (m *MockVpc) EXPECT() *MockVpcMockRecorder { return m.recorder } +// CreateImage mocks base method. +func (m *MockVpc) CreateImage(options *vpcv1.CreateImageOptions) (*vpcv1.Image, *core.DetailedResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateImage", options) + ret0, _ := ret[0].(*vpcv1.Image) + ret1, _ := ret[1].(*core.DetailedResponse) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// CreateImage indicates an expected call of CreateImage. +func (mr *MockVpcMockRecorder) CreateImage(options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateImage", reflect.TypeOf((*MockVpc)(nil).CreateImage), options) +} + // CreateInstance mocks base method. func (m *MockVpc) CreateInstance(options *vpcv1.CreateInstanceOptions) (*vpcv1.Instance, *core.DetailedResponse, error) { m.ctrl.T.Helper() @@ -288,6 +304,37 @@ func (mr *MockVpcMockRecorder) DeleteVPC(options any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteVPC", reflect.TypeOf((*MockVpc)(nil).DeleteVPC), options) } +// GetImage mocks base method. +func (m *MockVpc) GetImage(options *vpcv1.GetImageOptions) (*vpcv1.Image, *core.DetailedResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetImage", options) + ret0, _ := ret[0].(*vpcv1.Image) + ret1, _ := ret[1].(*core.DetailedResponse) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetImage indicates an expected call of GetImage. +func (mr *MockVpcMockRecorder) GetImage(options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImage", reflect.TypeOf((*MockVpc)(nil).GetImage), options) +} + +// GetImageByName mocks base method. +func (m *MockVpc) GetImageByName(imageName string) (*vpcv1.Image, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetImageByName", imageName) + ret0, _ := ret[0].(*vpcv1.Image) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetImageByName indicates an expected call of GetImageByName. +func (mr *MockVpcMockRecorder) GetImageByName(imageName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImageByName", reflect.TypeOf((*MockVpc)(nil).GetImageByName), imageName) +} + // GetInstance mocks base method. func (m *MockVpc) GetInstance(options *vpcv1.GetInstanceOptions) (*vpcv1.Instance, *core.DetailedResponse, error) { m.ctrl.T.Helper() diff --git a/pkg/cloud/services/vpc/service.go b/pkg/cloud/services/vpc/service.go index dfbc9cdc1..25b0cdcdf 100644 --- a/pkg/cloud/services/vpc/service.go +++ b/pkg/cloud/services/vpc/service.go @@ -159,11 +159,21 @@ func (s *Service) ListKeys(options *vpcv1.ListKeysOptions) (*vpcv1.KeyCollection return s.vpcService.ListKeys(options) } +// CreateImage creates a new VPC Custom Image. +func (s *Service) CreateImage(options *vpcv1.CreateImageOptions) (*vpcv1.Image, *core.DetailedResponse, error) { + return s.vpcService.CreateImage(options) +} + // ListImages returns list of images in a region. func (s *Service) ListImages(options *vpcv1.ListImagesOptions) (*vpcv1.ImageCollection, *core.DetailedResponse, error) { return s.vpcService.ListImages(options) } +// GetImage returns a VPC Custom image. +func (s *Service) GetImage(options *vpcv1.GetImageOptions) (*vpcv1.Image, *core.DetailedResponse, error) { + return s.vpcService.GetImage(options) +} + // GetInstanceProfile returns instance profile. func (s *Service) GetInstanceProfile(options *vpcv1.GetInstanceProfileOptions) (*vpcv1.InstanceProfile, *core.DetailedResponse, error) { return s.vpcService.GetInstanceProfile(options) @@ -213,6 +223,45 @@ func (s *Service) GetVPCByName(vpcName string) (*vpcv1.VPC, error) { return vpc, nil } +// GetImageByName returns the VPC Custom Image with given name. If not found, returns nil. +func (s *Service) GetImageByName(imageName string) (*vpcv1.Image, error) { + var image *vpcv1.Image + f := func(start string) (bool, string, error) { + // check for existing images + listImagesOptions := &vpcv1.ListImagesOptions{} + if start != "" { + listImagesOptions.Start = &start + } + + imagesList, _, err := s.ListImages(listImagesOptions) + if err != nil { + return false, "", err + } + + if imagesList == nil { + return false, "", fmt.Errorf("image list returned is nil") + } + + for i, v := range imagesList.Images { + if *v.Name == imageName { + image = &imagesList.Images[i] + return true, "", nil + } + } + + if imagesList.Next != nil && *imagesList.Next.Href != "" { + return false, *imagesList.Next.Href, nil + } + return true, "", nil + } + + if err := utils.PagingHelper(f); err != nil { + return nil, err + } + + return image, nil +} + // GetSubnet return subnet. func (s *Service) GetSubnet(options *vpcv1.GetSubnetOptions) (*vpcv1.Subnet, *core.DetailedResponse, error) { return s.vpcService.GetSubnet(options) diff --git a/pkg/cloud/services/vpc/vpc.go b/pkg/cloud/services/vpc/vpc.go index 511815fbc..69afc89eb 100644 --- a/pkg/cloud/services/vpc/vpc.go +++ b/pkg/cloud/services/vpc/vpc.go @@ -51,10 +51,13 @@ type Vpc interface { DeleteLoadBalancerPoolMember(options *vpcv1.DeleteLoadBalancerPoolMemberOptions) (*core.DetailedResponse, error) ListLoadBalancerPoolMembers(options *vpcv1.ListLoadBalancerPoolMembersOptions) (*vpcv1.LoadBalancerPoolMemberCollection, *core.DetailedResponse, error) ListKeys(options *vpcv1.ListKeysOptions) (*vpcv1.KeyCollection, *core.DetailedResponse, error) + CreateImage(options *vpcv1.CreateImageOptions) (*vpcv1.Image, *core.DetailedResponse, error) ListImages(options *vpcv1.ListImagesOptions) (*vpcv1.ImageCollection, *core.DetailedResponse, error) + GetImage(options *vpcv1.GetImageOptions) (*vpcv1.Image, *core.DetailedResponse, error) GetInstanceProfile(options *vpcv1.GetInstanceProfileOptions) (*vpcv1.InstanceProfile, *core.DetailedResponse, error) GetVPC(*vpcv1.GetVPCOptions) (*vpcv1.VPC, *core.DetailedResponse, error) GetVPCByName(vpcName string) (*vpcv1.VPC, error) + GetImageByName(imageName string) (*vpcv1.Image, error) GetSubnet(*vpcv1.GetSubnetOptions) (*vpcv1.Subnet, *core.DetailedResponse, error) GetVPCSubnetByName(subnetName string) (*vpcv1.Subnet, error) GetLoadBalancerByName(loadBalancerName string) (*vpcv1.LoadBalancer, error)