diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ab4982ca9e..4e6013c523 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 55625e181c..9cc2e9aa72 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 2f0d9b76a7..43491d0cca 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/config_test.go b/cmd/osbuild-worker/config_test.go index aa2cb33b79..34b5115c54 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/cmd/osbuild-worker/jobimpl-osbuild.go b/cmd/osbuild-worker/jobimpl-osbuild.go index 1f4fe95712..3625fc0b87 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 bc0159e52c..3c5a7b70e5 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 231780652a..390c828c78 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 27cad02890..19527a8309 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 6e44799f84..de31d32e41 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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", + "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 a60dd39efd..0130d15f62 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 ffe4a9bd7d..5759332def 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 65c814f20b..8ab023248b 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 98abd25126..0fd1922d8f 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 a8eacc1b67..bfd07f5bc6 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 67beb97323..2692cdd31a 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/test/cases/api.sh b/test/cases/api.sh index 441736bb24..fa2f4c09e6 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 347295ede3..c3d476df50 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 0000000000..d0a46e6216 --- /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 < /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