package gcp import ( "context" "fmt" "strings" compute "cloud.google.com/go/compute/apiv1" "google.golang.org/api/option" computepb "google.golang.org/genproto/googleapis/cloud/compute/v1" "github.com/osbuild/osbuild-composer/internal/common" ) // Guest OS Features for RHEL8 images var GuestOsFeaturesRHEL8 []*computepb.GuestOsFeature = []*computepb.GuestOsFeature{ {Type: common.ToPtr(computepb.GuestOsFeature_UEFI_COMPATIBLE.String())}, {Type: common.ToPtr(computepb.GuestOsFeature_VIRTIO_SCSI_MULTIQUEUE.String())}, {Type: common.ToPtr(computepb.GuestOsFeature_SEV_CAPABLE.String())}, } // Guest OS Features for RHEL9 images. Note that if you update this, also // consider changing the code in https://github.com/coreos/coreos-assembler/blob/0083086c4720b602b8243effb85c0a1f73f013dd/mantle/platform/api/gcloud/image.go#L105 // for RHEL CoreOS which uses coreos-assembler today. var GuestOsFeaturesRHEL9 []*computepb.GuestOsFeature = []*computepb.GuestOsFeature{ {Type: common.ToPtr(computepb.GuestOsFeature_UEFI_COMPATIBLE.String())}, {Type: common.ToPtr(computepb.GuestOsFeature_VIRTIO_SCSI_MULTIQUEUE.String())}, {Type: common.ToPtr(computepb.GuestOsFeature_SEV_CAPABLE.String())}, {Type: common.ToPtr(computepb.GuestOsFeature_GVNIC.String())}, } // GuestOsFeaturesByDistro returns the the list of Guest OS Features, which // should be used when importing an image of the specified distribution. // // In case the provided distribution does not have any specific Guest OS // Features list defined, nil is returned. func GuestOsFeaturesByDistro(distroName string) []*computepb.GuestOsFeature { switch { case strings.HasPrefix(distroName, "centos-8"): fallthrough case strings.HasPrefix(distroName, "rhel-8"): return GuestOsFeaturesRHEL8 case strings.HasPrefix(distroName, "centos-9"): fallthrough case strings.HasPrefix(distroName, "rhel-9"): return GuestOsFeaturesRHEL9 default: return nil } } // ComputeImageInsert imports a previously uploaded archive with raw image into Compute Engine. // // The image must be RAW image named 'disk.raw' inside a gzip-ed tarball. // // To delete the Storage object (image) used for the image import, use StorageObjectDelete(). // // bucket - Google storage bucket name with the uploaded image archive // object - Google storage object name of the uploaded image // imageName - Desired image name after the import. This must be unique within the whole project. // regions - A list of valid Google Storage regions where the resulting image should be located. // // It is possible to specify multiple regions. Also multi and dual regions are allowed. // If not provided, the region of the used Storage object is used. // See: https://cloud.google.com/storage/docs/locations // // guestOsFeatures - A list of features supported by the Guest OS on the imported image. // // Uses: // - Compute Engine API func (g *GCP) ComputeImageInsert( ctx context.Context, bucket, object, imageName string, regions []string, guestOsFeatures []*computepb.GuestOsFeature) (*computepb.Image, error) { imagesClient, err := compute.NewImagesRESTClient(ctx, option.WithCredentials(g.creds)) if err != nil { return nil, fmt.Errorf("failed to get Compute Engine Images client: %v", err) } defer imagesClient.Close() operationsClient, err := compute.NewGlobalOperationsRESTClient(ctx, option.WithCredentials(g.creds)) if err != nil { return nil, fmt.Errorf("failed to get Compute Engine Operations client: %v", err) } defer operationsClient.Close() imgInsertReq := &computepb.InsertImageRequest{ Project: g.GetProjectID(), ImageResource: &computepb.Image{ Name: &imageName, StorageLocations: regions, GuestOsFeatures: guestOsFeatures, RawDisk: &computepb.RawDisk{ ContainerType: common.ToPtr(computepb.RawDisk_TAR.String()), Source: common.ToPtr(fmt.Sprintf("https://storage.googleapis.com/%s/%s", bucket, object)), }, }, } operation, err := imagesClient.Insert(ctx, imgInsertReq) if err != nil { return nil, fmt.Errorf("failed to insert provided image into GCE: %v", err) } // wait for the operation to finish var operationResource *computepb.Operation for { waitOperationReq := &computepb.WaitGlobalOperationRequest{ Operation: operation.Proto().GetName(), Project: g.GetProjectID(), } operationResource, err = operationsClient.Wait(ctx, waitOperationReq) if err != nil { return nil, fmt.Errorf("failed to wait for an Image Import operation: %v", err) } // The operation finished if operationResource.GetStatus() != computepb.Operation_RUNNING && operationResource.GetStatus() != computepb.Operation_PENDING { break } } // If the operation failed, the HttpErrorStatusCode is set to a non-zero value if operationStatusCode := operationResource.GetHttpErrorStatusCode(); operationStatusCode != 0 { operationErrorMsg := operationResource.GetHttpErrorMessage() operationErrors := operationResource.GetError().GetErrors() return nil, fmt.Errorf("failed to insert image into GCE. HTTPErrorCode:%d HTTPErrorMsg:%v Errors:%v", operationStatusCode, operationErrorMsg, operationErrors) } getImageReq := &computepb.GetImageRequest{ Image: imageName, Project: g.GetProjectID(), } image, err := imagesClient.Get(ctx, getImageReq) if err != nil { return nil, fmt.Errorf("failed to get information about the imported Image: %v", err) } return image, nil } // ComputeImageURL returns an image's URL to Google Cloud Console. The method does // not check at all, if the image actually exists or not. func (g *GCP) ComputeImageURL(imageName string) string { return fmt.Sprintf("https://console.cloud.google.com/compute/imagesDetail/projects/%s/global/images/%s", g.GetProjectID(), imageName) } // ComputeImageShare shares the specified Compute Engine image with list of accounts. // // "shareWith" is a list of accounts to share the image with. Items can be one // of the following options: // // - `user:{emailid}`: An email address that represents a specific // Google account. For example, `alice@example.com`. // // - `serviceAccount:{emailid}`: An email address that represents a // service account. For example, `my-other-app@appspot.gserviceaccount.com`. // // - `group:{emailid}`: An email address that represents a Google group. // For example, `admins@example.com`. // // - `domain:{domain}`: The G Suite domain (primary) that represents all // the users of that domain. For example, `google.com` or `example.com`. // // Uses: // - Compute Engine API func (g *GCP) ComputeImageShare(ctx context.Context, imageName string, shareWith []string) error { imagesClient, err := compute.NewImagesRESTClient(ctx, option.WithCredentials(g.creds)) if err != nil { return fmt.Errorf("failed to get Compute Engine Images client: %v", err) } defer imagesClient.Close() // Standard role to enable account to view and use a specific Image imageDesiredRole := "roles/compute.imageUser" // Get the current Policy set on the Image getIamPolicyReq := &computepb.GetIamPolicyImageRequest{ Project: g.GetProjectID(), Resource: imageName, } policy, err := imagesClient.GetIamPolicy(ctx, getIamPolicyReq) if err != nil { return fmt.Errorf("failed to get image's policy: %v", err) } // Add new members, who can use the image // Completely override the old policy userBinding := &computepb.Binding{ Members: shareWith, Role: common.ToPtr(imageDesiredRole), } newPolicy := &computepb.Policy{ Bindings: []*computepb.Binding{userBinding}, Etag: policy.Etag, } setIamPolicyReq := &computepb.SetIamPolicyImageRequest{ Project: g.GetProjectID(), Resource: imageName, GlobalSetPolicyRequestResource: &computepb.GlobalSetPolicyRequest{ Policy: newPolicy, }, } _, err = imagesClient.SetIamPolicy(ctx, setIamPolicyReq) if err != nil { return fmt.Errorf("failed to set new image policy: %v", err) } // Users won't see the shared image in their images.list requests, unless // they are also granted a specific "imagesList" role on the project. If you // don't need users to be able to view the list of shared images, this // step can be skipped. // // Downside of granting the "imagesList" role to a project is that the user // will be able to list all available images in the project, even those that // they can't use because of insufficient permissions. // // Even without the ability to view / list shared images, the user can still // create a Compute Engine instance using the image via API or 'gcloud' tool. // // Custom role to enable account to only list images in the project. // Without this role, the account won't be able to list and see the image // in the GCP Web UI. // For now, the decision is that the account should not get any role to the // project, where the image has been imported. return nil } // ComputeImageDelete deletes a Compute Engine image with the given name. If the // image existed and was successfully deleted, no error is returned. // // Uses: // - Compute Engine API func (g *GCP) ComputeImageDelete(ctx context.Context, name string) error { imagesClient, err := compute.NewImagesRESTClient(ctx, option.WithCredentials(g.creds)) if err != nil { return fmt.Errorf("failed to get Compute Engine Images client: %v", err) } defer imagesClient.Close() req := &computepb.DeleteImageRequest{ Project: g.GetProjectID(), Image: name, } _, err = imagesClient.Delete(ctx, req) return err } // ComputeExecuteFunctionForImages will pass all the compute images in the account to a function, // which is able to iterate over the images. Useful if something needs to be execute for each image. // Uses: // - Compute Engine API func (g *GCP) ComputeExecuteFunctionForImages(ctx context.Context, f func(*compute.ImageIterator) error) error { imagesClient, err := compute.NewImagesRESTClient(ctx, option.WithCredentials(g.creds)) if err != nil { return fmt.Errorf("failed to get Compute Engine Images client: %v", err) } defer imagesClient.Close() req := &computepb.ListImagesRequest{ Project: g.GetProjectID(), } imagesIterator := imagesClient.List(ctx, req) return f(imagesIterator) }