mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-09-26 03:21:06 -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
0
image/BUILD.bazel
Normal file
0
image/BUILD.bazel
Normal 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
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