image: implement idempotent upload of os images

This commit is contained in:
Malte Poll 2023-04-21 10:47:07 +02:00 committed by Malte Poll
parent 17c45bc881
commit ee91d8b1cc
42 changed files with 4272 additions and 95 deletions

0
image/BUILD.bazel Normal file
View file

View file

@ -57,7 +57,6 @@
jq \
mtools \
ovmf \
python3-crc32c \
python3-pefile \
python3-pyelftools \
python3-setuptools \
@ -177,6 +176,8 @@ secure-boot/azure/delete.sh --name "${AZURE_DISK_NAME}-setup-secure-boot"
## Upload to CSP
Warning! Never set `--version` to a value that is already used for a release image.
<details>
<summary>AWS</summary>
@ -188,19 +189,9 @@ secure-boot/azure/delete.sh --name "${AZURE_DISK_NAME}-setup-secure-boot"
- `pki_prod` is used for release images
```sh
# set these variables
export AWS_IMAGE_NAME= # e.g. "constellation-v1.0.0"
export PKI=${PWD}/pki
export AWS_REGION=eu-central-1
export AWS_REPLICATION_REGIONS="us-east-2"
export AWS_BUCKET=constellation-images
export AWS_EFIVARS_PATH=${PWD}/mkosi.output.aws/fedora~37/efivars.bin
export AWS_IMAGE_PATH=${PWD}/mkosi.output.aws/fedora~37/image.raw
export AWS_IMAGE_FILENAME=image-$(date +%s).raw
export AWS_JSON_OUTPUT=${PWD}/mkosi.output.aws/fedora~37/image-upload.json
secure-boot/aws/create_uefivars.sh "${AWS_EFIVARS_PATH}"
upload/upload_aws.sh
# Warning! Never set `--version` to a value that is already used for a release image.
# Instead, use a `ref` that corresponds to your branch name.
bazel run //image/upload -- aws --verbose --raw-image mkosi.output.aws/fedora~37/image.raw --variant "" --version ref/foo/stream/nightly/v2.7.0-pre-asdf
```
</details>
@ -216,20 +207,12 @@ upload/upload_aws.sh
- `pki_prod` is used for release images
```sh
# set these variables
export GCP_IMAGE_FAMILY= # e.g. "constellation"
export GCP_IMAGE_NAME= # e.g. "constellation-v1.0.0"
export PKI=${PWD}/pki
export GCP_PROJECT=constellation-images
export GCP_REGION=europe-west3
export GCP_BUCKET=constellation-images
export GCP_RAW_IMAGE_PATH=${PWD}/mkosi.output.gcp/fedora~37/image.raw
export GCP_IMAGE_FILENAME=$(date +%s).tar.gz
export GCP_IMAGE_PATH=${PWD}/mkosi.output.gcp/fedora~37/image.tar.gz
export GCP_JSON_OUTPUT=${PWD}/mkosi.output.gcp/fedora~37/image-upload.json
upload/pack.sh gcp ${GCP_RAW_IMAGE_PATH} ${GCP_IMAGE_PATH}
upload/upload_gcp.sh
# Warning! Never set `--version` to a value that is already used for a release image.
# Instead, use a `ref` that corresponds to your branch name.
bazel run //image/upload -- gcp --verbose --raw-image "${GCP_IMAGE_PATH}" --variant "sev-es" --version ref/foo/stream/nightly/v2.7.0-pre-asdf
```
</details>
@ -247,31 +230,12 @@ Note:
- Optional (if Secure Boot should be enabled) [Prepare virtual machine guest state (VMGS) with customized NVRAM or use existing VMGS blob](#azure-secure-boot)
```sh
# set these variables
export AZURE_GALLERY_NAME= # e.g. "Constellation"
export AZURE_IMAGE_DEFINITION= # e.g. "constellation"
export AZURE_IMAGE_VERSION= # e.g. "1.0.0"
# Set this variable to a path if you want to use Secure Boot.
# Otherwise, set it to export AZURE_VMGS_PATH=
export AZURE_VMGS_PATH= # e.g. nothing OR "path/to/ConfidentialVM.vmgs"
# AZURE_SECURITY_TYPE can be one of
# - "ConfidentialVMSupported" (ConfidentialVM with secure boot disabled),
# - "ConfidentialVM" (ConfidentialVM with Secure Boot) or
# - TrustedLaunch" (Trusted Launch with or without Secure Boot)
export AZURE_SECURITY_TYPE=ConfidentialVMSupported
export AZURE_RESOURCE_GROUP_NAME=constellation-images
export AZURE_REGION=northeurope
export AZURE_REPLICATION_REGIONS="northeurope eastus westeurope westus"
export AZURE_IMAGE_OFFER=constellation
export AZURE_SKU=${AZURE_IMAGE_DEFINITION}
export AZURE_PUBLISHER=edgelesssys
export AZURE_DISK_NAME=constellation-$(date +%s)
export AZURE_RAW_IMAGE_PATH=${PWD}/mkosi.output.azure/fedora~37/image.raw
export AZURE_IMAGE_PATH=${PWD}/mkosi.output.azure/fedora~37/image.vhd
export AZURE_JSON_OUTPUT=${PWD}/mkosi.output.azure/fedora~37/image-upload.json
upload/pack.sh azure "${AZURE_RAW_IMAGE_PATH}" "${AZURE_IMAGE_PATH}"
upload/upload_azure.sh -g --disk-name "${AZURE_DISK_NAME}" "${AZURE_VMGS_PATH}"
# Warning! Never set `--version` to a value that is already used for a release image.
# Instead, use a `ref` that corresponds to your branch name.
bazel run //image/upload -- azure --verbose --raw-image "${AZURE_IMAGE_PATH}" --variant "cvm" --version ref/foo/stream/nightly/v2.7.0-pre-asdf
```
</details>
@ -288,15 +252,9 @@ Note:
- Login to AWS (see [here](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html))
```sh
# set these variables
export REF= # e.g. feat-xyz (branch name encoded with dashes)
export STREAM= # e.g. "nightly", "debug", "stable" (depends on the type of image and if it is a release)
export IMAGE_VERSION= # e.g. v2.1.0" or output of pseudo-version tool
export OPENSTACK_BUCKET=cdn-constellation-backend
export OPENSTACK_BASE_URL="https://cdn.confidential.cloud"
export OPENSTACK_IMAGE_PATH=${PWD}/mkosi.output.qemu/fedora~37/image.raw
export OPENSTACK_JSON_OUTPUT=${PWD}/mkosi.output.qemu/fedora~37/image-upload.json
upload/upload_openstack.sh
# Warning! Never set `--version` to a value that is already used for a release image.
# Instead, use a `ref` that corresponds to your branch name.
bazel run //image/upload -- openstack --verbose --raw-image mkosi.output.openstack/fedora~37/image.raw --variant "sev" --version ref/foo/stream/nightly/v2.7.0-pre-asdf
```
</details>
@ -308,15 +266,9 @@ upload/upload_openstack.sh
- Login to AWS (see [here](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html))
```sh
# set these variables
export REF= # e.g. feat-xyz (branch name encoded with dashes)
export STREAM= # e.g. "nightly", "debug", "stable" (depends on the type of image and if it is a release)
export IMAGE_VERSION= # e.g. v2.1.0" or output of pseudo-version tool
export QEMU_BUCKET=cdn-constellation-backend
export QEMU_BASE_URL="https://cdn.confidential.cloud"
export QEMU_IMAGE_PATH=${PWD}/mkosi.output.qemu/fedora~37/image.raw
export QEMU_JSON_OUTPUT=${PWD}/mkosi.output.qemu/fedora~37/image-upload.json
upload/upload_qemu.sh
# Warning! Never set `--version` to a value that is already used for a release image.
# Instead, use a `ref` that corresponds to your branch name.
bazel run //image/upload -- qemu --verbose --raw-image mkosi.output.qemu/fedora~37/image.raw --variant "default" --version ref/foo/stream/nightly/v2.7.0-pre-asdf
```
</details>

18
image/upload/BUILD.bazel Normal file
View 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"],
)

View file

@ -0,0 +1 @@

View 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",
],
)

View 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)
}

View 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)
}

View 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)
}

View 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
}

View 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)
}

View 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)
}
}

View 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)
}

View 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)
}

View 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)
}

View 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
}

View 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
View 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)
}
}