cli: change upgrade-plan to upgrade-check

Upgrade check is used to find updates for the current cluster.
Optionally the found upgrades can be persisted to the config
for consumption by the upgrade-execute cmd.
The old `upgrade execute` in this commit does not work with
the new `upgrade plan`.
The current versions are read from the cluster.
Supported versions are read from the cli and the versionsapi.
Adds a new config field MicroserviceVersion that will be used
by `upgrade execute` to update the service versions.
The field is optional until 2.7
A deprecation warning for the upgrade key is printed during
config validation.
Kubernetes versions now specify the patch version to make it
explicit for users if an upgrade changes the k8s version.
This commit is contained in:
Otto Bittner 2023-01-31 12:12:19 +01:00
parent f204c24174
commit c275464634
27 changed files with 1080 additions and 992 deletions

View File

@ -149,6 +149,9 @@ runs:
run: |
yq eval -i '(.debugCluster) = true' constellation-conf.yaml
# Uses --force flag since the CLI currently does not have a pre-release version and is always on the latest released version.
# However, many of our pipelines work on prerelease images. Thus the used images are newer than the CLI's version.
# This makes the version validation in the CLI fail.
- name: Constellation create
shell: bash
run: |

View File

@ -82,30 +82,40 @@ func (u *Upgrader) Upgrade(ctx context.Context, imageReference, imageVersion str
// 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-version")
return u.getFromConstellationVersion(ctx, "imageVersion")
}
// GetCurrentKubernetesVersion returns the currently used Kubernetes version.
func (u *Upgrader) GetCurrentKubernetesVersion(ctx context.Context) (*unstructured.Unstructured, string, error) {
return u.getFromConstellationVersion(ctx, "kubernetesClusterVersion")
}
// getFromConstellationVersion queries the constellation-version object for a given field.
func (u *Upgrader) getFromConstellationVersion(ctx context.Context, fieldName string) (*unstructured.Unstructured, string, error) {
versionStruct, err := u.dynamicInterface.getCurrent(ctx, "constellation-version")
if err != nil {
return nil, "", err
}
spec, ok := imageStruct.Object["spec"]
spec, ok := versionStruct.Object["spec"]
if !ok {
return nil, "", errors.New("image spec missing")
return nil, "", errors.New("spec missing")
}
retErr := errors.New("invalid image spec")
retErr := errors.New("invalid spec")
specMap, ok := spec.(map[string]any)
if !ok {
return nil, "", retErr
}
currentImageVersion, ok := specMap["imageVersion"]
fieldValue, ok := specMap[fieldName]
if !ok {
return nil, "", retErr
}
imageVersion, ok := currentImageVersion.(string)
fieldValueString, ok := fieldValue.(string)
if !ok {
return nil, "", retErr
}
return imageStruct, imageVersion, nil
return versionStruct, fieldValueString, nil
}
// UpgradeHelmServices upgrade helm services.

View File

@ -478,7 +478,7 @@ 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"
conf.Image = constants.VersionInfo
switch csp {
case cloudprovider.Azure:

View File

@ -14,12 +14,12 @@ import (
func NewUpgradeCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "upgrade",
Short: "Plan and perform an upgrade of a Constellation cluster",
Long: "Plan and perform an upgrade of a Constellation cluster.",
Short: "Find and execute upgrades to your Constellation cluster",
Long: "Find and execute upgrades to your Constellation cluster.",
Args: cobra.ExactArgs(0),
}
cmd.AddCommand(newUpgradePlanCmd())
cmd.AddCommand(newUpgradeCheckCmd())
cmd.AddCommand(newUpgradeExecuteCmd())
return cmd

View File

@ -0,0 +1,535 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"sort"
"strings"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/cli/internal/helm"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/compatibility"
"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/kubernetes/kubectl"
"github.com/edgelesssys/constellation/v2/internal/sigstore"
"github.com/edgelesssys/constellation/v2/internal/versions"
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
"github.com/edgelesssys/constellation/v2/internal/versionsapi/fetcher"
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"golang.org/x/mod/semver"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func newUpgradeCheckCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "check",
Short: "Check for possible upgrades.",
Long: "Check which upgrades can be applied to your Constellation Cluster.",
Args: cobra.NoArgs,
RunE: runUpgradeCheck,
}
cmd.Flags().BoolP("write-config", "w", false, "Update the specified config file with the suggested versions")
cmd.Flags().String("ref", versionsapi.ReleaseRef, "Specify the reference used when querying the versionsapi for new versions.")
cmd.Flags().String("stream", "stable", "Specify the stream used when querying the versionsapi for new versions.")
return cmd
}
func runUpgradeCheck(cmd *cobra.Command, args []string) error {
log, err := newCLILogger(cmd)
if err != nil {
return fmt.Errorf("creating logger: %w", err)
}
defer log.Sync()
fileHandler := file.NewHandler(afero.NewOsFs())
flags, err := parseUpgradeCheckFlags(cmd)
if err != nil {
return err
}
checker, err := cloudcmd.NewUpgrader(cmd.OutOrStdout(), log)
if err != nil {
return err
}
versionListFetcher := fetcher.NewFetcher()
rekor, err := sigstore.NewRekor()
if err != nil {
return fmt.Errorf("constructing Rekor client: %w", err)
}
up := &upgradeCheckCmd{
collect: &versionCollector{
writer: cmd.OutOrStderr(),
checker: checker,
verListFetcher: versionListFetcher,
fileHandler: fileHandler,
client: http.DefaultClient,
rekor: rekor,
flags: flags,
cliVersion: constants.VersionInfo,
},
log: log,
}
return up.upgradeCheck(cmd, fileHandler, flags)
}
func parseUpgradeCheckFlags(cmd *cobra.Command) (upgradeCheckFlags, error) {
configPath, err := cmd.Flags().GetString("config")
if err != nil {
return upgradeCheckFlags{}, err
}
force, err := cmd.Flags().GetBool("force")
if err != nil {
return upgradeCheckFlags{}, err
}
writeConfig, err := cmd.Flags().GetBool("write-config")
if err != nil {
return upgradeCheckFlags{}, err
}
ref, err := cmd.Flags().GetString("ref")
if err != nil {
return upgradeCheckFlags{}, err
}
stream, err := cmd.Flags().GetString("stream")
if err != nil {
return upgradeCheckFlags{}, err
}
return upgradeCheckFlags{
configPath: configPath,
force: force,
writeConfig: writeConfig,
ref: ref,
stream: stream,
cosignPubKey: constants.CosignPublicKey,
}, nil
}
type upgradeCheckCmd struct {
collect collector
log debugLog
}
// upgradePlan plans an upgrade of a Constellation cluster.
func (u *upgradeCheckCmd) upgradeCheck(cmd *cobra.Command, fileHandler file.Handler, flags upgradeCheckFlags) error {
conf, err := config.New(fileHandler, flags.configPath, flags.force)
if err != nil {
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
}
u.log.Debugf("Read configuration from %q", flags.configPath)
// get current image version of the cluster
csp := conf.GetProvider()
u.log.Debugf("Using provider %s", csp.String())
currentServices, currentImage, currentK8s, err := u.collect.currentVersions(cmd.Context())
if err != nil {
return err
}
supportedServices, supportedImages, supportedK8s, err := u.collect.supportedVersions(cmd.Context(), currentImage, csp)
if err != nil {
return err
}
u.log.Debugf("Current service version: %s", currentServices)
u.log.Debugf("Supported service version: %s", supportedServices)
u.log.Debugf("Current k8s version: %s", currentK8s)
u.log.Debugf("Supported k8s version: %s", supportedK8s)
// Filter versions to only include upgrades
newServices := supportedServices
if err := compatibility.IsValidUpgrade(currentServices, supportedServices); err != nil {
newServices = ""
}
newKubernetes := filterK8sUpgrades(currentK8s, supportedK8s)
sort.Strings(newKubernetes)
supportedImages = filterImageUpgrades(currentImage, supportedImages)
newImages, err := u.collect.newMeasurementes(cmd.Context(), csp, supportedImages)
if err != nil {
return err
}
upgrade := versionUpgrade{
newServices: newServices,
newImages: newImages,
newKubernetes: newKubernetes,
currentServices: currentServices,
currentImage: currentImage,
currentKubernetes: currentK8s,
}
updateMsg, err := upgrade.buildString()
if err != nil {
return err
}
// Using Print over Println as buildString already includes a trailing newline where necessary.
cmd.Print(updateMsg)
if flags.writeConfig {
if err := upgrade.writeConfig(conf, fileHandler, flags.configPath); err != nil {
return fmt.Errorf("writing config: %w", err)
}
cmd.Println("Wrote config successfully.")
}
return nil
}
func sortedMapKeys[T any](a map[string]T) []string {
keys := []string{}
for k := range a {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
func filterImageUpgrades(currentVersion string, newVersions []versionsapi.Version) []versionsapi.Version {
newImages := []versionsapi.Version{}
for i := range newVersions {
if err := compatibility.IsValidUpgrade(currentVersion, newVersions[i].Version); err != nil {
continue
}
newImages = append(newImages, newVersions[i])
}
return newImages
}
func filterK8sUpgrades(currentVersion string, newVersions []string) []string {
result := []string{}
for i := range newVersions {
if err := compatibility.IsValidUpgrade(currentVersion, newVersions[i]); err != nil {
continue
}
result = append(result, newVersions[i])
}
return result
}
type collector interface {
currentVersions(ctx context.Context) (serviceVersions string, imageVersion string, k8sVersion string, err error)
supportedVersions(ctx context.Context, version string, csp cloudprovider.Provider) (serviceVersions string, imageVersions []versionsapi.Version, k8sVersions []string, err error)
newImages(ctx context.Context, version string, csp cloudprovider.Provider) ([]versionsapi.Version, error)
newMeasurementes(ctx context.Context, csp cloudprovider.Provider, images []versionsapi.Version) (map[string]measurements.M, error)
newerVersions(ctx context.Context, currentVersion string, allowedVersions []string) ([]versionsapi.Version, error)
}
type versionCollector struct {
writer io.Writer
checker upgradeChecker
verListFetcher versionListFetcher
fileHandler file.Handler
client *http.Client
rekor rekorVerifier
flags upgradeCheckFlags
cliVersion string
log debugLog
}
func (v *versionCollector) newMeasurementes(ctx context.Context, csp cloudprovider.Provider, images []versionsapi.Version) (map[string]measurements.M, error) {
// get expected measurements for each image
upgrades, err := getCompatibleImageMeasurements(ctx, v.writer, v.client, v.rekor, []byte(v.flags.cosignPubKey), csp, images, v.log)
if err != nil {
return nil, fmt.Errorf("fetching measurements for compatible images: %w", err)
}
v.log.Debugf("Compatible image measurements are %v", upgrades)
return upgrades, nil
}
func (v *versionCollector) currentVersions(ctx context.Context) (serviceVersion string, imageVersion string, k8sVersion string, err error) {
helmClient, err := helm.NewClient(kubectl.New(), constants.AdminConfFilename, constants.HelmNamespace, v.log)
if err != nil {
return "", "", "", fmt.Errorf("setting up helm client: %w", err)
}
serviceVersion, err = helmClient.Versions()
if err != nil {
return "", "", "", fmt.Errorf("getting service versions: %w", err)
}
imageVersion, err = getCurrentImageVersion(ctx, v.checker)
if err != nil {
return "", "", "", fmt.Errorf("getting image version: %w", err)
}
k8sVersion, err = getCurrentKubernetesVersion(ctx, v.checker)
if err != nil {
return "", "", "", fmt.Errorf("getting image version: %w", err)
}
return serviceVersion, imageVersion, k8sVersion, nil
}
// supportedVersions returns slices of supported versions.
func (v *versionCollector) supportedVersions(ctx context.Context, version string, csp cloudprovider.Provider) (serviceVersion string, imageVersions []versionsapi.Version, k8sVersions []string, err error) {
k8sVersions = versions.SupportedK8sVersions()
serviceVersion, err = helm.AvailableServiceVersions()
if err != nil {
return "", nil, nil, fmt.Errorf("loading service versions: %w", err)
}
imageVersions, err = v.newImages(ctx, version, csp)
if err != nil {
return "", nil, nil, fmt.Errorf("loading image versions: %w", err)
}
return serviceVersion, imageVersions, k8sVersions, nil
}
func (v *versionCollector) newImages(ctx context.Context, version string, csp cloudprovider.Provider) ([]versionsapi.Version, error) {
// 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(v.cliVersion)
nextImageMinorVer, err := compatibility.NextMinorVersion(currentImageMinorVer)
if err != nil {
return nil, fmt.Errorf("calculating next image minor version: %w", err)
}
v.log.Debugf("Current image minor version is %s", currentImageMinorVer)
v.log.Debugf("Current CLI minor version is %s", currentCLIMinorVer)
v.log.Debugf("Next image minor version is %s", nextImageMinorVer)
allowedMinorVersions := []string{currentImageMinorVer, nextImageMinorVer}
switch cliImageCompare := semver.Compare(currentCLIMinorVer, currentImageMinorVer); {
case cliImageCompare < 0:
if !v.flags.force {
return nil, fmt.Errorf("cluster image version (%s) newer than CLI version (%s)", currentImageMinorVer, currentCLIMinorVer)
}
if _, err := fmt.Fprintf(v.writer, "WARNING: CLI version is older than cluster image version. Continuing due to force flag."); err != nil {
return nil, fmt.Errorf("writing to buffer: %w", err)
}
case cliImageCompare == 0:
allowedMinorVersions = []string{currentImageMinorVer}
case cliImageCompare > 0:
allowedMinorVersions = []string{currentImageMinorVer, nextImageMinorVer}
}
v.log.Debugf("Allowed minor versions are %#v", allowedMinorVersions)
newerImages, err := v.newerVersions(ctx, currentImageMinorVer, allowedMinorVersions)
if err != nil {
return nil, fmt.Errorf("newer versions: %w", err)
}
return newerImages, nil
}
func (v *versionCollector) newerVersions(ctx context.Context, currentVersion string, allowedVersions []string) ([]versionsapi.Version, error) {
var updateCandidates []versionsapi.Version
for _, minorVer := range allowedVersions {
patchList := versionsapi.List{
Ref: v.flags.ref,
Stream: v.flags.stream,
Base: minorVer,
Granularity: versionsapi.GranularityMinor,
Kind: versionsapi.VersionKindImage,
}
patchList, err := v.verListFetcher.FetchVersionList(ctx, patchList)
var notFound *fetcher.NotFoundError
if errors.As(err, &notFound) {
v.log.Debugf("Skipping version: %s", err)
continue
}
if err != nil {
return nil, fmt.Errorf("fetching version list: %w", err)
}
updateCandidates = append(updateCandidates, patchList.StructuredVersions()...)
}
v.log.Debugf("Update candidates are %v", updateCandidates)
return updateCandidates, nil
}
type versionUpgrade struct {
newServices string
newImages map[string]measurements.M
newKubernetes []string
currentServices string
currentImage string
currentKubernetes string
}
func (v *versionUpgrade) buildString() (string, error) {
upgradeMsg := strings.Builder{}
if len(v.newKubernetes) > 0 {
upgradeMsg.WriteString(fmt.Sprintf(" Kubernetes: %s --> %s\n", v.currentKubernetes, strings.Join(v.newKubernetes, " ")))
}
if len(v.newImages) > 0 {
imageMsgs := strings.Builder{}
newImagesSorted := sortedMapKeys(v.newImages)
for i, image := range newImagesSorted {
// prevent trailing newlines
if i > 0 {
imageMsgs.WriteString("\n")
}
content, err := encoder.NewEncoder(v.newImages[image]).Encode()
contentFormated := strings.ReplaceAll(string(content), "\n", "\n ")
if err != nil {
return "", fmt.Errorf("marshalling measurements: %w", err)
}
imageMsgs.WriteString(fmt.Sprintf(" %s --> %s\n Includes these measurements:\n %s", v.currentImage, image, contentFormated))
}
upgradeMsg.WriteString(" Images:\n")
upgradeMsg.WriteString(imageMsgs.String())
fmt.Fprintln(&upgradeMsg, "")
}
if v.newServices != "" {
upgradeMsg.WriteString(fmt.Sprintf(" Services: %s --> %s\n", v.currentServices, v.newServices))
}
result := strings.Builder{}
if upgradeMsg.Len() > 0 {
result.WriteString("The following updates are available with this CLI:\n")
result.WriteString(upgradeMsg.String())
return result.String(), nil
}
result.WriteString("No upgrades available with this CLI.\nNewer versions may be available at: https://github.com/edgelesssys/constellation/releases\n")
return result.String(), nil
}
func (v *versionUpgrade) writeConfig(conf *config.Config, fileHandler file.Handler, configPath string) error {
// can't sort image map because maps are unsorted. services is only one string, k8s versions are sorted.
if v.newServices != "" {
conf.MicroserviceVersion = v.newServices
}
if len(v.newServices) > 0 {
conf.KubernetesVersion = v.newKubernetes[0]
}
if len(v.newImages) > 0 {
imageUpgrade := sortedMapKeys(v.newImages)[0]
conf.Image = imageUpgrade
conf.UpdateMeasurements(v.newImages[imageUpgrade])
}
if err := fileHandler.WriteYAML(configPath, conf, file.OptOverwrite); err != nil {
return err
}
return 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, checker upgradeChecker) (string, error) {
_, imageVersion, err := checker.GetCurrentImage(ctx)
if err != nil {
return "", err
}
if !semver.IsValid(imageVersion) {
return "", fmt.Errorf("current image version is not a release image version: %q", imageVersion)
}
return imageVersion, nil
}
// getCurrentKubernetesVersion retrieves the semantic version of Kubernetes currently installed in the cluster.
func getCurrentKubernetesVersion(ctx context.Context, checker upgradeChecker) (string, error) {
_, k8sVersion, err := checker.GetCurrentKubernetesVersion(ctx)
if err != nil {
return "", err
}
if !semver.IsValid(k8sVersion) {
return "", fmt.Errorf("current kubernetes version is not a valid semver string: %q", k8sVersion)
}
return k8sVersion, nil
}
// getCompatibleImageMeasurements retrieves the expected measurements for each image.
func getCompatibleImageMeasurements(ctx context.Context, writer io.Writer, client *http.Client, rekor rekorVerifier, pubK []byte,
csp cloudprovider.Provider, versions []versionsapi.Version, log debugLog,
) (map[string]measurements.M, error) {
upgrades := make(map[string]measurements.M)
for _, version := range versions {
log.Debugf("Fetching measurements for image: %s", version)
shortPath := version.ShortPath()
measurementsURL, err := measurementURL(csp, shortPath, "measurements.json")
if err != nil {
return nil, err
}
signatureURL, err := measurementURL(csp, shortPath, "measurements.json.sig")
if err != nil {
return nil, err
}
var fetchedMeasurements measurements.M
log.Debugf("Fetching for measurement url: %s", measurementsURL)
hash, err := fetchedMeasurements.FetchAndVerify(
ctx, client,
measurementsURL,
signatureURL,
pubK,
measurements.WithMetadata{
CSP: csp,
Image: shortPath,
},
)
if err != nil {
if _, err := fmt.Fprintf(writer, "Skipping compatible image %q: %s\n", shortPath, err); err != nil {
return nil, fmt.Errorf("writing to buffer: %w", err)
}
continue
}
if err = verifyWithRekor(ctx, rekor, hash); err != nil {
if _, err := fmt.Fprintf(writer, "Warning: Unable to verify '%s' in Rekor.\n", hash); err != nil {
return nil, fmt.Errorf("writing to buffer: %w", err)
}
if _, err := fmt.Fprintf(writer, "Make sure measurements are correct.\n"); err != nil {
return nil, fmt.Errorf("writing to buffer: %w", err)
}
}
upgrades[shortPath] = fetchedMeasurements
}
return upgrades, nil
}
type upgradeCheckFlags struct {
configPath string
force bool
writeConfig bool
ref string
stream string
cosignPubKey string
}
type upgradeChecker interface {
GetCurrentImage(ctx context.Context) (*unstructured.Unstructured, string, error)
GetCurrentKubernetesVersion(ctx context.Context) (*unstructured.Unstructured, string, error)
}
type versionListFetcher interface {
FetchVersionList(ctx context.Context, list versionsapi.List) (versionsapi.List, error)
}

View File

@ -0,0 +1,304 @@
/*
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/internal/attestation/measurements"
"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/edgelesssys/constellation/v2/internal/versionsapi"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/mod/semver"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// 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),
},
newKubernetes: []string{"v1.24.12", "v1.25.6"},
currentServices: "v2.4.0",
currentImage: "v2.4.0",
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 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",
},
"no upgrades": {
upgrade: versionUpgrade{
newServices: "",
newImages: map[string]measurements.M{},
newKubernetes: []string{},
currentServices: "v2.5.0",
currentImage: "v2.5.0",
currentKubernetes: "v1.25.6",
},
expected: "No upgrades available with this CLI.\nNewer versions may be available at: https://github.com/edgelesssys/constellation/releases\n",
},
"no upgrades #2": {
upgrade: versionUpgrade{},
expected: "No upgrades available with this CLI.\nNewer versions may be available at: https://github.com/edgelesssys/constellation/releases\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
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),
}
})
pubK := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----")
upgrades, err := getCompatibleImageMeasurements(context.Background(), &bytes.Buffer{}, client, singleUUIDVerifier(), pubK, csp, 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,
}
testCases := map[string]struct {
collector stubVersionCollector
flags upgradeCheckFlags
csp cloudprovider.Provider
cliVersion string
wantError bool
}{
"upgrades gcp": {
collector: stubVersionCollector{
supportedServicesVersions: "v2.5.0",
supportedImages: []versionsapi.Version{v2_3},
supportedImageVersions: map[string]measurements.M{
"v2.3.0": measurements.DefaultsFor(cloudprovider.QEMU),
},
supportedK8sVersions: []string{"v1.24.5", "v1.24.12", "v1.25.6"},
currentServicesVersions: "v2.4.0",
currentImageVersion: "v2.4.0",
currentK8sVersion: "v1.24.5",
images: []versionsapi.Version{v2_5},
newCLIVersions: []string{"v2.5.0", "v2.6.0"},
},
flags: upgradeCheckFlags{
configPath: constants.ConfigFilename,
},
csp: cloudprovider.GCP,
cliVersion: "v1.0.0",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
constants.VersionInfo = "v0.0.0"
fileHandler := file.NewHandler(afero.NewMemMapFs())
cfg := defaultConfigWithExpectedMeasurements(t, config.Default(), tc.csp)
require.NoError(fileHandler.WriteYAML(tc.flags.configPath, cfg))
checkCmd := upgradeCheckCmd{
collect: &tc.collector,
log: logger.NewTest(t),
}
cmd := newUpgradeCheckCmd()
err := checkCmd.upgradeCheck(cmd, fileHandler, 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
currentServicesVersions string
currentImageVersion string
currentK8sVersion string
images []versionsapi.Version
newCLIVersions []string
someErr error
}
func (s *stubVersionCollector) newMeasurementes(ctx context.Context, csp cloudprovider.Provider, images []versionsapi.Version) (map[string]measurements.M, error) {
return s.supportedImageVersions, nil
}
func (s *stubVersionCollector) currentVersions(ctx context.Context) (serviceVersions string, imageVersion string, k8sVersion string, err error) {
return s.currentServicesVersions, s.currentImageVersion, s.currentK8sVersion, s.someErr
}
func (s *stubVersionCollector) supportedVersions(ctx context.Context, version string, csp cloudprovider.Provider) (serviceVersions string, imageVersions []versionsapi.Version, k8sVersions []string, err error) {
return s.supportedServicesVersions, s.supportedImages, s.supportedK8sVersions, s.someErr
}
func (s *stubVersionCollector) newImages(ctx context.Context, version string, csp cloudprovider.Provider) ([]versionsapi.Version, error) {
return s.images, nil
}
func (s *stubVersionCollector) newerVersions(ctx context.Context, currentVersion string, allowedVersions []string) ([]versionsapi.Version, error) {
return s.images, nil
}
type stubUpgradeChecker struct {
image string
k8sVersion string
err error
}
func (u stubUpgradeChecker) GetCurrentImage(context.Context) (*unstructured.Unstructured, string, error) {
return nil, u.image, u.err
}
func (u stubUpgradeChecker) GetCurrentKubernetesVersion(ctx context.Context) (*unstructured.Unstructured, string, error) {
return nil, u.k8sVersion, u.err
}

View File

@ -1,366 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"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/sigstore"
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
"github.com/edgelesssys/constellation/v2/internal/versionsapi/fetcher"
"github.com/manifoldco/promptui"
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"golang.org/x/mod/semver"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func newUpgradePlanCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "plan",
Short: "Plan an upgrade of a Constellation cluster",
Long: "Plan an upgrade of a Constellation cluster by fetching compatible image versions and their measurements.",
Args: cobra.NoArgs,
RunE: runUpgradePlan,
}
cmd.Flags().StringP("file", "f", "", "path to output file, or '-' for stdout (omit for interactive mode)")
return cmd
}
type upgradePlanCmd struct {
log debugLog
}
func runUpgradePlan(cmd *cobra.Command, args []string) error {
log, err := newCLILogger(cmd)
if err != nil {
return fmt.Errorf("creating logger: %w", err)
}
defer log.Sync()
fileHandler := file.NewHandler(afero.NewOsFs())
flags, err := parseUpgradePlanFlags(cmd)
if err != nil {
return err
}
planner, err := cloudcmd.NewUpgrader(cmd.OutOrStdout(), log)
if err != nil {
return err
}
versionListFetcher := fetcher.NewFetcher()
rekor, err := sigstore.NewRekor()
if err != nil {
return fmt.Errorf("constructing Rekor client: %w", err)
}
cliVersion := getCurrentCLIVersion()
up := &upgradePlanCmd{log: log}
return up.upgradePlan(cmd, planner, versionListFetcher, fileHandler, http.DefaultClient, rekor, flags, cliVersion)
}
// upgradePlan plans an upgrade of a Constellation cluster.
func (up *upgradePlanCmd) upgradePlan(cmd *cobra.Command, planner upgradePlanner, verListFetcher versionListFetcher,
fileHandler file.Handler, client *http.Client, rekor rekorVerifier, flags upgradePlanFlags,
cliVersion string,
) error {
conf, err := config.New(fileHandler, flags.configPath, true)
if err != nil {
return config.DisplayValidationErrors(cmd.ErrOrStderr(), err)
}
up.log.Debugf("Read configuration from %q", flags.configPath)
// get current image version of the cluster
csp := conf.GetProvider()
up.log.Debugf("Using provider %s", csp.String())
version, err := getCurrentImageVersion(cmd.Context(), planner)
if err != nil {
return fmt.Errorf("checking current image version: %w", err)
}
up.log.Debugf("Using image version %s", version)
// 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("calculating next image minor version: %w", err)
}
up.log.Debugf("Current image minor version is %s", currentImageMinorVer)
up.log.Debugf("Current CLI minor version is %s", currentCLIMinorVer)
up.log.Debugf("Next image minor version is %s", nextImageMinorVer)
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}
}
up.log.Debugf("Allowed minor versions are %#v", allowedMinorVersions)
var updateCandidates []string
for _, minorVer := range allowedMinorVersions {
patchList := versionsapi.List{
Ref: versionsapi.ReleaseRef,
Stream: "stable",
Base: minorVer,
Granularity: versionsapi.GranularityMinor,
Kind: versionsapi.VersionKindImage,
}
patchList, err = verListFetcher.FetchVersionList(cmd.Context(), patchList)
if err == nil {
updateCandidates = append(updateCandidates, patchList.Versions...)
}
}
up.log.Debugf("Update candidates are %v", updateCandidates)
// filter out versions that are not compatible with the current cluster
compatibleImages := getCompatibleImages(version, updateCandidates)
up.log.Debugf("Of those images, these ones are compatible %v", compatibleImages)
// get expected measurements for each image
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)
}
up.log.Debugf("Compatible image measurements are %v", upgrades)
if len(upgrades) == 0 {
cmd.PrintErrln("No compatible images found to upgrade to.")
return nil
}
// interactive mode
if flags.filePath == "" {
up.log.Debugf("Writing upgrade plan in interactive mode")
cmd.Printf("Current version: %s\n", version)
return upgradePlanInteractive(
&nopWriteCloser{cmd.OutOrStdout()},
io.NopCloser(cmd.InOrStdin()),
flags.configPath, conf, fileHandler,
upgrades,
)
}
// write upgrade plan to stdout
if flags.filePath == "-" {
up.log.Debugf("Writing upgrade plan to stdout")
content, err := encoder.NewEncoder(upgrades).Encode()
if err != nil {
return fmt.Errorf("encoding compatible images: %w", err)
}
_, err = cmd.OutOrStdout().Write(content)
return err
}
// write upgrade plan to file
up.log.Debugf("Writing upgrade plan to file")
return fileHandler.WriteYAML(flags.filePath, upgrades)
}
// getCompatibleImages trims the list of images to only ones compatible with the current cluster.
func getCompatibleImages(currentImageVersion string, images []string) []string {
var compatibleImages []string
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,
csp cloudprovider.Provider, images []string,
) (map[string]config.UpgradeConfig, error) {
upgrades := make(map[string]config.UpgradeConfig)
for _, img := range images {
measurementsURL, err := measurementURL(csp, img, "measurements.json")
if err != nil {
return nil, err
}
signatureURL, err := measurementURL(csp, img, "measurements.json.sig")
if err != nil {
return nil, err
}
var fetchedMeasurements measurements.M
hash, err := fetchedMeasurements.FetchAndVerify(
ctx, client,
measurementsURL,
signatureURL,
pubK,
measurements.WithMetadata{
CSP: csp,
Image: img,
},
)
if err != nil {
cmd.PrintErrf("Skipping image %q: %s\n", img, err)
continue
}
if err = verifyWithRekor(ctx, rekor, hash); err != nil {
cmd.PrintErrf("Warning: Unable to verify '%s' in Rekor.\n", hash)
cmd.PrintErrf("Make sure measurements are correct.\n")
}
upgrades[img] = config.UpgradeConfig{
Image: img,
Measurements: fetchedMeasurements,
CSP: csp,
}
}
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) (string, error) {
_, imageVersion, err := planner.GetCurrentImage(ctx)
if err != nil {
return "", err
}
if !semver.IsValid(imageVersion) {
return "", fmt.Errorf("current image version is not a release image version: %q", imageVersion)
}
return imageVersion, nil
}
func getCurrentCLIVersion() string {
return "v" + constants.VersionInfo
}
func parseUpgradePlanFlags(cmd *cobra.Command) (upgradePlanFlags, error) {
configPath, err := cmd.Flags().GetString("config")
if err != nil {
return upgradePlanFlags{}, err
}
filePath, err := cmd.Flags().GetString("file")
if err != nil {
return upgradePlanFlags{}, err
}
return upgradePlanFlags{
configPath: configPath,
filePath: filePath,
cosignPubKey: constants.CosignPublicKey,
}, nil
}
func upgradePlanInteractive(out io.WriteCloser, in io.ReadCloser,
configPath string, config *config.Config, fileHandler file.Handler,
compatibleUpgrades map[string]config.UpgradeConfig,
) error {
var imageVersions []string
for k := range compatibleUpgrades {
imageVersions = append(imageVersions, k)
}
semver.Sort(imageVersions)
prompt := promptui.Select{
Label: "Select an image version to upgrade to",
Items: imageVersions,
Searcher: func(input string, index int) bool {
version := imageVersions[index]
trimmedVersion := strings.TrimPrefix(strings.Replace(version, ".", "", -1), "v")
input = strings.TrimPrefix(strings.Replace(input, ".", "", -1), "v")
return strings.Contains(trimmedVersion, input)
},
Size: 10,
Stdin: in,
Stdout: out,
}
_, res, err := prompt.Run()
if err != nil {
return err
}
fmt.Fprintln(out, "Updating config to the following:")
fmt.Fprintf(out, "Image: %s\n", compatibleUpgrades[res].Image)
fmt.Fprintln(out, "Measurements:")
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 = 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 nopWriteCloser struct {
io.Writer
}
func (c *nopWriteCloser) Close() error { return nil }
type upgradePlanner interface {
GetCurrentImage(ctx context.Context) (*unstructured.Unstructured, string, error)
}
type versionListFetcher interface {
FetchVersionList(ctx context.Context, list versionsapi.List) (versionsapi.List, error)
}

View File

@ -1,498 +0,0 @@
/*
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/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/edgelesssys/constellation/v2/internal/versionsapi"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/mod/semver"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestGetCurrentImageVersion(t *testing.T) {
testCases := map[string]struct {
stubUpgradePlanner stubUpgradePlanner
wantErr bool
}{
"valid version": {
stubUpgradePlanner: stubUpgradePlanner{
image: "v1.0.0",
},
},
"invalid version": {
stubUpgradePlanner: stubUpgradePlanner{
image: "invalid",
},
wantErr: true,
},
"GetCurrentImage error": {
stubUpgradePlanner: stubUpgradePlanner{
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.stubUpgradePlanner)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
assert.True(semver.IsValid(version))
})
}
}
func TestGetCompatibleImages(t *testing.T) {
imageList := []string{
"v0.0.0",
"v1.0.0",
"v1.0.1",
"v1.0.2",
"v1.1.0",
}
testCases := map[string]struct {
images []string
version string
wantImages []string
}{
"filters <= v1.0.0": {
images: imageList,
version: "v1.0.0",
wantImages: []string{
"v1.0.1",
"v1.0.2",
"v1.1.0",
},
},
"no compatible images": {
images: imageList,
version: "v999.999.999",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
compatibleImages := getCompatibleImages(tc.version, tc.images)
assert.EqualValues(tc.wantImages, compatibleImages)
})
}
}
func TestGetCompatibleImageMeasurements(t *testing.T) {
assert := assert.New(t)
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") {
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),
}
})
pubK := []byte("-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----")
upgrades, err := getCompatibleImageMeasurements(context.Background(), &cobra.Command{}, client, singleUUIDVerifier(), pubK, csp, images)
assert.NoError(err)
for _, image := range upgrades {
assert.NotEmpty(image.Measurements)
}
}
func TestUpgradePlan(t *testing.T) {
availablePatches := versionsapi.List{
Versions: []string{"v1.0.0", "v1.0.1"},
}
// Cosign private key used to sign the measurements.
// Generated with: cosign generate-key-pair
// Password left empty.
//
// -----BEGIN ENCRYPTED COSIGN PRIVATE KEY-----
// eyJrZGYiOnsibmFtZSI6InNjcnlwdCIsInBhcmFtcyI6eyJOIjozMjc2OCwiciI6
// OCwicCI6MX0sInNhbHQiOiJlRHVYMWRQMGtIWVRnK0xkbjcxM0tjbFVJaU92eFVX
// VXgvNi9BbitFVk5BPSJ9LCJjaXBoZXIiOnsibmFtZSI6Im5hY2wvc2VjcmV0Ym94
// Iiwibm9uY2UiOiJwaWhLL2txNmFXa2hqSVVHR3RVUzhTVkdHTDNIWWp4TCJ9LCJj
// aXBoZXJ0ZXh0Ijoidm81SHVWRVFWcUZ2WFlQTTVPaTVaWHM5a255bndZU2dvcyth
// VklIeHcrOGFPamNZNEtvVjVmL3lHRHR0K3BHV2toanJPR1FLOWdBbmtsazFpQ0c5
// a2czUXpPQTZsU2JRaHgvZlowRVRZQ0hLeElncEdPRVRyTDlDenZDemhPZXVSOXJ6
// TDcvRjBBVy9vUDVqZXR3dmJMNmQxOEhjck9kWE8yVmYxY2w0YzNLZjVRcnFSZzlN
// dlRxQWFsNXJCNHNpY1JaMVhpUUJjb0YwNHc9PSJ9
// -----END ENCRYPTED COSIGN PRIVATE KEY-----
pubK := "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu78QgxOOcao6U91CSzEXxrKhvFTt\nJHNy+eX6EMePtDm8CnDF9HSwnTlD0itGJ/XHPQA5YX10fJAqI1y+ehlFMw==\n-----END PUBLIC KEY-----"
testCases := map[string]struct {
patchLister stubVersionListFetcher
planner stubUpgradePlanner
flags upgradePlanFlags
cliVersion string
csp cloudprovider.Provider
verifier rekorVerifier
measurementsFetchStatus int
wantUpgrade bool
wantErr bool
}{
"upgrades gcp": {
patchLister: stubVersionListFetcher{list: availablePatches},
planner: stubUpgradePlanner{
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: stubVersionListFetcher{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: stubVersionListFetcher{list: availablePatches},
planner: stubUpgradePlanner{
image: "v999.999.999",
},
measurementsFetchStatus: http.StatusOK,
flags: upgradePlanFlags{
configPath: constants.ConfigFilename,
filePath: "upgrade-plan.yaml",
cosignPubKey: pubK,
},
csp: cloudprovider.GCP,
verifier: singleUUIDVerifier(),
wantUpgrade: false,
},
"current image newer than cli": {
patchLister: stubVersionListFetcher{list: availablePatches},
planner: stubUpgradePlanner{
image: "v999.999.999",
},
measurementsFetchStatus: http.StatusOK,
flags: upgradePlanFlags{
configPath: constants.ConfigFilename,
filePath: "upgrade-plan.yaml",
cosignPubKey: pubK,
},
csp: cloudprovider.GCP,
cliVersion: "v1.0.0",
verifier: singleUUIDVerifier(),
wantUpgrade: false,
},
"upgrade to stdout": {
patchLister: stubVersionListFetcher{list: availablePatches},
planner: stubUpgradePlanner{
image: "v1.0.0",
},
measurementsFetchStatus: http.StatusOK,
flags: upgradePlanFlags{
configPath: constants.ConfigFilename,
filePath: "-",
cosignPubKey: pubK,
},
csp: cloudprovider.GCP,
cliVersion: "v1.0.0",
verifier: singleUUIDVerifier(),
wantUpgrade: true,
},
"current image not valid": {
patchLister: stubVersionListFetcher{list: availablePatches},
planner: stubUpgradePlanner{
image: "not-valid",
},
measurementsFetchStatus: http.StatusOK,
flags: upgradePlanFlags{
configPath: constants.ConfigFilename,
filePath: "upgrade-plan.yaml",
cosignPubKey: pubK,
},
csp: cloudprovider.GCP,
cliVersion: "v1.0.0",
verifier: singleUUIDVerifier(),
wantErr: true,
},
"image fetch error": {
patchLister: stubVersionListFetcher{err: errors.New("error")},
planner: stubUpgradePlanner{
image: "v1.0.0",
},
measurementsFetchStatus: http.StatusOK,
flags: upgradePlanFlags{
configPath: constants.ConfigFilename,
filePath: "upgrade-plan.yaml",
cosignPubKey: pubK,
},
csp: cloudprovider.GCP,
cliVersion: "v1.0.0",
verifier: singleUUIDVerifier(),
},
"measurements fetch error": {
patchLister: stubVersionListFetcher{list: availablePatches},
planner: stubUpgradePlanner{
image: "v1.0.0",
},
measurementsFetchStatus: http.StatusInternalServerError,
flags: upgradePlanFlags{
configPath: constants.ConfigFilename,
filePath: "upgrade-plan.yaml",
cosignPubKey: pubK,
},
csp: cloudprovider.GCP,
cliVersion: "v1.0.0",
verifier: singleUUIDVerifier(),
},
"failing search should not result in error": {
patchLister: stubVersionListFetcher{list: availablePatches},
planner: stubUpgradePlanner{
image: "v1.0.0",
},
measurementsFetchStatus: http.StatusOK,
flags: upgradePlanFlags{
configPath: constants.ConfigFilename,
filePath: "upgrade-plan.yaml",
cosignPubKey: pubK,
},
csp: cloudprovider.GCP,
cliVersion: "v1.0.0",
verifier: &stubRekorVerifier{
SearchByHashUUIDs: []string{},
SearchByHashError: errors.New("some error"),
},
wantUpgrade: true,
},
"failing verify should not result in error": {
patchLister: stubVersionListFetcher{list: availablePatches},
planner: stubUpgradePlanner{
image: "v1.0.0",
},
measurementsFetchStatus: http.StatusOK,
flags: upgradePlanFlags{
configPath: constants.ConfigFilename,
filePath: "upgrade-plan.yaml",
cosignPubKey: pubK,
},
csp: cloudprovider.GCP,
cliVersion: "v1.0.0",
verifier: &stubRekorVerifier{
SearchByHashUUIDs: []string{"11111111111111111111111111111111111111111111111111111111111111111111111111111111"},
VerifyEntryError: errors.New("some error"),
},
wantUpgrade: 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))
cmd := newUpgradePlanCmd()
cmd.SetContext(context.Background())
var outTarget bytes.Buffer
cmd.SetOut(&outTarget)
var errTarget bytes.Buffer
cmd.SetErr(&errTarget)
client := newTestClient(func(req *http.Request) *http.Response {
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.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("MEYCIQDu2Sft91FjN278uP+r/HFMms6IH/tRtaHzYvIN0xPgdwIhAJhiFxVsHCa0NK6bZOGLE9c4miZHIqFTKvgpTf3rJ9dW")),
Header: make(http.Header),
}
}
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.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("MEQCIBUssv92LpSMiXE1UAVf2fW8J9pZHiLseo2tdZjxv2OMAiB6K8e8yL0768jWjlFnRe3Rc2x/dX34uzX3h0XUrlYt1A==")),
Header: make(http.Header),
}
}
return &http.Response{
StatusCode: http.StatusNotFound,
Body: io.NopCloser(strings.NewReader("Not found.")),
Header: make(http.Header),
}
})
up := &upgradePlanCmd{log: logger.NewTest(t)}
err := up.upgradePlan(cmd, tc.planner, tc.patchLister, fileHandler, client, tc.verifier, tc.flags, tc.cliVersion)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
if !tc.wantUpgrade {
assert.Contains(errTarget.String(), "No compatible images")
return
}
var availableUpgrades map[string]config.UpgradeConfig
if tc.flags.filePath == "-" {
require.NoError(yaml.Unmarshal(outTarget.Bytes(), &availableUpgrades))
} else {
require.NoError(fileHandler.ReadYAMLStrict(tc.flags.filePath, &availableUpgrades))
}
assert.GreaterOrEqual(len(availableUpgrades), 1)
for _, upgrade := range availableUpgrades {
assert.NotEmpty(upgrade.Image)
assert.NotEmpty(upgrade.Measurements)
}
})
}
}
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)
})
}
}
type stubUpgradePlanner struct {
image string
err error
}
func (u stubUpgradePlanner) GetCurrentImage(context.Context) (*unstructured.Unstructured, string, error) {
return nil, u.image, u.err
}
type stubVersionListFetcher struct {
list versionsapi.List
err error
}
func (s stubVersionListFetcher) FetchVersionList(context.Context, versionsapi.List) (versionsapi.List, error) {
return s.list, s.err
}

View File

@ -118,26 +118,26 @@ func (v *verifyCmd) parseVerifyFlags(cmd *cobra.Command, fileHandler file.Handle
if err != nil {
return verifyFlags{}, fmt.Errorf("parsing config path argument: %w", err)
}
v.log.Debugf("Configuration file flag is %q", configPath)
v.log.Debugf("Flag 'config' set to %q", configPath)
ownerID := ""
clusterID, err := cmd.Flags().GetString("cluster-id")
if err != nil {
return verifyFlags{}, fmt.Errorf("parsing cluster-id argument: %w", err)
}
v.log.Debugf("Cluster ID flag is %q", clusterID)
v.log.Debugf("Flag 'cluster-id' set to %q", clusterID)
endpoint, err := cmd.Flags().GetString("node-endpoint")
if err != nil {
return verifyFlags{}, fmt.Errorf("parsing node-endpoint argument: %w", err)
}
v.log.Debugf("'node-endpoint' flag is %q", endpoint)
v.log.Debugf("Flag 'node-endpoint' set to %q", endpoint)
force, err := cmd.Flags().GetBool("force")
if err != nil {
return verifyFlags{}, fmt.Errorf("parsing force argument: %w", err)
}
v.log.Debugf("'force' flag is %t", force)
v.log.Debugf("Flag 'force' set to %t", force)
// Get empty values from ID file
emptyEndpoint := endpoint == ""

View File

@ -12,6 +12,7 @@ import (
"strings"
"time"
"github.com/edgelesssys/constellation/v2/internal/compatibility"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/deploy/helm"
@ -91,6 +92,16 @@ func (c *Client) Upgrade(ctx context.Context, config *config.Config, timeout tim
return nil
}
// Versions queries the cluster for running versions and returns a map of releaseName -> version.
func (c *Client) Versions() (string, error) {
serviceVersion, err := c.currentVersion(conServicesReleaseName)
if err != nil {
return "", fmt.Errorf("getting constellation-services version: %w", err)
}
return compatibility.EnsurePrefixV(serviceVersion), nil
}
// currentVersion returns the version of the currently installed helm release.
func (c *Client) currentVersion(release string) (string, error) {
rel, err := c.actions.listAction(release)

View File

@ -18,6 +18,7 @@ import (
"strings"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/compatibility"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/deploy/helm"
@ -90,6 +91,16 @@ func NewLoader(csp cloudprovider.Provider, k8sVersion versions.ValidK8sVersion)
}
}
// AvailableServiceVersions returns the chart version number of the bundled service versions.
func AvailableServiceVersions() (string, error) {
servicesChart, err := loadChartsDir(helmFS, conServicesPath)
if err != nil {
return "", fmt.Errorf("loading constellation-services chart: %w", err)
}
return compatibility.EnsurePrefixV(servicesChart.Metadata.Version), nil
}
// Load the embedded helm charts.
func (i *ChartLoader) Load(config *config.Config, conformanceMode bool, masterSecret, salt []byte) ([]byte, error) {
ciliumRelease, err := i.loadCilium(config.GetProvider(), conformanceMode)

3
go.mod
View File

@ -74,7 +74,6 @@ require (
github.com/hashicorp/hc-install v0.4.0
github.com/hashicorp/terraform-exec v0.17.3
github.com/hashicorp/terraform-json v0.14.0
github.com/manifoldco/promptui v0.9.0
github.com/martinjungblut/go-cryptsetup v0.0.0-20220520180014-fd0874fd07a6
github.com/mattn/go-isatty v0.0.17
github.com/microsoft/ApplicationInsights-Go v0.4.4
@ -86,7 +85,6 @@ require (
github.com/spf13/afero v1.9.3
github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.1
github.com/tj/assert v0.0.0-20171129193455-018094318fb0
go.uber.org/goleak v1.2.0
go.uber.org/multierr v1.9.0
go.uber.org/zap v1.24.0
@ -162,7 +160,6 @@ require (
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/containerd/cgroups v1.0.4 // indirect
github.com/containerd/containerd v1.6.12 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20210303052042-6bc126869bf4 // indirect

6
go.sum
View File

@ -315,11 +315,8 @@ github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cb
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@ -946,8 +943,6 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI=
github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
@ -1310,7 +1305,6 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=

View File

@ -107,7 +107,6 @@ require (
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/cloudflare/circl v1.1.0 // indirect
github.com/containerd/containerd v1.6.12 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
@ -203,7 +202,6 @@ require (
github.com/lib/pq v1.10.6 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect

View File

@ -290,11 +290,8 @@ github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cb
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@ -940,8 +937,6 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI=
github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
@ -1305,7 +1300,6 @@ github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhV
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0=
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA=
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=

View File

@ -24,11 +24,11 @@ var (
// ErrMinorDrift signals that the minor version of two compared versions are further apart than one.
ErrMinorDrift = errors.New("target version needs to be equal or up to one minor version higher")
// ErrSemVer signals that a given version does not adhere to the Semver syntax.
ErrSemVer = errors.New("invalid semver")
ErrSemVer = errors.New("invalid semantic version")
)
// ensurePrefixV returns the input string prefixed with the letter "v", if the string doesn't already start with that letter.
func ensurePrefixV(str string) string {
// EnsurePrefixV returns the input string prefixed with the letter "v", if the string doesn't already start with that letter.
func EnsurePrefixV(str string) string {
if strings.HasPrefix(str, "v") {
return str
}
@ -37,8 +37,8 @@ func ensurePrefixV(str string) string {
// IsValidUpgrade checks that a and b adhere to a version drift of 1 and b is greater than a.
func IsValidUpgrade(a, b string) error {
a = ensurePrefixV(a)
b = ensurePrefixV(b)
a = EnsurePrefixV(a)
b = EnsurePrefixV(b)
if !semver.IsValid(a) || !semver.IsValid(b) {
return ErrSemVer
@ -70,8 +70,8 @@ func IsValidUpgrade(a, b string) error {
// BinaryWith tests that this binarie's version is greater or equal than some target version, but not further away than one minor version.
func BinaryWith(target string) error {
binaryVersion := ensurePrefixV(constants.VersionInfo)
target = ensurePrefixV(target)
binaryVersion := EnsurePrefixV(constants.VersionInfo)
target = EnsurePrefixV(target)
if !semver.IsValid(binaryVersion) || !semver.IsValid(target) {
return ErrSemVer
}
@ -101,11 +101,11 @@ func BinaryWith(target string) error {
// FilterNewerVersion filters the list of versions to only include versions newer than currentVersion.
func FilterNewerVersion(currentVersion string, newVersions []string) []string {
currentVersion = ensurePrefixV(currentVersion)
currentVersion = EnsurePrefixV(currentVersion)
var result []string
for _, image := range newVersions {
image = ensurePrefixV(image)
image = EnsurePrefixV(image)
// check if image is newer than current version
if semver.Compare(image, currentVersion) <= 0 {
continue
@ -118,7 +118,7 @@ func FilterNewerVersion(currentVersion string, newVersions []string) []string {
// NextMinorVersion returns the next minor version for a given canonical semver.
// The returned format is vMAJOR.MINOR.
func NextMinorVersion(version string) (string, error) {
major, minor, err := parseCanonicalSemver(ensurePrefixV(version))
major, minor, err := parseCanonicalSemver(EnsurePrefixV(version))
if err != nil {
return "", err
}
@ -126,14 +126,14 @@ func NextMinorVersion(version string) (string, error) {
}
func parseCanonicalSemver(version string) (major int, minor int, err error) {
version = semver.Canonical(version) // ensure version is in canonical form (vX.Y.Z)
num, err := fmt.Sscanf(version, "v%d.%d", &major, &minor)
version = semver.MajorMinor(version) // ensure version is in canonical form (vX.Y.Z)
if version == "" {
return 0, 0, fmt.Errorf("invalid semver: '%s'", version)
}
_, err = fmt.Sscanf(version, "v%d.%d", &major, &minor)
if err != nil {
return 0, 0, fmt.Errorf("parsing version: %w", err)
}
if num != 2 {
return 0, 0, fmt.Errorf("parsing version: expected 3 numbers, got %d", num)
}
return major, minor, nil
}

View File

@ -10,7 +10,7 @@ import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/tj/assert"
"github.com/stretchr/testify/assert"
)
func TestFilterNewerVersion(t *testing.T) {
@ -206,3 +206,60 @@ func TestIsValidUpgrade(t *testing.T) {
})
}
}
func TestParseCanonicalSemver(t *testing.T) {
testCases := map[string]struct {
version string
major int
minor int
wantError bool
}{
"canonical input": {
version: "v1.1.1",
major: 1,
minor: 1,
},
"vMAJOR.MINOR input": {
version: "v1.1",
major: 1,
minor: 1,
},
"vMAJOR input": {
version: "v1",
major: 1,
minor: 0,
},
"invalid (go)semver": {
version: "1.1", // valid semver, but invalid according to go's semver
wantError: true,
},
"invalid (go)semver #2": {
version: "asdf",
wantError: true,
},
"invalid (go)semver #3": {
version: "v1.1.1.1.1",
wantError: true,
},
"pseudoversion": {
version: "v2.6.0-pre.0.20230125085856-aaaaaaaaaaaa",
major: 2,
minor: 6,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
major, minor, err := parseCanonicalSemver(tc.version)
if tc.wantError {
assert.Error(err)
return
}
assert.NoError(err)
assert.Equal(tc.major, major)
assert.Equal(tc.minor, minor)
})
}
}

View File

@ -28,6 +28,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/compatibility"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/edgelesssys/constellation/v2/internal/versions"
@ -63,9 +64,12 @@ type Config struct {
// Size (in GB) of a node's disk to store the non-volatile state.
StateDiskSizeGB int `yaml:"stateDiskSizeGB" validate:"min=0"`
// description: |
// Kubernetes version to be installed in the cluster.
// Kubernetes version to be installed into the cluster.
KubernetesVersion string `yaml:"kubernetesVersion" validate:"supported_k8s_version"`
// description: |
// Microservice version to be installed into the cluster. Setting this value is optional until v2.7. Defaults to the version of the CLI.
MicroserviceVersion string `yaml:"microserviceVersion" validate:"omitempty,version_compatibility"`
// 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
DebugCluster *bool `yaml:"debugCluster" validate:"required"`
// description: |
@ -75,7 +79,7 @@ type Config struct {
// Configuration to apply during constellation upgrade.
// examples:
// - value: 'UpgradeConfig{ Image: "", Measurements: Measurements{} }'
Upgrade UpgradeConfig `yaml:"upgrade,omitempty"`
Upgrade UpgradeConfig `yaml:"upgrade,omitempty" validate:"required"`
}
// UpgradeConfig defines configuration used during constellation upgrade.
@ -248,6 +252,8 @@ func Default() *Config {
return &Config{
Version: Version2,
Image: defaultImage,
MicroserviceVersion: compatibility.EnsurePrefixV(constants.VersionInfo),
KubernetesVersion: string(versions.Default),
StateDiskSizeGB: 30,
DebugCluster: func() *bool { b := false; return &b }(),
Provider: ProviderConfig{
@ -295,14 +301,13 @@ func Default() *Config {
Measurements: measurements.DefaultsFor(cloudprovider.QEMU),
},
},
KubernetesVersion: string(versions.Default),
}
}
// FromFile returns config file with `name` read from `fileHandler` by parsing
// fromFile returns config file with `name` read from `fileHandler` by parsing
// it as YAML. You should prefer config.New to read env vars and validate
// config in a consistent manner.
func FromFile(fileHandler file.Handler, name string) (*Config, error) {
func fromFile(fileHandler file.Handler, name string) (*Config, error) {
var conf Config
if err := fileHandler.ReadYAMLStrict(name, &conf); err != nil {
if errors.Is(err, fs.ErrNotExist) {
@ -316,10 +321,10 @@ func FromFile(fileHandler file.Handler, name string) (*Config, error) {
// New creates a new config by:
// 1. Reading config file via provided fileHandler from file with name.
// 2. Read secrets from environment variables.
// 3. Validate config.
// 3. Validate config. If `--force` is set the version validation will be disabled and any version combination is allowed.
func New(fileHandler file.Handler, name string, force bool) (*Config, error) {
// Read config file
c, err := FromFile(fileHandler, name)
c, err := fromFile(fileHandler, name)
if err != nil {
return nil, err
}
@ -330,6 +335,10 @@ func New(fileHandler file.Handler, name string, force bool) (*Config, error) {
c.Provider.Azure.ClientSecretValue = clientSecretValue
}
// Backwards compatibility: configs without the field `microserviceVersion` are valid in version 2.6.
// In case the field is not set in an old config we prefil it with the default value.
c.MicroserviceVersion = Default().MicroserviceVersion
return c, c.Validate(force)
}
@ -536,6 +545,9 @@ func (c *Config) Validate(force bool) error {
// Register provider validation
validate.RegisterStructValidation(validateProvider, ProviderConfig{})
// register custom validator that prints a deprecation warning.
validate.RegisterStructValidation(validateUpgradeConfig, UpgradeConfig{})
err := validate.Struct(c)
if err == nil {
return nil

View File

@ -24,7 +24,7 @@ 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 = ""
@ -43,25 +43,30 @@ func init() {
ConfigDoc.Fields[3].Name = "kubernetesVersion"
ConfigDoc.Fields[3].Type = "string"
ConfigDoc.Fields[3].Note = ""
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[3].Description = "Kubernetes version to be installed into the cluster."
ConfigDoc.Fields[3].Comments[encoder.LineComment] = "Kubernetes version to be installed into the cluster."
ConfigDoc.Fields[4].Name = "microserviceVersion"
ConfigDoc.Fields[4].Type = "string"
ConfigDoc.Fields[4].Note = ""
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[4].Description = "Microservice version to be installed into the cluster. Setting this value is optional until v2.7. Defaults to the version of the CLI."
ConfigDoc.Fields[4].Comments[encoder.LineComment] = "Microservice version to be installed into the cluster. Setting this value is optional until v2.7. Defaults to the version of the CLI."
ConfigDoc.Fields[5].Name = "debugCluster"
ConfigDoc.Fields[5].Type = "bool"
ConfigDoc.Fields[5].Note = ""
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 = "upgrade"
ConfigDoc.Fields[6].Type = "UpgradeConfig"
ConfigDoc.Fields[5].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[5].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[6].Name = "provider"
ConfigDoc.Fields[6].Type = "ProviderConfig"
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 = "Supported cloud providers and their specific configurations."
ConfigDoc.Fields[6].Comments[encoder.LineComment] = "Supported cloud providers and their specific configurations."
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[6].AddExample("", UpgradeConfig{Image: "", Measurements: Measurements{}})
ConfigDoc.Fields[7].AddExample("", UpgradeConfig{Image: "", Measurements: Measurements{}})
UpgradeConfigDoc.Type = "UpgradeConfig"
UpgradeConfigDoc.Comments[encoder.LineComment] = "UpgradeConfig defines configuration used during constellation upgrade."

View File

@ -99,7 +99,7 @@ func TestFromFile(t *testing.T) {
require.NoError(fileHandler.WriteYAML(tc.configName, tc.config, file.OptNone))
}
result, err := FromFile(fileHandler, tc.configName)
result, err := fromFile(fileHandler, tc.configName)
if tc.wantErr {
assert.Error(err)
@ -802,7 +802,7 @@ func TestConfigVersionCompatibility(t *testing.T) {
fileHandler := file.NewHandler(afero.NewOsFs())
config, err := FromFile(fileHandler, tc.config)
config, err := fromFile(fileHandler, tc.config)
assert.NoError(err)
assert.Equal(tc.expectedConfig, config)

View File

@ -55,7 +55,7 @@ func translateInvalidK8sVersionError(ut ut.Translator, fe validator.FieldError)
validVersionsSorted := semver.ByVersion(validVersions)
sort.Sort(validVersionsSorted)
var errorMsg string
errorMsg := fmt.Sprintf("Supported versions: %s", strings.Join(validVersionsSorted, " "))
configured, ok := fe.Value().(string)
if !ok {
errorMsg = "The configured version is not a valid string"
@ -310,12 +310,12 @@ func translateVersionCompatibilityError(ut ut.Translator, fe validator.FieldErro
err := validateVersionCompatibilityHelper(fe.Field(), fe.Value().(string))
var msg string
switch err {
case compatibility.ErrSemVer:
switch {
case errors.Is(err, compatibility.ErrSemVer):
msg = fmt.Sprintf("configured version (%s) does not adhere to SemVer syntax", fe.Value().(string))
case compatibility.ErrMajorMismatch:
case errors.Is(err, compatibility.ErrMajorMismatch):
msg = fmt.Sprintf("the CLI's major version (%s) has to match your configured major version (%s)", constants.VersionInfo, fe.Value().(string))
case compatibility.ErrMinorDrift:
case errors.Is(err, compatibility.ErrMinorDrift):
msg = fmt.Sprintf("only the CLI (%s) can be up to one minor version newer than the configured version (%s)", constants.VersionInfo, fe.Value().(string))
default:
msg = err.Error()
@ -350,3 +350,8 @@ func validateVersionCompatibilityHelper(fieldName string, configuredVersion stri
func returnsTrue(fl validator.FieldLevel) bool {
return true
}
// validateUpgradeConfig prints a warning to STDERR and validates the field successfully.
func validateUpgradeConfig(sl validator.StructLevel) {
fmt.Printf("WARNING: the config key `upgrade` will be deprecated in an upcoming version. Please check the documentation for more information.\n")
}

View File

@ -13,12 +13,28 @@ package versions
import (
"fmt"
"sort"
"strings"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/versions/components"
"golang.org/x/mod/semver"
)
// SupportedK8sVersions returns a list (sorted) of supported Kubernetes versions.
func SupportedK8sVersions() []string {
validVersions := make([]string, len(VersionConfigs))
i := 0
for _, conf := range VersionConfigs {
validVersions[i] = conf.ClusterVersion
i++
}
validVersionsSorted := semver.ByVersion(validVersions)
sort.Sort(validVersionsSorted)
return validVersionsSorted
}
// ValidK8sVersion represents any of the three currently supported k8s versions.
type ValidK8sVersion string
@ -85,11 +101,11 @@ const (
// currently supported versions.
//nolint:revive
V1_24 ValidK8sVersion = "1.24"
V1_24 ValidK8sVersion = "v1.24.9"
//nolint:revive
V1_25 ValidK8sVersion = "1.25"
V1_25 ValidK8sVersion = "v1.25.6"
//nolint:revive
V1_26 ValidK8sVersion = "1.26"
V1_26 ValidK8sVersion = "v1.26.1"
// Default k8s version deployed by Constellation.
Default ValidK8sVersion = V1_25

View File

@ -54,12 +54,12 @@ func runAdd(cmd *cobra.Command, args []string) (retErr error) {
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
log.Debugf("Validating flags.")
log.Debugf("Validating flags")
if err := flags.validate(log); err != nil {
return err
}
log.Debugf("Creating version struct.")
log.Debugf("Creating version struct")
ver := versionsapi.Version{
Ref: flags.ref,
Stream: flags.stream,
@ -70,19 +70,19 @@ func runAdd(cmd *cobra.Command, args []string) (retErr error) {
return err
}
log.Debugf("Creating versions API client.")
log.Debugf("Creating versions API client")
client, err := verclient.NewClient(cmd.Context(), flags.region, flags.bucket, flags.distributionID, flags.dryRun, log)
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
defer func(retErr *error) {
log.Infof("Invalidating cache. This may take some time.")
log.Infof("Invalidating cache. This may take some time")
if err := client.InvalidateCache(cmd.Context()); err != nil && retErr == nil {
*retErr = fmt.Errorf("invalidating cache: %w", err)
}
}(&retErr)
log.Infof("Adding version.")
log.Infof("Adding version")
if err := ensureVersion(cmd.Context(), client, ver, versionsapi.GranularityMajor, log); err != nil {
return err
}
@ -127,19 +127,19 @@ func ensureVersion(ctx context.Context, client *verclient.Client, ver versionsap
insertVersion := ver.WithGranularity(insertGran)
if verList.Contains(insertVersion) {
log.Infof("Version %q already exists in list %v.", insertVersion, verList.Versions)
log.Infof("Version %q already exists in list %v", insertVersion, verList.Versions)
return nil
}
log.Infof("Inserting %s version %q into list.", insertGran.String(), insertVersion)
log.Infof("Inserting %s version %q into list", insertGran.String(), insertVersion)
verList.Versions = append(verList.Versions, insertVersion)
log.Debugf("New %s version list: %v.", gran.String(), verList)
log.Debugf("New %s version list: %v", gran.String(), verList)
if err := client.UpdateVersionList(ctx, verList); err != nil {
return fmt.Errorf("failed to add %s version: %w", gran.String(), err)
}
log.Infof("Added %q to list.", insertVersion)
log.Infof("Added %q to list", insertVersion)
return nil
}
@ -162,7 +162,7 @@ func updateLatest(ctx context.Context, client *verclient.Client, ver versionsapi
return nil
}
log.Infof("Setting %q as latest version.", ver)
log.Infof("Setting %q as latest version", ver)
latest = versionsapi.Latest{
Ref: ver.Ref,
Stream: ver.Stream,

View File

@ -41,18 +41,18 @@ func runLatest(cmd *cobra.Command, args []string) error {
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
log.Debugf("Validating flags.")
log.Debugf("Validating flags")
if err := flags.validate(); err != nil {
return err
}
log.Debugf("Creating versions API client.")
log.Debugf("Creating versions API client")
client, err := verclient.NewReadOnlyClient(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
log.Debugf("Requesting latest version.")
log.Debugf("Requesting latest version")
latest := versionsapi.Latest{
Ref: flags.ref,
Stream: flags.stream,

View File

@ -45,12 +45,12 @@ func runList(cmd *cobra.Command, args []string) error {
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
log.Debugf("Validating flags.")
log.Debugf("Validating flags")
if err := flags.validate(); err != nil {
return err
}
log.Debugf("Creating versions API client.")
log.Debugf("Creating versions API client")
client, err := verclient.NewReadOnlyClient(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return fmt.Errorf("creating client: %w", err)
@ -60,7 +60,7 @@ func runList(cmd *cobra.Command, args []string) error {
if flags.minorVersion != "" {
minorVersions = []string{flags.minorVersion}
} else {
log.Debugf("Getting minor versions.")
log.Debugf("Getting minor versions")
minorVersions, err = listMinorVersions(cmd.Context(), client, flags.ref, flags.stream)
var errNotFound *verclient.NotFoundError
if err != nil && errors.As(err, &errNotFound) {
@ -71,7 +71,7 @@ func runList(cmd *cobra.Command, args []string) error {
}
}
log.Debugf("Getting patch versions.")
log.Debugf("Getting patch versions")
patchVersions, err := listPatchVersions(cmd.Context(), client, flags.ref, flags.stream, minorVersions)
var errNotFound *verclient.NotFoundError
if err != nil && errors.As(err, &errNotFound) {
@ -82,7 +82,7 @@ func runList(cmd *cobra.Command, args []string) error {
}
if flags.json {
log.Debugf("Printing versions as JSON.")
log.Debugf("Printing versions as JSON")
var vers []string
for _, v := range patchVersions {
vers = append(vers, v.Version)
@ -95,7 +95,7 @@ func runList(cmd *cobra.Command, args []string) error {
return nil
}
log.Debugf("Printing versions.")
log.Debugf("Printing versions")
for _, v := range patchVersions {
fmt.Println(v.ShortPath())
}

View File

@ -78,36 +78,36 @@ func runRemove(cmd *cobra.Command, args []string) (retErr error) {
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
log.Debugf("Validating flags.")
log.Debugf("Validating flags")
if err := flags.validate(); err != nil {
return err
}
log.Debugf("Creating GCP client.")
log.Debugf("Creating GCP client")
gcpClient, err := newGCPClient(cmd.Context(), flags.gcpProject)
if err != nil {
return fmt.Errorf("creating GCP client: %w", err)
}
log.Debugf("Creating AWS client.")
log.Debugf("Creating AWS client")
awsClient, err := newAWSClient(cmd.Context(), flags.region)
if err != nil {
return fmt.Errorf("creating AWS client: %w", err)
}
log.Debugf("Creating Azure client.")
log.Debugf("Creating Azure client")
azClient, err := newAzureClient(flags.azSubscription, flags.azLocation, flags.azResourceGroup)
if err != nil {
return fmt.Errorf("creating Azure client: %w", err)
}
log.Debugf("Creating versions API client.")
log.Debugf("Creating versions API client")
verclient, err := verclient.NewClient(cmd.Context(), flags.region, flags.bucket, flags.distributionID, flags.dryrun, log)
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
defer func(retErr *error) {
log.Infof("Invalidating cache. This may take some time.")
log.Infof("Invalidating cache. This may take some time")
if err := verclient.InvalidateCache(cmd.Context()); err != nil && retErr == nil {
*retErr = fmt.Errorf("invalidating cache: %w", err)
}
@ -205,8 +205,8 @@ func deleteImage(ctx context.Context, clients rmImageClients, ver versionsapi.Ve
imageInfo, err := clients.version.FetchImageInfo(ctx, imageInfo)
var notFound *verclient.NotFoundError
if errors.As(err, &notFound) {
log.Warnf("Image info for %s not found.", ver.Version)
log.Warnf("Skipping image deletion.")
log.Warnf("Image info for %s not found", ver.Version)
log.Warnf("Skipping image deletion")
return nil
} else if err != nil {
return fmt.Errorf("fetching image info: %w", err)
@ -550,7 +550,7 @@ func (g *gcpClient) deleteImage(ctx context.Context, image string, dryrun bool,
return fmt.Errorf("deleting image %s: %w", image, err)
}
log.Debugf("Waiting for operation to finish.")
log.Debugf("Waiting for operation to finish")
if err := op.Wait(ctx); err != nil {
return fmt.Errorf("waiting for operation: %w", err)
}
@ -643,7 +643,7 @@ func (a *azureClient) deleteImage(ctx context.Context, image string, dryrun bool
return fmt.Errorf("begin delete image version: %w", err)
}
log.Debugf("Waiting for operation to finish.")
log.Debugf("Waiting for operation to finish")
if _, err := poller.PollUntilDone(ctx, nil); err != nil {
return fmt.Errorf("waiting for operation: %w", err)
}
@ -670,7 +670,7 @@ func (a *azureClient) deleteImage(ctx context.Context, image string, dryrun bool
return fmt.Errorf("deleting image definition %s: %w", azImage.imageDefinition, err)
}
log.Debugf("Waiting for operation to finish.")
log.Debugf("Waiting for operation to finish")
if _, err := op.PollUntilDone(ctx, nil); err != nil {
return fmt.Errorf("waiting for operation: %w", err)
}
@ -688,7 +688,7 @@ type azImage struct {
func (a *azureClient) parseImage(ctx context.Context, image string, log *logger.Logger) (azImage, error) {
if m := azImageRegex.FindStringSubmatch(image); len(m) == 5 {
log.Debugf(
"Image matches local image format, resource group: %s, gallery: %s, image definition: %s, version: %s.",
"Image matches local image format, resource group: %s, gallery: %s, image definition: %s, version: %s",
m[1], m[2], m[3], m[4],
)
return azImage{
@ -709,7 +709,7 @@ func (a *azureClient) parseImage(ctx context.Context, image string, log *logger.
version := m[3]
log.Debugf(
"Image matches community image format, gallery public name: %s, image definition: %s, version: %s.",
"Image matches community image format, gallery public name: %s, image definition: %s, version: %s",
galleryPublicName, imageDefinition, version,
)
@ -722,24 +722,24 @@ func (a *azureClient) parseImage(ctx context.Context, image string, log *logger.
}
for _, v := range nextResult.Value {
if v.Name == nil {
log.Debugf("Skipping gallery with nil name.")
log.Debugf("Skipping gallery with nil name")
continue
}
if v.Properties.SharingProfile == nil {
log.Debugf("Skipping gallery %s with nil sharing profile.", *v.Name)
log.Debugf("Skipping gallery %s with nil sharing profile", *v.Name)
continue
}
if v.Properties.SharingProfile.CommunityGalleryInfo == nil {
log.Debugf("Skipping gallery %s with nil community gallery info.", *v.Name)
log.Debugf("Skipping gallery %s with nil community gallery info", *v.Name)
continue
}
if v.Properties.SharingProfile.CommunityGalleryInfo.PublicNames == nil {
log.Debugf("Skipping gallery %s with nil public names.", *v.Name)
log.Debugf("Skipping gallery %s with nil public names", *v.Name)
continue
}
for _, publicName := range v.Properties.SharingProfile.CommunityGalleryInfo.PublicNames {
if publicName == nil {
log.Debugf("Skipping nil public name.")
log.Debugf("Skipping nil public name")
continue
}
if *publicName == galleryPublicName {

View File

@ -187,7 +187,7 @@ func (c *Client) DeleteVersion(ctx context.Context, ver versionsapi.Version) err
// The function should be deferred after the client has been created.
func (c *Client) InvalidateCache(ctx context.Context) error {
if len(c.dirtyPaths) == 0 {
c.log.Debugf("No dirty paths, skipping cache invalidation.")
c.log.Debugf("No dirty paths, skipping cache invalidation")
return nil
}
@ -213,7 +213,7 @@ func (c *Client) InvalidateCache(ctx context.Context) error {
return fmt.Errorf("creating invalidation: %w", err)
}
c.log.Debugf("Waiting for invalidation %s to complete.", *invalidation.Invalidation.Id)
c.log.Debugf("Waiting for invalidation %s to complete", *invalidation.Invalidation.Id)
waiter := cloudfront.NewInvalidationCompletedWaiter(c.cloudfrontClient)
waitIn := &cloudfront.GetInvalidationInput{
DistributionId: &c.distributionID,
@ -361,23 +361,23 @@ func (c *Client) deleteVersionFromLatest(ctx context.Context, ver versionsapi.Ve
Stream: ver.Stream,
Kind: versionsapi.VersionKindImage,
}
c.log.Debugf("Fetching latest version from %s.", latest.JSONPath())
c.log.Debugf("Fetching latest version from %s", latest.JSONPath())
latest, err := c.FetchVersionLatest(ctx, latest)
var notFoundErr *NotFoundError
if errors.As(err, &notFoundErr) {
c.log.Warnf("Latest version for %s not found.", latest.JSONPath())
c.log.Warnf("Latest version for %s not found", latest.JSONPath())
return nil
} else if err != nil {
return fmt.Errorf("fetching latest version: %w", err)
}
if latest.Version != ver.Version {
c.log.Debugf("Latest version is %s, not the deleted version %s.", latest.Version, ver.Version)
c.log.Debugf("Latest version is %s, not the deleted version %s", latest.Version, ver.Version)
return nil
}
if possibleNewLatest == nil {
c.log.Errorf("Latest version is %s, but no new latest version was found.", latest.Version)
c.log.Errorf("Latest version is %s, but no new latest version was found", latest.Version)
c.log.Errorf("A manual update of latest at %s might be needed", latest.JSONPath())
return fmt.Errorf("latest version is %s, but no new latest version was found", latest.Version)
}