Skip to content

Commit

Permalink
VPC: Add Custom Image reconciliation
Browse files Browse the repository at this point in the history
Add support to reconcile a VPC Custom Image for the new v2
VPC Infrastructure reconcile logic.

Related: kubernetes-sigs#1896
  • Loading branch information
cjschaef committed Sep 4, 2024
1 parent 1f473c2 commit 1f882b8
Show file tree
Hide file tree
Showing 10 changed files with 393 additions and 5 deletions.
2 changes: 1 addition & 1 deletion api/v1beta2/ibmvpccluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down
182 changes: 180 additions & 2 deletions cloud/scope/vpc_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/go-logr/logr"

"github.com/IBM-Cloud/bluemix-go/crn"
"github.com/IBM/go-sdk-core/v5/core"
"github.com/IBM/platform-services-go-sdk/globaltaggingv1"
"github.com/IBM/platform-services-go-sdk/resourcecontrollerv2"
Expand Down Expand Up @@ -275,7 +276,7 @@ func (s *VPCClusterScope) GetResourceGroupID() (string, error) {
// 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
resourceGroupName = s.Name()
}

// Retrieve the Resource Group based on the name.
Expand Down Expand Up @@ -486,9 +487,186 @@ func (s *VPCClusterScope) createVPC() (*vpcv1.VPC, error) {
} else if vpcDetails == nil {
return nil, fmt.Errorf("no vpc details after creation")
}
if err = s.TagResource(s.IBMVPCCluster.Name, *vpcDetails.CRN); err != nil {

// NOTE: This tagging is only attempted once. We may wish to refactor in case this single attempt fails.
if err = s.TagResource(s.Name(), *vpcDetails.CRN); err != nil {
return nil, fmt.Errorf("error tagging vpc: %w", err)
}

return vpcDetails, nil
}

// ReconcileVPCCustomImage reconciles the VPC Custom Image.
func (s *VPCClusterScope) ReconcileVPCCustomImage() (bool, error) {
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.Status.Image.Name != nil {
image, err := s.VPCClient.GetImageByName(*s.IBMVPCCluster.Status.Image.Name)
if err != nil {
return false, fmt.Errorf("error checking vpc custom image by name: %w", err)
}
// If the image was found via name, we should be able to get its ID.
if image != nil {
imageID = image.ID
}
}
} else if s.IBMVPCCluster.Spec.Image.CRN != nil {
// Parse the supplied Image CRN for Id, to perform image lookup.
imageCRN, err := crn.Parse(*s.IBMVPCCluster.Spec.Image.CRN)
if err != nil {
return false, fmt.Errorf("error parsing vpc custom image crn: %w", err)
}
if 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, check if the Image spec was defined, as it contains all the data necessary to reconcile.
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
}

// Create Custom Image.
s.Info("Creating a VPC Custom Image")
image, err := s.createCustomImage()
if err != nil {
return false, fmt.Errorf("error failure trying to create vpc custom image: %w", err)
} else if image == nil {
return false, fmt.Errorf("error no vpc custom image creation results")
}

s.Info("Successfully created VPC Custom Image")
s.SetResourceStatus(infrav1beta2.ResourceTypeCustomImage, &infrav1beta2.ResourceStatus{
ID: *image.ID,
Name: image.Name,
// We must wait for the image to be ready, on followup reconciliation loops.
Ready: false,
})
return true, nil
}

// createCustomImage will create a new VPC Custom Image.
func (s *VPCClusterScope) createCustomImage() (*vpcv1.Image, error) {
if s.IBMVPCCluster.Spec.Image == nil {
return nil, fmt.Errorf("error failed to create vpc custom image, no image spec defined")
}

// 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 nil, 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 nil, fmt.Errorf("error retrieving resource group id: %w", err)
}
resourceGroupID = ptr.To(id)
}

// We must have an OperatingSystem value supplied in order to create the Custom Image.
// NOTE(cjschaef): Perhaps we could try defaulting this value, so it isn't required for Custom Image creation.
if s.IBMVPCCluster.Spec.Image.OperatingSystem == nil {
return nil, fmt.Errorf("error failed to create vpc custom image due to missing operatingSystem")
}

// Build the COS Object URL using the ImageSpec
fileHRef, err := s.buildCOSObjectHRef()
if err != nil {
return nil, fmt.Errorf("error building vpc custom image file href: %w", err)
} else if fileHRef == nil {
return nil, fmt.Errorf("error failed to build vpc custom image file href")
}

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 nil, fmt.Errorf("error unknown failure creating vpc custom image: %w", err)
}
if imageDetails == nil || imageDetails.ID == nil || imageDetails.Name == nil || imageDetails.CRN == nil {
return nil, fmt.Errorf("error failed creating custom image")
}

// 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 nil, fmt.Errorf("error failure tagging vpc custom image: %w", err)
}
return imageDetails, 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) {
// 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://<bucket_region>/<bucket_name>/<object_name>
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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions controllers/ibmvpccluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.VPCReconciliationFailedReason, 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.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ require (
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/BurntSushi/toml v1.0.0 // indirect
github.com/IBM-Cloud/bluemix-go v0.0.0-20240719075425-078fcb3a55be
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
Expand Down
Loading

0 comments on commit 1f882b8

Please sign in to comment.