-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cloud: add new Uploader interface and implement for AWS
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
Showing
2 changed files
with
140 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |