mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-03-12 10:06:49 -04:00
image: replace "upload {aws|azure|gcp}" with uplosi
This commit is contained in:
parent
fb392c2d50
commit
b7bab7c3c8
@ -4,22 +4,15 @@ go_library(
|
|||||||
name = "cmd",
|
name = "cmd",
|
||||||
srcs = [
|
srcs = [
|
||||||
"api.go",
|
"api.go",
|
||||||
"aws.go",
|
|
||||||
"azure.go",
|
|
||||||
"flags.go",
|
"flags.go",
|
||||||
"gcp.go",
|
|
||||||
"image.go",
|
|
||||||
"info.go",
|
"info.go",
|
||||||
"measurements.go",
|
"measurements.go",
|
||||||
"measurementsenvelope.go",
|
"measurementsenvelope.go",
|
||||||
"measurementsmerge.go",
|
"measurementsmerge.go",
|
||||||
"measurementsupload.go",
|
"measurementsupload.go",
|
||||||
"must.go",
|
"must.go",
|
||||||
"nop.go",
|
|
||||||
"openstack.go",
|
|
||||||
"qemu.go",
|
|
||||||
"secureboot.go",
|
|
||||||
"upload.go",
|
"upload.go",
|
||||||
|
"uplosi.go",
|
||||||
],
|
],
|
||||||
importpath = "github.com/edgelesssys/constellation/v2/image/upload/internal/cmd",
|
importpath = "github.com/edgelesssys/constellation/v2/image/upload/internal/cmd",
|
||||||
visibility = ["//image/upload:__subpackages__"],
|
visibility = ["//image/upload:__subpackages__"],
|
||||||
@ -31,14 +24,10 @@ go_library(
|
|||||||
"//internal/logger",
|
"//internal/logger",
|
||||||
"//internal/osimage",
|
"//internal/osimage",
|
||||||
"//internal/osimage/archive",
|
"//internal/osimage/archive",
|
||||||
"//internal/osimage/aws",
|
|
||||||
"//internal/osimage/azure",
|
|
||||||
"//internal/osimage/gcp",
|
|
||||||
"//internal/osimage/imageinfo",
|
"//internal/osimage/imageinfo",
|
||||||
"//internal/osimage/measurementsuploader",
|
"//internal/osimage/measurementsuploader",
|
||||||
"//internal/osimage/nop",
|
"//internal/osimage/nop",
|
||||||
"//internal/osimage/secureboot",
|
"//internal/osimage/uplosi",
|
||||||
"@com_github_spf13_afero//:afero",
|
|
||||||
"@com_github_spf13_cobra//:cobra",
|
"@com_github_spf13_cobra//:cobra",
|
||||||
"@org_uber_go_zap//zapcore",
|
"@org_uber_go_zap//zapcore",
|
||||||
],
|
],
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -8,9 +8,8 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"strings"
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
@ -18,200 +17,6 @@ import (
|
|||||||
"go.uber.org/zap/zapcore"
|
"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 {
|
type s3Flags struct {
|
||||||
region string
|
region string
|
||||||
bucket string
|
bucket string
|
||||||
@ -357,3 +162,130 @@ func parseEnvelopeMeasurementsFlags(cmd *cobra.Command) (envelopeMeasurementsFla
|
|||||||
logLevel: logLevel,
|
logLevel: logLevel,
|
||||||
}, nil
|
}, 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
|
||||||
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -19,12 +19,13 @@ import (
|
|||||||
|
|
||||||
func uploadImage(ctx context.Context, archiveC archivist, uploadC uploader, req *osimage.UploadRequest, out io.Writer) error {
|
func uploadImage(ctx context.Context, archiveC archivist, uploadC uploader, req *osimage.UploadRequest, out io.Writer) error {
|
||||||
// upload to S3 archive
|
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// rewind reader so we can read again
|
defer imageReader.Close()
|
||||||
if _, err := req.Image.Seek(0, io.SeekStart); err != nil {
|
archiveURL, err := archiveC.Archive(ctx, req.Version, strings.ToLower(req.Provider.String()), req.AttestationVariant, imageReader)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// upload to CSP
|
// upload to CSP
|
||||||
|
115
image/upload/internal/cmd/uplosi.go
Normal file
115
image/upload/internal/cmd/uplosi.go
Normal 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()
|
||||||
|
}
|
@ -40,7 +40,7 @@ func newRootCmd() *cobra.Command {
|
|||||||
|
|
||||||
rootCmd.SetOut(os.Stdout)
|
rootCmd.SetOut(os.Stdout)
|
||||||
|
|
||||||
rootCmd.AddCommand(cmd.NewImageCmd())
|
rootCmd.AddCommand(cmd.NewUplosiCmd())
|
||||||
rootCmd.AddCommand(cmd.NewInfoCmd())
|
rootCmd.AddCommand(cmd.NewInfoCmd())
|
||||||
rootCmd.AddCommand(cmd.NewMeasurementsCmd())
|
rootCmd.AddCommand(cmd.NewMeasurementsCmd())
|
||||||
|
|
||||||
|
@ -8,6 +8,5 @@ go_library(
|
|||||||
deps = [
|
deps = [
|
||||||
"//internal/api/versionsapi",
|
"//internal/api/versionsapi",
|
||||||
"//internal/cloud/cloudprovider",
|
"//internal/cloud/cloudprovider",
|
||||||
"//internal/osimage/secureboot",
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -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",
|
|
||||||
],
|
|
||||||
)
|
|
@ -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"}
|
|
||||||
)
|
|
@ -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",
|
|
||||||
],
|
|
||||||
)
|
|
@ -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
|
|
||||||
}
|
|
@ -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]]
|
|
||||||
}
|
|
@ -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",
|
|
||||||
],
|
|
||||||
)
|
|
@ -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
|
|
||||||
}
|
|
@ -13,7 +13,6 @@ import (
|
|||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"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.
|
// UploadRequest is a request to upload an os image.
|
||||||
@ -21,10 +20,7 @@ type UploadRequest struct {
|
|||||||
Provider cloudprovider.Provider
|
Provider cloudprovider.Provider
|
||||||
Version versionsapi.Version
|
Version versionsapi.Version
|
||||||
AttestationVariant string
|
AttestationVariant string
|
||||||
SecureBoot bool
|
|
||||||
SBDatabase secureboot.Database
|
|
||||||
UEFIVarStore secureboot.UEFIVarStore
|
|
||||||
Size int64
|
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
Image io.ReadSeeker
|
ImageReader func() (io.ReadSeekCloser, error)
|
||||||
|
ImagePath string
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user