Skip to content

Commit 9b28686

Browse files
committed
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 752b778 commit 9b28686

10 files changed

+367
-3
lines changed

api/v1beta2/conditions_consts.go

+3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ const (
5959

6060
// ImageImportFailedReason used when the image import is failed.
6161
ImageImportFailedReason = "ImageImportFailed"
62+
63+
// ImageReconciliationFailedReason used when an error occurs during VPC Custom Image reconciliation.
64+
ImageReconciliationFailedReason = "ImageReconciliationFailed"
6265
)
6366

6467
const (

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/util.go

+56
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222
"strconv"
23+
"strings"
2324

2425
"sigs.k8s.io/controller-runtime/pkg/client"
2526

@@ -57,3 +58,58 @@ func CheckCreateInfraAnnotation(cluster infrav1beta2.IBMPowerVSCluster) bool {
5758
}
5859
return createInfra
5960
}
61+
62+
// CRN is a local duplicate of IBM Cloud CRN for parsing and references.
63+
type CRN struct {
64+
Scheme string
65+
Version string
66+
CName string
67+
CType string
68+
ServiceName string
69+
Region string
70+
ScopeType string
71+
Scope string
72+
ServiceInstance string
73+
ResourceType string
74+
Resource string
75+
}
76+
77+
// 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.
78+
func ParseCRN(s string) (*CRN, error) {
79+
if s == "" {
80+
return nil, nil
81+
}
82+
83+
segments := strings.Split(s, ":")
84+
if len(segments) != 10 || segments[0] != "crn" {
85+
return nil, fmt.Errorf("malformed CRN")
86+
}
87+
88+
crn := &CRN{
89+
Scheme: segments[0],
90+
Version: segments[1],
91+
CName: segments[2],
92+
CType: segments[3],
93+
ServiceName: segments[4],
94+
Region: segments[5],
95+
ServiceInstance: segments[7],
96+
ResourceType: segments[8],
97+
Resource: segments[9],
98+
}
99+
100+
// Scope portions require additional parsing.
101+
scopeSegments := segments[6]
102+
if scopeSegments != "" {
103+
if scopeSegments == "global" {
104+
crn.Scope = scopeSegments
105+
} else {
106+
scopeParts := strings.Split(scopeSegments, "/")
107+
if len(scopeParts) != 2 {
108+
return nil, fmt.Errorf("malformed scope in CRN")
109+
}
110+
crn.ScopeType, crn.Scope = scopeParts[0], scopeParts[1]
111+
}
112+
}
113+
114+
return crn, nil
115+
}

cloud/scope/vpc_cluster.go

+192
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,16 @@ func (s *VPCClusterScope) SetResourceStatus(resourceType infrav1beta2.ResourceTy
367367
s.IBMVPCCluster.Status.Network.VPC = resource
368368
}
369369
s.NetworkStatus().VPC.Set(*resource)
370+
case infrav1beta2.ResourceTypeCustomImage:
371+
if s.IBMVPCCluster.Status.Image == nil {
372+
s.IBMVPCCluster.Status.Image = &infrav1beta2.ResourceStatus{
373+
ID: resource.ID,
374+
Name: resource.Name,
375+
Ready: resource.Ready,
376+
}
377+
return
378+
}
379+
s.IBMVPCCluster.Status.Image.Set(*resource)
370380
default:
371381
s.V(3).Info("unsupported resource type", "resourceType", resourceType)
372382
}
@@ -494,3 +504,185 @@ func (s *VPCClusterScope) createVPC() (*vpcv1.VPC, error) {
494504

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

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.ImageReconciliationFailedReason, 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.

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.

0 commit comments

Comments
 (0)