CLI: use global image version field

- Restructure config by removing CSP-specific image references
- Add global image field
- Download image lookup table on create
- Download QEMU image on QEMU create
This commit is contained in:
Malte Poll 2022-11-22 18:47:08 +01:00 committed by Malte Poll
parent 9222468d3b
commit 575b6e93f6
21 changed files with 1068 additions and 380 deletions

View File

@ -8,9 +8,11 @@ package cloudcmd
import (
"context"
"io"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
)
type terraformClient interface {
@ -25,3 +27,11 @@ type libvirtRunner interface {
Start(ctx context.Context, containerName, imageName string) error
Stop(ctx context.Context) error
}
type imageFetcher interface {
FetchReference(ctx context.Context, config *config.Config) (string, error)
}
type rawDownloader interface {
Download(ctx context.Context, errWriter io.Writer, isTTY bool, source, version string) (string, error)
}

View File

@ -8,10 +8,12 @@ package cloudcmd
import (
"context"
"io"
"testing"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"go.uber.org/goleak"
)
@ -72,3 +74,21 @@ func (r *stubLibvirtRunner) Stop(context.Context) error {
r.stopCalled = true
return r.stopErr
}
type stubImageFetcher struct {
reference string
fetchReferenceErr error
}
func (f *stubImageFetcher) FetchReference(_ context.Context, _ *config.Config) (string, error) {
return f.reference, f.fetchReferenceErr
}
type stubRawDownloader struct {
destination string
downloadErr error
}
func (d *stubRawDownloader) Download(_ context.Context, _ io.Writer, _ bool, _ string, _ string) (string, error) {
return d.destination, d.downloadErr
}

View File

@ -22,31 +22,43 @@ import (
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/image"
)
// Creator creates cloud resources.
type Creator struct {
out io.Writer
image imageFetcher
newTerraformClient func(ctx context.Context) (terraformClient, error)
newLibvirtRunner func() libvirtRunner
newRawDownloader func() rawDownloader
}
// NewCreator creates a new creator.
func NewCreator(out io.Writer) *Creator {
return &Creator{
out: out,
out: out,
image: image.New(),
newTerraformClient: func(ctx context.Context) (terraformClient, error) {
return terraform.New(ctx, constants.TerraformWorkingDir)
},
newLibvirtRunner: func() libvirtRunner {
return libvirt.New()
},
newRawDownloader: func() rawDownloader {
return image.NewDownloader()
},
}
}
// Create creates the handed amount of instances and all the needed resources.
func (c *Creator) Create(ctx context.Context, provider cloudprovider.Provider, config *config.Config, name, insType string, controlPlaneCount, workerCount int,
) (clusterid.File, error) {
image, err := c.image.FetchReference(ctx, config)
if err != nil {
return clusterid.File{}, fmt.Errorf("fetching image reference: %w", err)
}
switch provider {
case cloudprovider.AWS:
cl, err := c.newTerraformClient(ctx)
@ -54,21 +66,21 @@ func (c *Creator) Create(ctx context.Context, provider cloudprovider.Provider, c
return clusterid.File{}, err
}
defer cl.RemoveInstaller()
return c.createAWS(ctx, cl, config, name, insType, controlPlaneCount, workerCount)
return c.createAWS(ctx, cl, config, name, insType, controlPlaneCount, workerCount, image)
case cloudprovider.GCP:
cl, err := c.newTerraformClient(ctx)
if err != nil {
return clusterid.File{}, err
}
defer cl.RemoveInstaller()
return c.createGCP(ctx, cl, config, name, insType, controlPlaneCount, workerCount)
return c.createGCP(ctx, cl, config, name, insType, controlPlaneCount, workerCount, image)
case cloudprovider.Azure:
cl, err := c.newTerraformClient(ctx)
if err != nil {
return clusterid.File{}, err
}
defer cl.RemoveInstaller()
return c.createAzure(ctx, cl, config, name, insType, controlPlaneCount, workerCount)
return c.createAzure(ctx, cl, config, name, insType, controlPlaneCount, workerCount, image)
case cloudprovider.QEMU:
if runtime.GOARCH != "amd64" || runtime.GOOS != "linux" {
return clusterid.File{}, fmt.Errorf("creation of a QEMU based Constellation is not supported for %s/%s", runtime.GOOS, runtime.GOARCH)
@ -79,14 +91,14 @@ func (c *Creator) Create(ctx context.Context, provider cloudprovider.Provider, c
}
defer cl.RemoveInstaller()
lv := c.newLibvirtRunner()
return c.createQEMU(ctx, cl, lv, name, config, controlPlaneCount, workerCount)
return c.createQEMU(ctx, cl, lv, name, config, controlPlaneCount, workerCount, image)
default:
return clusterid.File{}, fmt.Errorf("unsupported cloud provider: %s", provider)
}
}
func (c *Creator) createAWS(ctx context.Context, cl terraformClient, config *config.Config,
name, insType string, controlPlaneCount, workerCount int,
name, insType string, controlPlaneCount, workerCount int, image string,
) (idFile clusterid.File, retErr error) {
vars := terraform.AWSVariables{
CommonVariables: terraform.CommonVariables{
@ -99,7 +111,7 @@ func (c *Creator) createAWS(ctx context.Context, cl terraformClient, config *con
Region: config.Provider.AWS.Region,
Zone: config.Provider.AWS.Zone,
InstanceType: insType,
AMIImageID: config.Provider.AWS.Image,
AMIImageID: image,
IAMProfileControlPlane: config.Provider.AWS.IAMProfileControlPlane,
IAMProfileWorkerNodes: config.Provider.AWS.IAMProfileWorkerNodes,
Debug: config.IsDebugCluster(),
@ -122,7 +134,7 @@ func (c *Creator) createAWS(ctx context.Context, cl terraformClient, config *con
}
func (c *Creator) createGCP(ctx context.Context, cl terraformClient, config *config.Config,
name, insType string, controlPlaneCount, workerCount int,
name, insType string, controlPlaneCount, workerCount int, image string,
) (idFile clusterid.File, retErr error) {
vars := terraform.GCPVariables{
CommonVariables: terraform.CommonVariables{
@ -137,7 +149,7 @@ func (c *Creator) createGCP(ctx context.Context, cl terraformClient, config *con
CredentialsFile: config.Provider.GCP.ServiceAccountKeyPath,
InstanceType: insType,
StateDiskType: config.Provider.GCP.StateDiskType,
ImageID: config.Provider.GCP.Image,
ImageID: image,
Debug: config.IsDebugCluster(),
}
@ -158,7 +170,7 @@ func (c *Creator) createGCP(ctx context.Context, cl terraformClient, config *con
}
func (c *Creator) createAzure(ctx context.Context, cl terraformClient, config *config.Config,
name, insType string, controlPlaneCount, workerCount int,
name, insType string, controlPlaneCount, workerCount int, image string,
) (idFile clusterid.File, retErr error) {
vars := terraform.AzureVariables{
CommonVariables: terraform.CommonVariables{
@ -172,7 +184,7 @@ func (c *Creator) createAzure(ctx context.Context, cl terraformClient, config *c
UserAssignedIdentity: config.Provider.Azure.UserAssignedIdentity,
InstanceType: insType,
StateDiskType: config.Provider.Azure.StateDiskType,
ImageID: config.Provider.Azure.Image,
ImageID: image,
ConfidentialVM: *config.Provider.Azure.ConfidentialVM,
SecureBoot: *config.Provider.Azure.SecureBoot,
Debug: config.IsDebugCluster(),
@ -223,11 +235,18 @@ func normalizeAzureURIs(vars terraform.AzureVariables) terraform.AzureVariables
}
func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirtRunner, name string, config *config.Config,
controlPlaneCount, workerCount int,
controlPlaneCount, workerCount int, source string,
) (idFile clusterid.File, retErr error) {
qemuRollbacker := &rollbackerQEMU{client: cl, libvirt: lv, createdWorkspace: false}
defer rollbackOnError(context.Background(), c.out, &retErr, qemuRollbacker)
// TODO: render progress bar
downloader := c.newRawDownloader()
imagePath, err := downloader.Download(ctx, c.out, false, source, config.Image)
if err != nil {
return clusterid.File{}, fmt.Errorf("download raw image: %w", err)
}
libvirtURI := config.Provider.QEMU.LibvirtURI
libvirtSocketPath := "."
@ -273,7 +292,7 @@ func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirt
},
LibvirtURI: libvirtURI,
LibvirtSocketPath: libvirtSocketPath,
ImagePath: config.Provider.QEMU.Image,
ImagePath: imagePath,
ImageFormat: config.Provider.QEMU.ImageFormat,
CPUCount: config.Provider.QEMU.VCPUs,
MemorySizeMiB: config.Provider.QEMU.Memory,

View File

@ -98,12 +98,20 @@ func TestCreator(t *testing.T) {
creator := &Creator{
out: &bytes.Buffer{},
image: &stubImageFetcher{
reference: "some-image",
},
newTerraformClient: func(ctx context.Context) (terraformClient, error) {
return tc.tfClient, tc.newTfClientErr
},
newLibvirtRunner: func() libvirtRunner {
return tc.libvirt
},
newRawDownloader: func() rawDownloader {
return &stubRawDownloader{
destination: "some-destination",
}
},
}
idFile, err := creator.Create(context.Background(), tc.provider, tc.config, "name", "type", 2, 3)

View File

@ -11,13 +11,13 @@ import (
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/image"
"github.com/edgelesssys/constellation/v2/internal/sigstore"
"github.com/spf13/afero"
"github.com/spf13/cobra"
@ -49,10 +49,10 @@ func runConfigFetchMeasurements(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("constructing Rekor client: %w", err)
}
return configFetchMeasurements(cmd, rekor, fileHandler, http.DefaultClient)
return configFetchMeasurements(cmd, rekor, fileHandler, http.DefaultClient, image.New())
}
func configFetchMeasurements(cmd *cobra.Command, verifier rekorVerifier, fileHandler file.Handler, client *http.Client) error {
func configFetchMeasurements(cmd *cobra.Command, verifier rekorVerifier, fileHandler file.Handler, client *http.Client, img imageFetcher) error {
flags, err := parseFetchMeasurementsFlags(cmd)
if err != nil {
return err
@ -63,16 +63,17 @@ func configFetchMeasurements(cmd *cobra.Command, verifier rekorVerifier, fileHan
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
}
if conf.IsDebugImage() {
if !conf.IsReleaseImage() {
cmd.PrintErrln("Configured image doesn't look like a released production image. Double check image before deploying to production.")
}
if err := flags.updateURLs(conf); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
if err := flags.updateURLs(ctx, conf, img); err != nil {
return err
}
var fetchedMeasurements measurements.M
hash, err := fetchedMeasurements.FetchAndVerify(ctx, client, flags.measurementsURL, flags.signatureURL, []byte(constants.CosignPublicKey))
if err != nil {
@ -128,9 +129,15 @@ func parseFetchMeasurementsFlags(cmd *cobra.Command) (*fetchMeasurementsFlags, e
}, nil
}
func (f *fetchMeasurementsFlags) updateURLs(conf *config.Config) error {
func (f *fetchMeasurementsFlags) updateURLs(ctx context.Context, conf *config.Config, img imageFetcher) error {
imageRef, err := img.FetchReference(ctx, conf)
if err != nil {
return err
}
if f.measurementsURL == nil {
parsedURL, err := url.Parse(constants.S3PublicBucket + strings.ToLower(conf.Image()) + "/measurements.yaml")
// TODO(AB#2644): resolve image version to reference
parsedURL, err := url.Parse(constants.S3PublicBucket + imageRef + "/measurements.yaml")
if err != nil {
return err
}
@ -138,7 +145,7 @@ func (f *fetchMeasurementsFlags) updateURLs(conf *config.Config) error {
}
if f.signatureURL == nil {
parsedURL, err := url.Parse(constants.S3PublicBucket + strings.ToLower(conf.Image()) + "/measurements.yaml.sig")
parsedURL, err := url.Parse(constants.S3PublicBucket + imageRef + "/measurements.yaml.sig")
if err != nil {
return err
}
@ -146,3 +153,7 @@ func (f *fetchMeasurementsFlags) updateURLs(conf *config.Config) error {
}
return nil
}
type imageFetcher interface {
FetchReference(ctx context.Context, config *config.Config) (string, error)
}

View File

@ -8,6 +8,7 @@ package cmd
import (
"bytes"
"context"
"errors"
"io"
"net/http"
@ -102,10 +103,9 @@ func TestUpdateURLs(t *testing.T) {
}{
"both values nil": {
conf: &config.Config{
Image: "someImageVersion",
Provider: config.ProviderConfig{
GCP: &config.GCPConfig{
Image: "some/image/path/image-123456",
},
GCP: &config.GCPConfig{},
},
},
flags: &fetchMeasurementsFlags{},
@ -127,7 +127,9 @@ func TestUpdateURLs(t *testing.T) {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
err := tc.flags.updateURLs(tc.conf)
err := tc.flags.updateURLs(context.Background(), tc.conf, &stubImageFetcher{
reference: "some/image/path/image-123456",
})
assert.NoError(err)
assert.Equal(tc.wantMeasurementsURL, tc.flags.measurementsURL.String())
})
@ -162,14 +164,14 @@ func TestConfigFetchMeasurements(t *testing.T) {
signature := "MEUCIFdJ5dH6HDywxQWTUh9Bw77wMrq0mNCUjMQGYP+6QsVmAiEAmazj/L7rFGA4/Gz8y+kI5h5E5cDgc3brihvXBKF6qZA="
client := newTestClient(func(req *http.Request) *http.Response {
if req.URL.String() == "https://public-edgeless-constellation.s3.us-east-2.amazonaws.com/projects/constellation-images/global/images/constellation-coreos-1658216163/measurements.yaml" {
if req.URL.String() == "https://public-edgeless-constellation.s3.us-east-2.amazonaws.com/someImage/measurements.yaml" {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(measurements)),
Header: make(http.Header),
}
}
if req.URL.String() == "https://public-edgeless-constellation.s3.us-east-2.amazonaws.com/projects/constellation-images/global/images/constellation-coreos-1658216163/measurements.yaml.sig" {
if req.URL.String() == "https://public-edgeless-constellation.s3.us-east-2.amazonaws.com/someImage/measurements.yaml.sig" {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(signature)),
@ -213,12 +215,23 @@ func TestConfigFetchMeasurements(t *testing.T) {
fileHandler := file.NewHandler(afero.NewMemMapFs())
gcpConfig := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.GCP)
gcpConfig.Provider.GCP.Image = "projects/constellation-images/global/images/constellation-coreos-1658216163"
gcpConfig.Image = "someImage"
err := fileHandler.WriteYAML(constants.ConfigFilename, gcpConfig, file.OptMkdirAll)
require.NoError(err)
assert.NoError(configFetchMeasurements(cmd, tc.verifier, fileHandler, client))
assert.NoError(configFetchMeasurements(cmd, tc.verifier, fileHandler, client, &stubImageFetcher{
reference: "someImage",
}))
})
}
}
type stubImageFetcher struct {
reference string
fetchReferenceErr error
}
func (f *stubImageFetcher) FetchReference(_ context.Context, _ *config.Config) (string, error) {
return f.reference, f.fetchReferenceErr
}

View File

@ -67,7 +67,7 @@ func create(cmd *cobra.Command, creator cloudCreator, fileHandler file.Handler,
}
var printedAWarning bool
if conf.IsDebugImage() {
if !conf.IsReleaseImage() {
cmd.PrintErrln("Configured image doesn't look like a released production image. Double check image before deploying to production.")
printedAWarning = true
}

View File

@ -388,8 +388,8 @@ func TestAttestation(t *testing.T) {
require.NoError(fileHandler.WriteJSON(constants.ClusterIDsFileName, existingIDFile, file.OptNone))
cfg := config.Default()
cfg.Image = "image"
cfg.RemoveProviderExcept(cloudprovider.QEMU)
cfg.Provider.QEMU.Image = "some/image/location"
cfg.Provider.QEMU.Measurements[0] = measurements.PCRWithAllBytes(0x00)
cfg.Provider.QEMU.Measurements[1] = measurements.PCRWithAllBytes(0x11)
cfg.Provider.QEMU.Measurements[2] = measurements.PCRWithAllBytes(0x22)
@ -463,13 +463,14 @@ func (s *stubInitServer) Init(ctx context.Context, req *initproto.InitRequest) (
func defaultConfigWithExpectedMeasurements(t *testing.T, conf *config.Config, csp cloudprovider.Provider) *config.Config {
t.Helper()
conf.Image = "image"
switch csp {
case cloudprovider.Azure:
conf.Provider.Azure.SubscriptionID = "01234567-0123-0123-0123-0123456789ab"
conf.Provider.Azure.TenantID = "01234567-0123-0123-0123-0123456789ab"
conf.Provider.Azure.Location = "test-location"
conf.Provider.Azure.UserAssignedIdentity = "test-identity"
conf.Provider.Azure.Image = "some/image/location"
conf.Provider.Azure.ResourceGroup = "test-resource-group"
conf.Provider.Azure.AppClientID = "01234567-0123-0123-0123-0123456789ab"
conf.Provider.Azure.ClientSecretValue = "test-client-secret"
@ -479,14 +480,12 @@ func defaultConfigWithExpectedMeasurements(t *testing.T, conf *config.Config, cs
case cloudprovider.GCP:
conf.Provider.GCP.Region = "test-region"
conf.Provider.GCP.Project = "test-project"
conf.Provider.GCP.Image = "some/image/location"
conf.Provider.GCP.Zone = "test-zone"
conf.Provider.GCP.ServiceAccountKeyPath = "test-key-path"
conf.Provider.GCP.Measurements[4] = measurements.PCRWithAllBytes(0x44)
conf.Provider.GCP.Measurements[9] = measurements.PCRWithAllBytes(0x11)
conf.Provider.GCP.Measurements[12] = measurements.PCRWithAllBytes(0xcc)
case cloudprovider.QEMU:
conf.Provider.QEMU.Image = "some/image/location"
conf.Provider.QEMU.Measurements[4] = measurements.PCRWithAllBytes(0x44)
conf.Provider.QEMU.Measurements[9] = measurements.PCRWithAllBytes(0x11)
conf.Provider.QEMU.Measurements[12] = measurements.PCRWithAllBytes(0xcc)

View File

@ -13,7 +13,6 @@ import (
"fmt"
"io"
"net"
"net/http"
"os"
"runtime"
"strings"
@ -26,8 +25,6 @@ import (
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/grpc/dialer"
"github.com/edgelesssys/constellation/v2/internal/license"
"github.com/edgelesssys/constellation/v2/internal/versions"
"github.com/schollz/progressbar/v3"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"golang.org/x/sys/unix"
@ -188,23 +185,9 @@ func prepareConfig(cmd *cobra.Command, fileHandler file.Handler) (*config.Config
}
}
// download image to current directory if it doesn't exist
const imagePath = "./constellation.raw"
if _, err := os.Stat(imagePath); err == nil {
cmd.Printf("Using existing image at %s\n\n", imagePath)
} else if errors.Is(err, os.ErrNotExist) {
cmd.Printf("Downloading image to %s\n", imagePath)
if err := installImage(cmd.Context(), cmd.ErrOrStderr(), versions.ConstellationQEMUImageURL, imagePath); err != nil {
return nil, fmt.Errorf("downloading image to %s: %w", imagePath, err)
}
} else {
return nil, fmt.Errorf("checking if image exists: %w", err)
}
config := config.Default()
config.RemoveProviderExcept(cloudprovider.QEMU)
config.StateDiskSizeGB = 8
config.Provider.QEMU.Image = imagePath
return config, fileHandler.WriteYAML(constants.ConfigFilename, config, file.OptOverwrite)
}
@ -245,51 +228,3 @@ func initializeMiniCluster(cmd *cobra.Command, fileHandler file.Handler, spinner
}
return nil
}
// installImage downloads the image from sourceURL to the destination.
func installImage(ctx context.Context, errWriter io.Writer, sourceURL, destination string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("downloading image: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("downloading image: %s", resp.Status)
}
f, err := os.OpenFile(destination, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
bar := progressbar.NewOptions64(
resp.ContentLength,
progressbar.OptionSetWriter(errWriter),
progressbar.OptionShowBytes(true),
progressbar.OptionSetPredictTime(true),
progressbar.OptionFullWidth(),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "=",
SaucerHead: ">",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}),
progressbar.OptionClearOnFinish(),
progressbar.OptionOnCompletion(func() { fmt.Fprintf(errWriter, "Done.\n\n") }),
)
defer bar.Close()
_, err = io.Copy(io.MultiWriter(f, bar), resp.Body)
if err != nil {
return err
}
return nil
}

View File

@ -61,7 +61,7 @@ func deploy(cmd *cobra.Command, fileHandler file.Handler, constellationConfig *c
return err
}
if !constellationConfig.IsDebugImage() {
if constellationConfig.IsReleaseImage() {
log.Println("WARNING: Constellation image does not look like a debug image. Are you using a debug image?")
}

View File

@ -15,7 +15,7 @@ import (
"fmt"
"io/fs"
"os"
"regexp"
"strings"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
@ -38,17 +38,15 @@ const (
Version1 = "v1"
)
var (
azureReleaseImageRegex = regexp.MustCompile(`^(?i)\/CommunityGalleries\/ConstellationCVM-b3782fa0-0df7-4f2f-963e-fc7fc42663df\/Images\/constellation\/Versions\/[\d]+.[\d]+.[\d]+$`)
gcpReleaseImageRegex = regexp.MustCompile(`^projects\/constellation-images\/global\/images\/constellation-v[\d]+-[\d]+-[\d]+$`)
)
// Config defines configuration used by CLI.
type Config struct {
// description: |
// Schema version of this configuration file.
Version string `yaml:"version" validate:"eq=v1"`
// description: |
// Machine image used to create Constellation nodes.
Image string `yaml:"image" validate:"required,safe_image"`
// description: |
// Size (in GB) of a node's disk to store the non-volatile state.
StateDiskSizeGB int `yaml:"stateDiskSizeGB" validate:"min=0"`
// description: |
@ -75,7 +73,7 @@ type Config struct {
// UpgradeConfig defines configuration used during constellation upgrade.
type UpgradeConfig struct {
// description: |
// Updated machine image to install on all nodes.
// Updated Constellation machine image to install on all nodes.
Image string `yaml:"image"`
// description: |
// Measurements of the updated image.
@ -127,9 +125,6 @@ type AWSConfig struct {
// AWS data center zone name in defined region. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones
Zone string `yaml:"zone" validate:"required"`
// description: |
// AMI ID of the machine image used to create Constellation nodes.
Image string `yaml:"image" validate:"required"`
// description: |
// VM instance type to use for Constellation nodes. Needs to support NitroTPM. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-prerequisites.html
InstanceType string `yaml:"instanceType" validate:"lowercase,aws_instance_type"`
// description: |
@ -173,9 +168,6 @@ type AzureConfig struct {
// Client secret value of the Active Directory app registration credentials. Alternatively leave empty and pass value via CONSTELL_AZURE_CLIENT_SECRET_VALUE environment variable.
ClientSecretValue string `yaml:"clientSecretValue" validate:"required"`
// description: |
// Machine image used to create Constellation nodes.
Image string `yaml:"image" validate:"required"`
// description: |
// VM instance type to use for Constellation nodes.
InstanceType string `yaml:"instanceType" validate:"azure_instance_type"`
// description: |
@ -219,9 +211,6 @@ type GCPConfig struct {
// Path of service account key file. For required service account roles, see https://docs.edgeless.systems/constellation/getting-started/install#authorization
ServiceAccountKeyPath string `yaml:"serviceAccountKeyPath" validate:"required"`
// description: |
// Machine image used to create Constellation nodes.
Image string `yaml:"image" validate:"required"`
// description: |
// VM instance type to use for Constellation nodes.
InstanceType string `yaml:"instanceType" validate:"gcp_instance_type"`
// description: |
@ -240,9 +229,6 @@ type GCPConfig struct {
// QEMUConfig holds config information for QEMU based Constellation deployments.
type QEMUConfig struct {
// description: |
// Path to the image to use for the VMs.
Image string `yaml:"image" validate:"required"`
// description: |
// Format of the image to use for the VMs. Should be either qcow2 or raw.
ImageFormat string `yaml:"imageFormat" validate:"oneof=qcow2 raw"`
@ -279,12 +265,12 @@ type QEMUConfig struct {
func Default() *Config {
return &Config{
Version: Version1,
Image: defaultImage,
StateDiskSizeGB: 30,
DebugCluster: func() *bool { b := false; return &b }(),
Provider: ProviderConfig{
AWS: &AWSConfig{
Region: "",
Image: "",
InstanceType: "m6a.xlarge",
StateDiskType: "gp3",
IAMProfileControlPlane: "",
@ -298,7 +284,6 @@ func Default() *Config {
Location: "",
UserAssignedIdentity: "",
ResourceGroup: "",
Image: DefaultImageAzure,
InstanceType: "Standard_DC4as_v5",
StateDiskType: "Premium_LRS",
DeployCSIDriver: func() *bool { b := true; return &b }(),
@ -314,7 +299,6 @@ func Default() *Config {
Region: "",
Zone: "",
ServiceAccountKeyPath: "",
Image: DefaultImageGCP,
InstanceType: "n2d-standard-4",
StateDiskType: "pd-ssd",
DeployCSIDriver: func() *bool { b := true; return &b }(),
@ -386,22 +370,6 @@ func (c *Config) HasProvider(provider cloudprovider.Provider) bool {
return false
}
// Image returns OS image for the configured cloud provider.
// If multiple cloud providers are configured (which is not supported)
// only a single image is returned.
func (c *Config) Image() string {
if c.HasProvider(cloudprovider.AWS) {
return c.Provider.AWS.Image
}
if c.HasProvider(cloudprovider.Azure) {
return c.Provider.Azure.Image
}
if c.HasProvider(cloudprovider.GCP) {
return c.Provider.GCP.Image
}
return ""
}
// UpdateMeasurements overwrites measurements in config with the provided ones.
func (c *Config) UpdateMeasurements(newMeasurements Measurements) {
if c.Provider.AWS != nil {
@ -451,21 +419,9 @@ func (c *Config) IsDebugCluster() bool {
return false
}
// IsDebugImage checks whether image name looks like a release image, if not it is
// probably a debug image. In the end we do not if bootstrapper or debugd
// was put inside an image just by looking at its name.
func (c *Config) IsDebugImage() bool {
switch {
case c.Provider.AWS != nil:
// TODO: Add proper image name validation for AWS as part of rfc/image-discoverability.md
return false
case c.Provider.Azure != nil:
return !azureReleaseImageRegex.MatchString(c.Provider.Azure.Image)
case c.Provider.GCP != nil:
return !gcpReleaseImageRegex.MatchString(c.Provider.GCP.Image)
default:
return false
}
// IsReleaseImage checks whether image name looks like a release image.
func (c *Config) IsReleaseImage() bool {
return strings.HasPrefix(c.Image, "v")
}
// GetProvider returns the configured cloud provider.
@ -543,6 +499,10 @@ func (c *Config) Validate() error {
return err
}
if err := validate.RegisterValidation("safe_image", validateImage); err != nil {
return err
}
// register custom validator with label supported_k8s_version to validate version based on available versionConfigs.
if err := validate.RegisterValidation("supported_k8s_version", validateK8sVersion); err != nil {
return err

View File

@ -25,46 +25,51 @@ func init() {
ConfigDoc.Type = "Config"
ConfigDoc.Comments[encoder.LineComment] = "Config defines configuration used by CLI."
ConfigDoc.Description = "Config defines configuration used by CLI."
ConfigDoc.Fields = make([]encoder.Doc, 7)
ConfigDoc.Fields = make([]encoder.Doc, 8)
ConfigDoc.Fields[0].Name = "version"
ConfigDoc.Fields[0].Type = "string"
ConfigDoc.Fields[0].Note = ""
ConfigDoc.Fields[0].Description = "Schema version of this configuration file."
ConfigDoc.Fields[0].Comments[encoder.LineComment] = "Schema version of this configuration file."
ConfigDoc.Fields[1].Name = "stateDiskSizeGB"
ConfigDoc.Fields[1].Type = "int"
ConfigDoc.Fields[1].Name = "image"
ConfigDoc.Fields[1].Type = "string"
ConfigDoc.Fields[1].Note = ""
ConfigDoc.Fields[1].Description = "Size (in GB) of a node's disk to store the non-volatile state."
ConfigDoc.Fields[1].Comments[encoder.LineComment] = "Size (in GB) of a node's disk to store the non-volatile state."
ConfigDoc.Fields[2].Name = "kubernetesVersion"
ConfigDoc.Fields[2].Type = "string"
ConfigDoc.Fields[1].Description = "Machine image used to create Constellation nodes."
ConfigDoc.Fields[1].Comments[encoder.LineComment] = "Machine image used to create Constellation nodes."
ConfigDoc.Fields[2].Name = "stateDiskSizeGB"
ConfigDoc.Fields[2].Type = "int"
ConfigDoc.Fields[2].Note = ""
ConfigDoc.Fields[2].Description = "Kubernetes version to be installed in the cluster."
ConfigDoc.Fields[2].Comments[encoder.LineComment] = "Kubernetes version to be installed in the cluster."
ConfigDoc.Fields[3].Name = "debugCluster"
ConfigDoc.Fields[3].Type = "bool"
ConfigDoc.Fields[2].Description = "Size (in GB) of a node's disk to store the non-volatile state."
ConfigDoc.Fields[2].Comments[encoder.LineComment] = "Size (in GB) of a node's disk to store the non-volatile state."
ConfigDoc.Fields[3].Name = "kubernetesVersion"
ConfigDoc.Fields[3].Type = "string"
ConfigDoc.Fields[3].Note = ""
ConfigDoc.Fields[3].Description = "DON'T USE IN PRODUCTION: enable debug mode and use debug images. For usage, see: https://github.com/edgelesssys/constellation/blob/main/debugd/README.md"
ConfigDoc.Fields[3].Comments[encoder.LineComment] = "DON'T USE IN PRODUCTION: enable debug mode and use debug images. For usage, see: https://github.com/edgelesssys/constellation/blob/main/debugd/README.md"
ConfigDoc.Fields[4].Name = "provider"
ConfigDoc.Fields[4].Type = "ProviderConfig"
ConfigDoc.Fields[3].Description = "Kubernetes version to be installed in the cluster."
ConfigDoc.Fields[3].Comments[encoder.LineComment] = "Kubernetes version to be installed in the cluster."
ConfigDoc.Fields[4].Name = "debugCluster"
ConfigDoc.Fields[4].Type = "bool"
ConfigDoc.Fields[4].Note = ""
ConfigDoc.Fields[4].Description = "Supported cloud providers and their specific configurations."
ConfigDoc.Fields[4].Comments[encoder.LineComment] = "Supported cloud providers and their specific configurations."
ConfigDoc.Fields[5].Name = "sshUsers"
ConfigDoc.Fields[5].Type = "[]UserKey"
ConfigDoc.Fields[4].Description = "DON'T USE IN PRODUCTION: enable debug mode and use debug images. For usage, see: https://github.com/edgelesssys/constellation/blob/main/debugd/README.md"
ConfigDoc.Fields[4].Comments[encoder.LineComment] = "DON'T USE IN PRODUCTION: enable debug mode and use debug images. For usage, see: https://github.com/edgelesssys/constellation/blob/main/debugd/README.md"
ConfigDoc.Fields[5].Name = "provider"
ConfigDoc.Fields[5].Type = "ProviderConfig"
ConfigDoc.Fields[5].Note = ""
ConfigDoc.Fields[5].Description = "Deprecated: Does nothing! To get node SSH access, see: https://constellation-docs.edgeless.systems/constellation/workflows/troubleshooting#connect-to-nodes-via-ssh"
ConfigDoc.Fields[5].Comments[encoder.LineComment] = "Deprecated: Does nothing! To get node SSH access, see: https://constellation-docs.edgeless.systems/constellation/workflows/troubleshooting#connect-to-nodes-via-ssh"
ConfigDoc.Fields[5].AddExample("", []UserKey{{Username: "Alice", PublicKey: "ssh-rsa AAAAB3NzaC...5QXHKW1rufgtJeSeJ8= alice@domain.com"}})
ConfigDoc.Fields[6].Name = "upgrade"
ConfigDoc.Fields[6].Type = "UpgradeConfig"
ConfigDoc.Fields[5].Description = "Supported cloud providers and their specific configurations."
ConfigDoc.Fields[5].Comments[encoder.LineComment] = "Supported cloud providers and their specific configurations."
ConfigDoc.Fields[6].Name = "sshUsers"
ConfigDoc.Fields[6].Type = "[]UserKey"
ConfigDoc.Fields[6].Note = ""
ConfigDoc.Fields[6].Description = "Configuration to apply during constellation upgrade."
ConfigDoc.Fields[6].Comments[encoder.LineComment] = "Configuration to apply during constellation upgrade."
ConfigDoc.Fields[6].Description = "Deprecated: Does nothing! To get node SSH access, see: https://constellation-docs.edgeless.systems/constellation/workflows/troubleshooting#connect-to-nodes-via-ssh"
ConfigDoc.Fields[6].Comments[encoder.LineComment] = "Deprecated: Does nothing! To get node SSH access, see: https://constellation-docs.edgeless.systems/constellation/workflows/troubleshooting#connect-to-nodes-via-ssh"
ConfigDoc.Fields[6].AddExample("", UpgradeConfig{Image: "", Measurements: Measurements{}})
ConfigDoc.Fields[6].AddExample("", []UserKey{{Username: "Alice", PublicKey: "ssh-rsa AAAAB3NzaC...5QXHKW1rufgtJeSeJ8= alice@domain.com"}})
ConfigDoc.Fields[7].Name = "upgrade"
ConfigDoc.Fields[7].Type = "UpgradeConfig"
ConfigDoc.Fields[7].Note = ""
ConfigDoc.Fields[7].Description = "Configuration to apply during constellation upgrade."
ConfigDoc.Fields[7].Comments[encoder.LineComment] = "Configuration to apply during constellation upgrade."
ConfigDoc.Fields[7].AddExample("", UpgradeConfig{Image: "", Measurements: Measurements{}})
UpgradeConfigDoc.Type = "UpgradeConfig"
UpgradeConfigDoc.Comments[encoder.LineComment] = "UpgradeConfig defines configuration used during constellation upgrade."
@ -81,8 +86,8 @@ func init() {
UpgradeConfigDoc.Fields[0].Name = "image"
UpgradeConfigDoc.Fields[0].Type = "string"
UpgradeConfigDoc.Fields[0].Note = ""
UpgradeConfigDoc.Fields[0].Description = "Updated machine image to install on all nodes."
UpgradeConfigDoc.Fields[0].Comments[encoder.LineComment] = "Updated machine image to install on all nodes."
UpgradeConfigDoc.Fields[0].Description = "Updated Constellation machine image to install on all nodes."
UpgradeConfigDoc.Fields[0].Comments[encoder.LineComment] = "Updated Constellation machine image to install on all nodes."
UpgradeConfigDoc.Fields[1].Name = "measurements"
UpgradeConfigDoc.Fields[1].Type = "Measurements"
UpgradeConfigDoc.Fields[1].Note = ""
@ -152,7 +157,7 @@ func init() {
FieldName: "aws",
},
}
AWSConfigDoc.Fields = make([]encoder.Doc, 9)
AWSConfigDoc.Fields = make([]encoder.Doc, 8)
AWSConfigDoc.Fields[0].Name = "region"
AWSConfigDoc.Fields[0].Type = "string"
AWSConfigDoc.Fields[0].Note = ""
@ -163,41 +168,36 @@ func init() {
AWSConfigDoc.Fields[1].Note = ""
AWSConfigDoc.Fields[1].Description = "AWS data center zone name in defined region. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones"
AWSConfigDoc.Fields[1].Comments[encoder.LineComment] = "AWS data center zone name in defined region. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones"
AWSConfigDoc.Fields[2].Name = "image"
AWSConfigDoc.Fields[2].Name = "instanceType"
AWSConfigDoc.Fields[2].Type = "string"
AWSConfigDoc.Fields[2].Note = ""
AWSConfigDoc.Fields[2].Description = "AMI ID of the machine image used to create Constellation nodes."
AWSConfigDoc.Fields[2].Comments[encoder.LineComment] = "AMI ID of the machine image used to create Constellation nodes."
AWSConfigDoc.Fields[3].Name = "instanceType"
AWSConfigDoc.Fields[2].Description = "VM instance type to use for Constellation nodes. Needs to support NitroTPM. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-prerequisites.html"
AWSConfigDoc.Fields[2].Comments[encoder.LineComment] = "VM instance type to use for Constellation nodes. Needs to support NitroTPM. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-prerequisites.html"
AWSConfigDoc.Fields[3].Name = "stateDiskType"
AWSConfigDoc.Fields[3].Type = "string"
AWSConfigDoc.Fields[3].Note = ""
AWSConfigDoc.Fields[3].Description = "VM instance type to use for Constellation nodes. Needs to support NitroTPM. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-prerequisites.html"
AWSConfigDoc.Fields[3].Comments[encoder.LineComment] = "VM instance type to use for Constellation nodes. Needs to support NitroTPM. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-prerequisites.html"
AWSConfigDoc.Fields[4].Name = "stateDiskType"
AWSConfigDoc.Fields[3].Description = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html"
AWSConfigDoc.Fields[3].Comments[encoder.LineComment] = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html"
AWSConfigDoc.Fields[4].Name = "iamProfileControlPlane"
AWSConfigDoc.Fields[4].Type = "string"
AWSConfigDoc.Fields[4].Note = ""
AWSConfigDoc.Fields[4].Description = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html"
AWSConfigDoc.Fields[4].Comments[encoder.LineComment] = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html"
AWSConfigDoc.Fields[5].Name = "iamProfileControlPlane"
AWSConfigDoc.Fields[4].Description = "Name of the IAM profile to use for the control plane nodes."
AWSConfigDoc.Fields[4].Comments[encoder.LineComment] = "Name of the IAM profile to use for the control plane nodes."
AWSConfigDoc.Fields[5].Name = "iamProfileWorkerNodes"
AWSConfigDoc.Fields[5].Type = "string"
AWSConfigDoc.Fields[5].Note = ""
AWSConfigDoc.Fields[5].Description = "Name of the IAM profile to use for the control plane nodes."
AWSConfigDoc.Fields[5].Comments[encoder.LineComment] = "Name of the IAM profile to use for the control plane nodes."
AWSConfigDoc.Fields[6].Name = "iamProfileWorkerNodes"
AWSConfigDoc.Fields[6].Type = "string"
AWSConfigDoc.Fields[5].Description = "Name of the IAM profile to use for the worker nodes."
AWSConfigDoc.Fields[5].Comments[encoder.LineComment] = "Name of the IAM profile to use for the worker nodes."
AWSConfigDoc.Fields[6].Name = "measurements"
AWSConfigDoc.Fields[6].Type = "Measurements"
AWSConfigDoc.Fields[6].Note = ""
AWSConfigDoc.Fields[6].Description = "Name of the IAM profile to use for the worker nodes."
AWSConfigDoc.Fields[6].Comments[encoder.LineComment] = "Name of the IAM profile to use for the worker nodes."
AWSConfigDoc.Fields[7].Name = "measurements"
AWSConfigDoc.Fields[7].Type = "Measurements"
AWSConfigDoc.Fields[6].Description = "Expected VM measurements."
AWSConfigDoc.Fields[6].Comments[encoder.LineComment] = "Expected VM measurements."
AWSConfigDoc.Fields[7].Name = "enforcedMeasurements"
AWSConfigDoc.Fields[7].Type = "[]uint32"
AWSConfigDoc.Fields[7].Note = ""
AWSConfigDoc.Fields[7].Description = "Expected VM measurements."
AWSConfigDoc.Fields[7].Comments[encoder.LineComment] = "Expected VM measurements."
AWSConfigDoc.Fields[8].Name = "enforcedMeasurements"
AWSConfigDoc.Fields[8].Type = "[]uint32"
AWSConfigDoc.Fields[8].Note = ""
AWSConfigDoc.Fields[8].Description = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
AWSConfigDoc.Fields[8].Comments[encoder.LineComment] = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
AWSConfigDoc.Fields[7].Description = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
AWSConfigDoc.Fields[7].Comments[encoder.LineComment] = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
AzureConfigDoc.Type = "AzureConfig"
AzureConfigDoc.Comments[encoder.LineComment] = "AzureConfig are Azure specific configuration values used by the CLI."
@ -208,7 +208,7 @@ func init() {
FieldName: "azure",
},
}
AzureConfigDoc.Fields = make([]encoder.Doc, 17)
AzureConfigDoc.Fields = make([]encoder.Doc, 16)
AzureConfigDoc.Fields[0].Name = "subscription"
AzureConfigDoc.Fields[0].Type = "string"
AzureConfigDoc.Fields[0].Note = ""
@ -244,56 +244,51 @@ func init() {
AzureConfigDoc.Fields[6].Note = ""
AzureConfigDoc.Fields[6].Description = "Client secret value of the Active Directory app registration credentials. Alternatively leave empty and pass value via CONSTELL_AZURE_CLIENT_SECRET_VALUE environment variable."
AzureConfigDoc.Fields[6].Comments[encoder.LineComment] = "Client secret value of the Active Directory app registration credentials. Alternatively leave empty and pass value via CONSTELL_AZURE_CLIENT_SECRET_VALUE environment variable."
AzureConfigDoc.Fields[7].Name = "image"
AzureConfigDoc.Fields[7].Name = "instanceType"
AzureConfigDoc.Fields[7].Type = "string"
AzureConfigDoc.Fields[7].Note = ""
AzureConfigDoc.Fields[7].Description = "Machine image used to create Constellation nodes."
AzureConfigDoc.Fields[7].Comments[encoder.LineComment] = "Machine image used to create Constellation nodes."
AzureConfigDoc.Fields[8].Name = "instanceType"
AzureConfigDoc.Fields[7].Description = "VM instance type to use for Constellation nodes."
AzureConfigDoc.Fields[7].Comments[encoder.LineComment] = "VM instance type to use for Constellation nodes."
AzureConfigDoc.Fields[8].Name = "stateDiskType"
AzureConfigDoc.Fields[8].Type = "string"
AzureConfigDoc.Fields[8].Note = ""
AzureConfigDoc.Fields[8].Description = "VM instance type to use for Constellation nodes."
AzureConfigDoc.Fields[8].Comments[encoder.LineComment] = "VM instance type to use for Constellation nodes."
AzureConfigDoc.Fields[9].Name = "stateDiskType"
AzureConfigDoc.Fields[9].Type = "string"
AzureConfigDoc.Fields[8].Description = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://docs.microsoft.com/en-us/azure/virtual-machines/disks-types#disk-type-comparison"
AzureConfigDoc.Fields[8].Comments[encoder.LineComment] = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://docs.microsoft.com/en-us/azure/virtual-machines/disks-types#disk-type-comparison"
AzureConfigDoc.Fields[9].Name = "deployCSIDriver"
AzureConfigDoc.Fields[9].Type = "bool"
AzureConfigDoc.Fields[9].Note = ""
AzureConfigDoc.Fields[9].Description = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://docs.microsoft.com/en-us/azure/virtual-machines/disks-types#disk-type-comparison"
AzureConfigDoc.Fields[9].Comments[encoder.LineComment] = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://docs.microsoft.com/en-us/azure/virtual-machines/disks-types#disk-type-comparison"
AzureConfigDoc.Fields[10].Name = "deployCSIDriver"
AzureConfigDoc.Fields[9].Description = "Deploy Azure Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage"
AzureConfigDoc.Fields[9].Comments[encoder.LineComment] = "Deploy Azure Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage"
AzureConfigDoc.Fields[10].Name = "confidentialVM"
AzureConfigDoc.Fields[10].Type = "bool"
AzureConfigDoc.Fields[10].Note = ""
AzureConfigDoc.Fields[10].Description = "Deploy Azure Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage"
AzureConfigDoc.Fields[10].Comments[encoder.LineComment] = "Deploy Azure Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage"
AzureConfigDoc.Fields[11].Name = "confidentialVM"
AzureConfigDoc.Fields[10].Description = "Use Confidential VMs. If set to false, Trusted Launch VMs are used instead. See: https://docs.microsoft.com/en-us/azure/confidential-computing/confidential-vm-overview"
AzureConfigDoc.Fields[10].Comments[encoder.LineComment] = "Use Confidential VMs. If set to false, Trusted Launch VMs are used instead. See: https://docs.microsoft.com/en-us/azure/confidential-computing/confidential-vm-overview"
AzureConfigDoc.Fields[11].Name = "secureBoot"
AzureConfigDoc.Fields[11].Type = "bool"
AzureConfigDoc.Fields[11].Note = ""
AzureConfigDoc.Fields[11].Description = "Use Confidential VMs. If set to false, Trusted Launch VMs are used instead. See: https://docs.microsoft.com/en-us/azure/confidential-computing/confidential-vm-overview"
AzureConfigDoc.Fields[11].Comments[encoder.LineComment] = "Use Confidential VMs. If set to false, Trusted Launch VMs are used instead. See: https://docs.microsoft.com/en-us/azure/confidential-computing/confidential-vm-overview"
AzureConfigDoc.Fields[12].Name = "secureBoot"
AzureConfigDoc.Fields[12].Type = "bool"
AzureConfigDoc.Fields[11].Description = "Enable secure boot for VMs. If enabled, the OS image has to include a virtual machine guest state (VMGS) blob."
AzureConfigDoc.Fields[11].Comments[encoder.LineComment] = "Enable secure boot for VMs. If enabled, the OS image has to include a virtual machine guest state (VMGS) blob."
AzureConfigDoc.Fields[12].Name = "idKeyDigest"
AzureConfigDoc.Fields[12].Type = "string"
AzureConfigDoc.Fields[12].Note = ""
AzureConfigDoc.Fields[12].Description = "Enable secure boot for VMs. If enabled, the OS image has to include a virtual machine guest state (VMGS) blob."
AzureConfigDoc.Fields[12].Comments[encoder.LineComment] = "Enable secure boot for VMs. If enabled, the OS image has to include a virtual machine guest state (VMGS) blob."
AzureConfigDoc.Fields[13].Name = "idKeyDigest"
AzureConfigDoc.Fields[13].Type = "string"
AzureConfigDoc.Fields[12].Description = "Expected value for the field 'idkeydigest' in the AMD SEV-SNP attestation report. Only usable with ConfidentialVMs. See 4.6 and 7.3 in: https://www.amd.com/system/files/TechDocs/56860.pdf"
AzureConfigDoc.Fields[12].Comments[encoder.LineComment] = "Expected value for the field 'idkeydigest' in the AMD SEV-SNP attestation report. Only usable with ConfidentialVMs. See 4.6 and 7.3 in: https://www.amd.com/system/files/TechDocs/56860.pdf"
AzureConfigDoc.Fields[13].Name = "enforceIdKeyDigest"
AzureConfigDoc.Fields[13].Type = "bool"
AzureConfigDoc.Fields[13].Note = ""
AzureConfigDoc.Fields[13].Description = "Expected value for the field 'idkeydigest' in the AMD SEV-SNP attestation report. Only usable with ConfidentialVMs. See 4.6 and 7.3 in: https://www.amd.com/system/files/TechDocs/56860.pdf"
AzureConfigDoc.Fields[13].Comments[encoder.LineComment] = "Expected value for the field 'idkeydigest' in the AMD SEV-SNP attestation report. Only usable with ConfidentialVMs. See 4.6 and 7.3 in: https://www.amd.com/system/files/TechDocs/56860.pdf"
AzureConfigDoc.Fields[14].Name = "enforceIdKeyDigest"
AzureConfigDoc.Fields[14].Type = "bool"
AzureConfigDoc.Fields[13].Description = "Enforce the specified idKeyDigest value during remote attestation."
AzureConfigDoc.Fields[13].Comments[encoder.LineComment] = "Enforce the specified idKeyDigest value during remote attestation."
AzureConfigDoc.Fields[14].Name = "measurements"
AzureConfigDoc.Fields[14].Type = "Measurements"
AzureConfigDoc.Fields[14].Note = ""
AzureConfigDoc.Fields[14].Description = "Enforce the specified idKeyDigest value during remote attestation."
AzureConfigDoc.Fields[14].Comments[encoder.LineComment] = "Enforce the specified idKeyDigest value during remote attestation."
AzureConfigDoc.Fields[15].Name = "measurements"
AzureConfigDoc.Fields[15].Type = "Measurements"
AzureConfigDoc.Fields[14].Description = "Expected confidential VM measurements."
AzureConfigDoc.Fields[14].Comments[encoder.LineComment] = "Expected confidential VM measurements."
AzureConfigDoc.Fields[15].Name = "enforcedMeasurements"
AzureConfigDoc.Fields[15].Type = "[]uint32"
AzureConfigDoc.Fields[15].Note = ""
AzureConfigDoc.Fields[15].Description = "Expected confidential VM measurements."
AzureConfigDoc.Fields[15].Comments[encoder.LineComment] = "Expected confidential VM measurements."
AzureConfigDoc.Fields[16].Name = "enforcedMeasurements"
AzureConfigDoc.Fields[16].Type = "[]uint32"
AzureConfigDoc.Fields[16].Note = ""
AzureConfigDoc.Fields[16].Description = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
AzureConfigDoc.Fields[16].Comments[encoder.LineComment] = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
AzureConfigDoc.Fields[15].Description = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
AzureConfigDoc.Fields[15].Comments[encoder.LineComment] = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
GCPConfigDoc.Type = "GCPConfig"
GCPConfigDoc.Comments[encoder.LineComment] = "GCPConfig are GCP specific configuration values used by the CLI."
@ -304,7 +299,7 @@ func init() {
FieldName: "gcp",
},
}
GCPConfigDoc.Fields = make([]encoder.Doc, 10)
GCPConfigDoc.Fields = make([]encoder.Doc, 9)
GCPConfigDoc.Fields[0].Name = "project"
GCPConfigDoc.Fields[0].Type = "string"
GCPConfigDoc.Fields[0].Note = ""
@ -325,36 +320,31 @@ func init() {
GCPConfigDoc.Fields[3].Note = ""
GCPConfigDoc.Fields[3].Description = "Path of service account key file. For required service account roles, see https://docs.edgeless.systems/constellation/getting-started/install#authorization"
GCPConfigDoc.Fields[3].Comments[encoder.LineComment] = "Path of service account key file. For required service account roles, see https://docs.edgeless.systems/constellation/getting-started/install#authorization"
GCPConfigDoc.Fields[4].Name = "image"
GCPConfigDoc.Fields[4].Name = "instanceType"
GCPConfigDoc.Fields[4].Type = "string"
GCPConfigDoc.Fields[4].Note = ""
GCPConfigDoc.Fields[4].Description = "Machine image used to create Constellation nodes."
GCPConfigDoc.Fields[4].Comments[encoder.LineComment] = "Machine image used to create Constellation nodes."
GCPConfigDoc.Fields[5].Name = "instanceType"
GCPConfigDoc.Fields[4].Description = "VM instance type to use for Constellation nodes."
GCPConfigDoc.Fields[4].Comments[encoder.LineComment] = "VM instance type to use for Constellation nodes."
GCPConfigDoc.Fields[5].Name = "stateDiskType"
GCPConfigDoc.Fields[5].Type = "string"
GCPConfigDoc.Fields[5].Note = ""
GCPConfigDoc.Fields[5].Description = "VM instance type to use for Constellation nodes."
GCPConfigDoc.Fields[5].Comments[encoder.LineComment] = "VM instance type to use for Constellation nodes."
GCPConfigDoc.Fields[6].Name = "stateDiskType"
GCPConfigDoc.Fields[6].Type = "string"
GCPConfigDoc.Fields[5].Description = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://cloud.google.com/compute/docs/disks#disk-types"
GCPConfigDoc.Fields[5].Comments[encoder.LineComment] = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://cloud.google.com/compute/docs/disks#disk-types"
GCPConfigDoc.Fields[6].Name = "deployCSIDriver"
GCPConfigDoc.Fields[6].Type = "bool"
GCPConfigDoc.Fields[6].Note = ""
GCPConfigDoc.Fields[6].Description = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://cloud.google.com/compute/docs/disks#disk-types"
GCPConfigDoc.Fields[6].Comments[encoder.LineComment] = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://cloud.google.com/compute/docs/disks#disk-types"
GCPConfigDoc.Fields[7].Name = "deployCSIDriver"
GCPConfigDoc.Fields[7].Type = "bool"
GCPConfigDoc.Fields[6].Description = "Deploy Persistent Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage"
GCPConfigDoc.Fields[6].Comments[encoder.LineComment] = "Deploy Persistent Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage"
GCPConfigDoc.Fields[7].Name = "measurements"
GCPConfigDoc.Fields[7].Type = "Measurements"
GCPConfigDoc.Fields[7].Note = ""
GCPConfigDoc.Fields[7].Description = "Deploy Persistent Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage"
GCPConfigDoc.Fields[7].Comments[encoder.LineComment] = "Deploy Persistent Disk CSI driver with on-node encryption. For details see: https://docs.edgeless.systems/constellation/architecture/encrypted-storage"
GCPConfigDoc.Fields[8].Name = "measurements"
GCPConfigDoc.Fields[8].Type = "Measurements"
GCPConfigDoc.Fields[7].Description = "Expected confidential VM measurements."
GCPConfigDoc.Fields[7].Comments[encoder.LineComment] = "Expected confidential VM measurements."
GCPConfigDoc.Fields[8].Name = "enforcedMeasurements"
GCPConfigDoc.Fields[8].Type = "[]uint32"
GCPConfigDoc.Fields[8].Note = ""
GCPConfigDoc.Fields[8].Description = "Expected confidential VM measurements."
GCPConfigDoc.Fields[8].Comments[encoder.LineComment] = "Expected confidential VM measurements."
GCPConfigDoc.Fields[9].Name = "enforcedMeasurements"
GCPConfigDoc.Fields[9].Type = "[]uint32"
GCPConfigDoc.Fields[9].Note = ""
GCPConfigDoc.Fields[9].Description = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
GCPConfigDoc.Fields[9].Comments[encoder.LineComment] = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
GCPConfigDoc.Fields[8].Description = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
GCPConfigDoc.Fields[8].Comments[encoder.LineComment] = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
QEMUConfigDoc.Type = "QEMUConfig"
QEMUConfigDoc.Comments[encoder.LineComment] = "QEMUConfig holds config information for QEMU based Constellation deployments."
@ -365,62 +355,57 @@ func init() {
FieldName: "qemu",
},
}
QEMUConfigDoc.Fields = make([]encoder.Doc, 11)
QEMUConfigDoc.Fields[0].Name = "image"
QEMUConfigDoc.Fields = make([]encoder.Doc, 10)
QEMUConfigDoc.Fields[0].Name = "imageFormat"
QEMUConfigDoc.Fields[0].Type = "string"
QEMUConfigDoc.Fields[0].Note = ""
QEMUConfigDoc.Fields[0].Description = "Path to the image to use for the VMs."
QEMUConfigDoc.Fields[0].Comments[encoder.LineComment] = "Path to the image to use for the VMs."
QEMUConfigDoc.Fields[1].Name = "imageFormat"
QEMUConfigDoc.Fields[1].Type = "string"
QEMUConfigDoc.Fields[0].Description = "Format of the image to use for the VMs. Should be either qcow2 or raw."
QEMUConfigDoc.Fields[0].Comments[encoder.LineComment] = "Format of the image to use for the VMs. Should be either qcow2 or raw."
QEMUConfigDoc.Fields[1].Name = "vcpus"
QEMUConfigDoc.Fields[1].Type = "int"
QEMUConfigDoc.Fields[1].Note = ""
QEMUConfigDoc.Fields[1].Description = "Format of the image to use for the VMs. Should be either qcow2 or raw."
QEMUConfigDoc.Fields[1].Comments[encoder.LineComment] = "Format of the image to use for the VMs. Should be either qcow2 or raw."
QEMUConfigDoc.Fields[2].Name = "vcpus"
QEMUConfigDoc.Fields[1].Description = "vCPU count for the VMs."
QEMUConfigDoc.Fields[1].Comments[encoder.LineComment] = "vCPU count for the VMs."
QEMUConfigDoc.Fields[2].Name = "memory"
QEMUConfigDoc.Fields[2].Type = "int"
QEMUConfigDoc.Fields[2].Note = ""
QEMUConfigDoc.Fields[2].Description = "vCPU count for the VMs."
QEMUConfigDoc.Fields[2].Comments[encoder.LineComment] = "vCPU count for the VMs."
QEMUConfigDoc.Fields[3].Name = "memory"
QEMUConfigDoc.Fields[3].Type = "int"
QEMUConfigDoc.Fields[2].Description = "Amount of memory per instance (MiB)."
QEMUConfigDoc.Fields[2].Comments[encoder.LineComment] = "Amount of memory per instance (MiB)."
QEMUConfigDoc.Fields[3].Name = "metadataAPIServer"
QEMUConfigDoc.Fields[3].Type = "string"
QEMUConfigDoc.Fields[3].Note = ""
QEMUConfigDoc.Fields[3].Description = "Amount of memory per instance (MiB)."
QEMUConfigDoc.Fields[3].Comments[encoder.LineComment] = "Amount of memory per instance (MiB)."
QEMUConfigDoc.Fields[4].Name = "metadataAPIServer"
QEMUConfigDoc.Fields[3].Description = "Container image to use for the QEMU metadata server."
QEMUConfigDoc.Fields[3].Comments[encoder.LineComment] = "Container image to use for the QEMU metadata server."
QEMUConfigDoc.Fields[4].Name = "libvirtSocket"
QEMUConfigDoc.Fields[4].Type = "string"
QEMUConfigDoc.Fields[4].Note = ""
QEMUConfigDoc.Fields[4].Description = "Container image to use for the QEMU metadata server."
QEMUConfigDoc.Fields[4].Comments[encoder.LineComment] = "Container image to use for the QEMU metadata server."
QEMUConfigDoc.Fields[5].Name = "libvirtSocket"
QEMUConfigDoc.Fields[4].Description = "Libvirt connection URI. Leave empty to start a libvirt instance in Docker."
QEMUConfigDoc.Fields[4].Comments[encoder.LineComment] = "Libvirt connection URI. Leave empty to start a libvirt instance in Docker."
QEMUConfigDoc.Fields[5].Name = "libvirtContainerImage"
QEMUConfigDoc.Fields[5].Type = "string"
QEMUConfigDoc.Fields[5].Note = ""
QEMUConfigDoc.Fields[5].Description = "Libvirt connection URI. Leave empty to start a libvirt instance in Docker."
QEMUConfigDoc.Fields[5].Comments[encoder.LineComment] = "Libvirt connection URI. Leave empty to start a libvirt instance in Docker."
QEMUConfigDoc.Fields[6].Name = "libvirtContainerImage"
QEMUConfigDoc.Fields[5].Description = "Container image to use for launching a containerized libvirt daemon. Only relevant if `libvirtSocket = \"\"`."
QEMUConfigDoc.Fields[5].Comments[encoder.LineComment] = "Container image to use for launching a containerized libvirt daemon. Only relevant if `libvirtSocket = \"\"`."
QEMUConfigDoc.Fields[6].Name = "nvram"
QEMUConfigDoc.Fields[6].Type = "string"
QEMUConfigDoc.Fields[6].Note = ""
QEMUConfigDoc.Fields[6].Description = "Container image to use for launching a containerized libvirt daemon. Only relevant if `libvirtSocket = \"\"`."
QEMUConfigDoc.Fields[6].Comments[encoder.LineComment] = "Container image to use for launching a containerized libvirt daemon. Only relevant if `libvirtSocket = \"\"`."
QEMUConfigDoc.Fields[7].Name = "nvram"
QEMUConfigDoc.Fields[6].Description = "NVRAM template to be used for secure boot. Can be sentinel value \"production\", \"testing\" or a path to a custom NVRAM template"
QEMUConfigDoc.Fields[6].Comments[encoder.LineComment] = "NVRAM template to be used for secure boot. Can be sentinel value \"production\", \"testing\" or a path to a custom NVRAM template"
QEMUConfigDoc.Fields[7].Name = "firmware"
QEMUConfigDoc.Fields[7].Type = "string"
QEMUConfigDoc.Fields[7].Note = ""
QEMUConfigDoc.Fields[7].Description = "NVRAM template to be used for secure boot. Can be sentinel value \"production\", \"testing\" or a path to a custom NVRAM template"
QEMUConfigDoc.Fields[7].Comments[encoder.LineComment] = "NVRAM template to be used for secure boot. Can be sentinel value \"production\", \"testing\" or a path to a custom NVRAM template"
QEMUConfigDoc.Fields[8].Name = "firmware"
QEMUConfigDoc.Fields[8].Type = "string"
QEMUConfigDoc.Fields[7].Description = "Path to the OVMF firmware. Leave empty for auto selection."
QEMUConfigDoc.Fields[7].Comments[encoder.LineComment] = "Path to the OVMF firmware. Leave empty for auto selection."
QEMUConfigDoc.Fields[8].Name = "measurements"
QEMUConfigDoc.Fields[8].Type = "Measurements"
QEMUConfigDoc.Fields[8].Note = ""
QEMUConfigDoc.Fields[8].Description = "Path to the OVMF firmware. Leave empty for auto selection."
QEMUConfigDoc.Fields[8].Comments[encoder.LineComment] = "Path to the OVMF firmware. Leave empty for auto selection."
QEMUConfigDoc.Fields[9].Name = "measurements"
QEMUConfigDoc.Fields[9].Type = "Measurements"
QEMUConfigDoc.Fields[8].Description = "Measurement used to enable measured boot."
QEMUConfigDoc.Fields[8].Comments[encoder.LineComment] = "Measurement used to enable measured boot."
QEMUConfigDoc.Fields[9].Name = "enforcedMeasurements"
QEMUConfigDoc.Fields[9].Type = "[]uint32"
QEMUConfigDoc.Fields[9].Note = ""
QEMUConfigDoc.Fields[9].Description = "Measurement used to enable measured boot."
QEMUConfigDoc.Fields[9].Comments[encoder.LineComment] = "Measurement used to enable measured boot."
QEMUConfigDoc.Fields[10].Name = "enforcedMeasurements"
QEMUConfigDoc.Fields[10].Type = "[]uint32"
QEMUConfigDoc.Fields[10].Note = ""
QEMUConfigDoc.Fields[10].Description = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
QEMUConfigDoc.Fields[10].Comments[encoder.LineComment] = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
QEMUConfigDoc.Fields[9].Description = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
QEMUConfigDoc.Fields[9].Comments[encoder.LineComment] = "List of values that should be enforced to be equal to the ones from the measurement list. Any non-equal values not in this list will only result in a warning."
}
func (_ Config) Doc() *encoder.Doc {

View File

@ -121,13 +121,13 @@ func TestNewWithDefaultOptions(t *testing.T) {
confToWrite: func() *Config { // valid config with all, but clientSecretValue
c := Default()
c.RemoveProviderExcept(cloudprovider.Azure)
c.Image = "v0.0.0"
c.Provider.Azure.SubscriptionID = "f4278079-288c-4766-a98c-ab9d5dba01a5"
c.Provider.Azure.TenantID = "d4ff9d63-6d6d-4042-8f6a-21e804add5aa"
c.Provider.Azure.Location = "westus"
c.Provider.Azure.ResourceGroup = "test"
c.Provider.Azure.UserAssignedIdentity = "/subscriptions/8b8bd01f-efd9-4113-9bd1-c82137c32da7/resourcegroups/constellation-identity/providers/Microsoft.ManagedIdentity/userAssignedIdentities/constellation-identity"
c.Provider.Azure.AppClientID = "3ea4bdc1-1cc1-4237-ae78-0831eff3491e"
c.Provider.Azure.Image = "/communityGalleries/ConstellationCVM-b3782fa0-0df7-4f2f-963e-fc7fc42663df/images/constellation/versions/2.2.0"
return c
}(),
envToSet: map[string]string{
@ -139,6 +139,7 @@ func TestNewWithDefaultOptions(t *testing.T) {
confToWrite: func() *Config {
c := Default()
c.RemoveProviderExcept(cloudprovider.Azure)
c.Image = "v0.0.0"
c.Provider.Azure.SubscriptionID = "f4278079-288c-4766-a98c-ab9d5dba01a5"
c.Provider.Azure.TenantID = "d4ff9d63-6d6d-4042-8f6a-21e804add5aa"
c.Provider.Azure.Location = "westus"
@ -146,7 +147,6 @@ func TestNewWithDefaultOptions(t *testing.T) {
c.Provider.Azure.ClientSecretValue = "other-value" // < Note secret set in config, as well.
c.Provider.Azure.UserAssignedIdentity = "/subscriptions/8b8bd01f-efd9-4113-9bd1-c82137c32da7/resourcegroups/constellation-identity/providers/Microsoft.ManagedIdentity/userAssignedIdentities/constellation-identity"
c.Provider.Azure.AppClientID = "3ea4bdc1-1cc1-4237-ae78-0831eff3491e"
c.Provider.Azure.Image = "/communityGalleries/ConstellationCVM-b3782fa0-0df7-4f2f-963e-fc7fc42663df/images/constellation/versions/2.2.0"
return c
}(),
envToSet: map[string]string{
@ -182,7 +182,7 @@ func TestNewWithDefaultOptions(t *testing.T) {
}
func TestValidate(t *testing.T) {
const defaultErrCount = 20 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default
const defaultErrCount = 17 // expect this number of error messages by default because user-specific values are not set and multiple providers are defined by default
const azErrCount = 8
const gcpErrCount = 5
@ -229,12 +229,12 @@ func TestValidate(t *testing.T) {
"Azure config with all required fields is valid": {
cnf: func() *Config {
cnf := Default()
cnf.Image = "v0.0.0"
az := cnf.Provider.Azure
az.SubscriptionID = "01234567-0123-0123-0123-0123456789ab"
az.TenantID = "01234567-0123-0123-0123-0123456789ab"
az.Location = "test-location"
az.UserAssignedIdentity = "test-identity"
az.Image = "some/image/location"
az.ResourceGroup = "test-resource-group"
az.AppClientID = "01234567-0123-0123-0123-0123456789ab"
az.ClientSecretValue = "test-client-secret"
@ -257,10 +257,10 @@ func TestValidate(t *testing.T) {
"GCP config with all required fields is valid": {
cnf: func() *Config {
cnf := Default()
cnf.Image = "v0.0.0"
gcp := cnf.Provider.GCP
gcp.Region = "test-region"
gcp.Project = "test-project"
gcp.Image = "some/image/location"
gcp.Zone = "test-zone"
gcp.ServiceAccountKeyPath = "test-key-path"
cnf.Provider = ProviderConfig{}
@ -300,39 +300,6 @@ func TestHasProvider(t *testing.T) {
assert.False(cnfWithAzure.HasProvider(cloudprovider.GCP))
}
func TestImage(t *testing.T) {
testCases := map[string]struct {
cfg *Config
wantImage string
}{
"default aws": {
cfg: func() *Config { c := Default(); c.RemoveProviderExcept(cloudprovider.AWS); return c }(),
wantImage: Default().Provider.AWS.Image,
},
"default azure": {
cfg: func() *Config { c := Default(); c.RemoveProviderExcept(cloudprovider.Azure); return c }(),
wantImage: Default().Provider.Azure.Image,
},
"default gcp": {
cfg: func() *Config { c := Default(); c.RemoveProviderExcept(cloudprovider.GCP); return c }(),
wantImage: Default().Provider.GCP.Image,
},
"default qemu": {
cfg: func() *Config { c := Default(); c.RemoveProviderExcept(cloudprovider.QEMU); return c }(),
wantImage: "",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
image := tc.cfg.Image()
assert.Equal(tc.wantImage, image)
})
}
}
func TestConfigRemoveProviderExcept(t *testing.T) {
testCases := map[string]struct {
removeExcept cloudprovider.Provider
@ -388,6 +355,7 @@ func TestConfigGeneratedDocsFresh(t *testing.T) {
assert.Len(ConfigDoc.Fields, reflect.ValueOf(Config{}).NumField(), updateMsg)
assert.Len(UpgradeConfigDoc.Fields, reflect.ValueOf(UpgradeConfig{}).NumField(), updateMsg)
assert.Len(ProviderConfigDoc.Fields, reflect.ValueOf(ProviderConfig{}).NumField(), updateMsg)
assert.Len(AWSConfigDoc.Fields, reflect.ValueOf(AWSConfig{}).NumField(), updateMsg)
assert.Len(AzureConfigDoc.Fields, reflect.ValueOf(AzureConfig{}).NumField(), updateMsg)
assert.Len(GCPConfigDoc.Fields, reflect.ValueOf(GCPConfig{}).NumField(), updateMsg)
assert.Len(QEMUConfigDoc.Fields, reflect.ValueOf(QEMUConfig{}).NumField(), updateMsg)
@ -439,47 +407,34 @@ func TestConfig_UpdateMeasurements(t *testing.T) {
}
}
func TestConfig_IsImageDebug(t *testing.T) {
func TestConfig_IsReleaseImage(t *testing.T) {
testCases := map[string]struct {
conf *Config
want bool
}{
// TODO: Add AWS when we know the format of published images & debug images
"gcp release": {
"release image v0.0.0": {
conf: func() *Config {
conf := Default()
conf.RemoveProviderExcept(cloudprovider.GCP)
conf.Provider.GCP.Image = "projects/constellation-images/global/images/constellation-v1-3-0"
return conf
}(),
want: false,
},
"gcp debug": {
conf: func() *Config {
conf := Default()
conf.RemoveProviderExcept(cloudprovider.GCP)
conf.Provider.GCP.Image = "projects/constellation-images/global/images/constellation-20220812102023"
conf.Image = "v0.0.0"
return conf
}(),
want: true,
},
"azure release": {
"branch image": {
conf: func() *Config {
conf := Default()
conf.RemoveProviderExcept(cloudprovider.Azure)
conf.Provider.Azure.Image = "/CommunityGalleries/ConstellationCVM-b3782fa0-0df7-4f2f-963e-fc7fc42663df/Images/constellation/Versions/0.0.1"
conf.Image = "feat-x-vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef"
return conf
}(),
want: false,
},
"azure debug": {
"debug image": {
conf: func() *Config {
conf := Default()
conf.RemoveProviderExcept(cloudprovider.Azure)
conf.Provider.Azure.Image = "/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/constellation-images/providers/Microsoft.Compute/galleries/Constellation_Debug/images/v1.4.0/versions/2022.0805.151600"
conf.Image = "debug-vX.Y.Z-pre.0.yyyymmddhhmmss-abcdefabcdef"
return conf
}(),
want: true,
want: false,
},
"empty config": {
conf: &Config{},
@ -490,7 +445,7 @@ func TestConfig_IsImageDebug(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tc.want, tc.conf.IsDebugImage())
assert.Equal(tc.want, tc.conf.IsReleaseImage())
})
}
}

View File

@ -9,6 +9,6 @@ SPDX-License-Identifier: AGPL-3.0-only
package config
const (
DefaultImageAzure = "/communityGalleries/ConstellationCVM-b3782fa0-0df7-4f2f-963e-fc7fc42663df/images/constellation/versions/2.2.2"
DefaultImageGCP = "projects/constellation-images/global/images/constellation-v2-2-2"
// defaultImage is the default image for the enterprise build.
defaultImage = "v2.3.0"
)

View File

@ -9,8 +9,6 @@ SPDX-License-Identifier: AGPL-3.0-only
package config
const (
// DefaultImageAzure is not set for OSS build.
DefaultImageAzure = ""
// DefaultImageGCP is not set for OSS build.
DefaultImageGCP = ""
// defaultImage is not set for OSS build.
defaultImage = ""
)

View File

@ -17,6 +17,19 @@ import (
"github.com/go-playground/validator/v10"
)
func validateImage(fl validator.FieldLevel) bool {
image := fl.Field().String()
switch {
case image == "":
return false
case image == "..":
return false
case strings.Contains(image, "/"):
return false
}
return true
}
func validateK8sVersion(fl validator.FieldLevel) bool {
return versions.IsSupportedK8sVersion(fl.Field().String())
}

View File

@ -156,6 +156,10 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf8F1hpmwE+YCFXzjGtaQcrL6XZVT
JmEe5iSLvG1SyQSAew7WdMKF6o9t8e2TFuCkzlOhhlws2OHWbiFZnFWCFw==
-----END PUBLIC KEY-----
`
// ImageVersionRepositoryURL is the base URL of the repository containing
// image version information.
ImageVersionRepositoryURL = "https://cdn.confidential.cloud"
)
// VersionInfo is the version of a binary. Left as a separate variable to allow override during build.

161
internal/image/image.go Normal file
View File

@ -0,0 +1,161 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package image
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/spf13/afero"
)
// imageLookupTable is a lookup table for image references.
//
// Example:
//
// {
// "aws": {
// "us-west-2": "ami-0123456789abcdef0"
// },
// "azure": {
// "cvm": "cvm-image-id"
// },
// "gcp": {
// "sev-es": "projects/<project>/global/images/<image>"
// },
// "qemu": {
// "default": "https://cdn.example.com/image.raw"
// }
// }
type imageLookupTable map[string]map[string]string
// getReference returns the image reference for a given CSP and image variant.
func (l *imageLookupTable) getReference(csp, variant string) (string, error) {
if l == nil {
return "", fmt.Errorf("image lookup table is nil")
}
if _, ok := (*l)[csp]; !ok {
return "", fmt.Errorf("image not available for CSP %q", csp)
}
if _, ok := (*l)[csp][variant]; !ok {
return "", fmt.Errorf("image not available for variant %q", variant)
}
return (*l)[csp][variant], nil
}
// Fetcher fetches image references using a lookup table.
type Fetcher struct {
httpc httpc
fs *afero.Afero
}
// New returns a new image fetcher.
func New() *Fetcher {
return &Fetcher{
httpc: http.DefaultClient,
fs: &afero.Afero{Fs: afero.NewOsFs()},
}
}
// FetchReference fetches the image reference for a given image version uid, CSP and image variant.
func (f *Fetcher) FetchReference(ctx context.Context, config *config.Config) (string, error) {
provider := config.GetProvider()
variant, err := variant(provider, config)
if err != nil {
return "", err
}
return f.fetch(ctx, provider, config.Image, variant)
}
// fetch fetches the image reference for a given image version uid, CSP and image variant.
func (f *Fetcher) fetch(ctx context.Context, csp cloudprovider.Provider, version, variant string) (string, error) {
raw, err := getFromFile(f.fs, version)
if err != nil && os.IsNotExist(err) {
raw, err = getFromURL(ctx, f.httpc, version)
}
if err != nil {
return "", fmt.Errorf("fetching image reference: %w", err)
}
lut := make(imageLookupTable)
if err := json.Unmarshal(raw, &lut); err != nil {
return "", fmt.Errorf("decoding image reference: %w", err)
}
return lut.getReference(strings.ToLower(csp.String()), variant)
}
// variant returns the image variant for a given CSP and configuration.
func variant(provider cloudprovider.Provider, config *config.Config) (string, error) {
switch provider {
case cloudprovider.AWS:
return config.Provider.AWS.Region, nil
case cloudprovider.Azure:
if *config.Provider.Azure.ConfidentialVM {
return "cvm", nil
}
return "trustedlaunch", nil
case cloudprovider.GCP:
return "sev-es", nil
case cloudprovider.QEMU:
return "default", nil
default:
return "", fmt.Errorf("unsupported provider: %s", provider)
}
}
func getFromFile(fs *afero.Afero, version string) ([]byte, error) {
version = filepath.Base(version)
return fs.ReadFile(version + ".json")
}
// getFromURL fetches the image lookup table from a URL.
func getFromURL(ctx context.Context, client httpc, version string) ([]byte, error) {
url, err := url.Parse(constants.ImageVersionRepositoryURL)
if err != nil {
return nil, fmt.Errorf("parsing image version repository URL: %w", err)
}
versionFilename := path.Base(version) + ".json"
url.Path = path.Join("constellation/v1/images", versionFilename)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), http.NoBody)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
switch resp.StatusCode {
case http.StatusNotFound:
return nil, fmt.Errorf("image %q does not exist", version)
default:
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
}
}
content, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return content, nil
}
type httpc interface {
Do(req *http.Request) (*http.Response, error)
}

View File

@ -0,0 +1,254 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package image
import (
"bytes"
"context"
"io"
"net/http"
"os"
"testing"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
)
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestGetReference(t *testing.T) {
testCases := map[string]struct {
csp, variant string
wantReference string
wantErr bool
}{
"reference exists": {
csp: "someCSP",
variant: "someVariant",
wantReference: "someReference",
},
"csp does not exist": {
csp: "nonExistingCSP",
variant: "someVariant",
wantErr: true,
},
"variant does not exist": {
csp: "someCSP",
variant: "nonExistingVariant",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
lut := &imageLookupTable{
"someCSP": {
"someVariant": "someReference",
},
}
reference, err := lut.getReference(tc.csp, tc.variant)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantReference, reference)
})
}
}
func TestGetReferenceOnNil(t *testing.T) {
assert := assert.New(t)
var lut *imageLookupTable
_, err := lut.getReference("someCSP", "someVariant")
assert.Error(err)
}
func TestVariant(t *testing.T) {
testCases := map[string]struct {
csp cloudprovider.Provider
config *config.Config
wantVariant string
wantErr bool
}{
"AWS region": {
csp: cloudprovider.AWS,
config: &config.Config{Image: "someImage", Provider: config.ProviderConfig{
AWS: &config.AWSConfig{Region: "someRegion"},
}},
wantVariant: "someRegion",
},
"Azure cvm": {
csp: cloudprovider.Azure,
config: &config.Config{
Image: "someImage", Provider: config.ProviderConfig{
Azure: &config.AzureConfig{ConfidentialVM: func() *bool { b := true; return &b }()},
},
},
wantVariant: "cvm",
},
"Azure trustedlaunch": {
csp: cloudprovider.Azure,
config: &config.Config{
Image: "someImage", Provider: config.ProviderConfig{
Azure: &config.AzureConfig{ConfidentialVM: func() *bool { b := false; return &b }()},
},
},
wantVariant: "trustedlaunch",
},
"GCP": {
csp: cloudprovider.GCP,
config: &config.Config{Image: "someImage", Provider: config.ProviderConfig{
GCP: &config.GCPConfig{},
}},
wantVariant: "sev-es",
},
"QEMU": {
csp: cloudprovider.QEMU,
config: &config.Config{Image: "someImage", Provider: config.ProviderConfig{
QEMU: &config.QEMUConfig{},
}},
wantVariant: "default",
},
"invalid": {
csp: cloudprovider.Provider(9999),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
vari, err := variant(tc.csp, tc.config)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantVariant, vari)
})
}
}
func TestFetchReference(t *testing.T) {
imageVersionUID := "someImageVersionUID"
client := newTestClient(func(req *http.Request) *http.Response {
if req.URL.String() == "https://cdn.confidential.cloud/constellation/v1/images/someImageVersionUID.json" {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(lut)),
Header: make(http.Header),
}
}
return &http.Response{
StatusCode: http.StatusNotFound,
Body: io.NopCloser(bytes.NewBufferString("Not found.")),
Header: make(http.Header),
}
})
testCases := map[string]struct {
config *config.Config
overrideFile string
wantReference string
wantErr bool
}{
"reference fetched remotely": {
config: &config.Config{Image: imageVersionUID, Provider: config.ProviderConfig{
QEMU: &config.QEMUConfig{},
}},
wantReference: "someReference",
},
"reference fetched locally": {
config: &config.Config{Image: imageVersionUID, Provider: config.ProviderConfig{
QEMU: &config.QEMUConfig{},
}},
overrideFile: `{"qemu":{"default":"localOverrideReference"}}`,
wantReference: "localOverrideReference",
},
"lut is invalid": {
config: &config.Config{Image: imageVersionUID, Provider: config.ProviderConfig{
QEMU: &config.QEMUConfig{},
}},
overrideFile: `{`,
wantErr: true,
},
"image version does not exist": {
config: &config.Config{Image: "nonExistingImageVersionUID", Provider: config.ProviderConfig{
QEMU: &config.QEMUConfig{},
}},
wantErr: true,
},
"invalid config": {
config: &config.Config{},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
fetcher := &Fetcher{
httpc: client,
fs: newImageVersionStubFs(t, imageVersionUID, tc.overrideFile),
}
reference, err := fetcher.FetchReference(context.Background(), tc.config)
if tc.wantErr {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.wantReference, reference)
})
}
}
func must(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
// roundTripFunc .
type roundTripFunc func(req *http.Request) *http.Response
// RoundTrip .
func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req), nil
}
// newTestClient returns *http.Client with Transport replaced to avoid making real calls.
func newTestClient(fn roundTripFunc) *http.Client {
return &http.Client{
Transport: fn,
}
}
func newImageVersionStubFs(t *testing.T, imageVersionUID string, overrideFile string) *afero.Afero {
fs := afero.NewMemMapFs()
if overrideFile != "" {
must(t, afero.WriteFile(fs, imageVersionUID+".json", []byte(overrideFile), os.ModePerm))
}
return &afero.Afero{Fs: fs}
}
const lut = `{"qemu":{"default":"someReference"}}`

139
internal/image/raw.go Normal file
View File

@ -0,0 +1,139 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package image
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"os"
"path/filepath"
"github.com/schollz/progressbar/v3"
"github.com/spf13/afero"
)
// Downloader downloads raw images.
type Downloader struct {
httpc httpc
fs *afero.Afero
}
// NewDownloader creates a new Downloader.
func NewDownloader() *Downloader {
return &Downloader{
httpc: http.DefaultClient,
fs: &afero.Afero{Fs: afero.NewOsFs()},
}
}
// Download downloads the raw image from source.
func (d *Downloader) Download(ctx context.Context, errWriter io.Writer, showBar bool, source, version string) (string, error) {
url, err := url.Parse(source)
if err != nil {
return "", fmt.Errorf("parsing image source URL: %w", err)
}
version = filepath.Base(version)
var partfile, destination string
switch url.Scheme {
case "http", "https":
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("getting current working directory: %w", err)
}
partfile = filepath.Join(cwd, version+".raw.part")
destination = filepath.Join(cwd, version+".raw")
case "file":
return url.Path, nil
default:
return "", fmt.Errorf("unsupported image source URL scheme: %s", url.Scheme)
}
if !d.shouldDownload(destination) {
return destination, nil
}
if err := d.downloadWithProgress(ctx, errWriter, showBar, source, partfile); err != nil {
return "", err
}
return destination, d.fs.Rename(partfile, destination)
}
// shouldDownload checks if the image should be downloaded.
func (d *Downloader) shouldDownload(destination string) bool {
_, err := d.fs.Stat(destination)
return errors.Is(err, fs.ErrNotExist)
}
// downloadWithProgress downloads the raw image from source to the destination.
func (d *Downloader) downloadWithProgress(ctx context.Context, errWriter io.Writer, showBar bool, source, destination string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, source, nil)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
resp, err := d.httpc.Do(req)
if err != nil {
return fmt.Errorf("doing request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("downloading from %q: %s", source, resp.Status)
}
f, err := d.fs.OpenFile(destination, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
var bar io.WriteCloser
if showBar {
bar = prepareBar(errWriter, resp.ContentLength)
} else {
bar = &nopWriteCloser{}
}
defer bar.Close()
_, err = io.Copy(io.MultiWriter(f, bar), resp.Body)
if err != nil {
return err
}
return nil
}
func prepareBar(writer io.Writer, total int64) io.WriteCloser {
return progressbar.NewOptions64(
total,
progressbar.OptionSetWriter(writer),
progressbar.OptionShowBytes(true),
progressbar.OptionSetPredictTime(true),
progressbar.OptionFullWidth(),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "=",
SaucerHead: ">",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}),
progressbar.OptionClearOnFinish(),
progressbar.OptionOnCompletion(func() { fmt.Fprintf(writer, "Done.\n\n") }),
)
}
type nopWriteCloser struct{}
func (*nopWriteCloser) Write(p []byte) (int, error) {
return len(p), nil
}
func (*nopWriteCloser) Close() error {
return nil
}

204
internal/image/raw_test.go Normal file
View File

@ -0,0 +1,204 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package image
import (
"bytes"
"context"
"io"
"net/http"
"os"
"path"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
)
func TestShouldDownload(t *testing.T) {
testCases := map[string]struct {
partfile, destination string
wantDownload bool
}{
"no files exist yet": {
wantDownload: true,
},
"partial download": {
partfile: "some data",
wantDownload: true,
},
"download succeeded": {
destination: "all of the data",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
downloader := &Downloader{
fs: newDownloaderStubFs(t, "someVersion", tc.partfile, tc.destination),
}
gotDownload := downloader.shouldDownload("someVersion.raw")
assert.Equal(tc.wantDownload, gotDownload)
})
}
}
func TestDownloadWithProgress(t *testing.T) {
rawImage := "raw image"
client := newTestClient(func(req *http.Request) *http.Response {
if req.URL.String() == "https://cdn.example.com/image.raw" {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(rawImage)),
Header: make(http.Header),
}
}
return &http.Response{
StatusCode: http.StatusNotFound,
Body: io.NopCloser(bytes.NewBufferString("Not found.")),
Header: make(http.Header),
}
})
testCases := map[string]struct {
source string
wantErr bool
}{
"correct file requested": {
source: "https://cdn.example.com/image.raw",
},
"incorrect file requested": {
source: "https://cdn.example.com/incorrect.raw",
wantErr: true,
},
"invalid scheme": {
source: "xyz://",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
fs := newDownloaderStubFs(t, "someVersion", "", "")
downloader := &Downloader{
httpc: client,
fs: fs,
}
var outBuffer bytes.Buffer
err := downloader.downloadWithProgress(context.Background(), &outBuffer, false, tc.source, "someVersion.raw")
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
out, err := fs.ReadFile("someVersion.raw")
assert.NoError(err)
assert.Equal(rawImage, string(out))
})
}
}
func TestDownload(t *testing.T) {
rawImage := "raw image"
cwd, err := os.Getwd()
assert.NoError(t, err)
wantDestination := path.Join(cwd, "someVersion.raw")
client := newTestClient(func(req *http.Request) *http.Response {
if req.URL.String() == "https://cdn.example.com/image.raw" {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBufferString(rawImage)),
Header: make(http.Header),
}
}
return &http.Response{
StatusCode: http.StatusNotFound,
Body: io.NopCloser(bytes.NewBufferString("Not found.")),
Header: make(http.Header),
}
})
testCases := map[string]struct {
source string
destination string
overrideFile string
wantErr bool
}{
"correct file requested": {
source: "https://cdn.example.com/image.raw",
},
"file url": {
source: "file:///override.raw",
overrideFile: "override image",
},
"file exists": {
source: "https://cdn.example.com/image.raw",
destination: "already exists",
},
"incorrect file requested": {
source: "https://cdn.example.com/incorrect.raw",
wantErr: true,
},
"invalid scheme": {
source: "xyz://",
wantErr: true,
},
"invalid URL": {
source: "\x00",
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
fs := newDownloaderStubFs(t, cwd+"/someVersion", "", tc.destination)
if tc.overrideFile != "" {
must(t, fs.WriteFile("/override.raw", []byte(tc.overrideFile), os.ModePerm))
}
downloader := &Downloader{
httpc: client,
fs: fs,
}
var outBuffer bytes.Buffer
gotDestination, err := downloader.Download(context.Background(), &outBuffer, false, tc.source, "someVersion")
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
if tc.overrideFile == "" {
assert.Equal(wantDestination, gotDestination)
} else {
assert.Equal("/override.raw", gotDestination)
}
out, err := fs.ReadFile(gotDestination)
assert.NoError(err)
switch {
case tc.overrideFile != "":
assert.Equal(tc.overrideFile, string(out))
case tc.destination != "":
assert.Equal(tc.destination, string(out))
default:
assert.Equal(rawImage, string(out))
}
})
}
}
func newDownloaderStubFs(t *testing.T, version, partfile, destination string) *afero.Afero {
fs := afero.NewMemMapFs()
if partfile != "" {
must(t, afero.WriteFile(fs, version+".raw.part", []byte(partfile), os.ModePerm))
}
if destination != "" {
must(t, afero.WriteFile(fs, version+".raw", []byte(destination), os.ModePerm))
}
return &afero.Afero{Fs: fs}
}