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

@ -4,22 +4,15 @@ go_library(
name = "cmd",
srcs = [
"api.go",
"aws.go",
"azure.go",
"flags.go",
"gcp.go",
"image.go",
"info.go",
"measurements.go",
"measurementsenvelope.go",
"measurementsmerge.go",
"measurementsupload.go",
"must.go",
"nop.go",
"openstack.go",
"qemu.go",
"secureboot.go",
"upload.go",
"uplosi.go",
],
importpath = "github.com/edgelesssys/constellation/v2/image/upload/internal/cmd",
visibility = ["//image/upload:__subpackages__"],
@ -31,14 +24,10 @@ go_library(
"//internal/logger",
"//internal/osimage",
"//internal/osimage/archive",
"//internal/osimage/aws",
"//internal/osimage/azure",
"//internal/osimage/gcp",
"//internal/osimage/imageinfo",
"//internal/osimage/measurementsuploader",
"//internal/osimage/nop",
"//internal/osimage/secureboot",
"@com_github_spf13_afero//:afero",
"//internal/osimage/uplosi",
"@com_github_spf13_cobra//:cobra",
"@org_uber_go_zap//zapcore",
],

View File

@ -1,106 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"fmt"
"io"
"os"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/osimage"
"github.com/edgelesssys/constellation/v2/internal/osimage/archive"
awsupload "github.com/edgelesssys/constellation/v2/internal/osimage/aws"
"github.com/spf13/cobra"
)
// newAWSCmd returns the command that uploads an OS image to AWS.
func newAWSCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "aws",
Short: "Upload OS image to AWS",
Long: "Upload OS image to AWS.",
Args: cobra.ExactArgs(0),
RunE: runAWS,
}
cmd.Flags().String("aws-region", "eu-central-1", "AWS region used during AMI creation")
cmd.Flags().String("aws-bucket", "constellation-images", "S3 bucket used during AMI creation")
return cmd
}
func runAWS(cmd *cobra.Command, _ []string) error {
workdir := os.Getenv("BUILD_WORKING_DIRECTORY")
if len(workdir) > 0 {
must(os.Chdir(workdir))
}
flags, err := parseAWSFlags(cmd)
if err != nil {
return err
}
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
archiveC, archiveCClose, err := archive.New(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return err
}
defer func() {
if err := archiveCClose(cmd.Context()); err != nil {
log.Errorf("closing archive client: %v", err)
}
}()
uploadC, err := awsupload.New(flags.awsRegion, flags.awsBucket, log)
if err != nil {
return fmt.Errorf("uploading image: %w", err)
}
file, err := os.Open(flags.rawImage)
if err != nil {
return fmt.Errorf("uploading image: opening image file %w", err)
}
defer file.Close()
size, err := file.Seek(0, io.SeekEnd)
if err != nil {
return err
}
if _, err := file.Seek(0, io.SeekStart); err != nil {
return err
}
out := cmd.OutOrStdout()
if len(flags.out) > 0 {
outF, err := os.Create(flags.out)
if err != nil {
return fmt.Errorf("uploading image: opening output file %w", err)
}
defer outF.Close()
out = outF
}
uploadReq := &osimage.UploadRequest{
Provider: flags.provider,
Version: flags.version,
AttestationVariant: flags.attestationVariant,
SecureBoot: flags.secureBoot,
Size: size,
Timestamp: flags.timestamp,
Image: file,
}
if flags.secureBoot {
sbDatabase, uefiVarStore, err := loadSecureBootKeys(flags.pki)
if err != nil {
return err
}
uploadReq.SBDatabase = sbDatabase
uploadReq.UEFIVarStore = uefiVarStore
}
return uploadImage(cmd.Context(), archiveC, uploadC, uploadReq, out)
}

View File

@ -1,107 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"fmt"
"io"
"os"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/osimage"
"github.com/edgelesssys/constellation/v2/internal/osimage/archive"
azureupload "github.com/edgelesssys/constellation/v2/internal/osimage/azure"
"github.com/spf13/cobra"
)
// newAzureCmd returns the command that uploads an OS image to Azure.
func newAzureCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "azure",
Short: "Upload OS image to Azure",
Long: "Upload OS image to Azure.",
Args: cobra.ExactArgs(0),
RunE: runAzure,
}
cmd.Flags().String("az-subscription", "0d202bbb-4fa7-4af8-8125-58c269a05435", "Azure subscription to use")
cmd.Flags().String("az-location", "northeurope", "Azure location to use")
cmd.Flags().String("az-resource-group", "constellation-images", "Azure resource group to use")
return cmd
}
func runAzure(cmd *cobra.Command, _ []string) error {
workdir := os.Getenv("BUILD_WORKING_DIRECTORY")
if len(workdir) > 0 {
must(os.Chdir(workdir))
}
flags, err := parseAzureFlags(cmd)
if err != nil {
return err
}
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
archiveC, archiveCClose, err := archive.New(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return err
}
defer func() {
if err := archiveCClose(cmd.Context()); err != nil {
log.Errorf("closing archive client: %v", err)
}
}()
uploadC, err := azureupload.New(flags.azSubscription, flags.azLocation, flags.azResourceGroup, log)
if err != nil {
return fmt.Errorf("uploading image: %w", err)
}
file, err := os.Open(flags.rawImage)
if err != nil {
return fmt.Errorf("uploading image: opening image file %w", err)
}
defer file.Close()
size, err := file.Seek(0, io.SeekEnd)
if err != nil {
return err
}
if _, err := file.Seek(0, io.SeekStart); err != nil {
return err
}
out := cmd.OutOrStdout()
if len(flags.out) > 0 {
outF, err := os.Create(flags.out)
if err != nil {
return fmt.Errorf("uploading image: opening output file %w", err)
}
defer outF.Close()
out = outF
}
uploadReq := &osimage.UploadRequest{
Provider: flags.provider,
Version: flags.version,
AttestationVariant: flags.attestationVariant,
SecureBoot: flags.secureBoot,
Size: size,
Timestamp: flags.timestamp,
Image: file,
}
if flags.secureBoot {
sbDatabase, uefiVarStore, err := loadSecureBootKeys(flags.pki)
if err != nil {
return err
}
uploadReq.SBDatabase = sbDatabase
uploadReq.UEFIVarStore = uefiVarStore
}
return uploadImage(cmd.Context(), archiveC, uploadC, uploadReq, out)
}

View File

