Skip to content

Commit

Permalink
cloud: add new Uploader interface and implement for AWS
Browse files Browse the repository at this point in the history
This commit adds a new `cloud.Uploader` interface that combines
the upload and register into a single operation. The rational
is that with that we avoid leaking resource if e.g. the upload
works but the registration fails.
  • Loading branch information
mvo5 committed Jan 30, 2025
1 parent 689348b commit 97f0780
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 0 deletions.
116 changes: 116 additions & 0 deletions pkg/cloud/awscloud/uploader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package awscloud

import (
"errors"
"fmt"
"io"
"slices"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/google/uuid"

"github.com/osbuild/images/pkg/arch"
"github.com/osbuild/images/pkg/cloud"
)

type awsUploader struct {
client *AWS

region string
bucketName string
imageName string
targetArch string
}

type UploaderOptions struct {
TargetArch string
}

func NewUploader(region, bucketName, imageName string, opts *UploaderOptions) (cloud.Uploader, error) {
if opts == nil {
opts = &UploaderOptions{}
}

client, err := NewDefault(region)
if err != nil {
return nil, err
}

return &awsUploader{
client: client,
region: region,
bucketName: bucketName,
imageName: imageName,
targetArch: opts.TargetArch,
}, nil
}

var _ cloud.Uploader = &awsUploader{}

func (au *awsUploader) Check(status io.Writer) error {
regions, err := au.client.Regions()
if err != nil {
return fmt.Errorf("retrieving AWS regions for '%s' failed: %w", au.region, err)
}

if !slices.Contains(regions, au.region) {
return fmt.Errorf("given AWS region '%s' not found", au.region)
}

fmt.Fprintf(status, "Checking AWS bucket...\n")
buckets, err := au.client.Buckets()
if err != nil {
return fmt.Errorf("retrieving AWS list of buckets failed: %w", err)
}
if !slices.Contains(buckets, au.bucketName) {
return fmt.Errorf("bucket '%s' not found in the given AWS account", au.bucketName)
}

fmt.Fprintf(status, "Checking AWS bucket permissions...\n")
writePermission, err := au.client.CheckBucketPermission(au.bucketName, S3PermissionWrite)
if err != nil {
return err
}
if !writePermission {
return fmt.Errorf("you don't have write permissions to bucket '%s' with the given AWS account", au.bucketName)
}
fmt.Fprintf(status, "Upload conditions met.\n")
return nil
}

func (au *awsUploader) UploadAndRegister(r io.Reader, status io.Writer) (err error) {
keyName := fmt.Sprintf("%s-%s", uuid.New().String(), au.imageName)
fmt.Fprintf(status, "Uploading %s to %s:%s\n", au.imageName, au.bucketName, keyName)

res, err := au.client.UploadFromReader(r, au.bucketName, keyName)
if err != nil {
return err
}
defer func() {
if err != nil {
fmt.Fprintf(status, "Deleting S3 object %s:%s\n", au.bucketName, keyName)
_, aErr := au.client.s3.DeleteObject(&s3.DeleteObjectInput{
Bucket: aws.String(au.bucketName),
Key: aws.String(keyName),
})
err = errors.Join(err, aErr)
}
}()
fmt.Fprintf(status, "File uploaded to %s\n", aws.StringValue(&res.Location))
if au.targetArch == "" {
au.targetArch = arch.Current().String()
}
bootMode := ec2.BootModeValuesUefiPreferred

fmt.Fprintf(status, "Registering AMI %s\n", au.imageName)
ami, snapshot, err := au.client.Register(au.imageName, au.bucketName, keyName, nil, au.targetArch, &bootMode, nil)
fmt.Fprintf(status, "Deleted S3 object %s:%s\n", au.bucketName, keyName)
fmt.Fprintf(status, "AMI registered: %s\nSnapshot ID: %s\n", aws.StringValue(ami), aws.StringValue(snapshot))
if err != nil {
return err
}

return nil
}
24 changes: 24 additions & 0 deletions pkg/cloud/uploader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cloud

import (
"io"
)

// Uploader is an interface that is returned from the actual
// cloud implementation. The uploader will be parameterized
// by the actual cloud implemntation, e.g.
//
// awscloud.NewUploader(region, bucket, image) Uploader
//
// which is outside the scope of this interface.
type Uploader interface {
// Check can be called before the actual upload to ensure
// all permissions are correct
Check(status io.Writer) error

// UploadAndRegister will upload the given image from
// the reader and write status message to the given
// status writer.
// To implement progress a proxy reader can be used.
UploadAndRegister(f io.Reader, status io.Writer) error
}

0 comments on commit 97f0780

Please sign in to comment.