mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-01-26 07:16:08 -05:00
Add image update API and use for "upgrade plan"
This commit is contained in:
parent
954cbad214
commit
ebf852b3ba
@ -66,18 +66,18 @@ func NewUpgrader(outWriter io.Writer) (*Upgrader, error) {
|
||||
}
|
||||
|
||||
// Upgrade upgrades the cluster to the given measurements and image.
|
||||
func (u *Upgrader) Upgrade(ctx context.Context, image string, measurements measurements.M) error {
|
||||
func (u *Upgrader) Upgrade(ctx context.Context, imageReference, imageVersion string, measurements measurements.M) error {
|
||||
if err := u.updateMeasurements(ctx, measurements); err != nil {
|
||||
return fmt.Errorf("updating measurements: %w", err)
|
||||
}
|
||||
|
||||
if err := u.updateImage(ctx, image); err != nil {
|
||||
if err := u.updateImage(ctx, imageReference, imageVersion); err != nil {
|
||||
return fmt.Errorf("updating image: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCurrentImage returns the currently used image of the cluster.
|
||||
// GetCurrentImage returns the currently used image version of the cluster.
|
||||
func (u *Upgrader) GetCurrentImage(ctx context.Context) (*unstructured.Unstructured, string, error) {
|
||||
imageStruct, err := u.dynamicInterface.getCurrent(ctx, "constellation-os")
|
||||
if err != nil {
|
||||
@ -93,16 +93,16 @@ func (u *Upgrader) GetCurrentImage(ctx context.Context) (*unstructured.Unstructu
|
||||
if !ok {
|
||||
return nil, "", retErr
|
||||
}
|
||||
currentImageDefinition, ok := specMap["image"]
|
||||
currentImageVersion, ok := specMap["imageVersion"]
|
||||
if !ok {
|
||||
return nil, "", retErr
|
||||
}
|
||||
imageDefinition, ok := currentImageDefinition.(string)
|
||||
imageVersion, ok := currentImageVersion.(string)
|
||||
if !ok {
|
||||
return nil, "", retErr
|
||||
}
|
||||
|
||||
return imageStruct, imageDefinition, nil
|
||||
return imageStruct, imageVersion, nil
|
||||
}
|
||||
|
||||
// CurrentHelmVersion returns the version of the currently installed helm release.
|
||||
@ -154,18 +154,19 @@ func (u *Upgrader) updateMeasurements(ctx context.Context, newMeasurements measu
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Upgrader) updateImage(ctx context.Context, image string) error {
|
||||
currentImage, currentImageDefinition, err := u.GetCurrentImage(ctx)
|
||||
func (u *Upgrader) updateImage(ctx context.Context, imageReference, imageVersion string) error {
|
||||
currentImage, currentImageVersion, err := u.GetCurrentImage(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving current image: %w", err)
|
||||
}
|
||||
|
||||
if currentImageDefinition == image {
|
||||
if currentImageVersion == imageVersion {
|
||||
fmt.Fprintln(u.outWriter, "Cluster is already using the chosen image, skipping image upgrade")
|
||||
return nil
|
||||
}
|
||||
|
||||
currentImage.Object["spec"].(map[string]any)["image"] = image
|
||||
currentImage.Object["spec"].(map[string]any)["image"] = imageReference
|
||||
currentImage.Object["spec"].(map[string]any)["imageVersion"] = imageVersion
|
||||
if _, err := u.dynamicInterface.update(ctx, currentImage); err != nil {
|
||||
return fmt.Errorf("setting new image: %w", err)
|
||||
}
|
||||
|
@ -149,35 +149,40 @@ func (u *stubClientInterface) kubernetesVersion() (string, error) {
|
||||
func TestUpdateImage(t *testing.T) {
|
||||
someErr := errors.New("error")
|
||||
testCases := map[string]struct {
|
||||
updater *stubImageUpdater
|
||||
newImage string
|
||||
wantUpdate bool
|
||||
wantErr bool
|
||||
updater *stubImageUpdater
|
||||
newImageReference string
|
||||
newImageVersion string
|
||||
wantUpdate bool
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
updater: &stubImageUpdater{
|
||||
setImage: &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"spec": map[string]any{
|
||||
"image": "old-image",
|
||||
"image": "old-image-ref",
|
||||
"imageVersion": "old-image-ver",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
newImage: "new-image",
|
||||
wantUpdate: true,
|
||||
newImageReference: "new-image-ref",
|
||||
newImageVersion: "new-image-ver",
|
||||
wantUpdate: true,
|
||||
},
|
||||
"image is the same": {
|
||||
updater: &stubImageUpdater{
|
||||
setImage: &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"spec": map[string]any{
|
||||
"image": "old-image",
|
||||
"image": "old-image-ref",
|
||||
"imageVersion": "old-image-ver",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
newImage: "old-image",
|
||||
newImageReference: "old-image-ref",
|
||||
newImageVersion: "old-image-ver",
|
||||
},
|
||||
"getCurrent error": {
|
||||
updater: &stubImageUpdater{getErr: someErr},
|
||||
@ -188,14 +193,16 @@ func TestUpdateImage(t *testing.T) {
|
||||
setImage: &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"spec": map[string]any{
|
||||
"image": "old-image",
|
||||
"image": "old-image-ref",
|
||||
"imageVersion": "old-image-ver",
|
||||
},
|
||||
},
|
||||
},
|
||||
updateErr: someErr,
|
||||
},
|
||||
newImage: "new-image",
|
||||
wantErr: true,
|
||||
newImageReference: "new-image-ref",
|
||||
newImageVersion: "new-image-ver",
|
||||
wantErr: true,
|
||||
},
|
||||
"no spec": {
|
||||
updater: &stubImageUpdater{
|
||||
@ -203,8 +210,9 @@ func TestUpdateImage(t *testing.T) {
|
||||
Object: map[string]any{},
|
||||
},
|
||||
},
|
||||
newImage: "new-image",
|
||||
wantErr: true,
|
||||
newImageReference: "new-image-ref",
|
||||
newImageVersion: "new-image-ver",
|
||||
wantErr: true,
|
||||
},
|
||||
"not a map": {
|
||||
updater: &stubImageUpdater{
|
||||
@ -214,8 +222,9 @@ func TestUpdateImage(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
newImage: "new-image",
|
||||
wantErr: true,
|
||||
newImageReference: "new-image-ref",
|
||||
newImageVersion: "new-image-ver",
|
||||
wantErr: true,
|
||||
},
|
||||
"no spec.image": {
|
||||
updater: &stubImageUpdater{
|
||||
@ -225,8 +234,9 @@ func TestUpdateImage(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
newImage: "new-image",
|
||||
wantErr: true,
|
||||
newImageReference: "new-image-ref",
|
||||
newImageVersion: "new-image-ver",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
@ -239,7 +249,7 @@ func TestUpdateImage(t *testing.T) {
|
||||
outWriter: &bytes.Buffer{},
|
||||
}
|
||||
|
||||
err := upgrader.updateImage(context.Background(), tc.newImage)
|
||||
err := upgrader.updateImage(context.Background(), tc.newImageReference, tc.newImageVersion)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
@ -248,7 +258,8 @@ func TestUpdateImage(t *testing.T) {
|
||||
|
||||
assert.NoError(err)
|
||||
if tc.wantUpdate {
|
||||
assert.Equal(tc.newImage, tc.updater.updatedImage.Object["spec"].(map[string]any)["image"])
|
||||
assert.Equal(tc.newImageReference, tc.updater.updatedImage.Object["spec"].(map[string]any)["image"])
|
||||
assert.Equal(tc.newImageVersion, tc.updater.updatedImage.Object["spec"].(map[string]any)["imageVersion"])
|
||||
} else {
|
||||
assert.Nil(tc.updater.updatedImage)
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/edgelesssys/constellation/v2/internal/image"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -31,15 +32,16 @@ func newUpgradeExecuteCmd() *cobra.Command {
|
||||
|
||||
func runUpgradeExecute(cmd *cobra.Command, args []string) error {
|
||||
fileHandler := file.NewHandler(afero.NewOsFs())
|
||||
imageFetcher := image.New()
|
||||
upgrader, err := cloudcmd.NewUpgrader(cmd.OutOrStdout())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return upgradeExecute(cmd, upgrader, fileHandler)
|
||||
return upgradeExecute(cmd, imageFetcher, upgrader, fileHandler)
|
||||
}
|
||||
|
||||
func upgradeExecute(cmd *cobra.Command, upgrader cloudUpgrader, fileHandler file.Handler) error {
|
||||
func upgradeExecute(cmd *cobra.Command, imageFetcher imageFetcher, upgrader cloudUpgrader, fileHandler file.Handler) error {
|
||||
configPath, err := cmd.Flags().GetString("config")
|
||||
if err != nil {
|
||||
return err
|
||||
@ -52,9 +54,20 @@ func upgradeExecute(cmd *cobra.Command, upgrader cloudUpgrader, fileHandler file
|
||||
// TODO: validate upgrade config? Should be basic things like checking image is not an empty string
|
||||
// More sophisticated validation, like making sure we don't downgrade the cluster, should be done by `constellation upgrade plan`
|
||||
|
||||
return upgrader.Upgrade(cmd.Context(), conf.Upgrade.Image, conf.Upgrade.Measurements)
|
||||
// this config modification is temporary until we can remove the upgrade section from the config
|
||||
conf.Image = conf.Upgrade.Image
|
||||
imageReference, err := imageFetcher.FetchReference(cmd.Context(), conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return upgrader.Upgrade(cmd.Context(), imageReference, conf.Upgrade.Image, conf.Upgrade.Measurements)
|
||||
}
|
||||
|
||||
type cloudUpgrader interface {
|
||||
Upgrade(ctx context.Context, image string, measurements measurements.M) error
|
||||
Upgrade(ctx context.Context, imageReference, imageVersion string, measurements measurements.M) error
|
||||
}
|
||||
|
||||
type imageFetcher interface {
|
||||
FetchReference(ctx context.Context, config *config.Config) (string, error)
|
||||
}
|
||||
|
@ -23,11 +23,20 @@ import (
|
||||
|
||||
func TestUpgradeExecute(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
upgrader stubUpgrader
|
||||
wantErr bool
|
||||
upgrader stubUpgrader
|
||||
imageFetcher stubImageFetcher
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
upgrader: stubUpgrader{},
|
||||
imageFetcher: stubImageFetcher{
|
||||
reference: "someReference",
|
||||
},
|
||||
},
|
||||
"fetch error": {
|
||||
imageFetcher: stubImageFetcher{
|
||||
fetchReferenceErr: errors.New("error"),
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"upgrade error": {
|
||||
upgrader: stubUpgrader{err: errors.New("error")},
|
||||
@ -46,7 +55,7 @@ func TestUpgradeExecute(t *testing.T) {
|
||||
cfg := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.Azure)
|
||||
require.NoError(handler.WriteYAML(constants.ConfigFilename, cfg))
|
||||
|
||||
err := upgradeExecute(cmd, tc.upgrader, handler)
|
||||
err := upgradeExecute(cmd, &tc.imageFetcher, tc.upgrader, handler)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
@ -60,6 +69,15 @@ type stubUpgrader struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (u stubUpgrader) Upgrade(context.Context, string, measurements.M) error {
|
||||
func (u stubUpgrader) Upgrade(context.Context, string, string, measurements.M) error {
|
||||
return u.err
|
||||
}
|
||||
|
||||
type stubImageFetcher struct {
|
||||
reference string
|
||||
fetchReferenceErr error
|
||||
}
|
||||
|
||||
func (f *stubImageFetcher) FetchReference(_ context.Context, _ *config.Config) (string, error) {
|
||||
return f.reference, f.fetchReferenceErr
|
||||
}
|
||||
|
@ -8,16 +8,15 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/update"
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||
@ -32,13 +31,6 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
const imageReleaseURL = "https://github.com/edgelesssys/constellation/releases/latest/download/versions-manifest.json"
|
||||
|
||||
var (
|
||||
azureCVMRxp = regexp.MustCompile(`^(?i)\/CommunityGalleries\/ConstellationCVM-b3782fa0-0df7-4f2f-963e-fc7fc42663df\/Images\/constellation\/Versions\/[\d]+.[\d]+.[\d]+$`)
|
||||
gcpCVMRxp = regexp.MustCompile(`^projects\/constellation-images\/global\/images\/constellation-(v[\d]+-[\d]+-[\d]+)$`)
|
||||
)
|
||||
|
||||
func newUpgradePlanCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "plan",
|
||||
@ -63,17 +55,20 @@ func runUpgradePlan(cmd *cobra.Command, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patchLister := update.New()
|
||||
rekor, err := sigstore.NewRekor()
|
||||
if err != nil {
|
||||
return fmt.Errorf("constructing Rekor client: %w", err)
|
||||
}
|
||||
cliVersion := getCurrentCLIVersion()
|
||||
|
||||
return upgradePlan(cmd, planner, fileHandler, http.DefaultClient, rekor, flags)
|
||||
return upgradePlan(cmd, planner, patchLister, fileHandler, http.DefaultClient, rekor, flags, cliVersion)
|
||||
}
|
||||
|
||||
// upgradePlan plans an upgrade of a Constellation cluster.
|
||||
func upgradePlan(cmd *cobra.Command, planner upgradePlanner,
|
||||
func upgradePlan(cmd *cobra.Command, planner upgradePlanner, patchLister patchLister,
|
||||
fileHandler file.Handler, client *http.Client, rekor rekorVerifier, flags upgradePlanFlags,
|
||||
cliVersion string,
|
||||
) error {
|
||||
conf, err := config.New(fileHandler, flags.configPath)
|
||||
if err != nil {
|
||||
@ -83,27 +78,58 @@ func upgradePlan(cmd *cobra.Command, planner upgradePlanner,
|
||||
// get current image version of the cluster
|
||||
csp := conf.GetProvider()
|
||||
|
||||
version, err := getCurrentImageVersion(cmd.Context(), planner, csp)
|
||||
version, err := getCurrentImageVersion(cmd.Context(), planner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking current image version: %w", err)
|
||||
}
|
||||
|
||||
// fetch images definitions from GitHub and filter to only compatible images
|
||||
images, err := fetchImages(cmd.Context(), client)
|
||||
// find compatible images
|
||||
// image updates should always be possible for the current minor version of the cluster
|
||||
// (e.g. 0.1.0 -> 0.1.1, 0.1.2, 0.1.3, etc.)
|
||||
// additionally, we allow updates to the next minor version (e.g. 0.1.0 -> 0.2.0)
|
||||
// if the CLI minor version is newer than the cluster minor version
|
||||
currentImageMinorVer := semver.MajorMinor(version)
|
||||
currentCLIMinorVer := semver.MajorMinor(cliVersion)
|
||||
nextImageMinorVer, err := nextMinorVersion(currentImageMinorVer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching available images: %w", err)
|
||||
}
|
||||
compatibleImages := getCompatibleImages(csp, version, images)
|
||||
if len(compatibleImages) == 0 {
|
||||
cmd.PrintErrln("No compatible images found to upgrade to.")
|
||||
return nil
|
||||
return fmt.Errorf("calculating next image minor version: %w", err)
|
||||
}
|
||||
|
||||
var allowedMinorVersions []string
|
||||
|
||||
cliImageCompare := semver.Compare(currentCLIMinorVer, currentImageMinorVer)
|
||||
|
||||
switch {
|
||||
case cliImageCompare < 0:
|
||||
cmd.PrintErrln("Warning: CLI version is older than cluster image version. This is not supported.")
|
||||
case cliImageCompare == 0:
|
||||
allowedMinorVersions = []string{currentImageMinorVer}
|
||||
case cliImageCompare > 0:
|
||||
allowedMinorVersions = []string{currentImageMinorVer, nextImageMinorVer}
|
||||
}
|
||||
|
||||
var updateCandidates []string
|
||||
for _, minorVer := range allowedMinorVersions {
|
||||
versionList, err := patchLister.PatchVersionsOf(cmd.Context(), "stable", minorVer, "image")
|
||||
if err == nil {
|
||||
updateCandidates = append(updateCandidates, versionList.Versions...)
|
||||
}
|
||||
}
|
||||
|
||||
// filter out versions that are not compatible with the current cluster
|
||||
compatibleImages := getCompatibleImages(version, updateCandidates)
|
||||
|
||||
// get expected measurements for each image
|
||||
if err := getCompatibleImageMeasurements(cmd.Context(), cmd, client, rekor, []byte(flags.cosignPubKey), compatibleImages); err != nil {
|
||||
upgrades, err := getCompatibleImageMeasurements(cmd.Context(), cmd, client, rekor, []byte(flags.cosignPubKey), csp, compatibleImages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching measurements for compatible images: %w", err)
|
||||
}
|
||||
|
||||
if len(upgrades) == 0 {
|
||||
cmd.PrintErrln("No compatible images found to upgrade to.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// interactive mode
|
||||
if flags.filePath == "" {
|
||||
cmd.Printf("Current version: %s\n", version)
|
||||
@ -111,13 +137,13 @@ func upgradePlan(cmd *cobra.Command, planner upgradePlanner,
|
||||
&nopWriteCloser{cmd.OutOrStdout()},
|
||||
io.NopCloser(cmd.InOrStdin()),
|
||||
flags.configPath, conf, fileHandler,
|
||||
compatibleImages,
|
||||
upgrades,
|
||||
)
|
||||
}
|
||||
|
||||
// write upgrade plan to stdout
|
||||
if flags.filePath == "-" {
|
||||
content, err := encoder.NewEncoder(compatibleImages).Encode()
|
||||
content, err := encoder.NewEncoder(upgrades).Encode()
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding compatible images: %w", err)
|
||||
}
|
||||
@ -126,91 +152,53 @@ func upgradePlan(cmd *cobra.Command, planner upgradePlanner,
|
||||
}
|
||||
|
||||
// write upgrade plan to file
|
||||
return fileHandler.WriteYAML(flags.filePath, compatibleImages)
|
||||
}
|
||||
|
||||
// fetchImages retrieves a list of the latest Constellation node images from GitHub.
|
||||
func fetchImages(ctx context.Context, client *http.Client) (map[string]imageManifest, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, imageReleaseURL, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status code: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
imagesJSON, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
images := make(map[string]imageManifest)
|
||||
if err := json.Unmarshal(imagesJSON, &images); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return images, nil
|
||||
return fileHandler.WriteYAML(flags.filePath, upgrades)
|
||||
}
|
||||
|
||||
// getCompatibleImages trims the list of images to only ones compatible with the current cluster.
|
||||
func getCompatibleImages(csp cloudprovider.Provider, currentVersion string, images map[string]imageManifest) map[string]config.UpgradeConfig {
|
||||
compatibleImages := make(map[string]config.UpgradeConfig)
|
||||
func getCompatibleImages(currentImageVersion string, images []string) []string {
|
||||
var compatibleImages []string
|
||||
|
||||
switch csp {
|
||||
case cloudprovider.Azure:
|
||||
for imgVersion, image := range images {
|
||||
if semver.Compare(currentVersion, imgVersion) < 0 {
|
||||
compatibleImages[imgVersion] = config.UpgradeConfig{
|
||||
Image: image.AzureImage,
|
||||
CSP: cloudprovider.Azure,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case cloudprovider.GCP:
|
||||
for imgVersion, image := range images {
|
||||
if semver.Compare(currentVersion, imgVersion) < 0 {
|
||||
compatibleImages[imgVersion] = config.UpgradeConfig{
|
||||
Image: image.GCPImage,
|
||||
CSP: cloudprovider.GCP,
|
||||
}
|
||||
}
|
||||
for _, image := range images {
|
||||
// check if image is newer than current version
|
||||
if semver.Compare(image, currentImageVersion) <= 0 {
|
||||
continue
|
||||
}
|
||||
compatibleImages = append(compatibleImages, image)
|
||||
}
|
||||
|
||||
return compatibleImages
|
||||
}
|
||||
|
||||
// getCompatibleImageMeasurements retrieves the expected measurements for each image.
|
||||
func getCompatibleImageMeasurements(ctx context.Context, cmd *cobra.Command, client *http.Client, rekor rekorVerifier, pubK []byte, images map[string]config.UpgradeConfig) error {
|
||||
for idx, img := range images {
|
||||
measurementsURL, err := url.Parse(constants.CDNRepositoryURL + "/" + path.Join(img.Image, strings.ToLower(img.CSP.String()), "measurements.json"))
|
||||
func getCompatibleImageMeasurements(ctx context.Context, cmd *cobra.Command, client *http.Client, rekor rekorVerifier, pubK []byte,
|
||||
csp cloudprovider.Provider, images []string,
|
||||
) (map[string]config.UpgradeConfig, error) {
|
||||
upgrades := make(map[string]config.UpgradeConfig)
|
||||
for _, img := range images {
|
||||
measurementsURL, err := url.Parse(constants.CDNRepositoryURL + path.Join("/constellation/v1/measurements/", img, strings.ToLower(csp.String()), "measurements.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signatureURL, err := url.Parse(constants.CDNRepositoryURL + "/" + path.Join(img.Image, strings.ToLower(img.CSP.String()), "measurements.json.sig"))
|
||||
signatureURL, err := url.Parse(constants.CDNRepositoryURL + path.Join("/constellation/v1/measurements/", img, strings.ToLower(csp.String()), "measurements.json.sig"))
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hash, err := img.Measurements.FetchAndVerify(
|
||||
var fetchedMeasurements measurements.M
|
||||
hash, err := fetchedMeasurements.FetchAndVerify(
|
||||
ctx, client,
|
||||
measurementsURL,
|
||||
signatureURL,
|
||||
pubK,
|
||||
measurements.WithMetadata{
|
||||
Image: img.Image,
|
||||
CSP: img.CSP,
|
||||
CSP: csp,
|
||||
Image: img,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
cmd.PrintErrf("Skipping image %q: %s\n", img, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = verifyWithRekor(ctx, rekor, hash); err != nil {
|
||||
@ -218,42 +206,34 @@ func getCompatibleImageMeasurements(ctx context.Context, cmd *cobra.Command, cli
|
||||
cmd.PrintErrf("Make sure measurements are correct.\n")
|
||||
}
|
||||
|
||||
images[idx] = img
|
||||
upgrades[img] = config.UpgradeConfig{
|
||||
Image: img,
|
||||
Measurements: fetchedMeasurements,
|
||||
CSP: csp,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
return upgrades, nil
|
||||
}
|
||||
|
||||
// getCurrentImageVersion retrieves the semantic version of the image currently installed in the cluster.
|
||||
// If the cluster is not using a release image, an error is returned.
|
||||
func getCurrentImageVersion(ctx context.Context, planner upgradePlanner, csp cloudprovider.Provider) (string, error) {
|
||||
_, image, err := planner.GetCurrentImage(ctx)
|
||||
func getCurrentImageVersion(ctx context.Context, planner upgradePlanner) (string, error) {
|
||||
_, imageVersion, err := planner.GetCurrentImage(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var version string
|
||||
switch csp {
|
||||
case cloudprovider.Azure:
|
||||
if !azureCVMRxp.MatchString(image) {
|
||||
return "", fmt.Errorf("image %q does not look like a released production image for Azure", image)
|
||||
}
|
||||
versionRxp := regexp.MustCompile(`[\d]+.[\d]+.[\d]+$`)
|
||||
version = "v" + versionRxp.FindString(image)
|
||||
case cloudprovider.GCP:
|
||||
gcpVersion := gcpCVMRxp.FindStringSubmatch(image)
|
||||
if len(gcpVersion) != 2 {
|
||||
return "", fmt.Errorf("image %q does not look like a released production image for GCP", image)
|
||||
}
|
||||
version = strings.ReplaceAll(gcpVersion[1], "-", ".")
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported cloud provider: %s", csp.String())
|
||||
if !semver.IsValid(imageVersion) {
|
||||
return "", fmt.Errorf("current image version is not a release image version: %q", imageVersion)
|
||||
}
|
||||
|
||||
if !semver.IsValid(version) {
|
||||
return "", fmt.Errorf("image %q has no valid semantic version", image)
|
||||
}
|
||||
return version, nil
|
||||
return imageVersion, nil
|
||||
}
|
||||
|
||||
func getCurrentCLIVersion() string {
|
||||
return "v" + constants.VersionInfo
|
||||
}
|
||||
|
||||
func parseUpgradePlanFlags(cmd *cobra.Command) (upgradePlanFlags, error) {
|
||||
@ -275,10 +255,10 @@ func parseUpgradePlanFlags(cmd *cobra.Command) (upgradePlanFlags, error) {
|
||||
|
||||
func upgradePlanInteractive(out io.WriteCloser, in io.ReadCloser,
|
||||
configPath string, config *config.Config, fileHandler file.Handler,
|
||||
compatibleImages map[string]config.UpgradeConfig,
|
||||
compatibleUpgrades map[string]config.UpgradeConfig,
|
||||
) error {
|
||||
var imageVersions []string
|
||||
for k := range compatibleImages {
|
||||
for k := range compatibleUpgrades {
|
||||
imageVersions = append(imageVersions, k)
|
||||
}
|
||||
semver.Sort(imageVersions)
|
||||
@ -304,30 +284,46 @@ func upgradePlanInteractive(out io.WriteCloser, in io.ReadCloser,
|
||||
|
||||
fmt.Fprintln(out, "Updating config to the following:")
|
||||
|
||||
fmt.Fprintf(out, "Image: %s\n", compatibleImages[res].Image)
|
||||
fmt.Fprintf(out, "Image: %s\n", compatibleUpgrades[res].Image)
|
||||
fmt.Fprintln(out, "Measurements:")
|
||||
content, err := encoder.NewEncoder(compatibleImages[res].Measurements).Encode()
|
||||
content, err := encoder.NewEncoder(compatibleUpgrades[res].Measurements).Encode()
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding measurements: %w", err)
|
||||
}
|
||||
measurements := strings.TrimSuffix(strings.Replace("\t"+string(content), "\n", "\n\t", -1), "\n\t")
|
||||
fmt.Fprintln(out, measurements)
|
||||
|
||||
config.Upgrade = compatibleImages[res]
|
||||
config.Upgrade = compatibleUpgrades[res]
|
||||
return fileHandler.WriteYAML(configPath, config, file.OptOverwrite)
|
||||
}
|
||||
|
||||
func nextMinorVersion(version string) (string, error) {
|
||||
major, minor, _, err := parseCanonicalSemver(version)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("v%d.%d", major, minor+1), nil
|
||||
}
|
||||
|
||||
func parseCanonicalSemver(version string) (major int, minor int, patch int, err error) {
|
||||
version = semver.Canonical(version) // ensure version is in canonical form (vX.Y.Z)
|
||||
num, err := fmt.Sscanf(version, "v%d.%d.%d", &major, &minor, &patch)
|
||||
if err != nil {
|
||||
return 0, 0, 0, fmt.Errorf("parsing version: %w", err)
|
||||
}
|
||||
if num != 3 {
|
||||
return 0, 0, 0, fmt.Errorf("parsing version: expected 3 numbers, got %d", num)
|
||||
}
|
||||
|
||||
return major, minor, patch, nil
|
||||
}
|
||||
|
||||
type upgradePlanFlags struct {
|
||||
configPath string
|
||||
filePath string
|
||||
cosignPubKey string
|
||||
}
|
||||
|
||||
type imageManifest struct {
|
||||
AzureImage string `json:"AzureOSImage"`
|
||||
GCPImage string `json:"GCPOSImage"`
|
||||
}
|
||||
|
||||
type nopWriteCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
@ -337,3 +333,7 @@ func (c *nopWriteCloser) Close() error { return nil }
|
||||
type upgradePlanner interface {
|
||||
GetCurrentImage(ctx context.Context) (*unstructured.Unstructured, string, error)
|
||||
}
|
||||
|
||||
type patchLister interface {
|
||||
PatchVersionsOf(ctx context.Context, stream, minor, kind string) (*update.VersionsList, error)
|
||||
}
|
||||
|
@ -9,13 +9,13 @@ package cmd
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/update"
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
@ -32,47 +32,23 @@ import (
|
||||
func TestGetCurrentImageVersion(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
stubUpgradePlanner stubUpgradePlanner
|
||||
csp cloudprovider.Provider
|
||||
wantErr bool
|
||||
}{
|
||||
"valid Azure": {
|
||||
"valid version": {
|
||||
stubUpgradePlanner: stubUpgradePlanner{
|
||||
image: "/CommunityGalleries/ConstellationCVM-b3782fa0-0df7-4f2f-963e-fc7fc42663df/Images/constellation/Versions/0.0.0",
|
||||
image: "v1.0.0",
|
||||
},
|
||||
csp: cloudprovider.Azure,
|
||||
},
|
||||
"invalid Azure": {
|
||||
"invalid version": {
|
||||
stubUpgradePlanner: stubUpgradePlanner{
|
||||
image: "/CommunityGalleries/someone-else/Images/constellation/Versions/0.0.1",
|
||||
image: "invalid",
|
||||
},
|
||||
csp: cloudprovider.Azure,
|
||||
wantErr: true,
|
||||
},
|
||||
"valid GCP": {
|
||||
stubUpgradePlanner: stubUpgradePlanner{
|
||||
image: "projects/constellation-images/global/images/constellation-v0-0-0",
|
||||
},
|
||||
csp: cloudprovider.GCP,
|
||||
},
|
||||
"invalid GCP": {
|
||||
stubUpgradePlanner: stubUpgradePlanner{
|
||||
image: "projects/constellation-images/global/images/constellation-debug-image",
|
||||
},
|
||||
csp: cloudprovider.GCP,
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid CSP": {
|
||||
stubUpgradePlanner: stubUpgradePlanner{
|
||||
image: "some-image",
|
||||
},
|
||||
csp: cloudprovider.Unknown,
|
||||
wantErr: true,
|
||||
},
|
||||
"GetCurrentImage error": {
|
||||
stubUpgradePlanner: stubUpgradePlanner{
|
||||
err: errors.New("error"),
|
||||
},
|
||||
csp: cloudprovider.Azure,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
@ -81,7 +57,7 @@ func TestGetCurrentImageVersion(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
version, err := getCurrentImageVersion(context.Background(), tc.stubUpgradePlanner, tc.csp)
|
||||
version, err := getCurrentImageVersion(context.Background(), tc.stubUpgradePlanner)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
@ -93,141 +69,32 @@ func TestGetCurrentImageVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type stubUpgradePlanner struct {
|
||||
image string
|
||||
err error
|
||||
}
|
||||
|
||||
func (u stubUpgradePlanner) GetCurrentImage(context.Context) (*unstructured.Unstructured, string, error) {
|
||||
return nil, u.image, u.err
|
||||
}
|
||||
|
||||
func TestFetchImages(t *testing.T) {
|
||||
testImages := map[string]imageManifest{
|
||||
"v0.0.0": {
|
||||
AzureImage: "azure-v0.0.0",
|
||||
GCPImage: "gcp-v0.0.0",
|
||||
},
|
||||
"v999.999.999": {
|
||||
AzureImage: "azure-v999.999.999",
|
||||
GCPImage: "gcp-v999.999.999",
|
||||
},
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
client *http.Client
|
||||
wantErr bool
|
||||
}{
|
||||
"success": {
|
||||
client: newTestClient(func(req *http.Request) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBuffer(mustMarshal(t, testImages))),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}),
|
||||
},
|
||||
"error": {
|
||||
client: newTestClient(func(req *http.Request) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Body: io.NopCloser(bytes.NewBuffer([]byte{})),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
images, err := fetchImages(context.Background(), tc.client)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(err)
|
||||
assert.NotNil(images)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCompatibleImages(t *testing.T) {
|
||||
imageList := map[string]imageManifest{
|
||||
"v0.0.0": {
|
||||
AzureImage: "azure-v0.0.0",
|
||||
GCPImage: "gcp-v0.0.0",
|
||||
},
|
||||
"v1.0.0": {
|
||||
AzureImage: "azure-v1.0.0",
|
||||
GCPImage: "gcp-v1.0.0",
|
||||
},
|
||||
"v1.0.1": {
|
||||
AzureImage: "azure-v1.0.1",
|
||||
GCPImage: "gcp-v1.0.1",
|
||||
},
|
||||
"v1.0.2": {
|
||||
AzureImage: "azure-v1.0.2",
|
||||
GCPImage: "gcp-v1.0.2",
|
||||
},
|
||||
"v1.1.0": {
|
||||
AzureImage: "azure-v1.1.0",
|
||||
GCPImage: "gcp-v1.1.0",
|
||||
},
|
||||
imageList := []string{
|
||||
"v0.0.0",
|
||||
"v1.0.0",
|
||||
"v1.0.1",
|
||||
"v1.0.2",
|
||||
"v1.1.0",
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
images map[string]imageManifest
|
||||
csp cloudprovider.Provider
|
||||
images []string
|
||||
version string
|
||||
wantImages map[string]config.UpgradeConfig
|
||||
wantImages []string
|
||||
}{
|
||||
"azure": {
|
||||
"filters <= v1.0.0": {
|
||||
images: imageList,
|
||||
csp: cloudprovider.Azure,
|
||||
version: "v1.0.0",
|
||||
wantImages: map[string]config.UpgradeConfig{
|
||||
"v1.0.1": {
|
||||
Image: "azure-v1.0.1",
|
||||
CSP: cloudprovider.Azure,
|
||||
},
|
||||
"v1.0.2": {
|
||||
Image: "azure-v1.0.2",
|
||||
CSP: cloudprovider.Azure,
|
||||
},
|
||||
"v1.1.0": {
|
||||
Image: "azure-v1.1.0",
|
||||
CSP: cloudprovider.Azure,
|
||||
},
|
||||
},
|
||||
},
|
||||
"gcp": {
|
||||
images: imageList,
|
||||
csp: cloudprovider.GCP,
|
||||
version: "v1.0.0",
|
||||
wantImages: map[string]config.UpgradeConfig{
|
||||
"v1.0.1": {
|
||||
Image: "gcp-v1.0.1",
|
||||
CSP: cloudprovider.GCP,
|
||||
},
|
||||
"v1.0.2": {
|
||||
Image: "gcp-v1.0.2",
|
||||
CSP: cloudprovider.GCP,
|
||||
},
|
||||
"v1.1.0": {
|
||||
Image: "gcp-v1.1.0",
|
||||
CSP: cloudprovider.GCP,
|
||||
},
|
||||
wantImages: []string{
|
||||
"v1.0.1",
|
||||
"v1.0.2",
|
||||
"v1.1.0",
|
||||
},
|
||||
},
|
||||
"no compatible images": {
|
||||
images: imageList,
|
||||
csp: cloudprovider.Azure,
|
||||
version: "v999.999.999",
|
||||
wantImages: map[string]config.UpgradeConfig{},
|
||||
images: imageList,
|
||||
version: "v999.999.999",
|
||||
},
|
||||
}
|
||||
|
||||
@ -235,8 +102,8 @@ func TestGetCompatibleImages(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
compatibleImages := getCompatibleImages(tc.csp, tc.version, tc.images)
|
||||
assert.Equal(tc.wantImages, compatibleImages)
|
||||
compatibleImages := getCompatibleImages(tc.version, tc.images)
|
||||
assert.EqualValues(tc.wantImages, compatibleImages)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -244,16 +111,8 @@ func TestGetCompatibleImages(t *testing.T) {
|
||||
func TestGetCompatibleImageMeasurements(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
testImages := map[string]config.UpgradeConfig{
|
||||
"v0.0.0": {
|
||||
Image: "v0.0.0",
|
||||
CSP: cloudprovider.Azure,
|
||||
},
|
||||
"v1.0.0": {
|
||||
Image: "v1.0.0",
|
||||
CSP: cloudprovider.Azure,
|
||||
},
|
||||
}
|
||||
csp := cloudprovider.Azure
|
||||
images := []string{"v0.0.0", "v1.0.0"}
|
||||
|
||||
client := newTestClient(func(req *http.Request) *http.Response {
|
||||
if strings.HasSuffix(req.URL.String(), "v0.0.0/azure/measurements.json") {
|
||||
@ -295,20 +154,17 @@ func TestGetCompatibleImageMeasurements(t *testing.T) {
|
||||
|
||||
pubK := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----")
|
||||
|
||||
err := getCompatibleImageMeasurements(context.Background(), &cobra.Command{}, client, singleUUIDVerifier(), pubK, testImages)
|
||||
upgrades, err := getCompatibleImageMeasurements(context.Background(), &cobra.Command{}, client, singleUUIDVerifier(), pubK, csp, images)
|
||||
assert.NoError(err)
|
||||
|
||||
for _, image := range testImages {
|
||||
for _, image := range upgrades {
|
||||
assert.NotEmpty(image.Measurements)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradePlan(t *testing.T) {
|
||||
testImages := map[string]imageManifest{
|
||||
"v1.0.0": {
|
||||
AzureImage: "v1.0.0",
|
||||
GCPImage: "v1.0.0",
|
||||
},
|
||||
availablePatches := update.VersionsList{
|
||||
Versions: []string{"v1.0.0", "v1.0.1"},
|
||||
}
|
||||
|
||||
// Cosign private key used to sign the measurements.
|
||||
@ -329,20 +185,53 @@ func TestUpgradePlan(t *testing.T) {
|
||||
pubK := "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"
|
||||
|
||||
testCases := map[string]struct {
|
||||
patchLister stubPatchLister
|
||||
planner stubUpgradePlanner
|
||||
flags upgradePlanFlags
|
||||
cliVersion string
|
||||
csp cloudprovider.Provider
|
||||
verifier rekorVerifier
|
||||
imageFetchStatus int
|
||||
measurementsFetchStatus int
|
||||
wantUpgrade bool
|
||||
wantErr bool
|
||||
}{
|
||||
"no compatible images": {
|
||||
"upgrades gcp": {
|
||||
patchLister: stubPatchLister{list: availablePatches},
|
||||
planner: stubUpgradePlanner{
|
||||
image: "projects/constellation-images/global/images/constellation-v999-999-999",
|
||||
image: "v1.0.0",
|
||||
},
|
||||
measurementsFetchStatus: http.StatusOK,
|
||||
flags: upgradePlanFlags{
|
||||
configPath: constants.ConfigFilename,
|
||||
filePath: "upgrade-plan.yaml",
|
||||
cosignPubKey: pubK,
|
||||
},
|
||||
cliVersion: "v1.0.0",
|
||||
csp: cloudprovider.GCP,
|
||||
verifier: singleUUIDVerifier(),
|
||||
wantUpgrade: true,
|
||||
},
|
||||
"upgrades azure": {
|
||||
patchLister: stubPatchLister{list: availablePatches},
|
||||
planner: stubUpgradePlanner{
|
||||
image: "v1.0.0",
|
||||
},
|
||||
measurementsFetchStatus: http.StatusOK,
|
||||
flags: upgradePlanFlags{
|
||||
configPath: constants.ConfigFilename,
|
||||
filePath: "upgrade-plan.yaml",
|
||||
cosignPubKey: pubK,
|
||||
},
|
||||
csp: cloudprovider.Azure,
|
||||
cliVersion: "v999.999.999",
|
||||
verifier: singleUUIDVerifier(),
|
||||
wantUpgrade: true,
|
||||
},
|
||||
"current image newer than updates": {
|
||||
patchLister: stubPatchLister{list: availablePatches},
|
||||
planner: stubUpgradePlanner{
|
||||
image: "v999.999.999",
|
||||
},
|
||||
imageFetchStatus: http.StatusOK,
|
||||
measurementsFetchStatus: http.StatusOK,
|
||||
flags: upgradePlanFlags{
|
||||
configPath: constants.ConfigFilename,
|
||||
@ -353,11 +242,11 @@ func TestUpgradePlan(t *testing.T) {
|
||||
verifier: singleUUIDVerifier(),
|
||||
wantUpgrade: false,
|
||||
},
|
||||
"upgrades gcp": {
|
||||
"current image newer than cli": {
|
||||
patchLister: stubPatchLister{list: availablePatches},
|
||||
planner: stubUpgradePlanner{
|
||||
image: "projects/constellation-images/global/images/constellation-v0-0-0",
|
||||
image: "v999.999.999",
|
||||
},
|
||||
imageFetchStatus: http.StatusOK,
|
||||
measurementsFetchStatus: http.StatusOK,
|
||||
flags: upgradePlanFlags{
|
||||
configPath: constants.ConfigFilename,
|
||||
@ -365,29 +254,15 @@ func TestUpgradePlan(t *testing.T) {
|
||||
cosignPubKey: pubK,
|
||||
},
|
||||
csp: cloudprovider.GCP,
|
||||
cliVersion: "v1.0.0",
|
||||
verifier: singleUUIDVerifier(),
|
||||
wantUpgrade: true,
|
||||
},
|
||||
"upgrades azure": {
|
||||
planner: stubUpgradePlanner{
|
||||
image: "/CommunityGalleries/ConstellationCVM-b3782fa0-0df7-4f2f-963e-fc7fc42663df/Images/constellation/Versions/0.0.0",
|
||||
},
|
||||
imageFetchStatus: http.StatusOK,
|
||||
measurementsFetchStatus: http.StatusOK,
|
||||
flags: upgradePlanFlags{
|
||||
configPath: constants.ConfigFilename,
|
||||
filePath: "upgrade-plan.yaml",
|
||||
cosignPubKey: pubK,
|
||||
},
|
||||
csp: cloudprovider.Azure,
|
||||
verifier: singleUUIDVerifier(),
|
||||
wantUpgrade: true,
|
||||
wantUpgrade: false,
|
||||
},
|
||||
"upgrade to stdout": {
|
||||
patchLister: stubPatchLister{list: availablePatches},
|
||||
planner: stubUpgradePlanner{
|
||||
image: "projects/constellation-images/global/images/constellation-v0-0-0",
|
||||
image: "v1.0.0",
|
||||
},
|
||||
imageFetchStatus: http.StatusOK,
|
||||
measurementsFetchStatus: http.StatusOK,
|
||||
flags: upgradePlanFlags{
|
||||
configPath: constants.ConfigFilename,
|
||||
@ -395,66 +270,69 @@ func TestUpgradePlan(t *testing.T) {
|
||||
cosignPubKey: pubK,
|
||||
},
|
||||
csp: cloudprovider.GCP,
|
||||
cliVersion: "v1.0.0",
|
||||
verifier: singleUUIDVerifier(),
|
||||
wantUpgrade: true,
|
||||
},
|
||||
"current image not valid": {
|
||||
patchLister: stubPatchLister{list: availablePatches},
|
||||
planner: stubUpgradePlanner{
|
||||
image: "not-valid",
|
||||
},
|
||||
imageFetchStatus: http.StatusOK,
|
||||
measurementsFetchStatus: http.StatusOK,
|
||||
flags: upgradePlanFlags{
|
||||
configPath: constants.ConfigFilename,
|
||||
filePath: "upgrade-plan.yaml",
|
||||
cosignPubKey: pubK,
|
||||
},
|
||||
csp: cloudprovider.GCP,
|
||||
verifier: singleUUIDVerifier(),
|
||||
wantErr: true,
|
||||
csp: cloudprovider.GCP,
|
||||
cliVersion: "v1.0.0",
|
||||
verifier: singleUUIDVerifier(),
|
||||
wantErr: true,
|
||||
},
|
||||
"image fetch error": {
|
||||
patchLister: stubPatchLister{err: errors.New("error")},
|
||||
planner: stubUpgradePlanner{
|
||||
image: "projects/constellation-images/global/images/constellation-v0-0-0",
|
||||
image: "v1.0.0",
|
||||
},
|
||||
imageFetchStatus: http.StatusInternalServerError,
|
||||
measurementsFetchStatus: http.StatusOK,
|
||||
flags: upgradePlanFlags{
|
||||
configPath: constants.ConfigFilename,
|
||||
filePath: "upgrade-plan.yaml",
|
||||
cosignPubKey: pubK,
|
||||
},
|
||||
csp: cloudprovider.GCP,
|
||||
verifier: singleUUIDVerifier(),
|
||||
wantErr: true,
|
||||
csp: cloudprovider.GCP,
|
||||
cliVersion: "v1.0.0",
|
||||
verifier: singleUUIDVerifier(),
|
||||
},
|
||||
"measurements fetch error": {
|
||||
patchLister: stubPatchLister{list: availablePatches},
|
||||
planner: stubUpgradePlanner{
|
||||
image: "projects/constellation-images/global/images/constellation-v0-0-0",
|
||||
image: "v1.0.0",
|
||||
},
|
||||
imageFetchStatus: http.StatusOK,
|
||||
measurementsFetchStatus: http.StatusInternalServerError,
|
||||
flags: upgradePlanFlags{
|
||||
configPath: constants.ConfigFilename,
|
||||
filePath: "upgrade-plan.yaml",
|
||||
cosignPubKey: pubK,
|
||||
},
|
||||
csp: cloudprovider.GCP,
|
||||
verifier: singleUUIDVerifier(),
|
||||
wantErr: true,
|
||||
csp: cloudprovider.GCP,
|
||||
cliVersion: "v1.0.0",
|
||||
verifier: singleUUIDVerifier(),
|
||||
},
|
||||
"failing search should not result in error": {
|
||||
patchLister: stubPatchLister{list: availablePatches},
|
||||
planner: stubUpgradePlanner{
|
||||
image: "projects/constellation-images/global/images/constellation-v0-0-0",
|
||||
image: "v1.0.0",
|
||||
},
|
||||
imageFetchStatus: http.StatusOK,
|
||||
measurementsFetchStatus: http.StatusOK,
|
||||
flags: upgradePlanFlags{
|
||||
configPath: constants.ConfigFilename,
|
||||
filePath: "upgrade-plan.yaml",
|
||||
cosignPubKey: pubK,
|
||||
},
|
||||
csp: cloudprovider.GCP,
|
||||
csp: cloudprovider.GCP,
|
||||
cliVersion: "v1.0.0",
|
||||
verifier: &stubRekorVerifier{
|
||||
SearchByHashUUIDs: []string{},
|
||||
SearchByHashError: errors.New("some error"),
|
||||
@ -462,17 +340,18 @@ func TestUpgradePlan(t *testing.T) {
|
||||
wantUpgrade: true,
|
||||
},
|
||||
"failing verify should not result in error": {
|
||||
patchLister: stubPatchLister{list: availablePatches},
|
||||
planner: stubUpgradePlanner{
|
||||
image: "projects/constellation-images/global/images/constellation-v0-0-0",
|
||||
image: "v1.0.0",
|
||||
},
|
||||
imageFetchStatus: http.StatusOK,
|
||||
measurementsFetchStatus: http.StatusOK,
|
||||
flags: upgradePlanFlags{
|
||||
configPath: constants.ConfigFilename,
|
||||
filePath: "upgrade-plan.yaml",
|
||||
cosignPubKey: pubK,
|
||||
},
|
||||
csp: cloudprovider.GCP,
|
||||
csp: cloudprovider.GCP,
|
||||
cliVersion: "v1.0.0",
|
||||
verifier: &stubRekorVerifier{
|
||||
SearchByHashUUIDs: []string{"11111111111111111111111111111111111111111111111111111111111111111111111111111111"},
|
||||
VerifyEntryError: errors.New("some error"),
|
||||
@ -499,24 +378,17 @@ func TestUpgradePlan(t *testing.T) {
|
||||
cmd.SetErr(&errTarget)
|
||||
|
||||
client := newTestClient(func(req *http.Request) *http.Response {
|
||||
if req.URL.String() == imageReleaseURL {
|
||||
return &http.Response{
|
||||
StatusCode: tc.imageFetchStatus,
|
||||
Body: io.NopCloser(bytes.NewBuffer(mustMarshal(t, testImages))),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(req.URL.String(), "azure/measurements.json") {
|
||||
return &http.Response{
|
||||
StatusCode: tc.measurementsFetchStatus,
|
||||
Body: io.NopCloser(strings.NewReader(`{"csp":"azure","image":"v1.0.0","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)),
|
||||
Body: io.NopCloser(strings.NewReader(`{"csp":"azure","image":"v1.0.1","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(req.URL.String(), "azure/measurements.json.sig") {
|
||||
return &http.Response{
|
||||
StatusCode: tc.measurementsFetchStatus,
|
||||
Body: io.NopCloser(strings.NewReader("MEQCIFh8CVELp/Da2U2Jt404OXsUeDfqtrf3pqGRuvxnxhI8AiBTHF9tHEPwFedYG3Jgn2ELOxss+Ybc6135vEtClBrbpg==")),
|
||||
Body: io.NopCloser(strings.NewReader("MEYCIQDu2Sft91FjN278uP+r/HFMms6IH/tRtaHzYvIN0xPgdwIhAJhiFxVsHCa0NK6bZOGLE9c4miZHIqFTKvgpTf3rJ9dW")),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
@ -524,14 +396,14 @@ func TestUpgradePlan(t *testing.T) {
|
||||
if strings.HasSuffix(req.URL.String(), "gcp/measurements.json") {
|
||||
return &http.Response{
|
||||
StatusCode: tc.measurementsFetchStatus,
|
||||
Body: io.NopCloser(strings.NewReader(`{"csp":"gcp","image":"v1.0.0","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)),
|
||||
Body: io.NopCloser(strings.NewReader(`{"csp":"gcp","image":"v1.0.1","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(req.URL.String(), "gcp/measurements.json.sig") {
|
||||
return &http.Response{
|
||||
StatusCode: tc.measurementsFetchStatus,
|
||||
Body: io.NopCloser(strings.NewReader("MEYCIQCr/gDGjj11mR5OeImwOLjxnBqMbBmqoK7yXqy0cXR3HQIhALpVDdYwR9VNJnWwtl8bTfrezyJbc7UNZJO4PJe+stFP")),
|
||||
Body: io.NopCloser(strings.NewReader("MEQCIBUssv92LpSMiXE1UAVf2fW8J9pZHiLseo2tdZjxv2OMAiB6K8e8yL0768jWjlFnRe3Rc2x/dX34uzX3h0XUrlYt1A==")),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
@ -543,7 +415,7 @@ func TestUpgradePlan(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
err := upgradePlan(cmd, tc.planner, fileHandler, client, tc.verifier, tc.flags)
|
||||
err := upgradePlan(cmd, tc.planner, tc.patchLister, fileHandler, client, tc.verifier, tc.flags, tc.cliVersion)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
@ -571,11 +443,55 @@ func TestUpgradePlan(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func mustMarshal(t *testing.T, v any) []byte {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal: %s", err)
|
||||
func TestNextMinorVersion(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
version string
|
||||
wantNextMinorVersion string
|
||||
wantErr bool
|
||||
}{
|
||||
"gets next": {
|
||||
version: "v1.0.0",
|
||||
wantNextMinorVersion: "v1.1",
|
||||
},
|
||||
"gets next from minor version": {
|
||||
version: "v1.0",
|
||||
wantNextMinorVersion: "v1.1",
|
||||
},
|
||||
"empty version": {
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
gotNext, err := nextMinorVersion(tc.version)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.wantNextMinorVersion, gotNext)
|
||||
})
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
type stubUpgradePlanner struct {
|
||||
image string
|
||||
err error
|
||||
}
|
||||
|
||||
func (u stubUpgradePlanner) GetCurrentImage(context.Context) (*unstructured.Unstructured, string, error) {
|
||||
return nil, u.image, u.err
|
||||
}
|
||||
|
||||
type stubPatchLister struct {
|
||||
list update.VersionsList
|
||||
err error
|
||||
}
|
||||
|
||||
func (s stubPatchLister) PatchVersionsOf(ctx context.Context, stream, minor, kind string) (*update.VersionsList, error) {
|
||||
return &s.list, s.err
|
||||
}
|
||||
|
173
cli/internal/update/update.go
Normal file
173
cli/internal/update/update.go
Normal file
@ -0,0 +1,173 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package update
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// VersionsList represents a list of versions for a kind of resource.
|
||||
// It has a granularity of either "major" or "minor".
|
||||
//
|
||||
// For example, a VersionsList with granularity "major" could contain
|
||||
// the base version "v1" and a list of minor versions "v1.0", "v1.1", "v1.2" etc.
|
||||
// A VersionsList with granularity "minor" could contain the base version
|
||||
// "v1.0" and a list of patch versions "v1.0.0", "v1.0.1", "v1.0.2" etc.
|
||||
type VersionsList struct {
|
||||
// Stream is the update stream of the list.
|
||||
// Currently, only "stable" is supported.
|
||||
Stream string `json:"stream"`
|
||||
// Granularity is the granularity of the base version of this list.
|
||||
// It can be either "major" or "minor".
|
||||
Granularity string `json:"granularity"`
|
||||
// Base is the base version of the list.
|
||||
// Every version in the list is a finer-grained version of this base version.
|
||||
Base string `json:"base"`
|
||||
// Kind is the kind of resource this list is for.
|
||||
Kind string `json:"kind"`
|
||||
// Versions is a list of all versions in this list.
|
||||
Versions []string `json:"versions"`
|
||||
}
|
||||
|
||||
// validate checks if the list is valid.
|
||||
// This performs the following checks:
|
||||
// - The stream is supported.
|
||||
// - The granularity is "major" or "minor".
|
||||
// - The kind is supported.
|
||||
// - The base version is a valid semantic version that matches the granularity.
|
||||
// - All versions in the list are valid semantic versions that are finer-grained than the base version.
|
||||
func (l *VersionsList) validate() error {
|
||||
var issues []string
|
||||
if l.Stream != "stable" {
|
||||
issues = append(issues, fmt.Sprintf("stream %q is not supported", l.Stream))
|
||||
}
|
||||
if l.Granularity != "major" && l.Granularity != "minor" {
|
||||
issues = append(issues, fmt.Sprintf("granularity %q is not supported", l.Granularity))
|
||||
}
|
||||
if l.Kind != "image" {
|
||||
issues = append(issues, fmt.Sprintf("kind %q is not supported", l.Kind))
|
||||
}
|
||||
if !semver.IsValid(l.Base) {
|
||||
issues = append(issues, fmt.Sprintf("base version %q is not a valid semantic version", l.Base))
|
||||
}
|
||||
var normalizeFunc func(string) string
|
||||
switch l.Granularity {
|
||||
case "major":
|
||||
normalizeFunc = semver.Major
|
||||
case "minor":
|
||||
normalizeFunc = semver.MajorMinor
|
||||
default:
|
||||
normalizeFunc = func(s string) string { return s }
|
||||
}
|
||||
if normalizeFunc(l.Base) != l.Base {
|
||||
issues = append(issues, fmt.Sprintf("base version %q is not a %v version", l.Base, l.Granularity))
|
||||
}
|
||||
for _, ver := range l.Versions {
|
||||
if !semver.IsValid(ver) {
|
||||
issues = append(issues, fmt.Sprintf("version %q in list is not a valid semantic version", ver))
|
||||
}
|
||||
if normalizeFunc(ver) != l.Base {
|
||||
issues = append(issues, fmt.Sprintf("version %q in list is not a finer-grained version of base version %q", ver, l.Base))
|
||||
}
|
||||
}
|
||||
if len(issues) > 0 {
|
||||
return fmt.Errorf("version list is invalid:\n%s", strings.Join(issues, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VersionsFetcher fetches a list of versions.
|
||||
type VersionsFetcher struct {
|
||||
httpc httpc
|
||||
}
|
||||
|
||||
// New returns a new VersionsFetcher.
|
||||
func New() *VersionsFetcher {
|
||||
return &VersionsFetcher{
|
||||
httpc: http.DefaultClient,
|
||||
}
|
||||
}
|
||||
|
||||
// MinorVersionsOf fetches the list of minor versions for a given stream, major version and kind.
|
||||
func (f *VersionsFetcher) MinorVersionsOf(ctx context.Context, stream, major, kind string) (*VersionsList, error) {
|
||||
return f.list(ctx, stream, "major", major, kind)
|
||||
}
|
||||
|
||||
// PatchVersionsOf fetches the list of patch versions for a given stream, minor version and kind.
|
||||
func (f *VersionsFetcher) PatchVersionsOf(ctx context.Context, stream, minor, kind string) (*VersionsList, error) {
|
||||
return f.list(ctx, stream, "minor", minor, kind)
|
||||
}
|
||||
|
||||
// list fetches the list of versions for a given stream, granularity, base and kind.
|
||||
func (f *VersionsFetcher) list(ctx context.Context, stream, granularity, base, kind string) (*VersionsList, error) {
|
||||
raw, err := getFromURL(ctx, f.httpc, stream, granularity, base, kind)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching versions list: %w", err)
|
||||
}
|
||||
list := &VersionsList{}
|
||||
if err := json.Unmarshal(raw, &list); err != nil {
|
||||
return nil, fmt.Errorf("decoding versions list: %w", err)
|
||||
}
|
||||
if err := list.validate(); err != nil {
|
||||
return nil, fmt.Errorf("validating versions list: %w", err)
|
||||
}
|
||||
if !f.listMatchesRequest(list, stream, granularity, base, kind) {
|
||||
return nil, fmt.Errorf("versions list does not match request")
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (f *VersionsFetcher) listMatchesRequest(list *VersionsList, stream, granularity, base, kind string) bool {
|
||||
return list.Stream == stream && list.Granularity == granularity && list.Base == base && list.Kind == kind
|
||||
}
|
||||
|
||||
// getFromURL fetches the versions list from a URL.
|
||||
func getFromURL(ctx context.Context, client httpc, stream, granularity, base, kind string) ([]byte, error) {
|
||||
url, err := url.Parse(constants.CDNRepositoryURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing image version repository URL: %w", err)
|
||||
}
|
||||
kindFilename := path.Base(kind) + ".json"
|
||||
url.Path = path.Join("constellation/v1/updates", stream, granularity, base, kindFilename)
|
||||
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("versions list %q does not exist", url.String())
|
||||
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)
|
||||
}
|
279
cli/internal/update/update_test.go
Normal file
279
cli/internal/update/update_test.go
Normal file
@ -0,0 +1,279 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package update
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
goleak.VerifyTestMain(m)
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
listFunc func() *VersionsList
|
||||
overrideFunc func(list *VersionsList)
|
||||
wantErr bool
|
||||
}{
|
||||
"valid major list": {
|
||||
listFunc: majorList,
|
||||
},
|
||||
"valid minor list": {
|
||||
listFunc: minorList,
|
||||
},
|
||||
"invalid stream": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *VersionsList) {
|
||||
list.Stream = "invalid"
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid granularity": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *VersionsList) {
|
||||
list.Granularity = "invalid"
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid kind": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *VersionsList) {
|
||||
list.Kind = "invalid"
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"base ver is not semantic version": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *VersionsList) {
|
||||
list.Base = "invalid"
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"base ver does not reflect major granularity": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *VersionsList) {
|
||||
list.Base = "v1.0"
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"base ver does not reflect minor granularity": {
|
||||
listFunc: minorList,
|
||||
overrideFunc: func(list *VersionsList) {
|
||||
list.Base = "v1"
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"version in list is not semantic version": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *VersionsList) {
|
||||
list.Versions[0] = "invalid"
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"version in list is not sub version of base": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *VersionsList) {
|
||||
list.Versions[0] = "v2.1"
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
list := tc.listFunc()
|
||||
if tc.overrideFunc != nil {
|
||||
tc.overrideFunc(list)
|
||||
}
|
||||
err := list.validate()
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
majorListJSON, err := json.Marshal(majorList())
|
||||
require.NoError(t, err)
|
||||
minorListJSON, err := json.Marshal(minorList())
|
||||
require.NoError(t, err)
|
||||
inconsistentList := majorList()
|
||||
inconsistentList.Base = "v2"
|
||||
inconsistentListJSON, err := json.Marshal(inconsistentList)
|
||||
require.NoError(t, err)
|
||||
client := newTestClient(func(req *http.Request) *http.Response {
|
||||
switch req.URL.Path {
|
||||
case "/constellation/v1/updates/stable/major/v1/image.json":
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBuffer(majorListJSON)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
case "/constellation/v1/updates/stable/minor/v1.1/image.json":
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBuffer(minorListJSON)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
case "/constellation/v1/updates/stable/major/v1/500.json": // 500 error
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
Body: io.NopCloser(bytes.NewBufferString("Server Error.")),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
case "/constellation/v1/updates/stable/major/v1/nojson.json": // invalid format
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBufferString("not json")),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
case "/constellation/v1/updates/stable/major/v2/image.json": // inconsistent list
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBuffer(inconsistentListJSON)),
|
||||
Header: make(http.Header),
|
||||
}
|
||||
case "/constellation/v1/updates/stable/major/v3/image.json": // does not match requested version
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBuffer(minorListJSON)),
|
||||
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 {
|
||||
stream, granularity, base, kind string
|
||||
overrideFile string
|
||||
wantList VersionsList
|
||||
wantErr bool
|
||||
}{
|
||||
"major list fetched remotely": {
|
||||
wantList: *majorList(),
|
||||
},
|
||||
"minor list fetched remotely": {
|
||||
granularity: "minor",
|
||||
base: "v1.1",
|
||||
wantList: *minorList(),
|
||||
},
|
||||
"list does not exist": {
|
||||
stream: "unknown",
|
||||
wantErr: true,
|
||||
},
|
||||
"unexpected error code": {
|
||||
kind: "500",
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid json returned": {
|
||||
kind: "nojson",
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid list returned": {
|
||||
base: "v2",
|
||||
wantErr: true,
|
||||
},
|
||||
"response does not match request": {
|
||||
base: "v3",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
stream := "stable"
|
||||
granularity := "major"
|
||||
base := "v1"
|
||||
kind := "image"
|
||||
if tc.stream != "" {
|
||||
stream = tc.stream
|
||||
}
|
||||
if tc.granularity != "" {
|
||||
granularity = tc.granularity
|
||||
}
|
||||
if tc.base != "" {
|
||||
base = tc.base
|
||||
}
|
||||
if tc.kind != "" {
|
||||
kind = tc.kind
|
||||
}
|
||||
|
||||
fetcher := &VersionsFetcher{
|
||||
httpc: client,
|
||||
}
|
||||
list, err := fetcher.list(context.Background(), stream, granularity, base, kind)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.wantList, *list)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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 majorList() *VersionsList {
|
||||
return &VersionsList{
|
||||
Stream: "stable",
|
||||
Granularity: "major",
|
||||
Base: "v1",
|
||||
Kind: "image",
|
||||
Versions: []string{
|
||||
"v1.0", "v1.1", "v1.2",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func minorList() *VersionsList {
|
||||
return &VersionsList{
|
||||
Stream: "stable",
|
||||
Granularity: "minor",
|
||||
Base: "v1.1",
|
||||
Kind: "image",
|
||||
Versions: []string{
|
||||
"v1.1.0", "v1.1.1", "v1.1.2",
|
||||
},
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||
@ -149,7 +150,7 @@ func TestVariant(t *testing.T) {
|
||||
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" {
|
||||
if strings.HasSuffix(req.URL.String(), "/constellation/v1/images/someImageVersionUID.json") {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewBufferString(lut)),
|
||||
|
Loading…
x
Reference in New Issue
Block a user