image: replace "upload {aws|azure|gcp}" with uplosi

This commit is contained in:
Malte Poll 2024-01-04 16:59:31 +01:00
parent fb392c2d50
commit b7bab7c3c8
22 changed files with 252 additions and 2478 deletions

View file

@ -8,6 +8,5 @@ go_library(
deps = [
"//internal/api/versionsapi",
"//internal/cloud/cloudprovider",
"//internal/osimage/secureboot",
],
)

View file

@ -1,21 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "aws",
srcs = ["awsupload.go"],
importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/aws",
visibility = ["//:__subpackages__"],
deps = [
"//internal/api/versionsapi",
"//internal/logger",
"//internal/osimage",
"//internal/osimage/secureboot",
"@com_github_aws_aws_sdk_go_v2_config//:config",
"@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager",
"@com_github_aws_aws_sdk_go_v2_service_ec2//:ec2",
"@com_github_aws_aws_sdk_go_v2_service_ec2//types",
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
"@com_github_aws_aws_sdk_go_v2_service_s3//types",
"@com_github_aws_smithy_go//:smithy-go",
],
)

View file

@ -1,603 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// package aws implements uploading os images to aws.
package aws
import (
"context"
"errors"
"fmt"
"io"
"time"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/ec2"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go"
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/osimage"
"github.com/edgelesssys/constellation/v2/internal/osimage/secureboot"
)
// Uploader can upload and remove os images on GCP.
type Uploader struct {
region string
bucketName string
ec2 func(ctx context.Context, region string) (ec2API, error)
s3 func(ctx context.Context, region string) (s3API, error)
s3uploader func(ctx context.Context, region string) (s3UploaderAPI, error)
log *logger.Logger
}
// New creates a new Uploader.
func New(region, bucketName string, log *logger.Logger) (*Uploader, error) {
return &Uploader{
region: region,
bucketName: bucketName,
ec2: func(ctx context.Context, region string) (ec2API, error) {
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
if err != nil {
return nil, err
}
return ec2.NewFromConfig(cfg), nil
},
s3: func(ctx context.Context, region string) (s3API, error) {
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
if err != nil {
return nil, err
}
return s3.NewFromConfig(cfg), nil
},
s3uploader: func(ctx context.Context, region string) (s3UploaderAPI, error) {
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
if err != nil {
return nil, err
}
return s3manager.NewUploader(s3.NewFromConfig(cfg)), nil
},
log: log,
}, nil
}
// Upload uploads an OS image to AWS.
func (u *Uploader) Upload(ctx context.Context, req *osimage.UploadRequest) ([]versionsapi.ImageInfoEntry, error) {
blobName := fmt.Sprintf("image-%s-%s-%d.raw", req.Version.Stream(), req.Version.Version(), req.Timestamp.Unix())
imageName := imageName(req.Version, req.AttestationVariant, req.Timestamp)
allRegions := []string{u.region}
allRegions = append(allRegions, replicationRegions...)
// TODO(malt3): make this configurable
publish := true
amiIDs := make(map[string]string, len(allRegions))
if err := u.ensureBucket(ctx); err != nil {
return nil, fmt.Errorf("ensuring bucket %s exists: %w", u.bucketName, err)
}
// pre-cleaning
for _, region := range allRegions {
if err := u.ensureImageDeleted(ctx, imageName, region); err != nil {
return nil, fmt.Errorf("pre-cleaning: ensuring no image under the name %s in region %s: %w", imageName, region, err)
}
}
if err := u.ensureSnapshotDeleted(ctx, imageName, u.region); err != nil {
return nil, fmt.Errorf("pre-cleaning: ensuring no snapshot using the same name exists: %w", err)
}
if err := u.ensureBlobDeleted(ctx, blobName); err != nil {
return nil, fmt.Errorf("pre-cleaning: ensuring no blob using the same name exists: %w", err)
}
// create primary image
if err := u.uploadBlob(ctx, blobName, req.Image); err != nil {
return nil, fmt.Errorf("uploading image to s3: %w", err)
}
defer func() {
if err := u.ensureBlobDeleted(ctx, blobName); err != nil {
u.log.Errorf("post-cleaning: deleting temporary blob from s3", err)
}
}()
snapshotID, err := u.importSnapshot(ctx, blobName, imageName)
if err != nil {
return nil, fmt.Errorf("importing snapshot: %w", err)
}
primaryAMIID, err := u.createImageFromSnapshot(ctx, req.Version, imageName, snapshotID, req.SecureBoot, req.UEFIVarStore)
if err != nil {
return nil, fmt.Errorf("creating image from snapshot: %w", err)
}
amiIDs[u.region] = primaryAMIID
if err := u.waitForImage(ctx, primaryAMIID, u.region); err != nil {
return nil, fmt.Errorf("waiting for primary image to become available: %w", err)
}
// replicate image
for _, region := range replicationRegions {
amiID, err := u.replicateImage(ctx, imageName, primaryAMIID, region)
if err != nil {
return nil, fmt.Errorf("replicating image to region %s: %w", region, err)
}
amiIDs[region] = amiID
}
// wait for replication, tag, publish
var imageInfo []versionsapi.ImageInfoEntry
for _, region := range allRegions {
if err := u.waitForImage(ctx, amiIDs[region], region); err != nil {
return nil, fmt.Errorf("waiting for image to become available in region %s: %w", region, err)
}
if err := u.tagImageAndSnapshot(ctx, imageName, amiIDs[region], region); err != nil {
return nil, fmt.Errorf("tagging image in region %s: %w", region, err)
}
if !publish {
continue
}
if err := u.publishImage(ctx, amiIDs[region], region); err != nil {
return nil, fmt.Errorf("publishing image in region %s: %w", region, err)
}
imageInfo = append(imageInfo, versionsapi.ImageInfoEntry{
CSP: "aws",
AttestationVariant: req.AttestationVariant,
Reference: amiIDs[region],
Region: region,
})
}
return imageInfo, nil
}
func (u *Uploader) ensureBucket(ctx context.Context) error {
s3C, err := u.s3(ctx, u.region)
if err != nil {
return fmt.Errorf("determining if bucket %s exists: %w", u.bucketName, err)
}
_, err = s3C.HeadBucket(ctx, &s3.HeadBucketInput{
Bucket: &u.bucketName,
})
if err == nil {
u.log.Debugf("Bucket %s exists", u.bucketName)
return nil
}
var noSuchBucketErr *types.NoSuchBucket
if !errors.As(err, &noSuchBucketErr) {
return fmt.Errorf("determining if bucket %s exists: %w", u.bucketName, err)
}
u.log.Debugf("Creating bucket %s", u.bucketName)
_, err = s3C.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: &u.bucketName,
})
if err != nil {
return fmt.Errorf("creating bucket %s: %w", u.bucketName, err)
}
return nil
}
func (u *Uploader) uploadBlob(ctx context.Context, blobName string, img io.Reader) error {
u.log.Debugf("Uploading os image as %s", blobName)
uploadC, err := u.s3uploader(ctx, u.region)
if err != nil {
return err
}
_, err = uploadC.Upload(ctx, &s3.PutObjectInput{
Bucket: &u.bucketName,
Key: &blobName,
Body: img,
ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256,
})
return err
}
func (u *Uploader) ensureBlobDeleted(ctx context.Context, blobName string) error {
s3C, err := u.s3(ctx, u.region)
if err != nil {
return err
}
_, err = s3C.HeadObject(ctx, &s3.HeadObjectInput{
Bucket: &u.bucketName,
Key: &blobName,
})
var apiError smithy.APIError
if errors.As(err, &apiError) && apiError.ErrorCode() == "NotFound" {
u.log.Debugf("Blob %s in %s doesn't exist. Nothing to clean up.", blobName, u.bucketName)
return nil
}
if err != nil {
return err
}
u.log.Debugf("Deleting blob %s", blobName)
_, err = s3C.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: &u.bucketName,
Key: &blobName,
})
return err
}
func (u *Uploader) findSnapshots(ctx context.Context, snapshotName, region string) ([]string, error) {
ec2C, err := u.ec2(ctx, region)
if err != nil {
return nil, fmt.Errorf("creating ec2 client: %w", err)
}
snapshots, err := ec2C.DescribeSnapshots(ctx, &ec2.DescribeSnapshotsInput{
Filters: []ec2types.Filter{
{
Name: toPtr("tag:Name"),
Values: []string{snapshotName},
},
},
})
if err != nil {
return nil, fmt.Errorf("describing snapshots: %w", err)
}
var snapshotIDs []string
for _, s := range snapshots.Snapshots {
if s.SnapshotId == nil {
continue
}
snapshotIDs = append(snapshotIDs, *s.SnapshotId)
}
return snapshotIDs, nil
}
func (u *Uploader) importSnapshot(ctx context.Context, blobName, snapshotName string) (string, error) {
u.log.Debugf("Importing %s as snapshot %s", blobName, snapshotName)
ec2C, err := u.ec2(ctx, u.region)
if err != nil {
return "", fmt.Errorf("creating ec2 client: %w", err)
}
importResp, err := ec2C.ImportSnapshot(ctx, &ec2.ImportSnapshotInput{
ClientData: &ec2types.ClientData{
Comment: &snapshotName,
},
Description: &snapshotName,
DiskContainer: &ec2types.SnapshotDiskContainer{
Description: &snapshotName,
Format: toPtr(string(ec2types.DiskImageFormatRaw)),
UserBucket: &ec2types.UserBucket{
S3Bucket: &u.bucketName,
S3Key: &blobName,
},
},
})
if err != nil {
return "", fmt.Errorf("importing snapshot: %w", err)
}
if importResp.ImportTaskId == nil {
return "", fmt.Errorf("importing snapshot: no import task ID returned")
}
u.log.Debugf("Waiting for snapshot %s to be ready", snapshotName)
return waitForSnapshotImport(ctx, ec2C, *importResp.ImportTaskId)
}
func (u *Uploader) ensureSnapshotDeleted(ctx context.Context, snapshotName, region string) error {
ec2C, err := u.ec2(ctx, region)
if err != nil {
return fmt.Errorf("creating ec2 client: %w", err)
}
snapshots, err := u.findSnapshots(ctx, snapshotName, region)
if err != nil {
return fmt.Errorf("finding snapshots: %w", err)
}
for _, snapshot := range snapshots {
u.log.Debugf("Deleting snapshot %s in %s", snapshot, region)
_, err = ec2C.DeleteSnapshot(ctx, &ec2.DeleteSnapshotInput{
SnapshotId: toPtr(snapshot),
})
if err != nil {
return fmt.Errorf("deleting snapshot %s: %w", snapshot, err)
}
}
return nil
}
func (u *Uploader) createImageFromSnapshot(ctx context.Context, version versionsapi.Version, imageName, snapshotID string, enableSecureBoot bool, uefiVarStore secureboot.UEFIVarStore) (string, error) {
u.log.Debugf("Creating image %s in %s", imageName, u.region)
ec2C, err := u.ec2(ctx, u.region)
if err != nil {
return "", fmt.Errorf("creating ec2 client: %w", err)
}
var uefiData *string
if enableSecureBoot {
awsUEFIData, err := uefiVarStore.ToAWS()
if err != nil {
return "", fmt.Errorf("creating uefi data: %w", err)
}
uefiData = toPtr(awsUEFIData)
}
createReq, err := ec2C.RegisterImage(ctx, &ec2.RegisterImageInput{
Name: &imageName,
Architecture: ec2types.ArchitectureValuesX8664,
BlockDeviceMappings: []ec2types.BlockDeviceMapping{
{
DeviceName: toPtr("/dev/xvda"),
Ebs: &ec2types.EbsBlockDevice{
DeleteOnTermination: toPtr(true),
SnapshotId: &snapshotID,
},
},
},
BootMode: ec2types.BootModeValuesUefi,
Description: toPtr("Constellation " + version.ShortPath()),
EnaSupport: toPtr(true),
RootDeviceName: toPtr("/dev/xvda"),
TpmSupport: ec2types.TpmSupportValuesV20,
UefiData: uefiData,
VirtualizationType: toPtr("hvm"),
})
if err != nil {
return "", fmt.Errorf("creating image: %w", err)
}
if createReq.ImageId == nil {
return "", fmt.Errorf("creating image: no image ID returned")
}
return *createReq.ImageId, nil
}
func (u *Uploader) replicateImage(ctx context.Context, imageName, amiID string, region string) (string, error) {
u.log.Debugf("Replicating image %s to %s", imageName, region)
ec2C, err := u.ec2(ctx, region)
if err != nil {
return "", fmt.Errorf("creating ec2 client: %w", err)
}
replicateReq, err := ec2C.CopyImage(ctx, &ec2.CopyImageInput{
Name: &imageName,
SourceImageId: &amiID,
SourceRegion: &u.region,
})
if err != nil {
return "", fmt.Errorf("replicating image: %w", err)
}
if replicateReq.ImageId == nil {
return "", fmt.Errorf("replicating image: no image ID returned")
}
return *replicateReq.ImageId, nil
}
func (u *Uploader) findImage(ctx context.Context, imageName, region string) (string, error) {
ec2C, err := u.ec2(ctx, region)
if err != nil {
return "", fmt.Errorf("creating ec2 client: %w", err)
}
snapshots, err := ec2C.DescribeImages(ctx, &ec2.DescribeImagesInput{
Filters: []ec2types.Filter{
{
Name: toPtr("name"),
Values: []string{imageName},
},
},
})
if err != nil {
return "", fmt.Errorf("describing images: %w", err)
}
if len(snapshots.Images) == 0 {
return "", errAMIDoesNotExist
}
if len(snapshots.Images) != 1 {
return "", fmt.Errorf("expected 1 image, got %d", len(snapshots.Images))
}
if snapshots.Images[0].ImageId == nil {
return "", fmt.Errorf("image ID is nil")
}
return *snapshots.Images[0].ImageId, nil
}
func (u *Uploader) waitForImage(ctx context.Context, amiID, region string) error {
u.log.Debugf("Waiting for image %s in %s to be created", amiID, region)
ec2C, err := u.ec2(ctx, region)
if err != nil {
return fmt.Errorf("creating ec2 client: %w", err)
}
waiter := ec2.NewImageAvailableWaiter(ec2C)
err = waiter.Wait(ctx, &ec2.DescribeImagesInput{
ImageIds: []string{amiID},
}, maxWait)
if err != nil {
return fmt.Errorf("waiting for image: %w", err)
}
return nil
}
func (u *Uploader) tagImageAndSnapshot(ctx context.Context, imageName, amiID, region string) error {
u.log.Debugf("Tagging backing snapshot of image %s in %s", amiID, region)
ec2C, err := u.ec2(ctx, region)
if err != nil {
return fmt.Errorf("creating ec2 client: %w", err)
}
snapshotID, err := getBackingSnapshotID(ctx, ec2C, amiID)
if err != nil {
return fmt.Errorf("getting backing snapshot ID: %w", err)
}
_, err = ec2C.CreateTags(ctx, &ec2.CreateTagsInput{
Resources: []string{amiID, snapshotID},
Tags: []ec2types.Tag{
{
Key: toPtr("Name"),
Value: toPtr(imageName),
},
},
})
if err != nil {
return fmt.Errorf("tagging ami and snapshot: %w", err)
}
return nil
}
func (u *Uploader) publishImage(ctx context.Context, imageName, region string) error {
u.log.Debugf("Publishing image %s in %s", imageName, region)
ec2C, err := u.ec2(ctx, region)
if err != nil {
return fmt.Errorf("creating ec2 client: %w", err)
}
_, err = ec2C.ModifyImageAttribute(ctx, &ec2.ModifyImageAttributeInput{
ImageId: &imageName,
LaunchPermission: &ec2types.LaunchPermissionModifications{
Add: []ec2types.LaunchPermission{
{
Group: ec2types.PermissionGroupAll,
},
},
},
})
if err != nil {
return fmt.Errorf("publishing image: %w", err)
}
return nil
}
func (u *Uploader) ensureImageDeleted(ctx context.Context, imageName, region string) error {
ec2C, err := u.ec2(ctx, region)
if err != nil {
return fmt.Errorf("creating ec2 client: %w", err)
}
amiID, err := u.findImage(ctx, imageName, region)
if err == errAMIDoesNotExist {
u.log.Debugf("Image %s in %s doesn't exist. Nothing to clean up.", imageName, region)
return nil
}
snapshotID, err := getBackingSnapshotID(ctx, ec2C, amiID)
if err == errAMIDoesNotExist {
u.log.Debugf("Image %s doesn't exist. Nothing to clean up.", amiID)
return nil
}
u.log.Debugf("Deleting image %s in %s with backing snapshot", amiID, region)
_, err = ec2C.DeregisterImage(ctx, &ec2.DeregisterImageInput{
ImageId: &amiID,
})
if err != nil {
return fmt.Errorf("deleting image: %w", err)
}
_, err = ec2C.DeleteSnapshot(ctx, &ec2.DeleteSnapshotInput{
SnapshotId: &snapshotID,
})
if err != nil {
return fmt.Errorf("deleting snapshot: %w", err)
}
return nil
}
func imageName(version versionsapi.Version, attestationVariant string, timestamp time.Time) string {
if version.Stream() == "stable" {
return fmt.Sprintf("constellation-%s-%s", version.Version(), attestationVariant)
}
return fmt.Sprintf("constellation-%s-%s-%s-%s", version.Stream(), version.Version(), attestationVariant, timestamp.Format(timestampFormat))
}
func waitForSnapshotImport(ctx context.Context, ec2C ec2API, importTaskID string) (string, error) {
for {
taskResp, err := ec2C.DescribeImportSnapshotTasks(ctx, &ec2.DescribeImportSnapshotTasksInput{
ImportTaskIds: []string{importTaskID},
})
if err != nil {
return "", fmt.Errorf("describing import snapshot task: %w", err)
}
if len(taskResp.ImportSnapshotTasks) == 0 {
return "", fmt.Errorf("describing import snapshot task: no tasks returned")
}
if taskResp.ImportSnapshotTasks[0].SnapshotTaskDetail == nil {
return "", fmt.Errorf("describing import snapshot task: no snapshot task detail returned")
}
if taskResp.ImportSnapshotTasks[0].SnapshotTaskDetail.Status == nil {
return "", fmt.Errorf("describing import snapshot task: no status returned")
}
switch *taskResp.ImportSnapshotTasks[0].SnapshotTaskDetail.Status {
case string(ec2types.SnapshotStateCompleted):
return *taskResp.ImportSnapshotTasks[0].SnapshotTaskDetail.SnapshotId, nil
case string(ec2types.SnapshotStateError):
return "", fmt.Errorf("importing snapshot: task failed")
}
time.Sleep(waitInterval)
}
}
func getBackingSnapshotID(ctx context.Context, ec2C ec2API, amiID string) (string, error) {
describeResp, err := ec2C.DescribeImages(ctx, &ec2.DescribeImagesInput{
ImageIds: []string{amiID},
})
if err != nil || len(describeResp.Images) == 0 {
return "", errAMIDoesNotExist
}
if len(describeResp.Images) != 1 {
return "", fmt.Errorf("describing image: expected 1 image, got %d", len(describeResp.Images))
}
image := describeResp.Images[0]
if len(image.BlockDeviceMappings) != 1 {
return "", fmt.Errorf("found %d block device mappings for image %s, expected 1", len(image.BlockDeviceMappings), amiID)
}
if image.BlockDeviceMappings[0].Ebs == nil {
return "", fmt.Errorf("image %s does not have an EBS block device mapping", amiID)
}
ebs := image.BlockDeviceMappings[0].Ebs
if ebs.SnapshotId == nil {
return "", fmt.Errorf("image %s does not have an EBS snapshot", amiID)
}
return *ebs.SnapshotId, nil
}
type ec2API interface {
DescribeImages(ctx context.Context, params *ec2.DescribeImagesInput,
optFns ...func(*ec2.Options),
) (*ec2.DescribeImagesOutput, error)
ModifyImageAttribute(ctx context.Context, params *ec2.ModifyImageAttributeInput,
optFns ...func(*ec2.Options),
) (*ec2.ModifyImageAttributeOutput, error)
RegisterImage(ctx context.Context, params *ec2.RegisterImageInput,
optFns ...func(*ec2.Options),
) (*ec2.RegisterImageOutput, error)
CopyImage(ctx context.Context, params *ec2.CopyImageInput, optFns ...func(*ec2.Options),
) (*ec2.CopyImageOutput, error)
DeregisterImage(ctx context.Context, params *ec2.DeregisterImageInput,
optFns ...func(*ec2.Options),
) (*ec2.DeregisterImageOutput, error)
ImportSnapshot(ctx context.Context, params *ec2.ImportSnapshotInput,
optFns ...func(*ec2.Options),
) (*ec2.ImportSnapshotOutput, error)
DescribeImportSnapshotTasks(ctx context.Context, params *ec2.DescribeImportSnapshotTasksInput,
optFns ...func(*ec2.Options),
) (*ec2.DescribeImportSnapshotTasksOutput, error)
DescribeSnapshots(ctx context.Context, params *ec2.DescribeSnapshotsInput,
optFns ...func(*ec2.Options),
) (*ec2.DescribeSnapshotsOutput, error)
DeleteSnapshot(ctx context.Context, params *ec2.DeleteSnapshotInput, optFns ...func(*ec2.Options),
) (*ec2.DeleteSnapshotOutput, error)
CreateTags(ctx context.Context, params *ec2.CreateTagsInput, optFns ...func(*ec2.Options),
) (*ec2.CreateTagsOutput, error)
}
type s3API interface {
HeadBucket(ctx context.Context, params *s3.HeadBucketInput, optFns ...func(*s3.Options),
) (*s3.HeadBucketOutput, error)
CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options),
) (*s3.CreateBucketOutput, error)
HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options),
) (*s3.HeadObjectOutput, error)
DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options),
) (*s3.DeleteObjectOutput, error)
}
type s3UploaderAPI interface {
Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader),
) (*s3manager.UploadOutput, error)
}
func toPtr[T any](v T) *T {
return &v
}
const (
waitInterval = 15 * time.Second
maxWait = 30 * time.Minute
timestampFormat = "20060102150405"
)
var (
errAMIDoesNotExist = errors.New("ami does not exist")
replicationRegions = []string{"eu-west-1", "eu-west-3", "us-east-2", "ap-south-1"}
)

View file

@ -1,21 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "azure",
srcs = [
"azureupload.go",
"disktype_string.go",
],
importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/azure",
visibility = ["//:__subpackages__"],
deps = [
"//internal/api/versionsapi",
"//internal/logger",
"//internal/osimage",
"@com_github_azure_azure_sdk_for_go_sdk_azcore//runtime",
"@com_github_azure_azure_sdk_for_go_sdk_azidentity//:azidentity",
"@com_github_azure_azure_sdk_for_go_sdk_resourcemanager_compute_armcompute_v5//:armcompute",
"@com_github_azure_azure_sdk_for_go_sdk_storage_azblob//blob",
"@com_github_azure_azure_sdk_for_go_sdk_storage_azblob//pageblob",
],
)

View file

@ -1,710 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// package azure implements uploading os images to azure.
package azure
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
armcomputev5 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/pageblob"
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/osimage"
)
// Uploader can upload and remove os images on Azure.
type Uploader struct {
subscription string
location string
resourceGroup string
pollingFrequency time.Duration
disks azureDiskAPI
managedImages azureManagedImageAPI
blob sasBlobUploader
galleries azureGalleriesAPI
image azureGalleriesImageAPI
imageVersions azureGalleriesImageVersionAPI
communityVersions azureCommunityGalleryImageVersionAPI
log *logger.Logger
}
// New creates a new Uploader.
func New(subscription, location, resourceGroup string, log *logger.Logger) (*Uploader, error) {
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, err
}
diskClient, err := armcomputev5.NewDisksClient(subscription, cred, nil)
if err != nil {
return nil, err
}
managedImagesClient, err := armcomputev5.NewImagesClient(subscription, cred, nil)
if err != nil {
return nil, err
}
galleriesClient, err := armcomputev5.NewGalleriesClient(subscription, cred, nil)
if err != nil {
return nil, err
}
galleriesImageClient, err := armcomputev5.NewGalleryImagesClient(subscription, cred, nil)
if err != nil {
return nil, err
}
galleriesImageVersionClient, err := armcomputev5.NewGalleryImageVersionsClient(subscription, cred, nil)
if err != nil {
return nil, err
}
communityImageVersionClient, err := armcomputev5.NewCommunityGalleryImageVersionsClient(subscription, cred, nil)
if err != nil {
return nil, err
}
return &Uploader{
subscription: subscription,
location: location,
resourceGroup: resourceGroup,
pollingFrequency: pollingFrequency,
disks: diskClient,
managedImages: managedImagesClient,
blob: func(sasBlobURL string) (azurePageblobAPI, error) {
return pageblob.NewClientWithNoCredential(sasBlobURL, nil)
},
galleries: galleriesClient,
image: galleriesImageClient,
imageVersions: galleriesImageVersionClient,
communityVersions: communityImageVersionClient,
log: log,
}, nil
}
// Upload uploads an OS image to Azure.
func (u *Uploader) Upload(ctx context.Context, req *osimage.UploadRequest) ([]versionsapi.ImageInfoEntry, error) {
formattedTime := req.Timestamp.Format(timestampFormat)
diskName := fmt.Sprintf("constellation-%s-%s-%s", req.Version.Stream(), formattedTime, req.AttestationVariant)
var sigName string
switch req.Version.Stream() {
case "stable":
sigName = sigNameStable
case "debug":
sigName = sigNameDebug
default:
sigName = sigNameDefault
}
definitionName := imageOffer(req.Version)
versionName, err := imageVersion(req.Version, req.Timestamp)
if err != nil {
return nil, fmt.Errorf("determining image version name: %w", err)
}
// ensure new image can be uploaded by deleting existing resources using the same name
if err := u.ensureImageVersionDeleted(ctx, sigName, definitionName, versionName); err != nil {
return nil, fmt.Errorf("pre-cleaning: ensuring no image version using the same name exists: %w", err)
}
if err := u.ensureManagedImageDeleted(ctx, diskName); err != nil {
return nil, fmt.Errorf("pre-cleaning: ensuring no managed image using the same name exists: %w", err)
}
if err := u.ensureDiskDeleted(ctx, diskName); err != nil {
return nil, fmt.Errorf("pre-cleaning: ensuring no temporary disk using the same name exists: %w", err)
}
diskID, err := u.createDisk(ctx, diskName, DiskTypeNormal, req.Image, nil, req.Size)
if err != nil {
return nil, fmt.Errorf("creating disk: %w", err)
}
defer func() {
// cleanup temp disk
err := u.ensureDiskDeleted(ctx, diskName)
if err != nil {
u.log.Errorf("post-cleaning: deleting disk image: %v", err)
}
}()
managedImageID, err := u.createManagedImage(ctx, diskName, diskID)
if err != nil {
return nil, fmt.Errorf("creating managed image: %w", err)
}
if err := u.ensureSIG(ctx, sigName); err != nil {
return nil, fmt.Errorf("ensuring sig exists: %w", err)
}
if err := u.ensureImageDefinition(ctx, sigName, definitionName, req.Version, req.AttestationVariant); err != nil {
return nil, fmt.Errorf("ensuring image definition exists: %w", err)
}
unsharedImageVersionID, err := u.createImageVersion(ctx, sigName, definitionName, versionName, managedImageID)
if err != nil {
return nil, fmt.Errorf("creating image version: %w", err)
}
imageReference, err := u.getImageReference(ctx, sigName, definitionName, versionName, unsharedImageVersionID)
if err != nil {
return nil, fmt.Errorf("getting image reference: %w", err)
}
return []versionsapi.ImageInfoEntry{
{
CSP: "azure",
AttestationVariant: req.AttestationVariant,
Reference: imageReference,
},
}, nil
}
// createDisk creates and initializes (uploads contents of) an azure disk.
func (u *Uploader) createDisk(ctx context.Context, diskName string, diskType DiskType, img io.ReadSeeker, vmgs io.ReadSeeker, size int64) (string, error) {
u.log.Debugf("Creating disk %s in %s", diskName, u.resourceGroup)
if diskType == DiskTypeWithVMGS && vmgs == nil {
return "", errors.New("cannot create disk with vmgs: vmgs reader is nil")
}
var createOption armcomputev5.DiskCreateOption
var requestVMGSSAS bool
switch diskType {
case DiskTypeNormal:
createOption = armcomputev5.DiskCreateOptionUpload
case DiskTypeWithVMGS:
createOption = armcomputev5.DiskCreateOptionUploadPreparedSecure
requestVMGSSAS = true
}
disk := armcomputev5.Disk{
Location: &u.location,
Properties: &armcomputev5.DiskProperties{
CreationData: &armcomputev5.CreationData{
CreateOption: &createOption,
UploadSizeBytes: toPtr(size),
},
HyperVGeneration: toPtr(armcomputev5.HyperVGenerationV2),
OSType: toPtr(armcomputev5.OperatingSystemTypesLinux),
},
}
createPoller, err := u.disks.BeginCreateOrUpdate(ctx, u.resourceGroup, diskName, disk, &armcomputev5.DisksClientBeginCreateOrUpdateOptions{})
if err != nil {
return "", fmt.Errorf("creating disk: %w", err)
}
createdDisk, err := createPoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency})
if err != nil {
return "", fmt.Errorf("waiting for disk to be created: %w", err)
}
u.log.Debugf("Granting temporary upload permissions via SAS token")
accessGrant := armcomputev5.GrantAccessData{
Access: toPtr(armcomputev5.AccessLevelWrite),
DurationInSeconds: toPtr(int32(uploadAccessDuration)),
GetSecureVMGuestStateSAS: &requestVMGSSAS,
}
accessPoller, err := u.disks.BeginGrantAccess(ctx, u.resourceGroup, diskName, accessGrant, &armcomputev5.DisksClientBeginGrantAccessOptions{})
if err != nil {
return "", fmt.Errorf("generating disk sas token: %w", err)
}
accesPollerResp, err := accessPoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency})
if err != nil {
return "", fmt.Errorf("waiting for sas token: %w", err)
}
if requestVMGSSAS {
u.log.Debugf("Uploading vmgs")
vmgsSize, err := vmgs.Seek(0, io.SeekEnd)
if err != nil {
return "", err
}
if _, err := vmgs.Seek(0, io.SeekStart); err != nil {
return "", err
}
if accesPollerResp.SecurityDataAccessSAS == nil {
return "", errors.New("uploading vmgs: grant access returned no vmgs sas")
}
if err := uploadBlob(ctx, *accesPollerResp.SecurityDataAccessSAS, vmgs, vmgsSize, u.blob); err != nil {
return "", fmt.Errorf("uploading vmgs: %w", err)
}
}
u.log.Debugf("Uploading os image")
if accesPollerResp.AccessSAS == nil {
return "", errors.New("uploading disk: grant access returned no disk sas")
}
if err := uploadBlob(ctx, *accesPollerResp.AccessSAS, img, size, u.blob); err != nil {
return "", fmt.Errorf("uploading image: %w", err)
}
revokePoller, err := u.disks.BeginRevokeAccess(ctx, u.resourceGroup, diskName, &armcomputev5.DisksClientBeginRevokeAccessOptions{})
if err != nil {
return "", fmt.Errorf("revoking disk sas token: %w", err)
}
if _, err := revokePoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency}); err != nil {
return "", fmt.Errorf("waiting for sas token revocation: %w", err)
}
if createdDisk.ID == nil {
return "", errors.New("created disk has no id")
}
return *createdDisk.ID, nil
}
func (u *Uploader) ensureDiskDeleted(ctx context.Context, diskName string) error {
_, err := u.disks.Get(ctx, u.resourceGroup, diskName, &armcomputev5.DisksClientGetOptions{})
if err != nil {
u.log.Debugf("Disk %s in %s doesn't exist. Nothing to clean up.", diskName, u.resourceGroup)
return nil
}
u.log.Debugf("Deleting disk %s in %s", diskName, u.resourceGroup)
deletePoller, err := u.disks.BeginDelete(ctx, u.resourceGroup, diskName, &armcomputev5.DisksClientBeginDeleteOptions{})
if err != nil {
return fmt.Errorf("deleting disk: %w", err)
}
if _, err = deletePoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency}); err != nil {
return fmt.Errorf("waiting for disk to be deleted: %w", err)
}
return nil
}
func (u *Uploader) createManagedImage(ctx context.Context, imageName string, diskID string) (string, error) {
u.log.Debugf("Creating managed image %s in %s", imageName, u.resourceGroup)
image := armcomputev5.Image{
Location: &u.location,
Properties: &armcomputev5.ImageProperties{
HyperVGeneration: toPtr(armcomputev5.HyperVGenerationTypesV2),
StorageProfile: &armcomputev5.ImageStorageProfile{
OSDisk: &armcomputev5.ImageOSDisk{
OSState: toPtr(armcomputev5.OperatingSystemStateTypesGeneralized),
OSType: toPtr(armcomputev5.OperatingSystemTypesLinux),
ManagedDisk: &armcomputev5.SubResource{
ID: &diskID,
},
},
},
},
}
createPoller, err := u.managedImages.BeginCreateOrUpdate(
ctx, u.resourceGroup, imageName, image,
&armcomputev5.ImagesClientBeginCreateOrUpdateOptions{},
)
if err != nil {
return "", fmt.Errorf("creating managed image: %w", err)
}
createdImage, err := createPoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency})
if err != nil {
return "", fmt.Errorf("waiting for image to be created: %w", err)
}
if createdImage.ID == nil {
return "", errors.New("created image has no id")
}
return *createdImage.ID, nil
}
func (u *Uploader) ensureManagedImageDeleted(ctx context.Context, imageName string) error {
_, err := u.managedImages.Get(ctx, u.resourceGroup, imageName, &armcomputev5.ImagesClientGetOptions{})
if err != nil {
u.log.Debugf("Managed image %s in %s doesn't exist. Nothing to clean up.", imageName, u.resourceGroup)
return nil
}
u.log.Debugf("Deleting managed image %s in %s", imageName, u.resourceGroup)
deletePoller, err := u.managedImages.BeginDelete(ctx, u.resourceGroup, imageName, &armcomputev5.ImagesClientBeginDeleteOptions{})
if err != nil {
return fmt.Errorf("deleting image: %w", err)
}
if _, err = deletePoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency}); err != nil {
return fmt.Errorf("waiting for image to be deleted: %w", err)
}
return nil
}
// ensureSIG creates a SIG if it does not exist yet.
func (u *Uploader) ensureSIG(ctx context.Context, sigName string) error {
_, err := u.galleries.Get(ctx, u.resourceGroup, sigName, &armcomputev5.GalleriesClientGetOptions{})
if err == nil {
u.log.Debugf("Image gallery %s in %s exists", sigName, u.resourceGroup)
return nil
}
u.log.Debugf("Creating image gallery %s in %s", sigName, u.resourceGroup)
gallery := armcomputev5.Gallery{
Location: &u.location,
}
createPoller, err := u.galleries.BeginCreateOrUpdate(ctx, u.resourceGroup, sigName, gallery,
&armcomputev5.GalleriesClientBeginCreateOrUpdateOptions{},
)
if err != nil {
return fmt.Errorf("creating image gallery: %w", err)
}
if _, err = createPoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency}); err != nil {
return fmt.Errorf("waiting for image gallery to be created: %w", err)
}
return nil
}
// ensureImageDefinition creates an image definition (component of a SIG) if it does not exist yet.
func (u *Uploader) ensureImageDefinition(ctx context.Context, sigName, definitionName string, version versionsapi.Version, attestationVariant string) error {
_, err := u.image.Get(ctx, u.resourceGroup, sigName, definitionName, &armcomputev5.GalleryImagesClientGetOptions{})
if err == nil {
u.log.Debugf("Image definition %s/%s in %s exists", sigName, definitionName, u.resourceGroup)
return nil
}
u.log.Debugf("Creating image definition %s/%s in %s", sigName, definitionName, u.resourceGroup)
var securityType string
// TODO(malt3): This needs to allow the *Supported or the normal variant
// based on wether a VMGS was provided or not.
// VMGS provided: ConfidentialVM
// No VMGS provided: ConfidentialVMSupported
switch strings.ToLower(attestationVariant) {
case "azure-sev-snp":
securityType = string("ConfidentialVMSupported")
case "azure-trustedlaunch":
securityType = string(armcomputev5.SecurityTypesTrustedLaunch)
}
offer := imageOffer(version)
galleryImage := armcomputev5.GalleryImage{
Location: &u.location,
Properties: &armcomputev5.GalleryImageProperties{
Identifier: &armcomputev5.GalleryImageIdentifier{
Offer: &offer,
Publisher: toPtr(imageDefinitionPublisher),
SKU: toPtr(imageDefinitionSKU),
},
OSState: toPtr(armcomputev5.OperatingSystemStateTypesGeneralized),
OSType: toPtr(armcomputev5.OperatingSystemTypesLinux),
Architecture: toPtr(armcomputev5.ArchitectureX64),
Features: []*armcomputev5.GalleryImageFeature{
{
Name: toPtr("SecurityType"),
Value: &securityType,
},
},
HyperVGeneration: toPtr(armcomputev5.HyperVGenerationV2),
},
}
createPoller, err := u.image.BeginCreateOrUpdate(ctx, u.resourceGroup, sigName, definitionName, galleryImage,
&armcomputev5.GalleryImagesClientBeginCreateOrUpdateOptions{},
)
if err != nil {
return fmt.Errorf("creating image definition: %w", err)
}
if _, err = createPoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency}); err != nil {
return fmt.Errorf("waiting for image definition to be created: %w", err)
}
return nil
}
func (u *Uploader) createImageVersion(ctx context.Context, sigName, definitionName, versionName, imageID string) (string, error) {
u.log.Debugf("Creating image version %s/%s/%s in %s", sigName, definitionName, versionName, u.resourceGroup)
imageVersion := armcomputev5.GalleryImageVersion{
Location: &u.location,
Properties: &armcomputev5.GalleryImageVersionProperties{
StorageProfile: &armcomputev5.GalleryImageVersionStorageProfile{
OSDiskImage: &armcomputev5.GalleryOSDiskImage{
HostCaching: toPtr(armcomputev5.HostCachingReadOnly),
},
Source: &armcomputev5.GalleryArtifactVersionFullSource{
ID: &imageID,
},
},
PublishingProfile: &armcomputev5.GalleryImageVersionPublishingProfile{
ReplicaCount: toPtr[int32](1),
ReplicationMode: toPtr(armcomputev5.ReplicationModeFull),
TargetRegions: targetRegions,
},
},
}
createPoller, err := u.imageVersions.BeginCreateOrUpdate(ctx, u.resourceGroup, sigName, definitionName, versionName, imageVersion,
&armcomputev5.GalleryImageVersionsClientBeginCreateOrUpdateOptions{},
)
if err != nil {
return "", fmt.Errorf("creating image version: %w", err)
}
createdImage, err := createPoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency})
if err != nil {
return "", fmt.Errorf("waiting for image version to be created: %w", err)
}
if createdImage.ID == nil {
return "", errors.New("created image has no id")
}
return *createdImage.ID, nil
}
func (u *Uploader) ensureImageVersionDeleted(ctx context.Context, sigName, definitionName, versionName string) error {
_, err := u.imageVersions.Get(ctx, u.resourceGroup, sigName, definitionName, versionName, &armcomputev5.GalleryImageVersionsClientGetOptions{})
if err != nil {
u.log.Debugf("Image version %s in %s/%s/%s doesn't exist. Nothing to clean up.", versionName, u.resourceGroup, sigName, definitionName)
return nil
}
u.log.Debugf("Deleting image version %s in %s/%s/%s", versionName, u.resourceGroup, sigName, definitionName)
deletePoller, err := u.imageVersions.BeginDelete(ctx, u.resourceGroup, sigName, definitionName, versionName, &armcomputev5.GalleryImageVersionsClientBeginDeleteOptions{})
if err != nil {
return fmt.Errorf("deleting image version: %w", err)
}
if _, err = deletePoller.PollUntilDone(ctx, &runtime.PollUntilDoneOptions{Frequency: u.pollingFrequency}); err != nil {
return fmt.Errorf("waiting for image version to be deleted: %w", err)
}
return nil
}
// getImageReference returns the image reference to use for the image version.
// If the shared image gallery is a community gallery, the community identifier is returned.
// Otherwise, the unshared identifier is returned.
func (u *Uploader) getImageReference(ctx context.Context, sigName, definitionName, versionName, unsharedID string) (string, error) {
galleryResp, err := u.galleries.Get(ctx, u.resourceGroup, sigName, &armcomputev5.GalleriesClientGetOptions{})
if err != nil {
return "", fmt.Errorf("getting image gallery %s: %w", sigName, err)
}
if galleryResp.Properties == nil ||
galleryResp.Properties.SharingProfile == nil ||
galleryResp.Properties.SharingProfile.CommunityGalleryInfo == nil ||
galleryResp.Properties.SharingProfile.CommunityGalleryInfo.CommunityGalleryEnabled == nil ||
!*galleryResp.Properties.SharingProfile.CommunityGalleryInfo.CommunityGalleryEnabled {
u.log.Warnf("Image gallery %s in %s is not shared. Using private identifier", sigName, u.resourceGroup)
return unsharedID, nil
}
if galleryResp.Properties == nil ||
galleryResp.Properties.SharingProfile == nil ||
galleryResp.Properties.SharingProfile.CommunityGalleryInfo == nil ||
galleryResp.Properties.SharingProfile.CommunityGalleryInfo.PublicNames == nil ||
len(galleryResp.Properties.SharingProfile.CommunityGalleryInfo.PublicNames) < 1 ||
galleryResp.Properties.SharingProfile.CommunityGalleryInfo.PublicNames[0] == nil {
return "", fmt.Errorf("image gallery %s in %s is a community gallery but has no public names", sigName, u.resourceGroup)
}
communityGalleryName := *galleryResp.Properties.SharingProfile.CommunityGalleryInfo.PublicNames[0]
u.log.Debugf("Image gallery %s in %s is shared. Using community identifier in %s", sigName, u.resourceGroup, communityGalleryName)
communityVersionResp, err := u.communityVersions.Get(ctx, u.location, communityGalleryName,
definitionName, versionName,
&armcomputev5.CommunityGalleryImageVersionsClientGetOptions{},
)
if err != nil {
return "", fmt.Errorf("getting community image version %s/%s/%s: %w", communityGalleryName, definitionName, versionName, err)
}
if communityVersionResp.Identifier == nil || communityVersionResp.Identifier.UniqueID == nil {
return "", fmt.Errorf("community image version %s/%s/%s has no id", communityGalleryName, definitionName, versionName)
}
return *communityVersionResp.Identifier.UniqueID, nil
}
func uploadBlob(ctx context.Context, sasURL string, disk io.ReadSeeker, size int64, uploader sasBlobUploader) error {
uploadClient, err := uploader(sasURL)
if err != nil {
return fmt.Errorf("uploading blob: %w", err)
}
var offset int64
var chunksize int
chunk := make([]byte, pageSizeMax)
var readErr error
for offset < size {
chunksize, readErr = io.ReadAtLeast(disk, chunk, 1)
if readErr != nil {
return fmt.Errorf("reading from disk: %w", err)
}
if err := uploadChunk(ctx, uploadClient, bytes.NewReader(chunk[:chunksize]), offset, int64(chunksize)); err != nil {
return fmt.Errorf("uploading chunk: %w", err)
}
offset += int64(chunksize)
}
return nil
}
func uploadChunk(ctx context.Context, uploader azurePageblobAPI, chunk io.ReadSeeker, offset, chunksize int64) error {
_, err := uploader.UploadPages(ctx, &readSeekNopCloser{chunk}, blob.HTTPRange{
Offset: offset,
Count: chunksize,
}, nil)
return err
}
func imageOffer(version versionsapi.Version) string {
switch {
case version.Stream() == "stable":
return "constellation"
case version.Stream() == "debug" && version.Ref() == "-":
return version.Version()
}
return version.Ref() + "-" + version.Stream()
}
// imageVersion determines the semantic version string used inside a sig image.
// For releases, the actual semantic version of the image (without leading v) is used (major.minor.patch).
// Otherwise, the version is derived from the commit timestamp.
func imageVersion(version versionsapi.Version, timestamp time.Time) (string, error) {
switch {
case version.Stream() == "stable":
fallthrough
case version.Stream() == "debug" && version.Ref() == "-":
return strings.TrimLeft(version.Version(), "v"), nil
}
formattedTime := timestamp.Format(timestampFormat)
if len(formattedTime) != len(timestampFormat) {
return "", errors.New("invalid timestamp")
}
// <year>.<month><day>.<time>
return formattedTime[:4] + "." + formattedTime[4:8] + "." + formattedTime[8:], nil
}
type sasBlobUploader func(sasBlobURL string) (azurePageblobAPI, error)
type azureDiskAPI interface {
Get(ctx context.Context, resourceGroupName string, diskName string,
options *armcomputev5.DisksClientGetOptions,
) (armcomputev5.DisksClientGetResponse, error)
BeginCreateOrUpdate(ctx context.Context, resourceGroupName string, diskName string, disk armcomputev5.Disk,
options *armcomputev5.DisksClientBeginCreateOrUpdateOptions,
) (*runtime.Poller[armcomputev5.DisksClientCreateOrUpdateResponse], error)
BeginDelete(ctx context.Context, resourceGroupName string, diskName string,
options *armcomputev5.DisksClientBeginDeleteOptions,
) (*runtime.Poller[armcomputev5.DisksClientDeleteResponse], error)
BeginGrantAccess(ctx context.Context, resourceGroupName string, diskName string, grantAccessData armcomputev5.GrantAccessData,
options *armcomputev5.DisksClientBeginGrantAccessOptions,
) (*runtime.Poller[armcomputev5.DisksClientGrantAccessResponse], error)
BeginRevokeAccess(ctx context.Context, resourceGroupName string, diskName string,
options *armcomputev5.DisksClientBeginRevokeAccessOptions,
) (*runtime.Poller[armcomputev5.DisksClientRevokeAccessResponse], error)
}
type azureManagedImageAPI interface {
Get(ctx context.Context, resourceGroupName string, imageName string,
options *armcomputev5.ImagesClientGetOptions,
) (armcomputev5.ImagesClientGetResponse, error)
BeginCreateOrUpdate(ctx context.Context, resourceGroupName string,
imageName string, parameters armcomputev5.Image,
options *armcomputev5.ImagesClientBeginCreateOrUpdateOptions,
) (*runtime.Poller[armcomputev5.ImagesClientCreateOrUpdateResponse], error)
BeginDelete(ctx context.Context, resourceGroupName string, imageName string,
options *armcomputev5.ImagesClientBeginDeleteOptions,
) (*runtime.Poller[armcomputev5.ImagesClientDeleteResponse], error)
}
type azurePageblobAPI interface {
UploadPages(ctx context.Context, body io.ReadSeekCloser, contentRange blob.HTTPRange,
options *pageblob.UploadPagesOptions,
) (pageblob.UploadPagesResponse, error)
}
type azureGalleriesAPI interface {
Get(ctx context.Context, resourceGroupName string, galleryName string,
options *armcomputev5.GalleriesClientGetOptions,
) (armcomputev5.GalleriesClientGetResponse, error)
NewListPager(options *armcomputev5.GalleriesClientListOptions,
) *runtime.Pager[armcomputev5.GalleriesClientListResponse]
BeginCreateOrUpdate(ctx context.Context, resourceGroupName string,
galleryName string, gallery armcomputev5.Gallery,
options *armcomputev5.GalleriesClientBeginCreateOrUpdateOptions,
) (*runtime.Poller[armcomputev5.GalleriesClientCreateOrUpdateResponse], error)
}
type azureGalleriesImageAPI interface {
Get(ctx context.Context, resourceGroupName string, galleryName string,
galleryImageName string, options *armcomputev5.GalleryImagesClientGetOptions,
) (armcomputev5.GalleryImagesClientGetResponse, error)
BeginCreateOrUpdate(ctx context.Context, resourceGroupName string, galleryName string,
galleryImageName string, galleryImage armcomputev5.GalleryImage,
options *armcomputev5.GalleryImagesClientBeginCreateOrUpdateOptions,
) (*runtime.Poller[armcomputev5.GalleryImagesClientCreateOrUpdateResponse], error)
BeginDelete(ctx context.Context, resourceGroupName string, galleryName string, galleryImageName string,
options *armcomputev5.GalleryImagesClientBeginDeleteOptions,
) (*runtime.Poller[armcomputev5.GalleryImagesClientDeleteResponse], error)
}
type azureGalleriesImageVersionAPI interface {
Get(ctx context.Context, resourceGroupName string, galleryName string, galleryImageName string, galleryImageVersionName string,
options *armcomputev5.GalleryImageVersionsClientGetOptions,
) (armcomputev5.GalleryImageVersionsClientGetResponse, error)
NewListByGalleryImagePager(resourceGroupName string, galleryName string, galleryImageName string,
options *armcomputev5.GalleryImageVersionsClientListByGalleryImageOptions,
) *runtime.Pager[armcomputev5.GalleryImageVersionsClientListByGalleryImageResponse]
BeginCreateOrUpdate(ctx context.Context, resourceGroupName string, galleryName string, galleryImageName string,
galleryImageVersionName string, galleryImageVersion armcomputev5.GalleryImageVersion,
options *armcomputev5.GalleryImageVersionsClientBeginCreateOrUpdateOptions,
) (*runtime.Poller[armcomputev5.GalleryImageVersionsClientCreateOrUpdateResponse], error)
BeginDelete(ctx context.Context, resourceGroupName string, galleryName string, galleryImageName string,
galleryImageVersionName string, options *armcomputev5.GalleryImageVersionsClientBeginDeleteOptions,
) (*runtime.Poller[armcomputev5.GalleryImageVersionsClientDeleteResponse], error)
}
type azureCommunityGalleryImageVersionAPI interface {
Get(ctx context.Context, location string,
publicGalleryName, galleryImageName, galleryImageVersionName string,
options *armcomputev5.CommunityGalleryImageVersionsClientGetOptions,
) (armcomputev5.CommunityGalleryImageVersionsClientGetResponse, error)
}
const (
pollingFrequency = 10 * time.Second
// uploadAccessDuration is the time in seconds that
// sas tokens should be valid for (24 hours).
uploadAccessDuration = 86400 // 24 hours
pageSizeMax = 4194304 // 4MiB
pageSizeMin = 512 // 512 bytes
sigNameStable = "Constellation_CVM"
sigNameDebug = "Constellation_Debug_CVM"
sigNameDefault = "Constellation_Testing_CVM"
imageDefinitionPublisher = "edgelesssys"
imageDefinitionSKU = "constellation"
timestampFormat = "20060102150405"
)
var targetRegions = []*armcomputev5.TargetRegion{
{
Name: toPtr("northeurope"),
RegionalReplicaCount: toPtr[int32](1),
},
{
Name: toPtr("eastus"),
RegionalReplicaCount: toPtr[int32](1),
},
{
Name: toPtr("westeurope"),
RegionalReplicaCount: toPtr[int32](1),
},
{
Name: toPtr("westus"),
RegionalReplicaCount: toPtr[int32](1),
},
{
Name: toPtr("southeastasia"),
RegionalReplicaCount: toPtr[int32](1),
},
}
//go:generate stringer -type=DiskType -trimprefix=DiskType
// DiskType is the kind of disk created using the Azure API.
type DiskType uint32
// FromString converts a string into an DiskType.
func FromString(s string) DiskType {
switch strings.ToLower(s) {
case strings.ToLower(DiskTypeNormal.String()):
return DiskTypeNormal
case strings.ToLower(DiskTypeWithVMGS.String()):
return DiskTypeWithVMGS
default:
return DiskTypeUnknown
}
}
const (
// DiskTypeUnknown is default value for DiskType.
DiskTypeUnknown DiskType = iota
// DiskTypeNormal creates a normal Azure disk (single block device).
DiskTypeNormal
// DiskTypeWithVMGS creates a disk with VMGS (also called secure disk)
// that has an additional block device for the VMGS disk.
DiskTypeWithVMGS
)
func toPtr[T any](v T) *T {
return &v
}
type readSeekNopCloser struct {
io.ReadSeeker
}
func (n *readSeekNopCloser) Close() error {
return nil
}

