mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
b25228d175
* upgrade versioning * dont pass upgrade kind as boolean * whitespace * fix godot lint check * clarify upgrade check directory suffix * cli: dry-run Terraform migrations on `upgrade check` (#1942) * dry-run Terraform migrations on upgrade check * clean whole upgrade dir * clean up check workspace after planning * fix parsing * extend upgrade check test * rename unused parameters * exclude false positives in test
504 lines
15 KiB
Go
504 lines
15 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/edgelesssys/constellation/v2/cli/internal/upgrade"
|
|
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
|
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
|
"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/file"
|
|
"github.com/edgelesssys/constellation/v2/internal/logger"
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/mod/semver"
|
|
)
|
|
|
|
// TestBuildString checks that the resulting user output is as expected. Slow part is the Sscanf in parseCanonicalSemver().
|
|
func TestBuildString(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
upgrade versionUpgrade
|
|
expected string
|
|
wantError bool
|
|
}{
|
|
"update everything": {
|
|
upgrade: versionUpgrade{
|
|
newServices: "v2.5.0",
|
|
newImages: map[string]measurements.M{
|
|
"v2.5.0": measurements.DefaultsFor(cloudprovider.QEMU, variant.QEMUVTPM{}),
|
|
},
|
|
newKubernetes: []string{"v1.24.12", "v1.25.6"},
|
|
newCLI: []string{"v2.5.0", "v2.6.0"},
|
|
currentServices: "v2.4.0",
|
|
currentImage: "v2.4.0",
|
|
currentKubernetes: "v1.24.5",
|
|
currentCLI: "v2.4.0",
|
|
},
|
|
expected: "The following updates are available with this CLI:\n Kubernetes: v1.24.5 --> v1.24.12 v1.25.6\n Images:\n v2.4.0 --> v2.5.0\n Includes these measurements:\n 4:\n expected: \"1234123412341234123412341234123412341234123412341234123412341234\"\n warnOnly: false\n 8:\n expected: \"0000000000000000000000000000000000000000000000000000000000000000\"\n warnOnly: false\n 9:\n expected: \"1234123412341234123412341234123412341234123412341234123412341234\"\n warnOnly: false\n 11:\n expected: \"0000000000000000000000000000000000000000000000000000000000000000\"\n warnOnly: false\n 12:\n expected: \"1234123412341234123412341234123412341234123412341234123412341234\"\n warnOnly: false\n 13:\n expected: \"0000000000000000000000000000000000000000000000000000000000000000\"\n warnOnly: false\n 15:\n expected: \"0000000000000000000000000000000000000000000000000000000000000000\"\n warnOnly: false\n \n Services: v2.4.0 --> v2.5.0\n",
|
|
},
|
|
"cli incompatible with K8s": {
|
|
upgrade: versionUpgrade{
|
|
newCLI: []string{"v2.5.0", "v2.6.0"},
|
|
currentCLI: "v2.4.0",
|
|
},
|
|
expected: "There are newer CLIs available (v2.5.0 v2.6.0), however, you need to upgrade your cluster's Kubernetes version first.\n",
|
|
},
|
|
"cli compatible with K8s": {
|
|
upgrade: versionUpgrade{
|
|
newCompatibleCLI: []string{"v2.5.0", "v2.6.0"},
|
|
currentCLI: "v2.4.0",
|
|
},
|
|
expected: "Newer CLI versions that are compatible with your cluster are: v2.5.0 v2.6.0\n",
|
|
},
|
|
"k8s only": {
|
|
upgrade: versionUpgrade{
|
|
newKubernetes: []string{"v1.24.12", "v1.25.6"},
|
|
currentKubernetes: "v1.24.5",
|
|
},
|
|
expected: "The following updates are available with this CLI:\n Kubernetes: v1.24.5 --> v1.24.12 v1.25.6\n",
|
|
},
|
|
"no upgrades": {
|
|
upgrade: versionUpgrade{
|
|
newServices: "",
|
|
newImages: map[string]measurements.M{},
|
|
newKubernetes: []string{},
|
|
newCLI: []string{},
|
|
currentServices: "v2.5.0",
|
|
currentImage: "v2.5.0",
|
|
currentKubernetes: "v1.25.6",
|
|
currentCLI: "v2.5.0",
|
|
},
|
|
expected: "You are up to date.\n",
|
|
},
|
|
"no upgrades #2": {
|
|
upgrade: versionUpgrade{},
|
|
expected: "You are up to date.\n",
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
result, err := tc.upgrade.buildString()
|
|
if tc.wantError {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
assert.NoError(err)
|
|
assert.Equal(tc.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetCurrentImageVersion(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
stubUpgradeChecker stubUpgradeChecker
|
|
wantErr bool
|
|
}{
|
|
"valid version": {
|
|
stubUpgradeChecker: stubUpgradeChecker{
|
|
image: "v1.0.0",
|
|
},
|
|
},
|
|
"invalid version": {
|
|
stubUpgradeChecker: stubUpgradeChecker{
|
|
image: "invalid",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
"GetCurrentImage error": {
|
|
stubUpgradeChecker: stubUpgradeChecker{
|
|
err: errors.New("error"),
|
|
},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
version, err := getCurrentImageVersion(context.Background(), tc.stubUpgradeChecker)
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
|
|
assert.NoError(err)
|
|
assert.True(semver.IsValid(version))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetCompatibleImageMeasurements(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
csp := cloudprovider.Azure
|
|
attestationVariant := variant.AzureSEVSNP{}
|
|
zero := versionsapi.Version{
|
|
Ref: "-",
|
|
Stream: "stable",
|
|
Version: "v0.0.0",
|
|
Kind: versionsapi.VersionKindImage,
|
|
}
|
|
one := versionsapi.Version{
|
|
Ref: "-",
|
|
Stream: "stable",
|
|
Version: "v1.0.0",
|
|
Kind: versionsapi.VersionKindImage,
|
|
}
|
|
images := []versionsapi.Version{zero, one}
|
|
|
|
client := newTestClient(func(req *http.Request) *http.Response {
|
|
if strings.HasSuffix(req.URL.String(), "v0.0.0/azure/measurements.json") {
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader(`{"csp":"azure","image":"v0.0.0","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)),
|
|
Header: make(http.Header),
|
|
}
|
|
}
|
|
if strings.HasSuffix(req.URL.String(), "v0.0.0/azure/measurements.json.sig") {
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader("MEQCIGRR7RaSMs892Ta06/Tz7LqPUxI05X4wQcP+nFFmZtmaAiBNl9X8mUKmUBfxg13LQBfmmpw6JwYQor5hOwM3NFVPAg==")),
|
|
Header: make(http.Header),
|
|
}
|
|
}
|
|
|
|
if strings.HasSuffix(req.URL.String(), "v1.0.0/azure/measurements.json") {
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader(`{"csp":"azure","image":"v1.0.0","measurements":{"0":{"expected":"0000000000000000000000000000000000000000000000000000000000000000","warnOnly":false}}}`)),
|
|
Header: make(http.Header),
|
|
}
|
|
}
|
|
if strings.HasSuffix(req.URL.String(), "v1.0.0/azure/measurements.json.sig") {
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Body: io.NopCloser(strings.NewReader("MEQCIFh8CVELp/Da2U2Jt404OXsUeDfqtrf3pqGRuvxnxhI8AiBTHF9tHEPwFedYG3Jgn2ELOxss+Ybc6135vEtClBrbpg==")),
|
|
Header: make(http.Header),
|
|
}
|
|
}
|
|
|
|
return &http.Response{
|
|
StatusCode: http.StatusNotFound,
|
|
Body: io.NopCloser(strings.NewReader("Not found.")),
|
|
Header: make(http.Header),
|
|
}
|
|
})
|
|
|
|
upgrades, err := getCompatibleImageMeasurements(context.Background(), &bytes.Buffer{}, client, &stubCosignVerifier{}, singleUUIDVerifier(), csp, attestationVariant, images, logger.NewTest(t))
|
|
assert.NoError(err)
|
|
|
|
for _, measurement := range upgrades {
|
|
assert.NotEmpty(measurement)
|
|
}
|
|
}
|
|
|
|
func TestUpgradeCheck(t *testing.T) {
|
|
v2_3 := versionsapi.Version{
|
|
Ref: "-",
|
|
Stream: "stable",
|
|
Version: "v2.3.0",
|
|
Kind: versionsapi.VersionKindImage,
|
|
}
|
|
v2_5 := versionsapi.Version{
|
|
Ref: "-",
|
|
Stream: "stable",
|
|
Version: "v2.5.0",
|
|
Kind: versionsapi.VersionKindImage,
|
|
}
|
|
collector := stubVersionCollector{
|
|
supportedServicesVersions: "v2.5.0",
|
|
supportedImages: []versionsapi.Version{v2_3},
|
|
supportedImageVersions: map[string]measurements.M{
|
|
"v2.3.0": measurements.DefaultsFor(cloudprovider.GCP, variant.GCPSEVES{}),
|
|
},
|
|
supportedK8sVersions: []string{"v1.24.5", "v1.24.12", "v1.25.6"},
|
|
currentServicesVersions: "v2.4.0",
|
|
currentImageVersion: "v2.4.0",
|
|
currentK8sVersion: "v1.24.5",
|
|
currentCLIVersion: "v2.4.0",
|
|
images: []versionsapi.Version{v2_5},
|
|
newCLIVersionsList: []string{"v2.5.0", "v2.6.0"},
|
|
}
|
|
|
|
testCases := map[string]struct {
|
|
collector stubVersionCollector
|
|
flags upgradeCheckFlags
|
|
csp cloudprovider.Provider
|
|
checker stubUpgradeChecker
|
|
imagefetcher stubImageFetcher
|
|
cliVersion string
|
|
wantError bool
|
|
}{
|
|
"upgrades gcp": {
|
|
collector: collector,
|
|
checker: stubUpgradeChecker{},
|
|
imagefetcher: stubImageFetcher{},
|
|
flags: upgradeCheckFlags{
|
|
configPath: constants.ConfigFilename,
|
|
},
|
|
csp: cloudprovider.GCP,
|
|
cliVersion: "v1.0.0",
|
|
},
|
|
"terraform err": {
|
|
collector: collector,
|
|
checker: stubUpgradeChecker{
|
|
err: assert.AnError,
|
|
},
|
|
imagefetcher: stubImageFetcher{},
|
|
flags: upgradeCheckFlags{
|
|
configPath: constants.ConfigFilename,
|
|
},
|
|
csp: cloudprovider.GCP,
|
|
cliVersion: "v1.0.0",
|
|
wantError: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
require := require.New(t)
|
|
|
|
fileHandler := file.NewHandler(afero.NewMemMapFs())
|
|
cfg := defaultConfigWithExpectedMeasurements(t, config.Default(), tc.csp)
|
|
require.NoError(fileHandler.WriteYAML(tc.flags.configPath, cfg))
|
|
|
|
checkCmd := upgradeCheckCmd{
|
|
canUpgradeCheck: true,
|
|
collect: &tc.collector,
|
|
checker: tc.checker,
|
|
imagefetcher: tc.imagefetcher,
|
|
log: logger.NewTest(t),
|
|
}
|
|
|
|
cmd := newUpgradeCheckCmd()
|
|
|
|
err := checkCmd.upgradeCheck(cmd, fileHandler, stubAttestationFetcher{}, tc.flags)
|
|
if tc.wantError {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
assert.NoError(err)
|
|
})
|
|
}
|
|
}
|
|
|
|
type stubVersionCollector struct {
|
|
supportedServicesVersions string
|
|
supportedImages []versionsapi.Version
|
|
supportedImageVersions map[string]measurements.M
|
|
supportedK8sVersions []string
|
|
supportedCLIVersions []string
|
|
currentServicesVersions string
|
|
currentImageVersion string
|
|
currentK8sVersion string
|
|
currentCLIVersion string
|
|
images []versionsapi.Version
|
|
newCLIVersionsList []string
|
|
newCompatibleCLIVersionsList []string
|
|
someErr error
|
|
}
|
|
|
|
func (s *stubVersionCollector) newMeasurements(_ context.Context, _ cloudprovider.Provider, _ variant.Variant, _ []versionsapi.Version) (map[string]measurements.M, error) {
|
|
return s.supportedImageVersions, nil
|
|
}
|
|
|
|
func (s *stubVersionCollector) currentVersions(_ context.Context) (currentVersionInfo, error) {
|
|
return currentVersionInfo{
|
|
service: s.currentServicesVersions,
|
|
image: s.currentImageVersion,
|
|
k8s: s.currentK8sVersion,
|
|
cli: s.currentCLIVersion,
|
|
}, s.someErr
|
|
}
|
|
|
|
func (s *stubVersionCollector) supportedVersions(_ context.Context, _, _ string) (supportedVersionInfo, error) {
|
|
return supportedVersionInfo{
|
|
service: s.supportedServicesVersions,
|
|
image: s.supportedImages,
|
|
k8s: s.supportedK8sVersions,
|
|
cli: s.supportedCLIVersions,
|
|
}, s.someErr
|
|
}
|
|
|
|
func (s *stubVersionCollector) newImages(_ context.Context, _ string) ([]versionsapi.Version, error) {
|
|
return s.images, nil
|
|
}
|
|
|
|
func (s *stubVersionCollector) newerVersions(_ context.Context, _ []string) ([]versionsapi.Version, error) {
|
|
return s.images, nil
|
|
}
|
|
|
|
func (s *stubVersionCollector) newCLIVersions(_ context.Context) ([]string, error) {
|
|
return s.newCLIVersionsList, nil
|
|
}
|
|
|
|
func (s *stubVersionCollector) filterCompatibleCLIVersions(_ context.Context, _ []string, _ string) ([]string, error) {
|
|
return s.newCompatibleCLIVersionsList, nil
|
|
}
|
|
|
|
type stubUpgradeChecker struct {
|
|
image string
|
|
k8sVersion string
|
|
tfDiff bool
|
|
err error
|
|
}
|
|
|
|
func (u stubUpgradeChecker) CurrentImage(context.Context) (string, error) {
|
|
return u.image, u.err
|
|
}
|
|
|
|
func (u stubUpgradeChecker) CurrentKubernetesVersion(context.Context) (string, error) {
|
|
return u.k8sVersion, u.err
|
|
}
|
|
|
|
func (u stubUpgradeChecker) PlanTerraformMigrations(context.Context, upgrade.TerraformUpgradeOptions) (bool, error) {
|
|
return u.tfDiff, u.err
|
|
}
|
|
|
|
func (u stubUpgradeChecker) CheckTerraformMigrations(file.Handler) error {
|
|
return u.err
|
|
}
|
|
|
|
func (u stubUpgradeChecker) CleanUpTerraformMigrations(file.Handler) error {
|
|
return u.err
|
|
}
|
|
|
|
func TestNewCLIVersions(t *testing.T) {
|
|
someErr := errors.New("some error")
|
|
minorList := func() versionsapi.List {
|
|
return versionsapi.List{
|
|
Versions: []string{"v0.2.0"},
|
|
}
|
|
}
|
|
patchList := func() versionsapi.List {
|
|
return versionsapi.List{
|
|
Versions: []string{"v0.2.1"},
|
|
}
|
|
}
|
|
emptyVerList := func() versionsapi.List {
|
|
return versionsapi.List{}
|
|
}
|
|
verCollector := func(minorList, patchList versionsapi.List, verListErr error) versionCollector {
|
|
return versionCollector{
|
|
cliVersion: "v0.1.0",
|
|
versionsapi: stubVersionFetcher{
|
|
minorList: minorList,
|
|
patchList: patchList,
|
|
versionListErr: verListErr,
|
|
},
|
|
}
|
|
}
|
|
|
|
testCases := map[string]struct {
|
|
verCollector versionCollector
|
|
wantErr bool
|
|
}{
|
|
"works": {
|
|
verCollector: verCollector(minorList(), patchList(), nil),
|
|
},
|
|
"empty versions list": {
|
|
verCollector: verCollector(emptyVerList(), emptyVerList(), nil),
|
|
},
|
|
"version list error": {
|
|
verCollector: verCollector(minorList(), patchList(), someErr),
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
_, err := tc.verCollector.newCLIVersions(context.Background())
|
|
if tc.wantErr {
|
|
require.Error(err)
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFilterCompatibleCLIVersions(t *testing.T) {
|
|
someErr := errors.New("some error")
|
|
verCollector := func(cliInfoErr error) versionCollector {
|
|
return versionCollector{
|
|
cliVersion: "v0.1.0",
|
|
versionsapi: stubVersionFetcher{
|
|
cliInfoErr: cliInfoErr,
|
|
},
|
|
}
|
|
}
|
|
|
|
testCases := map[string]struct {
|
|
verCollector versionCollector
|
|
cliPatchVersions []string
|
|
wantErr bool
|
|
}{
|
|
"works": {
|
|
verCollector: verCollector(nil),
|
|
cliPatchVersions: []string{"v0.1.1"},
|
|
},
|
|
"cli info error": {
|
|
verCollector: verCollector(someErr),
|
|
cliPatchVersions: []string{"v0.1.1"},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
_, err := tc.verCollector.filterCompatibleCLIVersions(context.Background(), tc.cliPatchVersions, "v1.24.5")
|
|
if tc.wantErr {
|
|
require.Error(err)
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
})
|
|
}
|
|
}
|
|
|
|
type stubVersionFetcher struct {
|
|
minorList versionsapi.List
|
|
patchList versionsapi.List
|
|
versionListErr error
|
|
cliInfoErr error
|
|
}
|
|
|
|
func (f stubVersionFetcher) FetchVersionList(_ context.Context, list versionsapi.List) (versionsapi.List, error) {
|
|
switch list.Granularity {
|
|
case versionsapi.GranularityMajor:
|
|
return f.minorList, f.versionListErr
|
|
case versionsapi.GranularityMinor:
|
|
return f.patchList, f.versionListErr
|
|
}
|
|
return versionsapi.List{}, f.versionListErr
|
|
}
|
|
|
|
func (f stubVersionFetcher) FetchCLIInfo(_ context.Context, _ versionsapi.CLIInfo) (versionsapi.CLIInfo, error) {
|
|
return versionsapi.CLIInfo{}, f.cliInfoErr
|
|
}
|