mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-08-03 12:36:09 -04:00
image: implement idempotent upload of os images
This commit is contained in:
parent
17c45bc881
commit
ee91d8b1cc
42 changed files with 4272 additions and 95 deletions
18
image/upload/BUILD.bazel
Normal file
18
image/upload/BUILD.bazel
Normal file
|
@ -0,0 +1,18 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "upload_lib",
|
||||
srcs = ["upload.go"],
|
||||
importpath = "github.com/edgelesssys/constellation/v2/image/upload",
|
||||
visibility = ["//visibility:private"],
|
||||
deps = [
|
||||
"//image/upload/internal/cmd",
|
||||
"@com_github_spf13_cobra//:cobra",
|
||||
],
|
||||
)
|
||||
|
||||
go_binary(
|
||||
name = "upload",
|
||||
embed = [":upload_lib"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
1
image/upload/delete_aws.sh
Normal file
1
image/upload/delete_aws.sh
Normal file
|
@ -0,0 +1 @@
|
|||
|
35
image/upload/internal/cmd/BUILD.bazel
Normal file
35
image/upload/internal/cmd/BUILD.bazel
Normal file
|
@ -0,0 +1,35 @@
|
|||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "cmd",
|
||||
srcs = [
|
||||
"api.go",
|
||||
"aws.go",
|
||||
"azure.go",
|
||||
"flags.go",
|
||||
"gcp.go",
|
||||
"must.go",
|
||||
"nop.go",
|
||||
"openstack.go",
|
||||
"qemu.go",
|
||||
"secureboot.go",
|
||||
"upload.go",
|
||||
],
|
||||
importpath = "github.com/edgelesssys/constellation/v2/image/upload/internal/cmd",
|
||||
visibility = ["//image/upload:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/cloud/cloudprovider",
|
||||
"//internal/logger",
|
||||
"//internal/osimage",
|
||||
"//internal/osimage/archive",
|
||||
"//internal/osimage/aws",
|
||||
"//internal/osimage/azure",
|
||||
"//internal/osimage/gcp",
|
||||
"//internal/osimage/nop",
|
||||
"//internal/osimage/secureboot",
|
||||
"//internal/versionsapi",
|
||||
"@com_github_spf13_afero//:afero",
|
||||
"@com_github_spf13_cobra//:cobra",
|
||||
"@org_uber_go_zap//zapcore",
|
||||
],
|
||||
)
|
25
image/upload/internal/cmd/api.go
Normal file
25
image/upload/internal/cmd/api.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/osimage"
|
||||
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
|
||||
)
|
||||
|
||||
type archivist interface {
|
||||
Archive(ctx context.Context,
|
||||
version versionsapi.Version, csp, variant string, img io.Reader,
|
||||
) (string, error)
|
||||
}
|
||||
|
||||
type uploader interface {
|
||||
Upload(ctx context.Context, req *osimage.UploadRequest) (map[string]string, error)
|
||||
}
|
97
image/upload/internal/cmd/aws.go
Normal file
97
image/upload/internal/cmd/aws.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
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, err := archive.New(cmd.Context(), flags.region, flags.bucket, log)
|
||||
if err != nil {
|
||||
return 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
|
||||
}
|
||||
|
||||
sbDatabase, uefiVarStore, err := loadSecureBootKeys(flags.pki)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploadReq := &osimage.UploadRequest{
|
||||
Provider: flags.provider,
|
||||
Version: flags.version,
|
||||
Variant: flags.variant,
|
||||
SBDatabase: sbDatabase,
|
||||
UEFIVarStore: uefiVarStore,
|
||||
Size: size,
|
||||
Timestamp: flags.timestamp,
|
||||
Image: file,
|
||||
}
|
||||
return uploadImage(cmd.Context(), archiveC, uploadC, uploadReq, out)
|
||||
}
|
98
image/upload/internal/cmd/azure.go
Normal file
98
image/upload/internal/cmd/azure.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
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, err := archive.New(cmd.Context(), flags.region, flags.bucket, log)
|
||||
if err != nil {
|
||||
return 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
|
||||
}
|
||||
|
||||
sbDatabase, uefiVarStore, err := loadSecureBootKeys(flags.pki)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploadReq := &osimage.UploadRequest{
|
||||
Provider: flags.provider,
|
||||
Version: flags.version,
|
||||
Variant: flags.variant,
|
||||
SBDatabase: sbDatabase,
|
||||
UEFIVarStore: uefiVarStore,
|
||||
Size: size,
|
||||
Timestamp: flags.timestamp,
|
||||
Image: file,
|
||||
}
|
||||
return uploadImage(cmd.Context(), archiveC, uploadC, uploadReq, out)
|
||||
}
|
200
image/upload/internal/cmd/flags.go
Normal file
200
image/upload/internal/cmd/flags.go
Normal file
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
type commonFlags struct {
|
||||
rawImage string
|
||||
pki string
|
||||
provider cloudprovider.Provider
|
||||
variant string
|
||||
version versionsapi.Version
|
||||
timestamp time.Time
|
||||
region string
|
||||
bucket 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")
|
||||
}
|
||||
variant, err := cmd.Flags().GetString("variant")
|
||||
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
|
||||
}
|
||||
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,
|
||||
variant: variant,
|
||||
version: ver,
|
||||
timestamp: timestmp,
|
||||
region: region,
|
||||
bucket: bucket,
|
||||
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
|
||||
}
|
98
image/upload/internal/cmd/gcp.go
Normal file
98
image/upload/internal/cmd/gcp.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
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-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, err := archive.New(cmd.Context(), flags.region, flags.bucket, log)
|
||||
if err != nil {
|
||||
return 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
|
||||
}
|
||||
|
||||
sbDatabase, uefiVarStore, err := loadSecureBootKeys(flags.pki)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploadReq := &osimage.UploadRequest{
|
||||
Provider: flags.provider,
|
||||
Version: flags.version,
|
||||
Variant: flags.variant,
|
||||
SBDatabase: sbDatabase,
|
||||
UEFIVarStore: uefiVarStore,
|
||||
Size: size,
|
||||
Timestamp: flags.timestamp,
|
||||
Image: file,
|
||||
}
|
||||
return uploadImage(cmd.Context(), archiveC, uploadC, uploadReq, out)
|
||||
}
|
13
image/upload/internal/cmd/must.go
Normal file
13
image/upload/internal/cmd/must.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
81
image/upload/internal/cmd/nop.go
Normal file
81
image/upload/internal/cmd/nop.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
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, err := archive.New(cmd.Context(), flags.region, flags.bucket, log)
|
||||
if err != nil {
|
||||
return 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
|
||||
}
|
||||
|
||||
sbDatabase, uefiVarStore, err := loadSecureBootKeys(flags.pki)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uploadReq := &osimage.UploadRequest{
|
||||
Provider: flags.provider,
|
||||
Version: flags.version,
|
||||
Variant: flags.variant,
|
||||
SBDatabase: sbDatabase,
|
||||
UEFIVarStore: uefiVarStore,
|
||||
Size: size,
|
||||
Timestamp: flags.timestamp,
|
||||
Image: file,
|
||||
}
|
||||
return uploadImage(cmd.Context(), archiveC, uploadC, uploadReq, out)
|
||||
}
|
29
image/upload/internal/cmd/openstack.go
Normal file
29
image/upload/internal/cmd/openstack.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
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)
|
||||
}
|
29
image/upload/internal/cmd/qemu.go
Normal file
29
image/upload/internal/cmd/qemu.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
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)
|
||||
}
|
44
image/upload/internal/cmd/secureboot.go
Normal file
44
image/upload/internal/cmd/secureboot.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
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
|
||||
}
|
66
image/upload/internal/cmd/upload.go
Normal file
66
image/upload/internal/cmd/upload.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/v2/internal/osimage"
|
||||
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
|
||||
)
|
||||
|
||||
func uploadImage(ctx context.Context, archiveC archivist, uploadC uploader, req *osimage.UploadRequest, out io.Writer) error {
|
||||
// upload to S3 archive
|
||||
archiveURL, err := archiveC.Archive(ctx, req.Version, req.Provider.String(), req.Variant, req.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// rewind reader so we can read again
|
||||
if _, err := req.Image.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
// upload to CSP
|
||||
imageReferences, err := uploadC.Upload(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(imageReferences) == 0 {
|
||||
imageReferences = map[string]string{
|
||||
req.Variant: archiveURL,
|
||||
}
|
||||
}
|
||||
|
||||
imageInfo := versionsapi.ImageInfo{
|
||||
Ref: req.Version.Ref,
|
||||
Stream: req.Version.Stream,
|
||||
Version: req.Version.Version,
|
||||
}
|
||||
switch req.Provider {
|
||||
case cloudprovider.AWS:
|
||||
imageInfo.AWS = imageReferences
|
||||
case cloudprovider.Azure:
|
||||
imageInfo.Azure = imageReferences
|
||||
case cloudprovider.GCP:
|
||||
imageInfo.GCP = imageReferences
|
||||
case cloudprovider.OpenStack:
|
||||
imageInfo.OpenStack = imageReferences
|
||||
case cloudprovider.QEMU:
|
||||
imageInfo.QEMU = imageReferences
|
||||
default:
|
||||
return fmt.Errorf("uploading image: cloud provider %s is not yet supported", req.Provider.String())
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(out).Encode(imageInfo); err != nil {
|
||||
return fmt.Errorf("uploading image: marshaling output: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
100
image/upload/upload.go
Normal file
100
image/upload/upload.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// upload uploads os images.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/image/upload/internal/cmd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func execute() error {
|
||||
rootCmd := newRootCmd()
|
||||
ctx, cancel := signalContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
return rootCmd.ExecuteContext(ctx)
|
||||
}
|
||||
|
||||
func newRootCmd() *cobra.Command {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "upload",
|
||||
Short: "Uploads OS images to supported CSPs",
|
||||
Long: "Uploads OS images to supported CSPs.",
|
||||
PersistentPreRun: preRunRoot,
|
||||
}
|
||||
|
||||
rootCmd.SetOut(os.Stdout)
|
||||
|
||||
rootCmd.PersistentFlags().String("raw-image", "", "Path to os image in CSP specific format that should be uploaded.")
|
||||
rootCmd.PersistentFlags().String("pki", "", "Base path to the PKI (secure boot signing) files.")
|
||||
rootCmd.PersistentFlags().String("variant", "", "Variant of the image being uploaded.")
|
||||
rootCmd.PersistentFlags().String("version", "", "Shortname of the os image version.")
|
||||
rootCmd.PersistentFlags().String("timestamp", "", "Optional timestamp to use for resource names. Uses format 2006-01-02T15:04:05Z07:00.")
|
||||
rootCmd.PersistentFlags().String("region", "eu-central-1", "AWS region of the archive S3 bucket")
|
||||
rootCmd.PersistentFlags().String("bucket", "cdn-constellation-backend", "S3 bucket name of the archive")
|
||||
rootCmd.PersistentFlags().String("out", "", "Optional path to write the upload result to. If not set, the result is written to stdout.")
|
||||
rootCmd.PersistentFlags().Bool("verbose", false, "Enable verbose output")
|
||||
must(rootCmd.MarkPersistentFlagRequired("raw-image"))
|
||||
must(rootCmd.MarkPersistentFlagRequired("variant"))
|
||||
must(rootCmd.MarkPersistentFlagRequired("version"))
|
||||
|
||||
rootCmd.AddCommand(cmd.NewAWSCmd())
|
||||
rootCmd.AddCommand(cmd.NewAzureCmd())
|
||||
rootCmd.AddCommand(cmd.NewGCPCommand())
|
||||
rootCmd.AddCommand(cmd.NewOpenStackCmd())
|
||||
rootCmd.AddCommand(cmd.NewQEMUCmd())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
// signalContext returns a context that is canceled on the handed signal.
|
||||
// The signal isn't watched after its first occurrence. Call the cancel
|
||||
// function to ensure the internal goroutine is stopped and the signal isn't
|
||||
// watched any longer.
|
||||
func signalContext(ctx context.Context, sig os.Signal) (context.Context, context.CancelFunc) {
|
||||
sigCtx, stop := signal.NotifyContext(ctx, sig)
|
||||
done := make(chan struct{}, 1)
|
||||
stopDone := make(chan struct{}, 1)
|
||||
|
||||
go func() {
|
||||
defer func() { stopDone <- struct{}{} }()
|
||||
defer stop()
|
||||
select {
|
||||
case <-sigCtx.Done():
|
||||
fmt.Println(" Signal caught. Press ctrl+c again to terminate the program immediately.")
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
|
||||
cancelFunc := func() {
|
||||
done <- struct{}{}
|
||||
<-stopDone
|
||||
}
|
||||
|
||||
return sigCtx, cancelFunc
|
||||
}
|
||||
|
||||
func preRunRoot(cmd *cobra.Command, _ []string) {
|
||||
cmd.SilenceUsage = true
|
||||
}
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue