mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
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:
parent
9222468d3b
commit
575b6e93f6
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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?")
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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 = ""
|
||||
)
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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
161
internal/image/image.go
Normal 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)
|
||||
}
|
254
internal/image/image_test.go
Normal file
254
internal/image/image_test.go
Normal 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
139
internal/image/raw.go
Normal 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
204
internal/image/raw_test.go
Normal 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}
|
||||
}
|
Loading…
Reference in New Issue
Block a user