@ -8,9 +8,8 @@ package cmd
import (
"errors"
"os"
"path/filepath"
"time"
"strings"
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
@ -18,200 +17,6 @@ import (
"go.uber.org/zap/zapcore"
)
type commonFlags struct {
rawImage string
pki string
provider cloudprovider.Provider
attestationVariant string
secureBoot bool
version versionsapi.Version
timestamp time.Time
region string
bucket string
distributionID string
out string
logLevel zapcore.Level
}
func parseCommonFlags(cmd *cobra.Command) (commonFlags, error) {
workspaceDir := os.Getenv("BUILD_WORKSPACE_DIRECTORY")
rawImage, err := cmd.Flags().GetString("raw-image")
if err != nil {
return commonFlags{}, err
}
pki, err := cmd.Flags().GetString("pki")
if err != nil {
return commonFlags{}, err
}
if pki == "" {
pki = filepath.Join(workspaceDir, "image/pki")
}
attestationVariant, err := cmd.Flags().GetString("attestation-variant")
if err != nil {
return commonFlags{}, err
}
secureBoot, err := cmd.Flags().GetBool("secure-boot")
if err != nil {
return commonFlags{}, err
}
version, err := cmd.Flags().GetString("version")
if err != nil {
return commonFlags{}, err
}
ver, err := versionsapi.NewVersionFromShortPath(version, versionsapi.VersionKindImage)
if err != nil {
return commonFlags{}, err
}
timestamp, err := cmd.Flags().GetString("timestamp")
if err != nil {
return commonFlags{}, err
}
if timestamp == "" {
timestamp = time.Now().Format("2006-01-02T15:04:05Z07:00")
}
timestmp, err := time.Parse("2006-01-02T15:04:05Z07:00", timestamp)
if err != nil {
return commonFlags{}, err
}
region, err := cmd.Flags().GetString("region")
if err != nil {
return commonFlags{}, err
}
bucket, err := cmd.Flags().GetString("bucket")
if err != nil {
return commonFlags{}, err
}
distributionID, err := cmd.Flags().GetString("distribution-id")
if err != nil {
return commonFlags{}, err
}
out, err := cmd.Flags().GetString("out")
if err != nil {
return commonFlags{}, err
}
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return commonFlags{}, err
}
logLevel := zapcore.InfoLevel
if verbose {
logLevel = zapcore.DebugLevel
}
return commonFlags{
rawImage: rawImage,
pki: pki,
attestationVariant: attestationVariant,
secureBoot: secureBoot,
version: ver,
timestamp: timestmp,
region: region,
bucket: bucket,
distributionID: distributionID,
out: out,
logLevel: logLevel,
}, nil
}
type awsFlags struct {
commonFlags
awsRegion string
awsBucket string
}
func parseAWSFlags(cmd *cobra.Command) (awsFlags, error) {
common, err := parseCommonFlags(cmd)
if err != nil {
return awsFlags{}, err
}
awsRegion, err := cmd.Flags().GetString("aws-region")
if err != nil {
return awsFlags{}, err
}
awsBucket, err := cmd.Flags().GetString("aws-bucket")
if err != nil {
return awsFlags{}, err
}
common.provider = cloudprovider.AWS
return awsFlags{
commonFlags: common,
awsRegion: awsRegion,
awsBucket: awsBucket,
}, nil
}
type azureFlags struct {
commonFlags
azSubscription string
azLocation string
azResourceGroup string
}
func parseAzureFlags(cmd *cobra.Command) (azureFlags, error) {
common, err := parseCommonFlags(cmd)
if err != nil {
return azureFlags{}, err
}
azSubscription, err := cmd.Flags().GetString("az-subscription")
if err != nil {
return azureFlags{}, err
}
azLocation, err := cmd.Flags().GetString("az-location")
if err != nil {
return azureFlags{}, err
}
azResourceGroup, err := cmd.Flags().GetString("az-resource-group")
if err != nil {
return azureFlags{}, err
}
common.provider = cloudprovider.Azure
return azureFlags{
commonFlags: common,
azSubscription: azSubscription,
azLocation: azLocation,
azResourceGroup: azResourceGroup,
}, nil
}
type gcpFlags struct {
commonFlags
gcpProject string
gcpLocation string
gcpBucket string
}
func parseGCPFlags(cmd *cobra.Command) (gcpFlags, error) {
common, err := parseCommonFlags(cmd)
if err != nil {
return gcpFlags{}, err
}
gcpProject, err := cmd.Flags().GetString("gcp-project")
if err != nil {
return gcpFlags{}, err
}
gcpLocation, err := cmd.Flags().GetString("gcp-location")
if err != nil {
return gcpFlags{}, err
}
gcpBucket, err := cmd.Flags().GetString("gcp-bucket")
if err != nil {
return gcpFlags{}, err
}
common.provider = cloudprovider.GCP
return gcpFlags{
commonFlags: common,
gcpProject: gcpProject,
gcpLocation: gcpLocation,
gcpBucket: gcpBucket,
}, nil
}
type s3Flags struct {
region string
bucket string
@ -357,3 +162,130 @@ func parseEnvelopeMeasurementsFlags(cmd *cobra.Command) (envelopeMeasurementsFla
logLevel: logLevel,
}, nil
}
type uplosiFlags struct {
rawImage string
provider cloudprovider.Provider
version versionsapi.Version
attestationVariant string
out string
uplosiPath string
// archiver flags
region string
bucket string
distributionID string
logLevel zapcore.Level
}
func parseUplosiFlags(cmd *cobra.Command) (uplosiFlags, error) {
rawImage, err := cmd.Flags().GetString("raw-image")
if err != nil {
return uplosiFlags{}, err
}
rawImage, err = filepath.Abs(rawImage)
if err != nil {
return uplosiFlags{}, err
}
// extract csp, attestation variant, and stream from the raw image name
// format: <csp>_<attestation-variant>_<stream>/constellation.raw
var guessedCSP, guessedAttestationVariant, guessedStream string
pathComponents := strings.Split(filepath.ToSlash(rawImage), "/")
if len(pathComponents) >= 2 && pathComponents[len(pathComponents)-1] == "constellation.raw" {
imageMetadata := pathComponents[len(pathComponents)-2]
imageMetadataComponents := strings.Split(imageMetadata, "_")
if len(imageMetadataComponents) == 3 {
guessedCSP = imageMetadataComponents[0]
guessedAttestationVariant = imageMetadataComponents[1]
guessedStream = imageMetadataComponents[2]
}
}
csp, err := cmd.Flags().GetString("csp")
if err != nil || csp == "" {
csp = guessedCSP
}
if csp == "" {
if err != nil {
return uplosiFlags{}, err
}
return uplosiFlags{}, errors.New("csp is required")
}
provider := cloudprovider.FromString(csp)
ref, err := cmd.Flags().GetString("ref")
if err != nil {
return uplosiFlags{}, err
}
stream, err := cmd.Flags().GetString("stream")
if err != nil || stream == "" {
stream = guessedStream
}
if stream == "" {
if err != nil {
return uplosiFlags{}, err
}
return uplosiFlags{}, errors.New("stream is required")
}
version, err := cmd.Flags().GetString("version")
if err != nil {
return uplosiFlags{}, err
}
ver, err := versionsapi.NewVersion(ref, stream, version, versionsapi.VersionKindImage)
if err != nil {
return uplosiFlags{}, err
}
attestationVariant, err := cmd.Flags().GetString("attestation-variant")
if err != nil || attestationVariant == "" {
attestationVariant = guessedAttestationVariant
}
if attestationVariant == "" {
if err != nil {
return uplosiFlags{}, err
}
return uplosiFlags{}, errors.New("attestation-variant is required")
}
out, err := cmd.Flags().GetString("out")
if err != nil {
return uplosiFlags{}, err
}
uplosiPath, err := cmd.Flags().GetString("uplosi-path")
if err != nil {
return uplosiFlags{}, err
}
region, err := cmd.Flags().GetString("region")
if err != nil {
return uplosiFlags{}, err
}
bucket, err := cmd.Flags().GetString("bucket")
if err != nil {
return uplosiFlags{}, err
}
distributionID, err := cmd.Flags().GetString("distribution-id")
if err != nil {
return uplosiFlags{}, err
}
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return uplosiFlags{}, err
}
logLevel := zapcore.InfoLevel
if verbose {
logLevel = zapcore.DebugLevel
}
return uplosiFlags{
rawImage: rawImage,
provider: provider,
version: ver,
attestationVariant: attestationVariant,
out: out,
uplosiPath: uplosiPath,
region: region,
bucket: bucket,
distributionID: distributionID,
logLevel: logLevel,
}, nil
}

