internal/target: add OCI object storage target
Uploads an OCI image to OCI object storage, and generates a pre-authenticated request for the object, which can be used to import it into custom images.
This commit is contained in:
parent
7259deea3a
commit
067366ed6a
7 changed files with 144 additions and 9 deletions
|
|
@ -39,11 +39,16 @@ var uploadCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
imageID, err := uploader.Upload(objectName, bucketName, bucketNamespace, file, compartment, fileName)
|
err = uploader.Upload(objectName, bucketName, bucketNamespace, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to upload the image: %v", err)
|
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)
|
fmt.Printf("Image %s was uploaded and created successfully\n", imageID)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -868,21 +868,73 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error {
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
i, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
|
i, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
|
||||||
imageID, err := ociClient.Upload(
|
err = ociClient.Upload(
|
||||||
fmt.Sprintf("osbuild-upload-%d", i),
|
fmt.Sprintf("osbuild-upload-%d", i),
|
||||||
targetOptions.Bucket,
|
targetOptions.Bucket,
|
||||||
targetOptions.Namespace,
|
targetOptions.Namespace,
|
||||||
file,
|
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,
|
targetOptions.Compartment,
|
||||||
jobTarget.ImageName,
|
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 {
|
if err != nil {
|
||||||
targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorInvalidConfig, err.Error(), nil)
|
targetResult.TargetError = clienterrors.WorkerClientError(clienterrors.ErrorInvalidConfig, err.Error(), nil)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
logWithId.Info("[OCI] 🎉 Image uploaded and registered!")
|
logWithId.Info("[OCI] 🔑 Logged in OCI")
|
||||||
targetResult.Options = &target.OCITargetResultOptions{ImageID: imageID}
|
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:
|
case *target.ContainerTargetOptions:
|
||||||
targetResult = target.NewContainerTargetResult(nil)
|
targetResult = target.NewContainerTargetResult(nil)
|
||||||
destination := jobTarget.ImageName
|
destination := jobTarget.ImageName
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,32 @@ func (OCITargetResultOptions) isTargetResultOptions() {}
|
||||||
func NewOCITargetResult(options *OCITargetResultOptions) *TargetResult {
|
func NewOCITargetResult(options *OCITargetResultOptions) *TargetResult {
|
||||||
return newTargetResult(TargetNameOCI, options)
|
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)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,8 @@ func (target *Target) UnmarshalJSON(data []byte) error {
|
||||||
options = new(VMWareTargetOptions)
|
options = new(VMWareTargetOptions)
|
||||||
case TargetNameOCI:
|
case TargetNameOCI:
|
||||||
options = new(OCITargetOptions)
|
options = new(OCITargetOptions)
|
||||||
|
case TargetNameOCIObjectStorage:
|
||||||
|
options = new(OCIObjectStorageTargetOptions)
|
||||||
case TargetNameContainer:
|
case TargetNameContainer:
|
||||||
options = new(ContainerTargetOptions)
|
options = new(ContainerTargetOptions)
|
||||||
case TargetNameWorkerServer:
|
case TargetNameWorkerServer:
|
||||||
|
|
@ -246,6 +248,18 @@ func (target Target) MarshalJSON() ([]byte, error) {
|
||||||
}
|
}
|
||||||
rawOptions, err = json.Marshal(compat)
|
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:
|
case *ContainerTargetOptions:
|
||||||
type compatOptionsType struct {
|
type compatOptionsType struct {
|
||||||
*ContainerTargetOptions
|
*ContainerTargetOptions
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,8 @@ func UnmarshalTargetResultOptions(trName TargetName, rawOptions json.RawMessage)
|
||||||
options = new(KojiTargetResultOptions)
|
options = new(KojiTargetResultOptions)
|
||||||
case TargetNameOCI:
|
case TargetNameOCI:
|
||||||
options = new(OCITargetResultOptions)
|
options = new(OCITargetResultOptions)
|
||||||
|
case TargetNameOCIObjectStorage:
|
||||||
|
options = new(OCIObjectStorageTargetResultOptions)
|
||||||
case TargetNameContainer:
|
case TargetNameContainer:
|
||||||
options = new(ContainerTargetResultOptions)
|
options = new(ContainerTargetResultOptions)
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Uploader interface {
|
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 {
|
type ImageCreator interface {
|
||||||
|
|
@ -31,17 +33,19 @@ type Client struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload uploads a file into an objectName under the bucketName in the namespace.
|
// 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)
|
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
|
// clean up the object even if we fail
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := c.deleteObjectFromBucket(objectName, bucketName, namespace); err != nil {
|
if err := c.deleteObjectFromBucket(objectName, bucketName, namespace); err != nil {
|
||||||
log.Printf("failed to clean up the object '%s' from bucket '%s'", objectName, bucketName)
|
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)
|
imageID, err := c.createImage(objectName, bucketName, namespace, compartmentID, imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -54,6 +58,32 @@ func (c Client) Upload(objectName string, bucketName string, namespace string, f
|
||||||
return imageID, nil
|
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 {
|
func (c Client) uploadToBucket(objectName string, bucketName string, namespace string, file *os.File) error {
|
||||||
req := transfer.UploadFileRequest{
|
req := transfer.UploadFileRequest{
|
||||||
UploadRequest: transfer.UploadRequest{
|
UploadRequest: transfer.UploadRequest{
|
||||||
|
|
@ -216,6 +246,7 @@ type ClientParams struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ociClient struct {
|
type ociClient struct {
|
||||||
|
region string
|
||||||
storageClient objectstorage.ObjectStorageClient
|
storageClient objectstorage.ObjectStorageClient
|
||||||
identityClient identity.IdentityClient
|
identityClient identity.IdentityClient
|
||||||
computeClient core.ComputeClient
|
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{}, fmt.Errorf("failed to create an Oracle workrequests client: %w", err)
|
||||||
}
|
}
|
||||||
return Client{ociClient: ociClient{
|
return Client{ociClient: ociClient{
|
||||||
|
region: clientParams.Region,
|
||||||
storageClient: storageClient,
|
storageClient: storageClient,
|
||||||
identityClient: identityClient,
|
identityClient: identityClient,
|
||||||
computeClient: computeClient,
|
computeClient: computeClient,
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ const (
|
||||||
ErrorOSTreeDependency ClientErrorCode = 35
|
ErrorOSTreeDependency ClientErrorCode = 35
|
||||||
ErrorRemoteFileResolution ClientErrorCode = 36
|
ErrorRemoteFileResolution ClientErrorCode = 36
|
||||||
ErrorJobPanicked ClientErrorCode = 37
|
ErrorJobPanicked ClientErrorCode = 37
|
||||||
|
ErrorGeneratingSignedURL ClientErrorCode = 38
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClientErrorCode int
|
type ClientErrorCode int
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue