mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-15 02:44:24 -05:00
299 lines
8.6 KiB
Go
299 lines
8.6 KiB
Go
/*
|
|
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
|
|
}
|