Repository: osbuild/osbuild-composer
Pull request #3675 (2023-09-06 12:51:39)

oci object storage target

kingsleyzissou: Neat, just a few inline comments :)

Patch:
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ab4982ca9..4e6013c52 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -490,6 +490,7 @@ cross-distro.sh:
     - vsphere
     - edge-commit generic.s3
     - edge-container
+    - oci
 
 API:
   stage: test
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/config.go b/cmd/osbuild-worker/config.go
index 2f0d9b76a..43491d0cc 100644
--- a/cmd/osbuild-worker/config.go
+++ b/cmd/osbuild-worker/config.go
@@ -38,6 +38,10 @@ type awsConfig struct {
 	Bucket      string `toml:"bucket"`
 }
 
+type ociConfig struct {
+	Credentials string `toml:"credentials"`
+}
+
 type genericS3Config struct {
 	Credentials         string `toml:"credentials"`
 	Endpoint            string `toml:"endpoint"`
@@ -71,6 +75,7 @@ type workerConfig struct {
 	GenericS3      *genericS3Config            `toml:"generic_s3"`
 	Authentication *authenticationConfig       `toml:"authentication"`
 	Containers     *containersConfig           `toml:"containers"`
+	OCI            *ociConfig                  `toml:"oci"`
 	// default value: /api/worker/v1
 	BasePath string `toml:"base_path"`
 	DNFJson  string `toml:"dnf-json"`
diff --git a/cmd/osbuild-worker/jobimpl-osbuild.go b/cmd/osbuild-worker/jobimpl-osbuild.go
index 1f4fe9571..3625fc0b8 100644
--- a/cmd/osbuild-worker/jobimpl-osbuild.go
+++ b/cmd/osbuild-worker/jobimpl-osbuild.go
@@ -58,12 +58,20 @@ type AzureConfiguration struct {
 	UploadThreads int
 }
 
+type OCIConfiguration struct {
+	ClientParams *oci.ClientParams
+	Compartment  string
+	Bucket       string
+	Namespace    string
+}
+
 type OSBuildJobImpl struct {
 	Store            string
 	Output           string
 	KojiServers      map[string]kojiServer
 	GCPConfig        GCPConfiguration
 	AzureConfig      AzureConfiguration
+	OCIConfig        OCIConfiguration
 	AWSCreds         string
 	AWSBucket        string
 	S3Config         S3Configuration
@@ -179,6 +187,30 @@ func (impl *OSBuildJobImpl) getGCP(credentials []byte) (*gcp.GCP, error) {
 	}
 }
 
+// Takes the worker config as a base and overwrites it with both t1 and t2's options
+func (impl *OSBuildJobImpl) getOCI(tcp oci.ClientParams) (oci.Client, error) {
+	var cp oci.ClientParams
+	if impl.OCIConfig.ClientParams != nil {
+		cp = *impl.OCIConfig.ClientParams
+	}
+	if tcp.User != "" {
+		cp.User = tcp.User
+	}
+	if tcp.Region != "" {
+		cp.Region = tcp.Region
+	}
+	if tcp.Tenancy != "" {
+		cp.Tenancy = tcp.Tenancy
+	}
+	if tcp.PrivateKey != "" {
+		cp.PrivateKey = tcp.PrivateKey
+	}
+	if tcp.Fingerprint != "" {
+		cp.Fingerprint = tcp.Fingerprint
+	}
+	return oci.NewClient(&cp)
+}
+
 func validateResult(result *worker.OSBuildJobResult, jobID string) {
 	logWithId := logrus.WithField("jobId", jobID)
 	if result.JobError != nil {
@@ -848,7 +880,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 			targetResult = target.NewOCITargetResult(nil)
 			// create an ociClient uploader with a valid storage client
 			var ociClient oci.Client
-			ociClient, err = oci.NewClient(&oci.ClientParams{
+			ociClient, err = impl.getOCI(oci.ClientParams{
 				User:        targetOptions.User,
 				Region:      targetOptions.Region,
 				Tenancy:     targetOptions.Tenancy,
@@ -868,21 +900,92 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
 			}
 			defer file.Close()
 			i, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
-			imageID, err := ociClient.Upload(
+			bucket := impl.OCIConfig.Bucket
+			if targetOptions.Bucket != "" {
+				bucket = targetOptions.Bucket
+			}
+			namespace := impl.OCIConfig.Namespace
+			if targetOptions.Namespace != "" {
+				namespace = targetOptions.Namespace
+			}
+			err = ociClient.Upload(
 				fmt.Sprintf("osbuild-upload-%d", i),
-				targetOptions.Bucket,
-				targetOptions.Namespace,
+				bucket,
+				namespace,
 				file,
-				targetOptions.Compartment,
+			)
+			if err != nil {
+				targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorUploadingImage, err.Error(), nil)
+				break
+			}
+
+			compartment := impl.OCIConfig.Compartment
+			if targetOptions.Compartment != "" {
+				compartment = targetOptions.Compartment
+			}
+			imageID, err := ociClient.CreateImage(
+				fmt.Sprintf("osbuild-upload-%d", i),
+				bucket,
+				namespace,
+				compartment,
 				jobTarget.ImageName,
 			)
 			if err != nil {
-				targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorInvalidConfig, err.Error(), 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
+			ociClient, err := impl.getOCI(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] 🔑 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))
+			bucket := impl.OCIConfig.Bucket
+			if targetOptions.Bucket != "" {
+				bucket = targetOptions.Bucket
+			}
+			namespace := impl.OCIConfig.Namespace
+			if targetOptions.Namespace != "" {
+				namespace = targetOptions.Namespace
+			}
+			err = ociClient.Upload(
+				fmt.Sprintf("osbuild-upload-%d", i),
+				bucket,
+				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), bucket, namespace)
+			if err != nil {
+				targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorGeneratingSignedURL, err.Error(), nil)
+				break
+			}
+			logWithId.Info("[OCI] 🎉 Image uploaded and pre-authenticated request generated!")
+			targetResult.Options = &target.OCIObjectStorageTargetResultOptions{URL: uri}
 		case *target.ContainerTargetOptions:
 			targetResult = target.NewContainerTargetResult(nil)
 			destination := jobTarget.ImageName
diff --git a/cmd/osbuild-worker/main.go b/cmd/osbuild-worker/main.go
index bc0159e52..3c5a7b70e 100644
--- a/cmd/osbuild-worker/main.go
+++ b/cmd/osbuild-worker/main.go
@@ -24,6 +24,7 @@ import (
 	"github.com/osbuild/osbuild-composer/internal/dnfjson"
 	"github.com/osbuild/osbuild-composer/internal/upload/azure"
 	"github.com/osbuild/osbuild-composer/internal/upload/koji"
+	"github.com/osbuild/osbuild-composer/internal/upload/oci"
 	"github.com/osbuild/osbuild-composer/internal/worker"
 )
 
@@ -395,6 +396,36 @@ func main() {
 		containersTLSVerify = config.Containers.TLSVerify
 	}
 
+	var ociConfig OCIConfiguration
+	if config.OCI != nil {
+		var creds struct {
+			User        string `toml:"user"`
+			Tenancy     string `toml:"tenancy"`
+			Region      string `toml:"region"`
+			Fingerprint string `toml:"fingerprint"`
+			PrivateKey  string `toml:"private_key"`
+			Bucket      string `toml:"bucket"`
+			Namespace   string `toml:"namespace"`
+			Compartment string `toml:"compartment"`
+		}
+		_, err := toml.DecodeFile(config.OCI.Credentials, &creds)
+		if err != nil {
+			logrus.Fatalf("cannot load oci credentials: %v", err)
+		}
+		ociConfig = OCIConfiguration{
+			ClientParams: &oci.ClientParams{
+				User:        creds.User,
+				Region:      creds.Region,
+				Tenancy:     creds.Tenancy,
+				PrivateKey:  creds.PrivateKey,
+				Fingerprint: creds.Fingerprint,
+			},
+			Bucket:      creds.Bucket,
+			Namespace:   creds.Namespace,
+			Compartment: creds.Compartment,
+		}
+	}
+
 	// depsolve jobs can be done during other jobs
 	depsolveCtx, depsolveCtxCancel := context.WithCancel(context.Background())
 	solver := dnfjson.NewBaseSolver(rpmmd_cache)
@@ -438,6 +469,7 @@ func main() {
 			KojiServers: kojiServers,
 			GCPConfig:   gcpConfig,
 			AzureConfig: azureConfig,
+			OCIConfig:   ociConfig,
 			AWSCreds:    awsCredentials,
 			AWSBucket:   awsBucket,
 			S3Config: S3Configuration{
diff --git a/internal/cloudapi/v2/handler.go b/internal/cloudapi/v2/handler.go
index 231780652..390c828c7 100644
--- a/internal/cloudapi/v2/handler.go
+++ b/internal/cloudapi/v2/handler.go
@@ -331,6 +331,8 @@ func imageTypeFromApiImageType(it ImageTypes, arch distro.Arch) string {
 		return "iot-raw-image"
 	case ImageTypesLiveInstaller:
 		return "live-installer"
+	case ImageTypesOci:
+		return "oci"
 	case ImageTypesWsl:
 		return "wsl"
 	}
@@ -376,6 +378,12 @@ func targetResultToUploadStatus(t *target.TargetResult) (*UploadStatus, error) {
 			Url:    containerOptions.URL,
 			Digest: containerOptions.Digest,
 		}
+	case target.TargetNameOCIObjectStorage:
+		uploadType = UploadTypesOciObjectstorage
+		ociOptions := t.Options.(*target.OCIObjectStorageTargetResultOptions)
+		uploadOptions = OCIUploadStatus{
+			Url: ociOptions.URL,
+		}
 
 	default:
 		return nil, fmt.Errorf("unknown upload target: %s", t.Name)
diff --git a/internal/cloudapi/v2/imagerequest.go b/internal/cloudapi/v2/imagerequest.go
index 27cad0289..19527a830 100644
--- a/internal/cloudapi/v2/imagerequest.go
+++ b/internal/cloudapi/v2/imagerequest.go
@@ -232,6 +232,22 @@ func (ir *ImageRequest) GetTarget(request *ComposeRequest, imageType distro.Imag
 		}
 		t.OsbuildArtifact.ExportFilename = imageType.Filename()
 
+		irTarget = t
+	case ImageTypesOci:
+		var ociUploadOptions OCIUploadOptions
+		jsonUploadOptions, err := json.Marshal(*ir.UploadOptions)
+		if err != nil {
+			return nil, HTTPError(ErrorJSONMarshallingError)
+		}
+		err = json.Unmarshal(jsonUploadOptions, &ociUploadOptions)
+		if err != nil {
+			return nil, HTTPError(ErrorJSONUnMarshallingError)
+		}
+
+		key := fmt.Sprintf("composer-api-%s", uuid.New().String())
+		t := target.NewOCIObjectStorageTarget(&target.OCIObjectStorageTargetOptions{})
+		t.ImageName = key
+		t.OsbuildArtifact.ExportFilename = imageType.Filename()
 		irTarget = t
 	default:
 		return nil, HTTPError(ErrorUnsupportedImageType)
diff --git a/internal/cloudapi/v2/openapi.v2.gen.go b/internal/cloudapi/v2/openapi.v2.gen.go
index 6e44799f8..de31d32e4 100644
--- a/internal/cloudapi/v2/openapi.v2.gen.go
+++ b/internal/cloudapi/v2/openapi.v2.gen.go
@@ -88,6 +88,8 @@ const (
 
 	ImageTypesLiveInstaller ImageTypes = "live-installer"
 
+	ImageTypesOci ImageTypes = "oci"
+
 	ImageTypesVsphere ImageTypes = "vsphere"
 
 	ImageTypesVsphereOva ImageTypes = "vsphere-ova"
@@ -117,6 +119,8 @@ const (
 	UploadTypesContainer UploadTypes = "container"
 
 	UploadTypesGcp UploadTypes = "gcp"
+
+	UploadTypesOciObjectstorage UploadTypes = "oci.objectstorage"
 )
 
 // AWSEC2CloneCompose defines model for AWSEC2CloneCompose.
@@ -520,6 +524,14 @@ type LocalUploadOptions struct {
 	LocalSave bool `json:"local_save"`
 }
 
+// OCIUploadOptions defines model for OCIUploadOptions.
+type OCIUploadOptions map[string]interface{}
+
+// OCIUploadStatus defines model for OCIUploadStatus.
+type OCIUploadStatus struct {
+	Url string `json:"url"`
+}
+
 // OSTree defines model for OSTree.
 type OSTree struct {
 	// A URL which, if set, is used for fetching content. Implies that `url` is set as well,
@@ -911,137 +923,138 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
 // Base64 encoded, gzipped, json marshaled Swagger object
 var swaggerSpec = []string{
 
-	"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",
+	"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",
+	"DIEAAA==",
 }
 
 // GetSwagger returns the content of the embedded swagger specification file
diff --git a/internal/cloudapi/v2/openapi.v2.yml b/internal/cloudapi/v2/openapi.v2.yml
index a60dd39ef..0130d15f6 100644
--- a/internal/cloudapi/v2/openapi.v2.yml
+++ b/internal/cloudapi/v2/openapi.v2.yml
@@ -611,6 +611,7 @@ components:
             - $ref: '#/components/schemas/GCPUploadStatus'
             - $ref: '#/components/schemas/AzureUploadStatus'
             - $ref: '#/components/schemas/ContainerUploadStatus'
+            - $ref: '#/components/schemas/OCIUploadStatus'
     UploadStatusValue:
       type: string
       enum: ['success', 'failure', 'pending', 'running']
@@ -622,6 +623,7 @@ components:
         - gcp
         - azure
         - container
+        - oci.objectstorage
     AWSEC2UploadStatus:
       type: object
       required:
@@ -683,6 +685,13 @@ components:
           type: string
           description: |
             Digest of the manifest of the uploaded container on the registry
+    OCIUploadStatus:
+      type: object
+      required:
+        - url
+      properties:
+        url:
+          type: string
     ComposeMetadata:
       allOf:
       - $ref: '#/components/schemas/ObjectReference'
@@ -790,6 +799,7 @@ components:
         - iot-installer
         - iot-raw-image
         - live-installer
+        - oci
         - vsphere
         - vsphere-ova
         - wsl
@@ -905,6 +915,7 @@ components:
       - $ref: '#/components/schemas/AzureUploadOptions'
       - $ref: '#/components/schemas/ContainerUploadOptions'
       - $ref: '#/components/schemas/LocalUploadOptions'
+      - $ref: '#/components/schemas/OCIUploadOptions'
       description: |
         This should really be oneOf but AWSS3UploadOptions is a subset of
         AWSEC2UploadOptions. This means that all AWSEC2UploadOptions objects
@@ -961,6 +972,9 @@ components:
 
             If set to true, a shorter URL is returned and
             its expiration is the same as for the other upload targets.
+    OCIUploadOptions:
+      type: object
+      additionalProperties: false
     GCPUploadOptions:
       type: object
       additionalProperties: false
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..bfd07f5bc 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{
@@ -208,14 +238,15 @@ func (c Client) createImage(objectName, bucketName, namespace, compartmentID, im
 }
 
 type ClientParams struct {
-	User        string
-	Region      string
-	Tenancy     string
-	PrivateKey  string
-	Fingerprint string
+	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
@@ -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
diff --git a/tools/provision.sh b/tools/provision.sh
index 49b982e22..1ede409e8 100755
--- a/tools/provision.sh
+++ b/tools/provision.sh
@@ -132,6 +132,38 @@ EOF
 [aws]
 credentials = "/etc/osbuild-worker/aws-credentials.toml"
 bucket = "${AWS_BUCKET}"
+EOF
+        set -x
+    fi
+
+    # if OCI credentials are defined in the ENV, add them to the worker's configuration
+
+    OCI_USER=$(jq -r '.user' "$OCI_SECRETS")
+    OCI_TENANCY=$(jq -r '.tenancy' "$OCI_SECRETS")
+    OCI_REGION=$(jq -r '.region' "$OCI_SECRETS")
+    OCI_FINGERPRINT=$(jq -r '.fingerprint' "$OCI_SECRETS")
+    OCI_BUCKET_NAME=$(jq -r '.bucket' "$OCI_SECRETS")
+    OCI_NAMESPACE=$(jq -r '.namespace' "$OCI_SECRETS")
+    OCI_COMPARTMENT=$(jq -r '.compartment' "$OCI_SECRETS")
+    OCI_PRIV_KEY=$(cat "$OCI_PRIVATE_KEY")
+
+    if [[ -n "$OCI_TENANCY" ]]; then
+        set +x
+        sudo tee /etc/osbuild-worker/oci-credentials.toml > /dev/null << EOF
+user = "$OCI_USER"
+tenancy = "$OCI_TENANCY"
+region = "$OCI_REGION"
+fingerprint = "$OCI_FINGERPRINT"
+namespace = "$OCI_NAMESPACE"
+bucket = "$OCI_BUCKET_NAME"
+private_key = """
+$OCI_PRIV_KEY
+"""
+compartment = "$OCI_COMPARTMENT"
+EOF
+        sudo tee -a /etc/osbuild-worker/osbuild-worker.toml > /dev/null << EOF
+[oci]
+credentials = "/etc/osbuild-worker/oci-credentials.toml"
 EOF
         set -x
     fi


Test patch:
diff --git a/cmd/osbuild-worker/config_test.go b/cmd/osbuild-worker/config_test.go
index aa2cb33b7..34b5115c5 100644
--- a/cmd/osbuild-worker/config_test.go
+++ b/cmd/osbuild-worker/config_test.go
@@ -49,6 +49,9 @@ upload_threads = 8
 credentials = "/etc/osbuild-worker/aws-creds"
 bucket = "buckethead"
 
+[oci]
+credentials = "/etc/osbuild-worker/oci-creds"
+
 [generic_s3]
 credentials = "/etc/osbuild-worker/s3-creds"
 endpoint = "http://s3.example.com"
@@ -96,6 +99,9 @@ offline_token = "/etc/osbuild-worker/offline_token"
 					Credentials: "/etc/osbuild-worker/aws-creds",
 					Bucket:      "buckethead",
 				},
+				OCI: &ociConfig{
+					Credentials: "/etc/osbuild-worker/oci-creds",
+				},
 				GenericS3: &genericS3Config{
 					Credentials:         "/etc/osbuild-worker/s3-creds",
 					Endpoint:            "http://s3.example.com",
diff --git a/test/cases/api.sh b/test/cases/api.sh
index 441736bb2..fa2f4c09e 100755
--- a/test/cases/api.sh
+++ b/test/cases/api.sh
@@ -5,12 +5,6 @@
 # uploading it to the appropriate cloud provider. The test currently supports
 # AWS and GCP.
 #
-# This script sets `-x` and is meant to always be run like that. This is
-# simpler than adding extensive error reporting, which would make this script
-# considerably more complex. Also, the full trace this produces is very useful
-# for the primary audience: developers of osbuild-composer looking at the log
-# from a run on a remote continuous integration system.
-#
 
 #
 # Cloud provider / target names
@@ -22,6 +16,7 @@ CLOUD_PROVIDER_AZURE="azure"
 CLOUD_PROVIDER_AWS_S3="aws.s3"
 CLOUD_PROVIDER_GENERIC_S3="generic.s3"
 CLOUD_PROVIDER_CONTAINER_IMAGE_REGISTRY="container"
+CLOUD_PROVIDER_OCI="oci"
 
 #
 # Supported Image type names
@@ -34,6 +29,7 @@ export IMAGE_TYPE_EDGE_INSTALLER="edge-installer"
 export IMAGE_TYPE_GCP="gcp"
 export IMAGE_TYPE_IMAGE_INSTALLER="image-installer"
 export IMAGE_TYPE_GUEST="guest-image"
+export IMAGE_TYPE_OCI="oci"
 export IMAGE_TYPE_VSPHERE="vsphere"
 export IMAGE_TYPE_IOT_COMMIT="iot-commit"
 
@@ -47,7 +43,7 @@ if (( $# == 0 )); then
     exit 1
 fi
 
-set -euxo pipefail
+set -euo pipefail
 
 IMAGE_TYPE="$1"
 
@@ -67,6 +63,9 @@ case ${IMAGE_TYPE} in
     "$IMAGE_TYPE_EDGE_CONTAINER")
         CLOUD_PROVIDER="${CLOUD_PROVIDER_CONTAINER_IMAGE_REGISTRY}"
         ;;
+    "$IMAGE_TYPE_OCI")
+        CLOUD_PROVIDER="${CLOUD_PROVIDER_OCI}"
+        ;;
     "$IMAGE_TYPE_EDGE_COMMIT"|"$IMAGE_TYPE_IOT_COMMIT"|"$IMAGE_TYPE_EDGE_INSTALLER"|"$IMAGE_TYPE_IMAGE_INSTALLER"|"$IMAGE_TYPE_GUEST"|"$IMAGE_TYPE_VSPHERE")
         # blobby image types: upload to s3 and provide download link
         CLOUD_PROVIDER="${2:-$CLOUD_PROVIDER_AWS_S3}"
@@ -176,6 +175,9 @@ case $CLOUD_PROVIDER in
   "$CLOUD_PROVIDER_AZURE")
     source /usr/libexec/tests/osbuild-composer/api/azure.sh
     ;;
+  "$CLOUD_PROVIDER_OCI")
+    source /usr/libexec/tests/osbuild-composer/api/oci.sh
+    ;;
   "$CLOUD_PROVIDER_CONTAINER_IMAGE_REGISTRY")
     source /usr/libexec/tests/osbuild-composer/api/container.registry.sh
     ;;
@@ -190,13 +192,9 @@ checkEnv
 [[ "$ID" == "rhel" ]] && printenv API_TEST_SUBSCRIPTION_ORG_ID API_TEST_SUBSCRIPTION_ACTIVATION_KEY_V2 > /dev/null
 
 function dump_db() {
-  # Disable -x for these commands to avoid printing the whole result and manifest into the log
-  set +x
-
   # Save the result, including the manifest, for the job, straight from the db
   sudo "${CONTAINER_RUNTIME}" exec "${DB_CONTAINER_NAME}" psql -U postgres -d osbuildcomposer -c "SELECT result FROM jobs WHERE type='manifest-id-only'" \
     | sudo tee "${ARTIFACTS}/build-result.txt"
-  set -x
 }
 
 WORKDIR=$(mktemp -d)
@@ -599,6 +597,9 @@ EXPECTED_UPLOAD_TYPE="$CLOUD_PROVIDER"
 if [ "${CLOUD_PROVIDER}" == "${CLOUD_PROVIDER_GENERIC_S3}" ]; then
   EXPECTED_UPLOAD_TYPE="${CLOUD_PROVIDER_AWS_S3}"
 fi
+if [ "${CLOUD_PROVIDER}" == "${CLOUD_PROVIDER_OCI}" ]; then
+    EXPECTED_UPLOAD_TYPE="oci.objectstorage"
+fi
 test "$UPLOAD_TYPE" = "$EXPECTED_UPLOAD_TYPE"
 test $((INIT_COMPOSES+1)) = "$SUBS_COMPOSES"
 
diff --git a/test/cases/api/azure.sh b/test/cases/api/azure.sh
index 347295ede..c3d476df5 100644
--- a/test/cases/api/azure.sh
+++ b/test/cases/api/azure.sh
@@ -110,9 +110,7 @@ function checkUploadStatusOptions() {
 
 # Log into Azure
 function cloud_login() {
-  set +x
   $AZURE_CMD login --service-principal --username "${V2_AZURE_CLIENT_ID}" --password "${V2_AZURE_CLIENT_SECRET}" --tenant "${AZURE_TENANT_ID}"
-  set -x
 }
 
 # Verify image in Azure
diff --git a/test/cases/api/oci.sh b/test/cases/api/oci.sh
new file mode 100644
index 000000000..d0a46e621
--- /dev/null
+++ b/test/cases/api/oci.sh
@@ -0,0 +1,186 @@
+#!/usr/bin/bash
+
+source /usr/libexec/tests/osbuild-composer/api/common/aws.sh
+source /usr/libexec/tests/osbuild-composer/api/common/common.sh
+
+function checkEnv() {
+    printenv AWS_REGION AWS_BUCKET V2_AWS_ACCESS_KEY_ID V2_AWS_SECRET_ACCESS_KEY AWS_API_TEST_SHARE_ACCOUNT > /dev/null
+}
+
+function cleanup() {
+    greenprint "🧼 Cleaning up OCI"
+    $OCI_CMD compute instance terminate --instance-id "${INSTANCE_ID}" --force
+    $OCI_CMD compute image delete --image-id "${OCI_IMAGE_ID}" --force
+}
+
+# Set up temporary files.
+TEMPDIR=$(mktemp -d)
+OCI_CONFIG=${TEMPDIR}/oci-config
+SSH_DATA_DIR=$(tools/gen-ssh.sh)
+SSH_KEY=${SSH_DATA_DIR}/id_rsa
+
+OCI_USER=$(jq -r '.user' "$OCI_SECRETS")
+OCI_TENANCY=$(jq -r '.tenancy' "$OCI_SECRETS")
+OCI_REGION=$(jq -r '.region' "$OCI_SECRETS")
+OCI_FINGERPRINT=$(jq -r '.fingerprint' "$OCI_SECRETS")
+OCI_COMPARTMENT=$(jq -r '.compartment' "$OCI_SECRETS")
+OCI_SUBNET=$(jq -r '.subnet' "$OCI_SECRETS")
+
+# copy private key to what oci considers a valid path
+cp -p "$OCI_PRIVATE_KEY" "$TEMPDIR/priv_key.pem"
+tee "$OCI_CONFIG" > /dev/null << EOF
+[DEFAULT]
+user=${OCI_USER}
+fingerprint=${OCI_FINGERPRINT}
+key_file=${TEMPDIR}/priv_key.pem
+tenancy=${OCI_TENANCY}
+region=${OCI_REGION}
+EOF
+
+function installClient() {
+    if ! hash oci; then
+        echo "Using 'oci' from a container"
+        sudo "${CONTAINER_RUNTIME}" pull "${CONTAINER_IMAGE_CLOUD_TOOLS}"
+
+        # OCI_CLI_AUTH
+        OCI_CMD="sudo ${CONTAINER_RUNTIME} run --rm \
+        -v ${TEMPDIR}:${TEMPDIR}:Z \
+        -v ${SSH_DATA_DIR}:${SSH_DATA_DIR}:Z \
+        -v ${OCI_PRIVATE_KEY}:${OCI_PRIVATE_KEY}:Z \
+        ${CONTAINER_IMAGE_CLOUD_TOOLS} /root/bin/oci --config-file $OCI_CONFIG --region $OCI_REGION --output json"
+    else
+        echo "Using pre-installed 'oci' from the system"
+        OCI_CMD="oci --config-file $OCI_CONFIG --region $OCI_REGION"
+    fi
+    $OCI_CMD --version
+    $OCI_CMD setup repair-file-permissions --file "${TEMPDIR}/priv_key.pem"
+    $OCI_CMD setup repair-file-permissions --file "$OCI_CONFIG"
+}
+
+function createReqFile() {
+    cat > "$REQUEST_FILE" << EOF
+{
+  "distribution": "$DISTRO",
+  "customizations": {
+    "filesystem": [
+      {
+        "mountpoint": "/var",
+        "min_size": 262144000
+      }
+    ],
+    "payload_repositories": [
+      {
+        "baseurl": "$PAYLOAD_REPO_URL"
+      }
+    ],
+    "packages": [
+      "postgresql",
+      "dummy"
+    ]${SUBSCRIPTION_BLOCK}${DIR_FILES_CUSTOMIZATION_BLOCK}${REPOSITORY_CUSTOMIZATION_BLOCK}${OPENSCAP_CUSTOMIZATION_BLOCK}
+  },
+  "image_request": {
+      "architecture": "$ARCH",
+      "image_type": "${IMAGE_TYPE}",
+      "repositories": $(jq ".\"$ARCH\"" /usr/share/tests/osbuild-composer/repositories/"$DISTRO".json),
+      "upload_options": {}
+  }
+}
+EOF
+}
+
+
+function checkUploadStatusOptions() {
+    local URL
+    URL=$(echo "$UPLOAD_OPTIONS" | jq -r '.url')
+    echo "$URL" | grep -qF "$OCI_REGION" -
+}
+
+
+function get_availability_domain_by_shape {
+    for ad in $($OCI_CMD iam availability-domain list -c "$OCI_COMPARTMENT" | jq -r '.data[].name');do
+        if [ "$($OCI_CMD compute shape list -c "$OCI_COMPARTMENT" --availability-domain "$ad" | jq --arg SHAPE "$1" -r '.data[]|select(.shape==$SHAPE)|.shape')" == "$1" ];then
+            echo "$ad"
+            return
+        fi
+    done
+    return 1
+}
+
+# Verify image in OCI
+function verify() {
+    # import image
+    echo "verifying oci image"
+    URL=$(echo "$UPLOAD_OPTIONS" | jq -r '.url')
+    OCI_IMAGE_DATA=$($OCI_CMD compute image import from-object-uri \
+                        -c "$OCI_COMPARTMENT" \
+                        --uri "$URL")
+    echo "oci image data: $OCI_IMAGE_DATA"
+    OCI_IMAGE_ID=$(echo "$OCI_IMAGE_DATA" | jq -r '.data.id')
+
+    for LOOP_COUNTER in {0..120}; do
+        STATE=$($OCI_CMD compute image get --image-id "$OCI_IMAGE_ID" | jq -r '.data["lifecycle-state"]')
+        if [ "$STATE" = "AVAILABLE" ]; then
+            echo "👻 the VM imported in time!"
+            break
+        fi
+        if [ "$LOOP_COUNTER" = "120" ]; then
+            echo "😞 the VM did not import in time ;_;"
+            exit 1
+        fi
+        sleep 15
+    done
+
+    echo "adding compatibility schema to image"
+    tee "$TEMPDIR/compat-schema.json" > /dev/null <<EOF
+{
+  "Storage.BootVolumeType": {
+    "defaultValue": "PARAVIRTUALIZED",
+    "descriptorType": "enumstring",
+    "source": "IMAGE",
+    "values": [
+      "PARAVIRTUALIZED"
+    ]
+  }
+}
+EOF
+    GLOBAL_SCHEMA=$($OCI_CMD compute global-image-capability-schema list | jq -r '.data[0]["current-version-name"]')
+    $OCI_CMD compute image-capability-schema create \
+            --compartment-id "$OCI_COMPARTMENT" \
+            --image-id "$OCI_IMAGE_ID" \
+            --global-image-capability-schema-version-name "$GLOBAL_SCHEMA" \
+            --schema-data "file://$TEMPDIR/compat-schema.json"
+
+    SHAPE="VM.Standard.E4.Flex"
+    OCI_AVAILABILITY_DOMAIN="$(get_availability_domain_by_shape "$SHAPE")"
+    INSTANCE=$($OCI_CMD compute instance launch  \
+                        --shape "$SHAPE" \
+                        --shape-config '{"memoryInGBs": 2.0, "ocpus": 1.0}' \
+                        -c "${OCI_COMPARTMENT}" \
+                        --availability-domain "${OCI_AVAILABILITY_DOMAIN}" \
+                        --subnet-id "${OCI_SUBNET}" \
+                        --image-id "${OCI_IMAGE_ID}" \
+                        --freeform-tags "{\"Name\": \"${TEST_ID}\", \"gitlab-ci-test\": \"true\"}" \
+                        --user-data-file "${SSH_DATA_DIR}/user-data")
+    echo "Attempted to launch instance: $INSTANCE"
+    INSTANCE_ID="$(echo "$INSTANCE" | jq -r '.data.id')"
+
+    while true; do
+        INSTANCE=$($OCI_CMD compute instance get --instance-id "$INSTANCE_ID")
+        if [[ $(echo "$INSTANCE" | jq -r '.data["lifecycle-state"]') == RUNNING ]]; then
+            break
+        fi
+        sleep 10
+    done
+
+    # Get data about the instance we built.
+    PUBLIC_IP=$($OCI_CMD compute instance list-vnics --instance-id "$INSTANCE_ID" |  jq -r '.data[0]["public-ip"]')
+
+
+
+    echo "⏱  Waiting for instance to respond to ssh"
+    _instanceWaitSSH "$PUBLIC_IP"
+
+    # Verify image
+    _ssh="ssh -oStrictHostKeyChecking=no -i $SSH_KEY redhat@$PUBLIC_IP"
+    _instanceCheck "$_ssh"
+}