View File

@ -1,107 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"fmt"
"io"
"os"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/osimage"
"github.com/edgelesssys/constellation/v2/internal/osimage/archive"
gcpupload "github.com/edgelesssys/constellation/v2/internal/osimage/gcp"
"github.com/spf13/cobra"
)
// newGCPCommand returns the command that uploads an OS image to GCP.
func newGCPCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "gcp",
Short: "Upload OS image to GCP",
Long: "Upload OS image to GCP.",
Args: cobra.ExactArgs(0),
RunE: runGCP,
}
cmd.Flags().String("gcp-project", "constellation-images", "GCP project to use")
cmd.Flags().String("gcp-location", "europe-west3", "GCP location to use")
cmd.Flags().String("gcp-bucket", "constellation-os-images", "GCP bucket to use")
return cmd
}
func runGCP(cmd *cobra.Command, _ []string) error {
workdir := os.Getenv("BUILD_WORKING_DIRECTORY")
if len(workdir) > 0 {
must(os.Chdir(workdir))
}
flags, err := parseGCPFlags(cmd)
if err != nil {
return err
}
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
archiveC, archiveCClose, err := archive.New(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return err
}
defer func() {
if err := archiveCClose(cmd.Context()); err != nil {
log.Errorf("closing archive client: %v", err)
}
}()
uploadC, err := gcpupload.New(cmd.Context(), flags.gcpProject, flags.gcpLocation, flags.gcpBucket, log)
if err != nil {
return fmt.Errorf("uploading image: %w", err)
}
file, err := os.Open(flags.rawImage)
if err != nil {
return fmt.Errorf("uploading image: opening image file %w", err)
}
defer file.Close()
size, err := file.Seek(0, io.SeekEnd)
if err != nil {
return err
}
if _, err := file.Seek(0, io.SeekStart); err != nil {
return err
}
out := cmd.OutOrStdout()
if len(flags.out) > 0 {
outF, err := os.Create(flags.out)
if err != nil {
return fmt.Errorf("uploading image: opening output file %w", err)
}
defer outF.Close()
out = outF
}
uploadReq := &osimage.UploadRequest{
Provider: flags.provider,
Version: flags.version,
AttestationVariant: flags.attestationVariant,
SecureBoot: flags.secureBoot,
Size: size,
Timestamp: flags.timestamp,
Image: file,
}
if flags.secureBoot {
sbDatabase, uefiVarStore, err := loadSecureBootKeys(flags.pki)
if err != nil {
return err
}
uploadReq.SBDatabase = sbDatabase
uploadReq.UEFIVarStore = uefiVarStore
}
return uploadImage(cmd.Context(), archiveC, uploadC, uploadReq, out)
}

View File

@ -1,50 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"os"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/spf13/cobra"
)
// NewImageCmd creates a new image parent command. Image needs another
// verb, and does nothing on its own.
func NewImageCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "image",
Short: "Uploads OS images to supported CSPs",
Long: "Uploads OS images to supported CSPs.",
Args: cobra.ExactArgs(0),
}
cmd.SetOut(os.Stdout)
cmd.PersistentFlags().String("raw-image", "", "Path to os image in CSP specific format that should be uploaded.")
cmd.PersistentFlags().Bool("secure-boot", false, "Enables secure boot support.")
cmd.PersistentFlags().String("pki", "", "Base path to the PKI (secure boot signing) files.")
cmd.PersistentFlags().String("attestation-variant", "", "Attestation variant of the image being uploaded.")
cmd.PersistentFlags().String("version", "", "Shortname of the os image version.")
cmd.PersistentFlags().String("timestamp", "", "Optional timestamp to use for resource names. Uses format 2006-01-02T15:04:05Z07:00.")
cmd.PersistentFlags().String("region", "eu-central-1", "AWS region of the archive S3 bucket")
cmd.PersistentFlags().String("bucket", "cdn-constellation-backend", "S3 bucket name of the archive")
cmd.PersistentFlags().String("distribution-id", constants.CDNDefaultDistributionID, "CloudFront distribution ID of the API")
cmd.PersistentFlags().String("out", "", "Optional path to write the upload result to. If not set, the result is written to stdout.")
cmd.PersistentFlags().Bool("verbose", false, "Enable verbose output")
must(cmd.MarkPersistentFlagRequired("raw-image"))
must(cmd.MarkPersistentFlagRequired("attestation-variant"))
must(cmd.MarkPersistentFlagRequired("version"))
cmd.AddCommand(newAWSCmd())
cmd.AddCommand(newAzureCmd())
cmd.AddCommand(newGCPCommand())
cmd.AddCommand(newOpenStackCmd())
cmd.AddCommand(newQEMUCmd())
return cmd
}

View File

@ -1,90 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"fmt"
"io"
"os"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/osimage"
"github.com/edgelesssys/constellation/v2/internal/osimage/archive"
nopupload "github.com/edgelesssys/constellation/v2/internal/osimage/nop"
"github.com/spf13/cobra"
)
func runNOP(cmd *cobra.Command, provider cloudprovider.Provider, _ []string) error {
workdir := os.Getenv("BUILD_WORKING_DIRECTORY")
if len(workdir) > 0 {
must(os.Chdir(workdir))
}
flags, err := parseCommonFlags(cmd)
if err != nil {
return err
}
flags.provider = provider
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
archiveC, archiveCClose, err := archive.New(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return err
}
defer func() {
if err := archiveCClose(cmd.Context()); err != nil {
log.Errorf("closing archive client: %v", err)
}
}()
uploadC := nopupload.New(log)
file, err := os.Open(flags.rawImage)
if err != nil {
return fmt.Errorf("uploading image: opening image file %w", err)
}
defer file.Close()
size, err := file.Seek(0, io.SeekEnd)
if err != nil {
return err
}
if _, err := file.Seek(0, io.SeekStart); err != nil {
return err
}
out := cmd.OutOrStdout()
if len(flags.out) > 0 {
outF, err := os.Create(flags.out)
if err != nil {
return fmt.Errorf("uploading image: opening output file %w", err)
}
defer outF.Close()
out = outF
}
uploadReq := &osimage.UploadRequest{
Provider: flags.provider,
Version: flags.version,
AttestationVariant: flags.attestationVariant,
SecureBoot: flags.secureBoot,
Size: size,
Timestamp: flags.timestamp,
Image: file,
}
if flags.secureBoot {
sbDatabase, uefiVarStore, err := loadSecureBootKeys(flags.pki)
if err != nil {
return err
}
uploadReq.SBDatabase = sbDatabase
uploadReq.UEFIVarStore = uefiVarStore
}
return uploadImage(cmd.Context(), archiveC, uploadC, uploadReq, out)
}