View file

@ -1,25 +0,0 @@
// Code generated by "stringer -type=DiskType -trimprefix=DiskType"; DO NOT EDIT.
package azure
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[DiskTypeUnknown-0]
_ = x[DiskTypeNormal-1]
_ = x[DiskTypeWithVMGS-2]
}
const _DiskType_name = "UnknownNormalWithVMGS"
var _DiskType_index = [...]uint8{0, 7, 13, 21}
func (i DiskType) String() string {
if i >= DiskType(len(_DiskType_index)-1) {
return "DiskType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _DiskType_name[_DiskType_index[i]:_DiskType_index[i+1]]
}

View file

@ -1,18 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "gcp",
srcs = ["gcpupload.go"],
importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/gcp",
visibility = ["//:__subpackages__"],
deps = [
"//internal/api/versionsapi",
"//internal/logger",
"//internal/osimage",
"//internal/osimage/secureboot",
"@com_github_googleapis_gax_go_v2//:gax-go",
"@com_google_cloud_go_compute//apiv1",
"@com_google_cloud_go_compute//apiv1/computepb",
"@com_google_cloud_go_storage//:storage",
],
)

View file

@ -1,298 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// package gcp implements uploading os images to gcp.
package gcp
import (
"context"
"encoding/base64"
"fmt"
"io"
"net/url"
"path"
"strings"
compute "cloud.google.com/go/compute/apiv1"
"cloud.google.com/go/compute/apiv1/computepb"
"cloud.google.com/go/storage"
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/osimage"
"github.com/edgelesssys/constellation/v2/internal/osimage/secureboot"
gaxv2 "github.com/googleapis/gax-go/v2"
)
// Uploader can upload and remove os images on GCP.
type Uploader struct {
project string
location string
bucketName string
image imagesAPI
bucket bucketAPI
log *logger.Logger
}
// New creates a new Uploader.
func New(ctx context.Context, project, location, bucketName string, log *logger.Logger) (*Uploader, error) {
image, err := compute.NewImagesRESTClient(ctx)
if err != nil {
return nil, err
}
storage, err := storage.NewClient(ctx)
if err != nil {
return nil, err
}
bucket := storage.Bucket(bucketName)
return &Uploader{
project: project,
location: location,
bucketName: bucketName,
image: image,
bucket: bucket,
log: log,
}, nil
}
// Upload uploads an OS image to GCP.
func (u *Uploader) Upload(ctx context.Context, req *osimage.UploadRequest) ([]versionsapi.ImageInfoEntry, error) {
imageName := u.imageName(req.Version, req.AttestationVariant)
blobName := imageName + ".tar.gz"
if err := u.ensureBucket(ctx); err != nil {
return nil, fmt.Errorf("setup: ensuring bucket exists: %w", err)
}
if err := u.ensureImageDeleted(ctx, imageName); err != nil {
return nil, fmt.Errorf("pre-cleaning: ensuring no image using the same name exists: %w", err)
}
if err := u.ensureBlobDeleted(ctx, blobName); err != nil {
return nil, fmt.Errorf("pre-cleaning: ensuring no blob using the same name exists: %w", err)
}
if err := u.uploadBlob(ctx, blobName, req.Image); err != nil {
return nil, fmt.Errorf("uploading blob: %w", err)
}
defer func() {
// cleanup temporary blob
if err := u.ensureBlobDeleted(ctx, blobName); err != nil {
u.log.Errorf("post-cleaning: deleting blob: %v", err)
}
}()
imageRef, err := u.createImage(ctx, req.Version, imageName, blobName, req.SecureBoot, req.SBDatabase)
if err != nil {
return nil, fmt.Errorf("creating image: %w", err)
}
return []versionsapi.ImageInfoEntry{
{
CSP: "gcp",
AttestationVariant: req.AttestationVariant,
Reference: imageRef,
},
}, nil
}
func (u *Uploader) ensureBucket(ctx context.Context) error {
_, err := u.bucket.Attrs(ctx)
if err == nil {
u.log.Debugf("Bucket %s exists", u.bucketName)
return nil
}
if err != storage.ErrBucketNotExist {
return err
}
u.log.Debugf("Creating bucket %s", u.bucketName)
return u.bucket.Create(ctx, u.project, &storage.BucketAttrs{
PublicAccessPrevention: storage.PublicAccessPreventionEnforced,
Location: u.location,
})
}
func (u *Uploader) uploadBlob(ctx context.Context, blobName string, img io.Reader) error {
u.log.Debugf("Uploading os image as %s", blobName)
writer := u.bucket.Object(blobName).NewWriter(ctx)
_, err := io.Copy(writer, img)
if err != nil {
return err
}
return writer.Close()
}
func (u *Uploader) ensureBlobDeleted(ctx context.Context, blobName string) error {
_, err := u.bucket.Object(blobName).Attrs(ctx)
if err == storage.ErrObjectNotExist {
u.log.Debugf("Blob %s in %s doesn't exist. Nothing to clean up.", blobName, u.bucketName)
return nil
}
if err != nil {
return err
}
u.log.Debugf("Deleting blob %s", blobName)
return u.bucket.Object(blobName).Delete(ctx)
}
func (u *Uploader) createImage(ctx context.Context, version versionsapi.Version, imageName, blobName string, enableSecureBoot bool, sbDatabase secureboot.Database) (string, error) {
u.log.Debugf("Creating image %s", imageName)
blobURL := u.blobURL(blobName)
family := u.imageFamily(version)
var initialState *computepb.InitialStateConfig
if enableSecureBoot {
initialState = &computepb.InitialStateConfig{
Pk: pk(&sbDatabase),
Keks: keks(&sbDatabase),
Dbs: dbs(&sbDatabase),
}
}
req := computepb.InsertImageRequest{
ImageResource: &computepb.Image{
Name: &imageName,
RawDisk: &computepb.RawDisk{
ContainerType: toPtr("TAR"),
Source: &blobURL,
},
Family: &family,
Architecture: toPtr("X86_64"),
GuestOsFeatures: []*computepb.GuestOsFeature{
{Type: toPtr("GVNIC")},
{Type: toPtr("SEV_CAPABLE")},
{Type: toPtr("SEV_SNP_CAPABLE")},
{Type: toPtr("VIRTIO_SCSI_MULTIQUEUE")},
{Type: toPtr("UEFI_COMPATIBLE")},
},
ShieldedInstanceInitialState: initialState,
},
Project: u.project,
}
op, err := u.image.Insert(ctx, &req)
if err != nil {
return "", fmt.Errorf("creating image: %w", err)
}
if err := op.Wait(ctx); err != nil {
return "", fmt.Errorf("waiting for image to be created: %w", err)
}
policy := &computepb.Policy{
Bindings: []*computepb.Binding{
{
Role: toPtr("roles/compute.imageUser"),
Members: []string{"allAuthenticatedUsers"},
},
},
}
if _, err = u.image.SetIamPolicy(ctx, &computepb.SetIamPolicyImageRequest{
Resource: imageName,
Project: u.project,
GlobalSetPolicyRequestResource: &computepb.GlobalSetPolicyRequest{
Policy: policy,
},
}); err != nil {
return "", fmt.Errorf("setting iam policy: %w", err)
}
image, err := u.image.Get(ctx, &computepb.GetImageRequest{
Image: imageName,
Project: u.project,
})
if err != nil {
return "", fmt.Errorf("created image doesn't exist: %w", err)
}
return strings.TrimPrefix(image.GetSelfLink(), "https://www.googleapis.com/compute/v1/"), nil
}
func (u *Uploader) ensureImageDeleted(ctx context.Context, imageName string) error {
_, err := u.image.Get(ctx, &computepb.GetImageRequest{
Image: imageName,
Project: u.project,
})
if err != nil {
u.log.Debugf("Image %s doesn't exist. Nothing to clean up.", imageName)
return nil
}
u.log.Debugf("Deleting image %s", imageName)
op, err := u.image.Delete(ctx, &computepb.DeleteImageRequest{
Image: imageName,
Project: u.project,
})
if err != nil {
return err
}
return op.Wait(ctx)
}
func (u *Uploader) blobURL(blobName string) string {
return (&url.URL{
Scheme: "https",
Host: "storage.googleapis.com",
Path: path.Join(u.bucketName, blobName),
}).String()
}
func (u *Uploader) imageName(version versionsapi.Version, attestationVariant string) string {
return strings.ReplaceAll(version.Version(), ".", "-") + "-" + attestationVariant + "-" + version.Stream()
}
func (u *Uploader) imageFamily(version versionsapi.Version) string {
if version.Stream() == "stable" {
return "constellation"
}
truncatedRef := version.Ref()
if len(version.Ref()) > 45 {
truncatedRef = version.Ref()[:45]
}
return "constellation-" + truncatedRef
}
func pk(sbDatabase *secureboot.Database) *computepb.FileContentBuffer {
encoded := base64.StdEncoding.EncodeToString(sbDatabase.PK)
return &computepb.FileContentBuffer{
Content: toPtr(encoded),
FileType: toPtr("X509"),
}
}
func keks(sbDatabase *secureboot.Database) []*computepb.FileContentBuffer {
keks := make([]*computepb.FileContentBuffer, 0, len(sbDatabase.Keks))
for _, kek := range sbDatabase.Keks {
encoded := base64.StdEncoding.EncodeToString(kek)
keks = append(keks, &computepb.FileContentBuffer{
Content: toPtr(encoded),
FileType: toPtr("X509"),
})
}
return keks
}
func dbs(sbDatabase *secureboot.Database) []*computepb.FileContentBuffer {
dbs := make([]*computepb.FileContentBuffer, 0, len(sbDatabase.DBs))
for _, db := range sbDatabase.DBs {
encoded := base64.StdEncoding.EncodeToString(db)
dbs = append(dbs, &computepb.FileContentBuffer{
Content: toPtr(encoded),
FileType: toPtr("X509"),
})
}
return dbs
}
type imagesAPI interface {
Get(ctx context.Context, req *computepb.GetImageRequest, opts ...gaxv2.CallOption,
) (*computepb.Image, error)
Insert(ctx context.Context, req *computepb.InsertImageRequest, opts ...gaxv2.CallOption,
) (*compute.Operation, error)
SetIamPolicy(ctx context.Context, req *computepb.SetIamPolicyImageRequest, opts ...gaxv2.CallOption,
) (*computepb.Policy, error)
Delete(ctx context.Context, req *computepb.DeleteImageRequest, opts ...gaxv2.CallOption,
) (*compute.Operation, error)
io.Closer
}
type bucketAPI interface {
Attrs(ctx context.Context) (attrs *storage.BucketAttrs, err error)
Create(ctx context.Context, projectID string, attrs *storage.BucketAttrs) (err error)
Object(name string) *storage.ObjectHandle
}
func toPtr[T any](v T) *T {
return &v
}

View file

@ -13,7 +13,6 @@ import (
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/osimage/secureboot"
)
// UploadRequest is a request to upload an os image.
@ -21,10 +20,7 @@ type UploadRequest struct {
Provider cloudprovider.Provider
Version versionsapi.Version
AttestationVariant string
SecureBoot bool
SBDatabase secureboot.Database
UEFIVarStore secureboot.UEFIVarStore
Size int64
Timestamp time.Time
Image io.ReadSeeker
ImageReader func() (io.ReadSeekCloser, error)
ImagePath string
}