diff --git a/cmd/osbuild-upload-aws/main.go b/cmd/osbuild-upload-aws/main.go index 7bc132b75..2490091ce 100644 --- a/cmd/osbuild-upload-aws/main.go +++ b/cmd/osbuild-upload-aws/main.go @@ -16,6 +16,7 @@ func main() { var keyName string var filename string var imageName string + var shareWith string flag.StringVar(&accessKeyID, "access-key-id", "", "access key ID") flag.StringVar(&secretAccessKey, "secret-access-key", "", "secret access key") flag.StringVar(®ion, "region", "", "target region") @@ -23,6 +24,7 @@ func main() { flag.StringVar(&keyName, "key", "", "target S3 key name") flag.StringVar(&filename, "image", "", "image file to upload") flag.StringVar(&imageName, "name", "", "AMI name") + flag.StringVar(&shareWith, "account-id", "", "account id to share image with") flag.Parse() a, err := awsupload.New(region, accessKeyID, secretAccessKey) @@ -39,7 +41,11 @@ func main() { fmt.Printf("file uploaded to %s\n", aws.StringValue(&uploadOutput.Location)) - ami, err := a.Register(imageName, bucketName, keyName) + var share []string + if shareWith != "" { + share = append(share, shareWith) + } + ami, err := a.Register(imageName, bucketName, keyName, share) if err != nil { println(err.Error()) return diff --git a/cmd/osbuild-worker/jobimpl-osbuild.go b/cmd/osbuild-worker/jobimpl-osbuild.go index c2b85746f..ba0ed8994 100644 --- a/cmd/osbuild-worker/jobimpl-osbuild.go +++ b/cmd/osbuild-worker/jobimpl-osbuild.go @@ -160,7 +160,7 @@ func (impl *OSBuildJobImpl) Run(job worker.Job) error { } /* TODO: communicate back the AMI */ - _, err = a.Register(t.ImageName, options.Bucket, key) + _, err = a.Register(t.ImageName, options.Bucket, key, options.ShareWithAccounts) if err != nil { r = append(r, err) continue diff --git a/internal/boot/aws.go b/internal/boot/aws.go index d161bd477..9153cd13d 100644 --- a/internal/boot/aws.go +++ b/internal/boot/aws.go @@ -97,7 +97,7 @@ func UploadImageToAWS(c *awsCredentials, imagePath string, imageName string) err if err != nil { return fmt.Errorf("cannot upload the image: %#v", err) } - _, err = uploader.Register(imageName, c.Bucket, imageName) + _, err = uploader.Register(imageName, c.Bucket, imageName, nil) if err != nil { return fmt.Errorf("cannot register the image: %#v", err) } diff --git a/internal/cloudapi/openapi.gen.go b/internal/cloudapi/openapi.gen.go index 47006c743..e27256c83 100644 --- a/internal/cloudapi/openapi.gen.go +++ b/internal/cloudapi/openapi.gen.go @@ -26,9 +26,10 @@ type AWSUploadRequestOptions struct { // AWSUploadRequestOptionsEc2 defines model for AWSUploadRequestOptionsEc2. type AWSUploadRequestOptionsEc2 struct { - AccessKeyId string `json:"access_key_id"` - SecretAccessKey string `json:"secret_access_key"` - SnapshotName *string `json:"snapshot_name,omitempty"` + AccessKeyId string `json:"access_key_id"` + SecretAccessKey string `json:"secret_access_key"` + ShareWithAccounts *[]string `json:"share_with_accounts,omitempty"` + SnapshotName *string `json:"snapshot_name,omitempty"` } // AWSUploadRequestOptionsS3 defines model for AWSUploadRequestOptionsS3. diff --git a/internal/cloudapi/openapi.yml b/internal/cloudapi/openapi.yml index 00bc2dd32..ba6840ba6 100644 --- a/internal/cloudapi/openapi.yml +++ b/internal/cloudapi/openapi.yml @@ -210,6 +210,11 @@ components: snapshot_name: type: string example: 'my-snapshot' + share_with_accounts: + type: array + example: ['123456789012'] + items: + type: string Customizations: type: object properties: diff --git a/internal/cloudapi/server.go b/internal/cloudapi/server.go index c0286533b..aa115b3ed 100644 --- a/internal/cloudapi/server.go +++ b/internal/cloudapi/server.go @@ -167,14 +167,19 @@ func (server *Server) Compose(w http.ResponseWriter, r *http.Request) { return } + var share []string + if awsUploadOptions.Ec2.ShareWithAccounts != nil { + share = *awsUploadOptions.Ec2.ShareWithAccounts + } key := fmt.Sprintf("composer-api-%s", uuid.New().String()) t := target.NewAWSTarget(&target.AWSTargetOptions{ - Filename: imageType.Filename(), - Region: awsUploadOptions.Region, - AccessKeyID: awsUploadOptions.S3.AccessKeyId, - SecretAccessKey: awsUploadOptions.S3.SecretAccessKey, - Bucket: awsUploadOptions.S3.Bucket, - Key: key, + Filename: imageType.Filename(), + Region: awsUploadOptions.Region, + AccessKeyID: awsUploadOptions.S3.AccessKeyId, + SecretAccessKey: awsUploadOptions.S3.SecretAccessKey, + Bucket: awsUploadOptions.S3.Bucket, + Key: key, + ShareWithAccounts: share, }) if awsUploadOptions.Ec2.SnapshotName != nil { t.ImageName = *awsUploadOptions.Ec2.SnapshotName diff --git a/internal/target/aws_target.go b/internal/target/aws_target.go index 3ff41f461..6e1849255 100644 --- a/internal/target/aws_target.go +++ b/internal/target/aws_target.go @@ -1,12 +1,13 @@ package target type AWSTargetOptions struct { - Filename string `json:"filename"` - Region string `json:"region"` - AccessKeyID string `json:"accessKeyID"` - SecretAccessKey string `json:"secretAccessKey"` - Bucket string `json:"bucket"` - Key string `json:"key"` + Filename string `json:"filename"` + Region string `json:"region"` + AccessKeyID string `json:"accessKeyID"` + SecretAccessKey string `json:"secretAccessKey"` + Bucket string `json:"bucket"` + Key string `json:"key"` + ShareWithAccounts []string `json:"shareWithAccounts"` } func (AWSTargetOptions) isTargetOptions() {} diff --git a/internal/upload/awsupload/awsupload.go b/internal/upload/awsupload/awsupload.go index 086d65687..583763e87 100644 --- a/internal/upload/awsupload/awsupload.go +++ b/internal/upload/awsupload/awsupload.go @@ -17,7 +17,7 @@ import ( type AWS struct { uploader *s3manager.Uploader - importer *ec2.EC2 + ec2 *ec2.EC2 s3 *s3.S3 } @@ -36,7 +36,7 @@ func New(region, accessKeyID, accessKey string) (*AWS, error) { return &AWS{ uploader: s3manager.NewUploader(sess), - importer: ec2.New(sess), + ec2: ec2.New(sess), s3: s3.New(sess), }, nil } @@ -117,10 +117,10 @@ func WaitUntilImportSnapshotTaskCompletedWithContext(c *ec2.EC2, ctx aws.Context // Register is a function that imports a snapshot, waits for the snapshot to // fully import, tags the snapshot, cleans up the image in S3, and registers // an AMI in AWS. -func (a *AWS) Register(name, bucket, key string) (*string, error) { +func (a *AWS) Register(name, bucket, key string, shareWith []string) (*string, error) { log.Printf("[AWS] 📥 Importing snapshot from image: %s/%s", bucket, key) snapshotDescription := fmt.Sprintf("Image Builder AWS Import of %s", name) - importTaskOutput, err := a.importer.ImportSnapshot( + importTaskOutput, err := a.ec2.ImportSnapshot( &ec2.ImportSnapshotInput{ Description: aws.String(snapshotDescription), DiskContainer: &ec2.SnapshotDiskContainer{ @@ -132,12 +132,13 @@ func (a *AWS) Register(name, bucket, key string) (*string, error) { }, ) if err != nil { + log.Printf("[AWS] error importing snapshot: %s", err) return nil, err } log.Printf("[AWS] 🚚 Waiting for snapshot to finish importing: %s", *importTaskOutput.ImportTaskId) err = WaitUntilImportSnapshotTaskCompleted( - a.importer, + a.ec2, &ec2.DescribeImportSnapshotTasksInput{ ImportTaskIds: []*string{ importTaskOutput.ImportTaskId, @@ -158,7 +159,7 @@ func (a *AWS) Register(name, bucket, key string) (*string, error) { return nil, err } - importOutput, err := a.importer.DescribeImportSnapshotTasks( + importOutput, err := a.ec2.DescribeImportSnapshotTasks( &ec2.DescribeImportSnapshotTasksInput{ ImportTaskIds: []*string{ importTaskOutput.ImportTaskId, @@ -171,8 +172,28 @@ func (a *AWS) Register(name, bucket, key string) (*string, error) { snapshotID := importOutput.ImportSnapshotTasks[0].SnapshotTaskDetail.SnapshotId + if len(shareWith) > 0 { + log.Printf("[AWS] 🎥 Sharing ec2 snapshot") + var userIds []*string + for _, v := range shareWith { + userIds = append(userIds, &v) + } + _, err := a.ec2.ModifySnapshotAttribute( + &ec2.ModifySnapshotAttributeInput{ + Attribute: aws.String("createVolumePermission"), + OperationType: aws.String("add"), + SnapshotId: snapshotID, + UserIds: userIds, + }, + ) + if err != nil { + return nil, err + } + log.Println("[AWS] 📨 Shared ec2 snapshot") + } + // Tag the snapshot with the image name. - req, _ := a.importer.CreateTagsRequest( + req, _ := a.ec2.CreateTagsRequest( &ec2.CreateTagsInput{ Resources: []*string{snapshotID}, Tags: []*ec2.Tag{ @@ -189,7 +210,7 @@ func (a *AWS) Register(name, bucket, key string) (*string, error) { } log.Printf("[AWS] 📋 Registering AMI from imported snapshot: %s", *snapshotID) - registerOutput, err := a.importer.RegisterImage( + registerOutput, err := a.ec2.RegisterImage( &ec2.RegisterImageInput{ Architecture: aws.String("x86_64"), VirtualizationType: aws.String("hvm"), @@ -211,5 +232,28 @@ func (a *AWS) Register(name, bucket, key string) (*string, error) { } log.Printf("[AWS] 🎉 AMI registered: %s", *registerOutput.ImageId) + + if len(shareWith) > 0 { + log.Println("[AWS] 💿 Sharing ec2 AMI") + var launchPerms []*ec2.LaunchPermission + for _, id := range shareWith { + launchPerms = append(launchPerms, &ec2.LaunchPermission{ + UserId: &id, + }) + } + _, err := a.ec2.ModifyImageAttribute( + &ec2.ModifyImageAttributeInput{ + ImageId: registerOutput.ImageId, + LaunchPermission: &ec2.LaunchPermissionModifications{ + Add: launchPerms, + }, + }, + ) + if err != nil { + return nil, err + } + log.Println("[AWS] 💿 Shared AMI") + } + return registerOutput.ImageId, nil } diff --git a/schutzbot/Jenkinsfile b/schutzbot/Jenkinsfile index 41b3902bb..61e44e4f2 100644 --- a/schutzbot/Jenkinsfile +++ b/schutzbot/Jenkinsfile @@ -143,6 +143,7 @@ pipeline { TEST_TYPE = "integration" AWS_CREDS = credentials('aws-credentials-osbuildci') AWS_IMAGE_TEST_CREDS = credentials('aws-credentials-osbuild-image-test') + AWS_API_TEST_SHARE_ACCOUNT = credentials('aws-credentials-share-account') } steps { run_tests('integration', 'bios') @@ -213,6 +214,7 @@ pipeline { TEST_TYPE = "integration" AWS_CREDS = credentials('aws-credentials-osbuildci') AWS_IMAGE_TEST_CREDS = credentials('aws-credentials-osbuild-image-test') + AWS_API_TEST_SHARE_ACCOUNT = credentials('aws-credentials-share-account') } steps { run_tests('integration', 'bios') @@ -281,6 +283,7 @@ pipeline { AWS_CREDS = credentials('aws-credentials-osbuildci') AWS_IMAGE_TEST_CREDS = credentials('aws-credentials-osbuild-image-test') RHN_REGISTRATION_SCRIPT = credentials('rhn-register-script-production') + AWS_API_TEST_SHARE_ACCOUNT = credentials('aws-credentials-share-account') } steps { run_tests('integration', 'bios') diff --git a/test/cases/api.sh b/test/cases/api.sh index 1f100e5c6..f5596b9f5 100644 --- a/test/cases/api.sh +++ b/test/cases/api.sh @@ -26,7 +26,7 @@ set -euxo pipefail # it needs variables is set to access AWS. # -printenv AWS_REGION AWS_BUCKET AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY > /dev/null +printenv AWS_REGION AWS_BUCKET AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_API_TEST_SHARE_ACCOUNT > /dev/null # @@ -103,7 +103,8 @@ cat > "$REQUEST_FILE" << EOF "ec2": { "access_key_id": "${AWS_ACCESS_KEY_ID}", "secret_access_key": "${AWS_SECRET_ACCESS_KEY}", - "snapshot_name": "${SNAPSHOT_NAME}" + "snapshot_name": "${SNAPSHOT_NAME}", + "share_with_accounts": ["${AWS_API_TEST_SHARE_ACCOUNT}"] } } } @@ -166,6 +167,30 @@ $AWS_CMD ec2 describe-images \ AMI_IMAGE_ID=$(jq -r '.Images[].ImageId' "$WORKDIR/ami.json") SNAPSHOT_ID=$(jq -r '.Images[].BlockDeviceMappings[].Ebs.SnapshotId' "$WORKDIR/ami.json") +SHARE_OK=1 + +# Verify that the ec2 snapshot was shared +$AWS_CMD ec2 describe-snapshot-attribute --snapshot-id "$SNAPSHOT_ID" --attribute createVolumePermission > "$WORKDIR/snapshot-attributes.json" + +SHARED_ID=$(jq -r '.CreateVolumePermissions[0].UserId' "$WORKDIR/snapshot-attributes.json") +if [ "$AWS_API_TEST_SHARE_ACCOUNT" != "$SHARED_ID" ]; then + SHARE_OK=0 +fi + +# Verify that the ec2 ami was shared +$AWS_CMD ec2 describe-image-attribute --image-id "$AMI_IMAGE_ID" --attribute launchPermission > "$WORKDIR/ami-attributes.json" + +SHARED_ID=$(jq -r '.LaunchPermissions[0].UserId' "$WORKDIR/ami-attributes.json") +if [ "$AWS_API_TEST_SHARE_ACCOUNT" != "$SHARED_ID" ]; then + SHARE_OK=0 +fi $AWS_CMD ec2 deregister-image --image-id "$AMI_IMAGE_ID" $AWS_CMD ec2 delete-snapshot --snapshot-id "$SNAPSHOT_ID" + +if [ "$SHARE_OK" != 1 ]; then + echo "EC2 snapshot wasn't shared with the AWS_API_TEST_SHARE_ACCOUNT. 😢" + exit 1 +fi + +exit 0