mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-24 23:19:39 -05:00
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:
parent
f204c24174
commit
c275464634
@ -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: |
|
||||
|
@ -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.
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
535
cli/internal/cmd/upgradecheck.go
Normal file
535
cli/internal/cmd/upgradecheck.go
Normal 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, ¬Found) {
|
||||
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)
|
||||
}
|
304
cli/internal/cmd/upgradecheck_test.go
Normal file
304
cli/internal/cmd/upgradecheck_test.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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 == ""
|
||||
|
@ -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)
|
||||
|
@ -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
3
go.mod
@ -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
6
go.sum
@ -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=
|
||||
|
@ -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
|
||||
|
@ -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=
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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."
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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, ¬Found) {
|
||||
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 {
|
||||
|
@ -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, ¬FoundErr) {
|
||||
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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user