Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4e23720

Browse files
committedSep 4, 2024·
VPC: Add Custom Image reconciliation
Add support to reconcile a VPC Custom Image for the new v2 VPC Infrastructure reconcile logic. Related: #1896
1 parent 1f473c2 commit 4e23720

10 files changed

+403
-5
lines changed
 

‎api/v1beta2/ibmvpccluster_types.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ type ImageSpec struct {
195195
// name is the name of the desired VPC Custom Image.
196196
// +kubebuilder:validation:MinLength:=1
197197
// +kubebuilder:validation:MaxLength:=63
198-
// +kubebuilder:validation:Pattern='/^([a-z]|[a-z][-a-z0-9]*[a-z0-9])$/'
198+
// +kubebuilder:validation:Pattern=`^([a-z]|[a-z][-a-z0-9]*[a-z0-9])$`
199199
// +optional
200200
Name *string `json:"name,omitempty"`
201201

‎cloud/scope/vpc_cluster.go

+190-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"github.com/go-logr/logr"
2525

26+
"github.com/IBM-Cloud/bluemix-go/crn"
2627
"github.com/IBM/go-sdk-core/v5/core"
2728
"github.com/IBM/platform-services-go-sdk/globaltaggingv1"
2829
"github.com/IBM/platform-services-go-sdk/resourcecontrollerv2"
@@ -275,7 +276,7 @@ func (s *VPCClusterScope) GetResourceGroupID() (string, error) {
275276
// If the Resource Group is not defined in Spec, we generate the name based on the cluster name.
276277
resourceGroupName := s.IBMVPCCluster.Spec.ResourceGroup
277278
if resourceGroupName == "" {
278-
resourceGroupName = s.IBMVPCCluster.Name
279+
resourceGroupName = s.Name()
279280
}
280281

281282
// Retrieve the Resource Group based on the name.
@@ -367,6 +368,16 @@ func (s *VPCClusterScope) SetResourceStatus(resourceType infrav1beta2.ResourceTy
367368
s.IBMVPCCluster.Status.Network.VPC = resource
368369
}
369370
s.NetworkStatus().VPC.Set(*resource)
371+
case infrav1beta2.ResourceTypeCustomImage:
372+
if s.IBMVPCCluster.Status.Image == nil {
373+
s.IBMVPCCluster.Status.Image = &infrav1beta2.ResourceStatus{
374+
ID: resource.ID,
375+
Name: resource.Name,
376+
Ready: resource.Ready,
377+
}
378+
return
379+
}
380+
s.IBMVPCCluster.Status.Image.Set(*resource)
370381
default:
371382
s.V(3).Info("unsupported resource type", "resourceType", resourceType)
372383
}
@@ -486,9 +497,186 @@ func (s *VPCClusterScope) createVPC() (*vpcv1.VPC, error) {
486497
} else if vpcDetails == nil {
487498
return nil, fmt.Errorf("no vpc details after creation")
488499
}
489-
if err = s.TagResource(s.IBMVPCCluster.Name, *vpcDetails.CRN); err != nil {
500+
501+
// NOTE: This tagging is only attempted once. We may wish to refactor in case this single attempt fails.
502+
if err = s.TagResource(s.Name(), *vpcDetails.CRN); err != nil {
490503
return nil, fmt.Errorf("error tagging vpc: %w", err)
491504
}
492505

493506
return vpcDetails, nil
494507
}
508+
509+
// ReconcileVPCCustomImage reconciles the VPC Custom Image.
510+
func (s *VPCClusterScope) ReconcileVPCCustomImage() (bool, error) {
511+
var imageID *string
512+
// Attempt to collect VPC Custom Image info from Status.
513+
if s.IBMVPCCluster.Status.Image != nil {
514+
if s.IBMVPCCluster.Status.Image.ID != "" {
515+
imageID = ptr.To(s.IBMVPCCluster.Status.Image.ID)
516+
} else if s.IBMVPCCluster.Status.Image.Name != nil {
517+
image, err := s.VPCClient.GetImageByName(*s.IBMVPCCluster.Status.Image.Name)
518+
if err != nil {
519+
return false, fmt.Errorf("error checking vpc custom image by name: %w", err)
520+
}
521+
// If the image was found via name, we should be able to get its ID.
522+
if image != nil {
523+
imageID = image.ID
524+
}
525+
}
526+
} else if s.IBMVPCCluster.Spec.Image.CRN != nil {
527+
// Parse the supplied Image CRN for Id, to perform image lookup.
528+
imageCRN, err := crn.Parse(*s.IBMVPCCluster.Spec.Image.CRN)
529+
if err != nil {
530+
return false, fmt.Errorf("error parsing vpc custom image crn: %w", err)
531+
}
532+
if imageCRN.Resource == "" {
533+
return false, fmt.Errorf("error parsing vpc custom image crn, missing resource id")
534+
}
535+
// If we didn't hit an error during parsing, and Resource was set, set that as the Image ID.
536+
imageID = ptr.To(imageCRN.Resource)
537+
}
538+
539+
// Check status of VPC Custom Image.
540+
if imageID != nil {
541+
image, _, err := s.VPCClient.GetImage(&vpcv1.GetImageOptions{
542+
ID: imageID,
543+
})
544+
if err != nil {
545+
return false, fmt.Errorf("error retrieving vpc custom image by id: %w", err)
546+
}
547+
if image == nil {
548+
return false, fmt.Errorf("error failed to retrieve vpc custom image with id %s", *imageID)
549+
}
550+
s.V(3).Info("Found VPC Custom Image with provided id", "imageID", imageID)
551+
552+
requeue := true
553+
if image.Status != nil && *image.Status == string(vpcv1.ImageStatusAvailableConst) {
554+
requeue = false
555+
}
556+
s.SetResourceStatus(infrav1beta2.ResourceTypeCustomImage, &infrav1beta2.ResourceStatus{
557+
ID: *imageID,
558+
Name: image.Name,
559+
// Ready status will be invert of the need to requeue.
560+
Ready: !requeue,
561+
})
562+
return requeue, nil
563+
}
564+
565+
// No VPC Custom Image exists or was found.
566+
// So, check if the Image spec was defined, as it contains all the data necessary to reconcile.
567+
if s.IBMVPCCluster.Spec.Image == nil {
568+
// 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.
569+
s.V(3).Info("No VPC Custom Image defined, skipping reconciliation")
570+
return false, nil
571+
}
572+
573+
// Create Custom Image.
574+
s.Info("Creating a VPC Custom Image")
575+
image, err := s.createCustomImage()
576+
if err != nil {
577+
return false, fmt.Errorf("error failure trying to create vpc custom image: %w", err)
578+
} else if image == nil {
579+
return false, fmt.Errorf("error no vpc custom image creation results")
580+
}
581+
582+
s.Info("Successfully created VPC Custom Image")
583+
s.SetResourceStatus(infrav1beta2.ResourceTypeCustomImage, &infrav1beta2.ResourceStatus{
584+
ID: *image.ID,
585+
Name: image.Name,
586+
// We must wait for the image to be ready, on followup reconciliation loops.
587+
Ready: false,
588+
})
589+
return true, nil
590+
}
591+
592+
// createCustomImage will create a new VPC Custom Image.
593+
func (s *VPCClusterScope) createCustomImage() (*vpcv1.Image, error) {
594+
if s.IBMVPCCluster.Spec.Image == nil {
595+
return nil, fmt.Errorf("error failed to create vpc custom image, no image spec defined")
596+
}
597+
598+
// Collect the Resource Group ID.
599+
var resourceGroupID *string
600+
// Check Resource Group in Image spec.
601+
if s.IBMVPCCluster.Spec.Image.ResourceGroup != nil {
602+
if s.IBMVPCCluster.Spec.Image.ResourceGroup.ID != "" {
603+
resourceGroupID = ptr.To(s.IBMVPCCluster.Spec.Image.ResourceGroup.ID)
604+
} else if s.IBMVPCCluster.Spec.Image.ResourceGroup.Name != nil {
605+
id, err := s.ResourceManagerClient.GetResourceGroupByName(*s.IBMVPCCluster.Spec.Image.ResourceGroup.Name)
606+
if err != nil {
607+
return nil, fmt.Errorf("error retrieving resource group by name: %w", err)
608+
}
609+
resourceGroupID = id.ID
610+
}
611+
} else {
612+
// Otherwise, we will use the cluster Resource Group ID, as we expect to create all resources in that Resource Group.
613+
id, err := s.GetResourceGroupID()
614+
if err != nil {
615+
return nil, fmt.Errorf("error retrieving resource group id: %w", err)
616+
}
617+
resourceGroupID = ptr.To(id)
618+
}
619+
620+
// We must have an OperatingSystem value supplied in order to create the Custom Image.
621+
// NOTE(cjschaef): Perhaps we could try defaulting this value, so it isn't required for Custom Image creation.
622+
if s.IBMVPCCluster.Spec.Image.OperatingSystem == nil {
623+
return nil, fmt.Errorf("error failed to create vpc custom image due to missing operatingSystem")
624+
}
625+
626+
// Build the COS Object URL using the ImageSpec
627+
fileHRef, err := s.buildCOSObjectHRef()
628+
if err != nil {
629+
return nil, fmt.Errorf("error building vpc custom image file href: %w", err)
630+
} else if fileHRef == nil {
631+
return nil, fmt.Errorf("error failed to build vpc custom image file href")
632+
}
633+
634+
options := &vpcv1.CreateImageOptions{
635+
ImagePrototype: &vpcv1.ImagePrototype{
636+
Name: s.IBMVPCCluster.Spec.Image.Name,
637+
File: &vpcv1.ImageFilePrototype{
638+
Href: fileHRef,
639+
},
640+
OperatingSystem: &vpcv1.OperatingSystemIdentity{
641+
Name: s.IBMVPCCluster.Spec.Image.OperatingSystem,
642+
},
643+
ResourceGroup: &vpcv1.ResourceGroupIdentity{
644+
ID: resourceGroupID,
645+
},
646+
},
647+
}
648+
649+
imageDetails, _, err := s.VPCClient.CreateImage(options)
650+
if err != nil {
651+
return nil, fmt.Errorf("error unknown failure creating vpc custom image: %w", err)
652+
}
653+
if imageDetails == nil || imageDetails.ID == nil || imageDetails.Name == nil || imageDetails.CRN == nil {
654+
return nil, fmt.Errorf("error failed creating custom image")
655+
}
656+
657+
// NOTE: This tagging is only attempted once. We may wish to refactor in case this single attempt fails.
658+
if err := s.TagResource(s.Name(), *imageDetails.CRN); err != nil {
659+
return nil, fmt.Errorf("error failure tagging vpc custom image: %w", err)
660+
}
661+
return imageDetails, nil
662+
}
663+
664+
// buildCOSObjectHRef will build the HRef path to a COS Object that can be used for VPC Custom Image creation.
665+
func (s *VPCClusterScope) buildCOSObjectHRef() (*string, error) {
666+
// We need COS details in order to create the Custom Image from.
667+
if s.IBMVPCCluster.Spec.Image.COSInstance == nil || s.IBMVPCCluster.Spec.Image.COSBucket == nil || s.IBMVPCCluster.Spec.Image.COSObject == nil {
668+
return nil, fmt.Errorf("error failed to build cos object href, cos details missing")
669+
}
670+
671+
// Get COS Bucket Region, defaulting to cluster Region if not specified.
672+
bucketRegion := s.IBMVPCCluster.Spec.Region
673+
if s.IBMVPCCluster.Spec.Image.COSBucketRegion != nil {
674+
bucketRegion = *s.IBMVPCCluster.Spec.Image.COSBucketRegion
675+
}
676+
677+
// Expected HRef format:
678+
// cos://<bucket_region>/<bucket_name>/<object_name>
679+
href := fmt.Sprintf("cos://%s/%s/%s", bucketRegion, *s.IBMVPCCluster.Spec.Image.COSBucket, *s.IBMVPCCluster.Spec.Image.COSObject)
680+
s.V(3).Info("building image ref", "href", href)
681+
return ptr.To(href), nil
682+
}

‎config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclusters.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ spec:
462462
description: name is the name of the desired VPC Custom Image.
463463
maxLength: 63
464464
minLength: 1
465-
pattern: '''/^([a-z]|[a-z][-a-z0-9]*[a-z0-9])$/'''
465+
pattern: ^([a-z]|[a-z][-a-z0-9]*[a-z0-9])$
466466
type: string
467467
operatingSystem:
468468
description: operatingSystem is the Custom Image's Operating System

‎config/crd/bases/infrastructure.cluster.x-k8s.io_ibmvpcclustertemplates.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ spec:
317317
Image.
318318
maxLength: 63
319319
minLength: 1
320-
pattern: '''/^([a-z]|[a-z][-a-z0-9]*[a-z0-9])$/'''
320+
pattern: ^([a-z]|[a-z][-a-z0-9]*[a-z0-9])$
321321
type: string
322322
operatingSystem:
323323
description: operatingSystem is the Custom Image's Operating

‎controllers/ibmvpccluster_controller.go

+14
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,22 @@ func (r *IBMVPCClusterReconciler) reconcileCluster(clusterScope *scope.VPCCluste
245245
clusterScope.Info("VPC creation is pending, requeuing")
246246
return reconcile.Result{RequeueAfter: 15 * time.Second}, nil
247247
}
248+
clusterScope.Info("Reconciliation of VPC complete")
248249
conditions.MarkTrue(clusterScope.IBMVPCCluster, infrav1beta2.VPCReadyCondition)
249250

251+
// Reconcile the cluster's VPC Custom Image.
252+
clusterScope.Info("Reconciling VPC Custom Image")
253+
if requeue, err := clusterScope.ReconcileVPCCustomImage(); err != nil {
254+
clusterScope.Error(err, "failed to reconcile VPC Custom Image")
255+
conditions.MarkFalse(clusterScope.IBMVPCCluster, infrav1beta2.ImageReadyCondition, infrav1beta2.VPCReconciliationFailedReason, capiv1beta1.ConditionSeverityError, err.Error())
256+
return reconcile.Result{}, err
257+
} else if requeue {
258+
clusterScope.Info("VPC Custom Image creation is pending, requeueing")
259+
return reconcile.Result{RequeueAfter: 15 * time.Second}, nil
260+
}
261+
clusterScope.Info("Reconciliation of VPC Custom Image complete")
262+
conditions.MarkTrue(clusterScope.IBMVPCCluster, infrav1beta2.ImageReadyCondition)
263+
250264
// TODO(cjschaef): add remaining resource reconciliation.
251265

252266
// Mark cluster as ready.

‎go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ require (
4848
require (
4949
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
5050
github.com/BurntSushi/toml v1.0.0 // indirect
51+
github.com/IBM-Cloud/bluemix-go v0.0.0-20240719075425-078fcb3a55be
5152
github.com/MakeNowJust/heredoc v1.0.0 // indirect
5253
github.com/Masterminds/goutils v1.1.1 // indirect
5354
github.com/Masterminds/semver/v3 v3.2.0 // indirect

‎go.sum

+96
Large diffs are not rendered by default.

‎pkg/cloud/services/vpc/mock/vpc_generated.go

+47
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pkg/cloud/services/vpc/service.go

+49
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,21 @@ func (s *Service) ListKeys(options *vpcv1.ListKeysOptions) (*vpcv1.KeyCollection
159159
return s.vpcService.ListKeys(options)
160160
}
161161

162+
// CreateImage creates a new VPC Custom Image.
163+
func (s *Service) CreateImage(options *vpcv1.CreateImageOptions) (*vpcv1.Image, *core.DetailedResponse, error) {
164+
return s.vpcService.CreateImage(options)
165+
}
166+
162167
// ListImages returns list of images in a region.
163168
func (s *Service) ListImages(options *vpcv1.ListImagesOptions) (*vpcv1.ImageCollection, *core.DetailedResponse, error) {
164169
return s.vpcService.ListImages(options)
165170
}
166171

172+
// GetImage returns a VPC Custom image.
173+
func (s *Service) GetImage(options *vpcv1.GetImageOptions) (*vpcv1.Image, *core.DetailedResponse, error) {
174+
return s.vpcService.GetImage(options)
175+
}
176+
167177
// GetInstanceProfile returns instance profile.
168178
func (s *Service) GetInstanceProfile(options *vpcv1.GetInstanceProfileOptions) (*vpcv1.InstanceProfile, *core.DetailedResponse, error) {
169179
return s.vpcService.GetInstanceProfile(options)
@@ -213,6 +223,45 @@ func (s *Service) GetVPCByName(vpcName string) (*vpcv1.VPC, error) {
213223
return vpc, nil
214224
}
215225

226+
// GetImageByName returns the VPC Custom Image with given name. If not found, returns nil.
227+
func (s *Service) GetImageByName(imageName string) (*vpcv1.Image, error) {
228+
var image *vpcv1.Image
229+
f := func(start string) (bool, string, error) {
230+
// check for existing images
231+
listImagesOptions := &vpcv1.ListImagesOptions{}
232+
if start != "" {
233+
listImagesOptions.Start = &start
234+
}
235+
236+
imagesList, _, err := s.ListImages(listImagesOptions)
237+
if err != nil {
238+
return false, "", err
239+
}
240+
241+
if imagesList == nil {
242+
return false, "", fmt.Errorf("image list returned is nil")
243+
}
244+
245+
for i, v := range imagesList.Images {
246+
if (*v.Name) == imageName {
247+
image = &imagesList.Images[i]
248+
return true, "", nil
249+
}
250+
}
251+
252+
if imagesList.Next != nil && *imagesList.Next.Href != "" {
253+
return false, *imagesList.Next.Href, nil
254+
}
255+
return true, "", nil
256+
}
257+
258+
if err := utils.PagingHelper(f); err != nil {
259+
return nil, err
260+
}
261+
262+
return image, nil
263+
}
264+
216265
// GetSubnet return subnet.
217266
func (s *Service) GetSubnet(options *vpcv1.GetSubnetOptions) (*vpcv1.Subnet, *core.DetailedResponse, error) {
218267
return s.vpcService.GetSubnet(options)

‎pkg/cloud/services/vpc/vpc.go

+3
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,13 @@ type Vpc interface {
5151
DeleteLoadBalancerPoolMember(options *vpcv1.DeleteLoadBalancerPoolMemberOptions) (*core.DetailedResponse, error)
5252
ListLoadBalancerPoolMembers(options *vpcv1.ListLoadBalancerPoolMembersOptions) (*vpcv1.LoadBalancerPoolMemberCollection, *core.DetailedResponse, error)
5353
ListKeys(options *vpcv1.ListKeysOptions) (*vpcv1.KeyCollection, *core.DetailedResponse, error)
54+
CreateImage(options *vpcv1.CreateImageOptions) (*vpcv1.Image, *core.DetailedResponse, error)
5455
ListImages(options *vpcv1.ListImagesOptions) (*vpcv1.ImageCollection, *core.DetailedResponse, error)
56+
GetImage(options *vpcv1.GetImageOptions) (*vpcv1.Image, *core.DetailedResponse, error)
5557
GetInstanceProfile(options *vpcv1.GetInstanceProfileOptions) (*vpcv1.InstanceProfile, *core.DetailedResponse, error)
5658
GetVPC(*vpcv1.GetVPCOptions) (*vpcv1.VPC, *core.DetailedResponse, error)
5759
GetVPCByName(vpcName string) (*vpcv1.VPC, error)
60+
GetImageByName(imageName string) (*vpcv1.Image, error)
5861
GetSubnet(*vpcv1.GetSubnetOptions) (*vpcv1.Subnet, *core.DetailedResponse, error)
5962
GetVPCSubnetByName(subnetName string) (*vpcv1.Subnet, error)
6063
GetLoadBalancerByName(loadBalancerName string) (*vpcv1.LoadBalancer, error)

0 commit comments

Comments
 (0)
Please sign in to comment.