mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-11 00:44:20 -05:00
290 lines
8.2 KiB
Go
290 lines
8.2 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/logger"
|
||
|
"github.com/edgelesssys/constellation/v2/internal/osimage"
|
||
|
"github.com/edgelesssys/constellation/v2/internal/osimage/secureboot"
|
||
|
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
|
||
|
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) (map[string]string, error) {
|
||
|
imageName := u.imageName(req.Version, req.Variant)
|
||
|
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.SBDatabase)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("creating image: %w", err)
|
||
|
}
|
||
|
return map[string]string{
|
||
|
req.Variant: 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, sbDatabase secureboot.Database) (string, error) {
|
||
|
u.log.Debugf("Creating image %s", imageName)
|
||
|
blobURL := u.blobURL(blobName)
|
||
|
family := u.imageFamily(version)
|
||
|
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("VIRTIO_SCSI_MULTIQUEUE")},
|
||
|
{Type: toPtr("UEFI_COMPATIBLE")},
|
||
|
},
|
||
|
ShieldedInstanceInitialState: &computepb.InitialStateConfig{
|
||
|
Pk: pk(&sbDatabase),
|
||
|
Keks: keks(&sbDatabase),
|
||
|
Dbs: dbs(&sbDatabase),
|
||
|
},
|
||
|
},
|
||
|
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, variant string) string {
|
||
|
return strings.ReplaceAll(version.Version, ".", "-") + "-" + variant
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|