diff --git a/cmd/osbuild-upload-oci/main.go b/cmd/osbuild-upload-oci/main.go index 55625e181..9cc2e9aa7 100644 --- a/cmd/osbuild-upload-oci/main.go +++ b/cmd/osbuild-upload-oci/main.go @@ -39,11 +39,16 @@ var uploadCmd = &cobra.Command{ } defer file.Close() - imageID, err := uploader.Upload(objectName, bucketName, bucketNamespace, file, compartment, fileName) + err = uploader.Upload(objectName, bucketName, bucketNamespace, file) if err != nil { return fmt.Errorf("failed to upload the image: %v", err) } + imageID, err := uploader.CreateImage(objectName, bucketName, bucketNamespace, compartment, fileName) + if err != nil { + return fmt.Errorf("failed to create the image from storage object: %v", err) + } + fmt.Printf("Image %s was uploaded and created successfully\n", imageID) return nil }, diff --git a/cmd/osbuild-worker/jobimpl-osbuild.go b/cmd/osbuild-worker/jobimpl-osbuild.go index 1f4fe9571..d68ada731 100644 --- a/cmd/osbuild-worker/jobimpl-osbuild.go +++ b/cmd/osbuild-worker/jobimpl-osbuild.go @@ -868,21 +868,73 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error { } defer file.Close() i, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) - imageID, err := ociClient.Upload( + err = ociClient.Upload( fmt.Sprintf("osbuild-upload-%d", i), targetOptions.Bucket, targetOptions.Namespace, file, + ) + if err != nil { + targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, err.Error(), nil) + break + } + + imageID, err := ociClient.CreateImage( + fmt.Sprintf("osbuild-upload-%d", i), + targetOptions.Bucket, + targetOptions.Namespace, targetOptions.Compartment, jobTarget.ImageName, ) + if err != nil { + targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, err.Error(), nil) + break + } + + logWithId.Info("[OCI] 🎉 Image uploaded and registered!") + targetResult.Options = &target.OCITargetResultOptions{ImageID: imageID} + case *target.OCIObjectStorageTargetOptions: + targetResult = target.NewOCIObjectStorageTargetResult(nil) + // create an ociClient uploader with a valid storage client + var ociClient oci.Client + ociClient, err = oci.NewClient(&oci.ClientParams{ + User: targetOptions.User, + Region: targetOptions.Region, + Tenancy: targetOptions.Tenancy, + Fingerprint: targetOptions.Fingerprint, + PrivateKey: targetOptions.PrivateKey, + }) if err != nil { targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorInvalidConfig, err.Error(), nil) break } - logWithId.Info("[OCI] 🎉 Image uploaded and registered!") - targetResult.Options = &target.OCITargetResultOptions{ImageID: imageID} + logWithId.Info("[OCI] 🔑 Logged in OCI") + logWithId.Info("[OCI] ⬆ Uploading the image") + file, err := os.Open(path.Join(outputDirectory, jobTarget.OsbuildArtifact.ExportName, jobTarget.OsbuildArtifact.ExportFilename)) + if err != nil { + targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorInvalidConfig, err.Error(), nil) + break + } + defer file.Close() + i, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + err = ociClient.Upload( + fmt.Sprintf("osbuild-upload-%d", i), + targetOptions.Bucket, + targetOptions.Namespace, + file, + ) + if err != nil { + targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, err.Error(), nil) + break + } + uri, err := ociClient.PreAuthenticatedRequest(fmt.Sprintf("osbuild-upload-%d", i), targetOptions.Bucket, targetOptions.Namespace) + if err != nil { + targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorGeneratingSignedURL, err.Error(), nil) + break + } + logWithId.Info("[OCI] 🎉 Image uploaded and registered!") + targetResult.Options = &target.OCIObjectStorageTargetResultOptions{URL: uri} case *target.ContainerTargetOptions: targetResult = target.NewContainerTargetResult(nil) destination := jobTarget.ImageName diff --git a/internal/target/oci_target.go b/internal/target/oci_target.go index ffe4a9bd7..5759332de 100644 --- a/internal/target/oci_target.go +++ b/internal/target/oci_target.go @@ -29,3 +29,32 @@ func (OCITargetResultOptions) isTargetResultOptions() {} func NewOCITargetResult(options *OCITargetResultOptions) *TargetResult { return newTargetResult(TargetNameOCI, options) } + +const TargetNameOCIObjectStorage TargetName = "org.osbuild.oci.objectstorage" + +func NewOCIObjectStorageTarget(options *OCIObjectStorageTargetOptions) *Target { + return newTarget(TargetNameOCIObjectStorage, options) +} + +type OCIObjectStorageTargetOptions struct { + User string `json:"user"` + Tenancy string `json:"tenancy"` + Region string `json:"region"` + Fingerprint string `json:"fingerprint"` + PrivateKey string `json:"private_key"` + Bucket string `json:"bucket"` + Namespace string `json:"namespace"` + Compartment string `json:"compartment_id"` +} + +func (OCIObjectStorageTargetOptions) isTargetOptions() {} + +type OCIObjectStorageTargetResultOptions struct { + URL string `json:"url"` +} + +func (OCIObjectStorageTargetResultOptions) isTargetResultOptions() {} + +func NewOCIObjectStorageTargetResult(options *OCIObjectStorageTargetResultOptions) *TargetResult { + return newTargetResult(TargetNameOCIObjectStorage, options) +} diff --git a/internal/target/target.go b/internal/target/target.go index 65c814f20..8ab023248 100644 --- a/internal/target/target.go +++ b/internal/target/target.go @@ -87,6 +87,8 @@ func (target *Target) UnmarshalJSON(data []byte) error { options = new(VMWareTargetOptions) case TargetNameOCI: options = new(OCITargetOptions) + case TargetNameOCIObjectStorage: + options = new(OCIObjectStorageTargetOptions) case TargetNameContainer: options = new(ContainerTargetOptions) case TargetNameWorkerServer: @@ -246,6 +248,18 @@ func (target Target) MarshalJSON() ([]byte, error) { } rawOptions, err = json.Marshal(compat) + case *OCIObjectStorageTargetOptions: + type compatOptionsType struct { + *OCIObjectStorageTargetOptions + // Deprecated: `Filename` is now set in the target itself as `ExportFilename`, not in its options. + Filename string `json:"filename"` + } + compat := compatOptionsType{ + OCIObjectStorageTargetOptions: t, + Filename: target.OsbuildArtifact.ExportFilename, + } + rawOptions, err = json.Marshal(compat) + case *ContainerTargetOptions: type compatOptionsType struct { *ContainerTargetOptions diff --git a/internal/target/targetresult.go b/internal/target/targetresult.go index 98abd2512..0fd1922d8 100644 --- a/internal/target/targetresult.go +++ b/internal/target/targetresult.go @@ -67,6 +67,8 @@ func UnmarshalTargetResultOptions(trName TargetName, rawOptions json.RawMessage) options = new(KojiTargetResultOptions) case TargetNameOCI: options = new(OCITargetResultOptions) + case TargetNameOCIObjectStorage: + options = new(OCIObjectStorageTargetResultOptions) case TargetNameContainer: options = new(ContainerTargetResultOptions) default: diff --git a/internal/upload/oci/upload.go b/internal/upload/oci/upload.go index a8eacc1b6..6b4c31a52 100644 --- a/internal/upload/oci/upload.go +++ b/internal/upload/oci/upload.go @@ -17,7 +17,9 @@ import ( ) type Uploader interface { - Upload(name string, bucketName string, namespace string, file *os.File, user, compartment string) (string, error) + 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 { @@ -31,17 +33,19 @@ type Client struct { } // Upload uploads a file into an objectName under the bucketName in the namespace. -func (c Client) Upload(objectName string, bucketName string, namespace string, file *os.File, compartmentID, imageName string) (string, error) { +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) } }() - if err != nil { - return "", err - } imageID, err := c.createImage(objectName, bucketName, namespace, compartmentID, imageName) if err != nil { @@ -54,6 +58,32 @@ func (c Client) Upload(objectName string, bucketName string, namespace string, f 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{ @@ -216,6 +246,7 @@ type ClientParams struct { } type ociClient struct { + region string storageClient objectstorage.ObjectStorageClient identityClient identity.IdentityClient computeClient core.ComputeClient @@ -272,6 +303,7 @@ func NewClient(clientParams *ClientParams) (Client, error) { 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, diff --git a/internal/worker/clienterrors/errors.go b/internal/worker/clienterrors/errors.go index 67beb9732..2692cdd31 100644 --- a/internal/worker/clienterrors/errors.go +++ b/internal/worker/clienterrors/errors.go @@ -42,6 +42,7 @@ const ( ErrorOSTreeDependency ClientErrorCode = 35 ErrorRemoteFileResolution ClientErrorCode = 36 ErrorJobPanicked ClientErrorCode = 37 + ErrorGeneratingSignedURL ClientErrorCode = 38 ) type ClientErrorCode int