View File

@ -1,29 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/spf13/cobra"
)
// newOpenStackCmd returns the command that uploads an OS image to OpenStack.
func newOpenStackCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "openstack",
Short: "Upload OS image to OpenStack",
Long: "Upload OS image to OpenStack.",
Args: cobra.ExactArgs(0),
RunE: runOpenStack,
}
return cmd
}
func runOpenStack(cmd *cobra.Command, args []string) error {
return runNOP(cmd, cloudprovider.OpenStack, args)
}

View File

@ -1,29 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/spf13/cobra"
)
// newQEMUCmd returns the command that uploads an OS image to QEMU.
func newQEMUCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "qemu",
Short: "Upload OS image to QEMU",
Long: "Upload OS image to QEMU.",
Args: cobra.ExactArgs(0),
RunE: runQEMU,
}
return cmd
}
func runQEMU(cmd *cobra.Command, args []string) error {
return runNOP(cmd, cloudprovider.QEMU, args)
}

View File

@ -1,44 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"fmt"
"path/filepath"
"github.com/edgelesssys/constellation/v2/internal/osimage/secureboot"
"github.com/spf13/afero"
)
func loadSecureBootKeys(basePath string) (secureboot.Database, secureboot.UEFIVarStore, error) {
platformKeyCert := filepath.Join(basePath, "PK.cer")
keyExchangeKeyCerts := []string{
filepath.Join(basePath, "KEK.cer"),
filepath.Join(basePath, "MicCorKEKCA2011_2011-06-24.crt"),
}
signatureDBCerts := []string{
filepath.Join(basePath, "db.cer"),
filepath.Join(basePath, "MicWinProPCA2011_2011-10-19.crt"),
filepath.Join(basePath, "MicCorUEFCA2011_2011-06-27.crt"),
}
sbDatabase, err := secureboot.DatabaseFromFiles(afero.NewOsFs(), platformKeyCert, keyExchangeKeyCerts, signatureDBCerts)
if err != nil {
return secureboot.Database{},
secureboot.UEFIVarStore{},
fmt.Errorf("preparing secure boot database: %w", err)
}
platformKeyESL := filepath.Join(basePath, "PK.esl")
keyExchangeKeyESL := filepath.Join(basePath, "KEK.esl")
signatureDBESL := filepath.Join(basePath, "db.esl")
uefiVarStore, err := secureboot.VarStoreFromFiles(afero.NewOsFs(), platformKeyESL, keyExchangeKeyESL, signatureDBESL, "")
if err != nil {
return secureboot.Database{},
secureboot.UEFIVarStore{},
fmt.Errorf("preparing secure boot variable store: %w", err)
}
return sbDatabase, uefiVarStore, nil
}

View File

