diff --git a/pkg/asset/machines/powervs/images.go b/pkg/asset/machines/powervs/images.go new file mode 100644 index 00000000000..8830fc09504 --- /dev/null +++ b/pkg/asset/machines/powervs/images.go @@ -0,0 +1,74 @@ +package powervs + +import ( + "context" + "fmt" + + "github.com/IBM-Cloud/power-go-client/clients/instance" + "github.com/IBM-Cloud/power-go-client/ibmpisession" + "github.com/IBM/go-sdk-core/v5/core" + "github.com/sirupsen/logrus" + + powervsconfig "github.com/openshift/installer/pkg/asset/installconfig/powervs" +) + +// GetBootImageFromWorkspace retrieves a boot image from the PowerVS workspace. +// If an active image is found in the workspace, it returns that image name. +// If no images are found, it returns a default name in the format "rhcos-{clusterID}". +func GetBootImageFromWorkspace(ctx context.Context, serviceInstanceGUID string, zone string, clusterID string) (string, error) { + // Create a new PowerVS client + client, err := powervsconfig.NewClient() + if err != nil { + return "", fmt.Errorf("failed to create PowerVS client: %w", err) + } + + // Create authenticator + authenticator := &core.IamAuthenticator{ + ApiKey: client.GetAPIKey(), + } + + // Create PI session + piSession, err := ibmpisession.NewIBMPISession(&ibmpisession.IBMPIOptions{ + Authenticator: authenticator, + UserAccount: client.BXCli.User.Account, + Zone: zone, + Debug: false, + }) + if err != nil { + return "", fmt.Errorf("failed to create PowerVS session: %w", err) + } + + // Create image client + imageClient := instance.NewIBMPIImageClient(ctx, piSession, serviceInstanceGUID) + if imageClient == nil { + return "", fmt.Errorf("failed to create PowerVS image client") + } + + // Get all images from the workspace + images, err := imageClient.GetAll() + if err != nil { + return "", fmt.Errorf("failed to list images from PowerVS workspace: %w", err) + } + + // If no images found in workspace, use default naming pattern + if images == nil || len(images.Images) == 0 { + defaultImage := fmt.Sprintf("rhcos-%s", clusterID) + logrus.Infof("No images found in PowerVS workspace, using default image name: %s", defaultImage) + return defaultImage, nil + } + + // Find the first active image + for _, image := range images.Images { + if image.State != nil && *image.State == "active" && image.Name != nil { + logrus.Infof("Selected PowerVS boot image from workspace: %s", *image.Name) + return *image.Name, nil + } + } + + // If no active images found, use default naming pattern + defaultImage := fmt.Sprintf("rhcos-%s", clusterID) + logrus.Infof("No active images found in PowerVS workspace, using default image name: %s", defaultImage) + return defaultImage, nil +} + +// Made with Bob diff --git a/pkg/asset/machines/powervs/machinesets.go b/pkg/asset/machines/powervs/machinesets.go index fa25c40cacf..f42350b5b0c 100644 --- a/pkg/asset/machines/powervs/machinesets.go +++ b/pkg/asset/machines/powervs/machinesets.go @@ -2,6 +2,7 @@ package powervs import ( + "context" "fmt" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -25,7 +26,12 @@ func MachineSets(clusterID string, config *types.InstallConfig, pool *types.Mach platform := config.Platform.PowerVS mpool := pool.Platform.PowerVS var network string - image := fmt.Sprintf("rhcos-%s", clusterID) + + // Get the boot image from the PowerVS workspace + image, err := GetBootImageFromWorkspace(context.TODO(), config.PowerVS.ServiceInstanceGUID, config.PowerVS.Zone, clusterID) + if err != nil { + return nil, fmt.Errorf("failed to get boot image from PowerVS workspace: %w", err) + } total := int32(0) if pool.Replicas != nil { diff --git a/pkg/asset/machines/powervs/powervsmachines.go b/pkg/asset/machines/powervs/powervsmachines.go index 59fe92a1d2b..bd04fa51682 100644 --- a/pkg/asset/machines/powervs/powervsmachines.go +++ b/pkg/asset/machines/powervs/powervsmachines.go @@ -2,8 +2,10 @@ package powervs import ( + "context" "fmt" + "github.com/sirupsen/logrus" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" @@ -37,10 +39,16 @@ func GenerateMachines(clusterID string, ic *types.InstallConfig, pool *types.Mac powerVSMachine *capibm.IBMPowerVSMachine dataSecret string machine *capi.Machine + err error ) - // Note: This will be created later - image = fmt.Sprintf("rhcos-%s", clusterID) + // Get the boot image from the PowerVS workspace + image, err = GetBootImageFromWorkspace(context.TODO(), ic.PowerVS.ServiceInstanceGUID, ic.PowerVS.Zone, clusterID) + if err != nil { + // Fallback to default image naming pattern + image = fmt.Sprintf("rhcos-%s", clusterID) + logrus.Warnf("Failed to get boot image from PowerVS workspace, using default: %s (error: %v)", image, err) + } if ic.PowerVS.ServiceInstanceGUID == "" { serviceName := fmt.Sprintf("%s-power-iaas", clusterID) @@ -112,13 +120,11 @@ func GenerateMachine(ic *types.InstallConfig, service capibm.IBMPowerVSResourceR ServiceInstanceID: ic.PowerVS.ServiceInstanceGUID, ServiceInstance: &service, SSHKey: "", - ImageRef: &v1.LocalObjectReference{ - Name: image, - }, - SystemType: mpool.SysType, - ProcessorType: capibm.PowerVSProcessorType(mpool.ProcType), - Processors: mpool.Processors, - MemoryGiB: mpool.MemoryGiB, + Image: &capibm.IBMPowerVSResourceReference{Name: &image}, + SystemType: mpool.SysType, + ProcessorType: capibm.PowerVSProcessorType(mpool.ProcType), + Processors: mpool.Processors, + MemoryGiB: mpool.MemoryGiB, }, } utils.SetMachineOSStreamLabels(machine, ic) diff --git a/pkg/asset/manifests/powervs/cluster.go b/pkg/asset/manifests/powervs/cluster.go index 8b27e525daf..6e72587bdbd 100644 --- a/pkg/asset/manifests/powervs/cluster.go +++ b/pkg/asset/manifests/powervs/cluster.go @@ -5,6 +5,9 @@ import ( "fmt" "time" + "github.com/IBM-Cloud/power-go-client/clients/instance" + "github.com/IBM-Cloud/power-go-client/ibmpisession" + "github.com/IBM/go-sdk-core/v5/core" "github.com/IBM/vpc-go-sdk/vpcv1" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" @@ -20,6 +23,59 @@ import ( powervstypes "github.com/openshift/installer/pkg/types/powervs" ) +// checkForExistingImage checks if any active images exist in the PowerVS workspace. +// Returns the name of the first active image found, or empty string if none exist. +func checkForExistingImage(ctx context.Context, serviceInstanceGUID string, zone string) (string, error) { + // Create a new PowerVS client + client, err := powervsconfig.NewClient() + if err != nil { + return "", fmt.Errorf("failed to create PowerVS client: %w", err) + } + + // Create authenticator + authenticator := &core.IamAuthenticator{ + ApiKey: client.GetAPIKey(), + } + + // Create PI session + piSession, err := ibmpisession.NewIBMPISession(&ibmpisession.IBMPIOptions{ + Authenticator: authenticator, + UserAccount: client.BXCli.User.Account, + Zone: zone, + Debug: false, + }) + if err != nil { + return "", fmt.Errorf("failed to create PowerVS session: %w", err) + } + + // Create image client + imageClient := instance.NewIBMPIImageClient(ctx, piSession, serviceInstanceGUID) + if imageClient == nil { + return "", fmt.Errorf("failed to create PowerVS image client") + } + + // Get all images from the workspace + images, err := imageClient.GetAll() + if err != nil { + return "", fmt.Errorf("failed to list images from PowerVS workspace: %w", err) + } + + // If no images found, return empty string + if images == nil || len(images.Images) == 0 { + return "", nil + } + + // Find the first active image + for _, image := range images.Images { + if image.State != nil && *image.State == "active" && image.Name != nil { + return *image.Name, nil + } + } + + // No active images found + return "", nil +} + // GenerateClusterAssets generates the manifests for the cluster-api. func GenerateClusterAssets(installConfig *installconfig.InstallConfig, clusterID *installconfig.ClusterID, bucket string, object string) (*capiutils.GenerateClusterAssetsOutput, error) { var ( @@ -285,28 +341,41 @@ func GenerateClusterAssets(installConfig *installconfig.InstallConfig, clusterID bucket = fmt.Sprintf("rhcos-powervs-images-%s", cosRegion) } - powerVSImage = &capibm.IBMPowerVSImage{ - TypeMeta: metav1.TypeMeta{ - APIVersion: capibm.GroupVersion.String(), - Kind: "IBMPowerVSImage", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: imageName, - Namespace: capiutils.Namespace, - }, - Spec: capibm.IBMPowerVSImageSpec{ - ClusterName: clusterID.InfraID, - ServiceInstance: &service, - Bucket: &bucket, - Object: &object, - Region: &cosRegion, - }, + // Check if an image already exists in the workspace + // If an image exists, skip creating the IBMPowerVSImage resource (which would import from COS) + existingImage, err := checkForExistingImage(ctx, installConfig.Config.PowerVS.ServiceInstanceGUID, installConfig.Config.PowerVS.Zone) + if err != nil { + logrus.Warnf("Failed to check for existing images in workspace: %v. Will proceed with image import.", err) + existingImage = "" } - manifests = append(manifests, &asset.RuntimeFile{ - Object: powerVSImage, - File: asset.File{Filename: "03_powervs-image.yaml"}, - }) + if existingImage != "" { + logrus.Infof("Found existing image '%s' in PowerVS workspace. Skipping image import from COS.", existingImage) + } else { + logrus.Infof("No existing images found in PowerVS workspace. Creating IBMPowerVSImage resource to import from COS.") + powerVSImage = &capibm.IBMPowerVSImage{ + TypeMeta: metav1.TypeMeta{ + APIVersion: capibm.GroupVersion.String(), + Kind: "IBMPowerVSImage", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: imageName, + Namespace: capiutils.Namespace, + }, + Spec: capibm.IBMPowerVSImageSpec{ + ClusterName: clusterID.InfraID, + ServiceInstance: &service, + Bucket: &bucket, + Object: &object, + Region: &cosRegion, + }, + } + + manifests = append(manifests, &asset.RuntimeFile{ + Object: powerVSImage, + File: asset.File{Filename: "03_powervs-image.yaml"}, + }) + } return &capiutils.GenerateClusterAssetsOutput{ Manifests: manifests, diff --git a/pkg/infrastructure/powervs/clusterapi/powervs.go b/pkg/infrastructure/powervs/clusterapi/powervs.go index 5a1f9e26477..5ef14954601 100644 --- a/pkg/infrastructure/powervs/clusterapi/powervs.go +++ b/pkg/infrastructure/powervs/clusterapi/powervs.go @@ -11,6 +11,7 @@ import ( "github.com/IBM/vpc-go-sdk/vpcv1" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/utils/ptr" @@ -100,7 +101,8 @@ func (p Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput) } logrus.Debugf("InfraReady: powerVSCluster.Status.VPC.ID = %s", *powerVSCluster.Status.VPC.ID) - // Get the image from the provider + // Get the image from the provider when an IBMPowerVSImage resource was created. + // Existing workspace images skip manifest creation, so a missing resource is expected. key = crclient.ObjectKey{ Name: fmt.Sprintf("rhcos-%s", in.InfraID), Namespace: capiutils.Namespace, @@ -108,9 +110,13 @@ func (p Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput) logrus.Debugf("InfraReady: image key = %+v", key) powerVSImage := &capibm.IBMPowerVSImage{} if err = in.Client.Get(ctx, key, powerVSImage); err != nil { - return fmt.Errorf("failed to get PowerVS image in InfraReady: %w", err) + if !apierrors.IsNotFound(err) { + return fmt.Errorf("failed to get PowerVS image in InfraReady: %w", err) + } + logrus.Debugf("InfraReady: image resource %q not found, assuming existing workspace image is being used", key.Name) + } else { + logrus.Debugf("InfraReady: image = %+v", powerVSImage) } - logrus.Debugf("InfraReady: image = %+v", powerVSImage) // We need to set the region we will eventually query inside vpcRegion := in.InstallConfig.Config.Platform.PowerVS.VPCRegion