package oci import ( "context" "fmt" "log" "net/http" "os" "time" "github.com/oracle/oci-go-sdk/v54/common" "github.com/oracle/oci-go-sdk/v54/core" "github.com/oracle/oci-go-sdk/v54/identity" "github.com/oracle/oci-go-sdk/v54/objectstorage" "github.com/oracle/oci-go-sdk/v54/objectstorage/transfer" "github.com/oracle/oci-go-sdk/v54/workrequests" ) type Uploader interface { Upload(name string, bucketName string, namespace string, file *os.File) error CreateImage(name, bucketName, namespace, user, compartment string) (string, error) PreAuthenticatedRequest(objectName, bucketName, namespace string) (string, error) } type ImageCreator interface { create(imageName, bucketName, namespace, compartmentID string) (string, error) } type Client struct { Uploader ImageCreator ociClient } // Upload uploads a file into an objectName under the bucketName in the namespace. func (c Client) Upload(objectName, bucketName, namespace string, file *os.File) error { err := c.uploadToBucket(objectName, bucketName, namespace, file) return err } // Creates an image from an existing storage object, deletes the storage object func (c Client) CreateImage(objectName, bucketName, namespace, compartmentID, imageName string) (string, error) { // clean up the object even if we fail defer func() { if err := c.deleteObjectFromBucket(objectName, bucketName, namespace); err != nil { log.Printf("failed to clean up the object '%s' from bucket '%s'", objectName, bucketName) } }() imageID, err := c.createImage(objectName, bucketName, namespace, compartmentID, imageName) if err != nil { return "", fmt.Errorf("failed to create a custom image using object '%s' bucket '%s' in namespace '%s': %w", objectName, bucketName, namespace, err) } return imageID, nil } // https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/usingpreauthenticatedrequests.htm func (c Client) PreAuthenticatedRequest(objectName, bucketName, namespace string) (string, error) { req := objectstorage.CreatePreauthenticatedRequestRequest{ BucketName: common.String(bucketName), NamespaceName: common.String(namespace), CreatePreauthenticatedRequestDetails: objectstorage.CreatePreauthenticatedRequestDetails{ ObjectName: common.String(objectName), TimeExpires: &common.SDKTime{Time: time.Now().Add(24 * time.Hour)}, AccessType: objectstorage.CreatePreauthenticatedRequestDetailsAccessTypeObjectread, BucketListingAction: objectstorage.PreauthenticatedRequestBucketListingActionDeny, Name: common.String(fmt.Sprintf("pre-auth-req-for-%s", objectName)), }, } resp, err := c.storageClient.CreatePreauthenticatedRequest(context.Background(), req) if err != nil { return "", fmt.Errorf("failed to create a pre-authenticated request for object '%s': %w", objectName, err) } sc := resp.HTTPResponse().StatusCode if sc != 200 { return "", fmt.Errorf("failed to create a pre-authenticated request for object, status %d", sc) } return fmt.Sprintf("https://%s.objectstorage.%s.oci.customer-oci.com%s", namespace, c.region, *resp.AccessUri), nil } func (c Client) uploadToBucket(objectName string, bucketName string, namespace string, file *os.File) error { req := transfer.UploadFileRequest{ UploadRequest: transfer.UploadRequest{ NamespaceName: common.String(namespace), BucketName: common.String(bucketName), ObjectName: common.String(objectName), CallBack: func(multiPartUploadPart transfer.MultiPartUploadPart) { if multiPartUploadPart.Err != nil { log.Printf("upload failure: %s\n", multiPartUploadPart.Err) } log.Printf("multipart upload stats: parts %d, total-parts %d\n", multiPartUploadPart.PartNum, multiPartUploadPart.TotalParts) }, ObjectStorageClient: &c.storageClient, }, FilePath: file.Name(), } uploadManager := transfer.NewUploadManager() ctx := context.Background() resp, err := uploadManager.UploadFile(ctx, req) if err != nil { // resp.IsResumable crashes if resp.MultipartUploadResponse == nil // Thus, we need to check for both. if resp.MultipartUploadResponse != nil && resp.IsResumable() { resp, err = uploadManager.ResumeUploadFile(ctx, *resp.MultipartUploadResponse.UploadID) if err != nil { return err } } else { return fmt.Errorf("failed to upload the file to object %s: %w", objectName, err) } } return nil } // Create creates an image from the storageObjectName stored in the bucketName. // The result is an image ID or an error if the operation failed. func (c Client) createImage(objectName, bucketName, namespace, compartmentID, imageName string) (string, error) { request := core.CreateImageRequest{ CreateImageDetails: core.CreateImageDetails{ DisplayName: common.String(imageName), CompartmentId: common.String(compartmentID), FreeformTags: map[string]string{ "Uploaded-By": "osbuild-composer", "Name": imageName, }, ImageSourceDetails: core.ImageSourceViaObjectStorageTupleDetails{ BucketName: common.String(bucketName), NamespaceName: common.String(namespace), ObjectName: common.String(objectName), SourceImageType: core.ImageSourceDetailsSourceImageTypeQcow2, }, }, } createImageResponse, err := c.computeClient.CreateImage(context.Background(), request) if err != nil { return "", fmt.Errorf("failed to create an image from storage object: %w", err) } log.Printf("waiting for the work request to complete, this may take a while. Work request ID: %s\n", *createImageResponse.OpcWorkRequestId) for { r, err := c.workRequestsClient.GetWorkRequest(context.Background(), workrequests.GetWorkRequestRequest{ WorkRequestId: createImageResponse.OpcWorkRequestId, OpcRequestId: createImageResponse.OpcRequestId, }) if err != nil { return "", fmt.Errorf("failed to fetch the work request for creating the image: %w", err) } if r.Status == workrequests.WorkRequestStatusSucceeded { break } if r.Status == workrequests.WorkRequestStatusCanceled || r.Status == workrequests.WorkRequestStatusFailed { return "", fmt.Errorf("the work request for creating an image is in status %s", r.Status) } time.Sleep(1 * time.Second) } log.Printf("work request complete, creating the compute image capability schema based on a global one") listGlobalCS := core.ListComputeGlobalImageCapabilitySchemasRequest{ Limit: common.Int(1), } globalCSR, err := c.computeClient.ListComputeGlobalImageCapabilitySchemas(context.Background(), listGlobalCS) if err != nil { return *createImageResponse.Id, fmt.Errorf("failed to list the global capability schemas: %w", err) } if globalCSR.HTTPResponse().StatusCode != 200 { return *createImageResponse.Id, fmt.Errorf("failed to list the global capability schemas: %d", globalCSR.HTTPResponse().StatusCode) } if len(globalCSR.Items) == 0 || globalCSR.Items[0].CurrentVersionName == nil { return *createImageResponse.Id, fmt.Errorf("no global capability schema version found") } createComputeCapabilitiesReq := core.CreateComputeImageCapabilitySchemaRequest{ CreateComputeImageCapabilitySchemaDetails: core.CreateComputeImageCapabilitySchemaDetails{ CompartmentId: common.String(compartmentID), ImageId: createImageResponse.Id, ComputeGlobalImageCapabilitySchemaVersionName: globalCSR.Items[0].CurrentVersionName, SchemaData: map[string]core.ImageCapabilitySchemaDescriptor{ "Storage.RemoteDataVolumeType": core.EnumStringImageCapabilitySchemaDescriptor{ Source: core.ImageCapabilitySchemaDescriptorSourceImage, Values: []string{ "PARAVIRTUALIZED", }, DefaultValue: common.String("PARAVIRTUALIZED"), }, "Storage.LocalDataVolumeType": core.EnumStringImageCapabilitySchemaDescriptor{ Source: core.ImageCapabilitySchemaDescriptorSourceImage, Values: []string{ "PARAVIRTUALIZED", }, DefaultValue: common.String("PARAVIRTUALIZED"), }, "Storage.BootVolumeType": core.EnumStringImageCapabilitySchemaDescriptor{ Source: core.ImageCapabilitySchemaDescriptorSourceImage, Values: []string{ "PARAVIRTUALIZED", }, DefaultValue: common.String("PARAVIRTUALIZED"), }, "Network.AttachmentType": core.EnumStringImageCapabilitySchemaDescriptor{ Source: core.ImageCapabilitySchemaDescriptorSourceImage, Values: []string{ "PARAVIRTUALIZED", }, DefaultValue: common.String("PARAVIRTUALIZED"), }, "Compute.LaunchMode": core.EnumStringImageCapabilitySchemaDescriptor{ Source: core.ImageCapabilitySchemaDescriptorSourceImage, Values: []string{ "NATIVE", "PARAVIRTUALIZED", }, DefaultValue: common.String("PARAVIRTUALIZED"), }, }, }, } createCICSR, err := c.computeClient.CreateComputeImageCapabilitySchema(context.Background(), createComputeCapabilitiesReq) if err != nil { return *createImageResponse.Id, fmt.Errorf("failed to create the image's capability schema: %w", err) } if createCICSR.HTTPResponse().StatusCode != 200 { return *createImageResponse.Id, fmt.Errorf("failed to create the image's capability schema: %d", createCICSR.HTTPResponse().StatusCode) } return *createImageResponse.Id, nil } type ClientParams struct { User string `toml:"user"` Region string `toml:"region"` Tenancy string `toml:"tenancy"` PrivateKey string `toml:"private_key"` Fingerprint string `toml:"fingerprint"` } type ociClient struct { region string storageClient objectstorage.ObjectStorageClient identityClient identity.IdentityClient computeClient core.ComputeClient workRequestsClient workrequests.WorkRequestClient } // deleteObjectFromBucket deletes the object by name from the bucket. func (c Client) deleteObjectFromBucket(name string, bucket string, namespace string) error { _, err := c.storageClient.DeleteObject(context.Background(), objectstorage.DeleteObjectRequest{ NamespaceName: common.String(namespace), BucketName: common.String(bucket), ObjectName: common.String(name), }) return err } // NewClient creates a new oci client from the passed in params. // Pass nil clientParams if you want the client to automatically detect // the configuration from the official disk location or and env file // From the docs its: $HOME/.oci/config, $HOME/.obmcs/config and variable // names starting with TF_VAR. // Last is the environment variable OCI_CONFIG_FILE func NewClient(clientParams *ClientParams) (Client, error) { var configProvider common.ConfigurationProvider if clientParams != nil { configProvider = common.NewRawConfigurationProvider( clientParams.Tenancy, clientParams.User, clientParams.Region, clientParams.Fingerprint, clientParams.PrivateKey, nil, ) } else { configProvider = common.DefaultConfigProvider() } storageClient, err := objectstorage.NewObjectStorageClientWithConfigurationProvider(configProvider) // this disables the default 60 seconds timeout, to support big files upload (the common scenario) storageClient.HTTPClient = &http.Client{} if err != nil { return Client{}, fmt.Errorf("failed to create an Oracle objectstorage client: %w", err) } identityClient, err := identity.NewIdentityClientWithConfigurationProvider(configProvider) if err != nil { return Client{}, fmt.Errorf("failed to create an Oracle identity client: %w", err) } computeClient, err := core.NewComputeClientWithConfigurationProvider(configProvider) if err != nil { return Client{}, fmt.Errorf("failed to create an Oracle compute client: %w", err) } workRequestsClient, err := workrequests.NewWorkRequestClientWithConfigurationProvider(configProvider) if err != nil { return Client{}, fmt.Errorf("failed to create an Oracle workrequests client: %w", err) } return Client{ociClient: ociClient{ region: clientParams.Region, storageClient: storageClient, identityClient: identityClient, computeClient: computeClient, workRequestsClient: workRequestsClient, }}, nil }