@ -19,12 +19,13 @@ import (
func uploadImage(ctx context.Context, archiveC archivist, uploadC uploader, req *osimage.UploadRequest, out io.Writer) error {
// upload to S3 archive
archiveURL, err := archiveC.Archive(ctx, req.Version, strings.ToLower(req.Provider.String()), req.AttestationVariant, req.Image)
imageReader, err := req.ImageReader()
if err != nil {
return err
}
// rewind reader so we can read again
if _, err := req.Image.Seek(0, io.SeekStart); err != nil {
defer imageReader.Close()
archiveURL, err := archiveC.Archive(ctx, req.Version, strings.ToLower(req.Provider.String()), req.AttestationVariant, imageReader)
if err != nil {
return err
}
// upload to CSP

View File

@ -0,0 +1,115 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"fmt"
"io"
"os"
"strconv"
"time"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/osimage"
"github.com/edgelesssys/constellation/v2/internal/osimage/archive"
nopupload "github.com/edgelesssys/constellation/v2/internal/osimage/nop"
uplosiupload "github.com/edgelesssys/constellation/v2/internal/osimage/uplosi"
"github.com/spf13/cobra"
)
// NewUplosiCmd returns the command that uses uplosi for uploading os images.
func NewUplosiCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "uplosi",
Short: "Templates uplosi configuration files",
Long: "Templates uplosi configuration files.",
Args: cobra.ExactArgs(0),
RunE: runUplosi,
}
cmd.SetOut(os.Stdout)
cmd.Flags().String("raw-image", "", "Path to os image in CSP specific format that should be uploaded.")
cmd.Flags().String("attestation-variant", "", "Attestation variant of the image being uploaded.")
cmd.Flags().String("csp", "", "Cloud service provider that we want to upload this image to. If not set, the csp is guessed from the raw-image file name.")
cmd.Flags().String("ref", "", "Ref of the OS image (part of image shortname).")
cmd.Flags().String("stream", "", "Stream of the OS image (part of the image shortname).")
cmd.Flags().String("version", "", "Semantic version of the os image (part of the image shortname).")
cmd.Flags().String("region", "eu-central-1", "AWS region of the archive S3 bucket")
cmd.Flags().String("bucket", "cdn-constellation-backend", "S3 bucket name of the archive")
cmd.Flags().String("distribution-id", "E1H77EZTHC3NE4", "CloudFront distribution ID of the API")
cmd.Flags().String("out", "", "Optional path to write the upload result to. If not set, the result is written to stdout.")
cmd.Flags().String("uplosi-path", "uplosi", "Path to the uplosi binary.")
cmd.Flags().Bool("verbose", false, "Enable verbose output")
must(cmd.MarkFlagRequired("raw-image"))
must(cmd.MarkFlagRequired("version"))
must(cmd.MarkFlagRequired("ref"))
return cmd
}
func runUplosi(cmd *cobra.Command, _ []string) error {
flags, err := parseUplosiFlags(cmd)
if err != nil {
return err
}
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
archiveC, archiveCClose, err := archive.New(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return err
}
defer func() {
if err := archiveCClose(cmd.Context()); err != nil {
log.Errorf("closing archive client: %v", err)
}
}()
var uploadC uploader
switch flags.provider {
case cloudprovider.AWS, cloudprovider.Azure, cloudprovider.GCP:
uploadC = uplosiupload.New(flags.uplosiPath, log)
default:
uploadC = nopupload.New(log)
}
imageOpener := func() (io.ReadSeekCloser, error) {
return os.Open(flags.rawImage)
}
out := cmd.OutOrStdout()
if len(flags.out) > 0 {
outF, err := os.Create(flags.out)
if err != nil {
return fmt.Errorf("uploading image: opening output file %w", err)
}
defer outF.Close()
out = outF
}
uploadReq := &osimage.UploadRequest{
Provider: flags.provider,
Version: flags.version,
AttestationVariant: flags.attestationVariant,
Timestamp: getTimestamp(),
ImageReader: imageOpener,
ImagePath: flags.rawImage,
}
return uploadImage(cmd.Context(), archiveC, uploadC, uploadReq, out)
}
func getTimestamp() time.Time {
epoch := os.Getenv("SOURCE_DATE_EPOCH")
epochSecs, err := strconv.ParseInt(epoch, 10, 64)
if epoch == "" || err != nil {
return time.Now().UTC()
}
return time.Unix(epochSecs, 0).UTC()
}

View File

@ -40,7 +40,7 @@ func newRootCmd() *cobra.Command {
rootCmd.SetOut(os.Stdout)
rootCmd.AddCommand(cmd.NewImageCmd())
rootCmd.AddCommand(cmd.NewUplosiCmd())
rootCmd.AddCommand(cmd.NewInfoCmd())
rootCmd.AddCommand(cmd.NewMeasurementsCmd())

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
}