From b7bab7c3c8f80b5b905ae71b238858671469e4f1 Mon Sep 17 00:00:00 2001 From: Malte Poll <1780588+malt3@users.noreply.github.com> Date: Thu, 4 Jan 2024 16:59:31 +0100 Subject: [PATCH] image: replace "upload {aws|azure|gcp}" with uplosi --- image/upload/internal/cmd/BUILD.bazel | 15 +- image/upload/internal/cmd/aws.go | 106 ---- image/upload/internal/cmd/azure.go | 107 ---- image/upload/internal/cmd/flags.go | 324 ++++------ image/upload/internal/cmd/gcp.go | 107 ---- image/upload/internal/cmd/image.go | 50 -- image/upload/internal/cmd/nop.go | 90 --- image/upload/internal/cmd/openstack.go | 29 - image/upload/internal/cmd/qemu.go | 29 - image/upload/internal/cmd/secureboot.go | 44 -- image/upload/internal/cmd/upload.go | 7 +- image/upload/internal/cmd/uplosi.go | 115 ++++ image/upload/upload.go | 2 +- internal/osimage/BUILD.bazel | 1 - internal/osimage/aws/BUILD.bazel | 21 - internal/osimage/aws/awsupload.go | 603 ------------------ internal/osimage/azure/BUILD.bazel | 21 - internal/osimage/azure/azureupload.go | 710 ---------------------- internal/osimage/azure/disktype_string.go | 25 - internal/osimage/gcp/BUILD.bazel | 18 - internal/osimage/gcp/gcpupload.go | 298 --------- internal/osimage/osimage.go | 8 +- 22 files changed, 252 insertions(+), 2478 deletions(-) delete mode 100644 image/upload/internal/cmd/aws.go delete mode 100644 image/upload/internal/cmd/azure.go delete mode 100644 image/upload/internal/cmd/gcp.go delete mode 100644 image/upload/internal/cmd/image.go delete mode 100644 image/upload/internal/cmd/nop.go delete mode 100644 image/upload/internal/cmd/openstack.go delete mode 100644 image/upload/internal/cmd/qemu.go delete mode 100644 image/upload/internal/cmd/secureboot.go create mode 100644 image/upload/internal/cmd/uplosi.go delete mode 100644 internal/osimage/aws/BUILD.bazel delete mode 100644 internal/osimage/aws/awsupload.go delete mode 100644 internal/osimage/azure/BUILD.bazel delete mode 100644 internal/osimage/azure/azureupload.go delete mode 100644 internal/osimage/azure/disktype_string.go delete mode 100644 internal/osimage/gcp/BUILD.bazel delete mode 100644 internal/osimage/gcp/gcpupload.go diff --git a/image/upload/internal/cmd/BUILD.bazel b/image/upload/internal/cmd/BUILD.bazel index 0717cab0e..1bff3aa8e 100644 --- a/image/upload/internal/cmd/BUILD.bazel +++ b/image/upload/internal/cmd/BUILD.bazel @@ -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", ], diff --git a/image/upload/internal/cmd/aws.go b/image/upload/internal/cmd/aws.go deleted file mode 100644 index b2c4f0058..000000000 --- a/image/upload/internal/cmd/aws.go +++ /dev/null @@ -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) -} diff --git a/image/upload/internal/cmd/azure.go b/image/upload/internal/cmd/azure.go deleted file mode 100644 index 18491bd07..000000000 --- a/image/upload/internal/cmd/azure.go +++ /dev/null @@ -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) -} diff --git a/image/upload/internal/cmd/flags.go b/image/upload/internal/cmd/flags.go index b0cf85f6d..24bbaf455 100644 --- a/image/upload/internal/cmd/flags.go +++ b/image/upload/internal/cmd/flags.go @@ -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: __/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 +} diff --git a/image/upload/internal/cmd/gcp.go b/image/upload/internal/cmd/gcp.go deleted file mode 100644 index 2cf1bc7e8..000000000 --- a/image/upload/internal/cmd/gcp.go +++ /dev/null @@ -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) -} diff --git a/image/upload/internal/cmd/image.go b/image/upload/internal/cmd/image.go deleted file mode 100644 index 048ef4e6f..000000000 --- a/image/upload/internal/cmd/image.go +++ /dev/null @@ -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 -} diff --git a/image/upload/internal/cmd/nop.go b/image/upload/internal/cmd/nop.go deleted file mode 100644 index f6da987ba..000000000 --- a/image/upload/internal/cmd/nop.go +++ /dev/null @@ -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) -} diff --git a/image/upload/internal/cmd/openstack.go b/image/upload/internal/cmd/openstack.go deleted file mode 100644 index b8810c183..000000000 --- a/image/upload/internal/cmd/openstack.go +++ /dev/null @@ -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) -} diff --git a/image/upload/internal/cmd/qemu.go b/image/upload/internal/cmd/qemu.go deleted file mode 100644 index d34348010..000000000 --- a/image/upload/internal/cmd/qemu.go +++ /dev/null @@ -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) -} diff --git a/image/upload/internal/cmd/secureboot.go b/image/upload/internal/cmd/secureboot.go deleted file mode 100644 index a73f2af0f..000000000 --- a/image/upload/internal/cmd/secureboot.go +++ /dev/null @@ -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 -} diff --git a/image/upload/internal/cmd/upload.go b/image/upload/internal/cmd/upload.go index 5fdd4bec7..ce107c234 100644 --- a/image/upload/internal/cmd/upload.go +++ b/image/upload/internal/cmd/upload.go @@ -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 diff --git a/image/upload/internal/cmd/uplosi.go b/image/upload/internal/cmd/uplosi.go new file mode 100644 index 000000000..6b5f6c0a5 --- /dev/null +++ b/image/upload/internal/cmd/uplosi.go @@ -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() +} diff --git a/image/upload/upload.go b/image/upload/upload.go index a26c79121..7f4886a2d 100644 --- a/image/upload/upload.go +++ b/image/upload/upload.go @@ -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()) diff --git a/internal/osimage/BUILD.bazel b/internal/osimage/BUILD.bazel index 3e9285662..0bf370060 100644 --- a/internal/osimage/BUILD.bazel +++ b/internal/osimage/BUILD.bazel @@ -8,6 +8,5 @@ go_library( deps = [ "//internal/api/versionsapi", "//internal/cloud/cloudprovider", - "//internal/osimage/secureboot", ], ) diff --git a/internal/osimage/aws/BUILD.bazel b/internal/osimage/aws/BUILD.bazel deleted file mode 100644 index 640714442..000000000 --- a/internal/osimage/aws/BUILD.bazel +++ /dev/null @@ -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", - ], -) diff --git a/internal/osimage/aws/awsupload.go b/internal/osimage/aws/awsupload.go deleted file mode 100644 index d5d097f47..000000000 --- a/internal/osimage/aws/awsupload.go +++ /dev/null @@ -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"} -) diff --git a/internal/osimage/azure/BUILD.bazel b/internal/osimage/azure/BUILD.bazel deleted file mode 100644 index 48b1b6e77..000000000 --- a/internal/osimage/azure/BUILD.bazel +++ /dev/null @@ -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", - ], -) diff --git a/internal/osimage/azure/azureupload.go b/internal/osimage/azure/azureupload.go deleted file mode 100644 index 32490c3ff..000000000 --- a/internal/osimage/azure/azureupload.go +++ /dev/null @@ -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") - } - // ..