2023-01-31 06:12:19 -05:00
/ *
Copyright ( c ) Edgeless Systems GmbH
SPDX - License - Identifier : AGPL - 3.0 - only
* /
package cmd
import (
"context"
"errors"
"fmt"
"io"
"net/http"
2023-08-16 03:59:32 -04:00
"path/filepath"
2023-01-31 06:12:19 -05:00
"sort"
"strings"
2023-07-21 04:04:29 -04:00
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
2023-05-26 11:50:55 -04:00
"github.com/edgelesssys/constellation/v2/cli/internal/featureset"
2023-01-31 06:12:19 -05:00
"github.com/edgelesssys/constellation/v2/cli/internal/helm"
2023-03-30 10:13:14 -04:00
"github.com/edgelesssys/constellation/v2/cli/internal/kubernetes"
2023-06-21 03:22:32 -04:00
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/cli/internal/upgrade"
2023-06-07 10:16:32 -04:00
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
2023-05-25 12:43:44 -04:00
"github.com/edgelesssys/constellation/v2/internal/api/fetcher"
2023-06-07 10:16:32 -04:00
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
2023-01-31 06:12:19 -05:00
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
2023-06-09 09:41:02 -04:00
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
2023-01-31 06:12:19 -05:00
"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"
2023-07-25 08:20:25 -04:00
consemver "github.com/edgelesssys/constellation/v2/internal/semver"
2023-01-31 06:12:19 -05:00
"github.com/edgelesssys/constellation/v2/internal/sigstore"
2023-08-01 10:48:13 -04:00
"github.com/edgelesssys/constellation/v2/internal/sigstore/keyselect"
2023-01-31 06:12:19 -05:00
"github.com/edgelesssys/constellation/v2/internal/versions"
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"golang.org/x/mod/semver"
)
func newUpgradeCheckCmd ( ) * cobra . Command {
cmd := & cobra . Command {
Use : "check" ,
2023-02-12 16:55:39 -05:00
Short : "Check for possible upgrades" ,
2023-01-31 06:12:19 -05:00
Long : "Check which upgrades can be applied to your Constellation Cluster." ,
Args : cobra . NoArgs ,
RunE : runUpgradeCheck ,
}
2023-06-28 08:47:44 -04:00
cmd . Flags ( ) . BoolP ( "update-config" , "u" , false , "update the specified config file with the suggested versions" )
2023-02-12 16:55:39 -05:00
cmd . Flags ( ) . String ( "ref" , versionsapi . ReleaseRef , "the reference to use for querying new versions" )
cmd . Flags ( ) . String ( "stream" , "stable" , "the stream to use for querying new versions" )
2023-01-31 06:12:19 -05:00
return cmd
}
2023-03-20 06:03:36 -04:00
func runUpgradeCheck ( cmd * cobra . Command , _ [ ] string ) error {
2023-01-31 06:12:19 -05:00
log , err := newCLILogger ( cmd )
if err != nil {
return fmt . Errorf ( "creating logger: %w" , err )
}
defer log . Sync ( )
2023-08-04 07:53:51 -04:00
2023-01-31 06:12:19 -05:00
flags , err := parseUpgradeCheckFlags ( cmd )
if err != nil {
return err
}
2023-08-04 07:53:51 -04:00
2023-07-24 04:30:53 -04:00
fileHandler := file . NewHandler ( afero . NewOsFs ( ) )
2023-08-16 03:59:32 -04:00
upgradeID := generateUpgradeID ( upgradeCmdKindCheck )
tfClient , err := terraform . New ( cmd . Context ( ) , filepath . Join ( constants . UpgradeDir , upgradeID , constants . TerraformUpgradeWorkingDir ) )
if err != nil {
return fmt . Errorf ( "setting up terraform client: %w" , err )
}
kubeChecker , err := kubernetes . NewUpgrader ( cmd . OutOrStdout ( ) , constants . AdminConfFilename , log )
2023-01-31 06:12:19 -05:00
if err != nil {
2023-07-24 04:30:53 -04:00
return fmt . Errorf ( "setting up Kubernetes upgrader: %w" , err )
2023-01-31 06:12:19 -05:00
}
2023-08-04 07:53:51 -04:00
2023-06-07 10:16:32 -04:00
versionfetcher := versionsapi . NewFetcher ( )
2023-01-31 06:12:19 -05:00
rekor , err := sigstore . NewRekor ( )
if err != nil {
return fmt . Errorf ( "constructing Rekor client: %w" , err )
}
up := & upgradeCheckCmd {
2023-05-26 11:50:55 -04:00
canUpgradeCheck : featureset . CanUpgradeCheck ,
2023-01-31 06:12:19 -05:00
collect : & versionCollector {
writer : cmd . OutOrStderr ( ) ,
2023-08-16 03:59:32 -04:00
kubeChecker : kubeChecker ,
2023-06-07 10:16:32 -04:00
verListFetcher : versionfetcher ,
2023-01-31 06:12:19 -05:00
fileHandler : fileHandler ,
client : http . DefaultClient ,
rekor : rekor ,
flags : flags ,
2023-07-25 08:20:25 -04:00
cliVersion : constants . BinaryVersion ( ) ,
2023-02-14 08:46:30 -05:00
log : log ,
2023-06-07 10:16:32 -04:00
versionsapi : versionfetcher ,
2023-01-31 06:12:19 -05:00
} ,
2023-08-16 03:59:32 -04:00
terraformChecker : upgrade . NewTerraformUpgrader ( tfClient , cmd . OutOrStdout ( ) , fileHandler , upgradeID ) ,
log : log ,
2023-01-31 06:12:19 -05:00
}
2023-06-07 10:16:32 -04:00
return up . upgradeCheck ( cmd , fileHandler , attestationconfigapi . NewFetcher ( ) , flags )
2023-01-31 06:12:19 -05:00
}
func parseUpgradeCheckFlags ( cmd * cobra . Command ) ( upgradeCheckFlags , error ) {
force , err := cmd . Flags ( ) . GetBool ( "force" )
if err != nil {
2023-06-21 03:22:32 -04:00
return upgradeCheckFlags { } , fmt . Errorf ( "parsing force bool: %w" , err )
2023-01-31 06:12:19 -05:00
}
2023-06-28 08:47:44 -04:00
updateConfig , err := cmd . Flags ( ) . GetBool ( "update-config" )
2023-01-31 06:12:19 -05:00
if err != nil {
2023-06-28 08:47:44 -04:00
return upgradeCheckFlags { } , fmt . Errorf ( "parsing update-config bool: %w" , err )
2023-01-31 06:12:19 -05:00
}
ref , err := cmd . Flags ( ) . GetString ( "ref" )
if err != nil {
2023-06-21 03:22:32 -04:00
return upgradeCheckFlags { } , fmt . Errorf ( "parsing ref string: %w" , err )
2023-01-31 06:12:19 -05:00
}
stream , err := cmd . Flags ( ) . GetString ( "stream" )
if err != nil {
2023-06-21 03:22:32 -04:00
return upgradeCheckFlags { } , fmt . Errorf ( "parsing stream string: %w" , err )
2023-01-31 06:12:19 -05:00
}
2023-06-21 03:22:32 -04:00
logLevelString , err := cmd . Flags ( ) . GetString ( "tf-log" )
if err != nil {
return upgradeCheckFlags { } , fmt . Errorf ( "parsing tf-log string: %w" , err )
}
logLevel , err := terraform . ParseLogLevel ( logLevelString )
if err != nil {
return upgradeCheckFlags { } , fmt . Errorf ( "parsing Terraform log level %s: %w" , logLevelString , err )
}
2023-01-31 06:12:19 -05:00
return upgradeCheckFlags {
2023-06-21 03:22:32 -04:00
force : force ,
2023-06-28 08:47:44 -04:00
updateConfig : updateConfig ,
2023-06-21 03:22:32 -04:00
ref : ref ,
stream : stream ,
terraformLogLevel : logLevel ,
2023-01-31 06:12:19 -05:00
} , nil
}
type upgradeCheckCmd struct {
2023-08-16 03:59:32 -04:00
canUpgradeCheck bool
collect collector
terraformChecker terraformChecker
log debugLog
2023-01-31 06:12:19 -05:00
}
// upgradePlan plans an upgrade of a Constellation cluster.
2023-06-07 10:16:32 -04:00
func ( u * upgradeCheckCmd ) upgradeCheck ( cmd * cobra . Command , fileHandler file . Handler , fetcher attestationconfigapi . Fetcher , flags upgradeCheckFlags ) error {
2023-08-04 07:53:51 -04:00
conf , err := config . New ( fileHandler , constants . ConfigFilename , fetcher , flags . force )
2023-02-07 06:56:25 -05:00
var configValidationErr * config . ValidationError
if errors . As ( err , & configValidationErr ) {
cmd . PrintErrln ( configValidationErr . LongMessage ( ) )
}
2023-01-31 06:12:19 -05:00
if err != nil {
2023-02-07 06:56:25 -05:00
return err
2023-01-31 06:12:19 -05:00
}
2023-05-26 11:50:55 -04:00
if ! u . canUpgradeCheck {
cmd . PrintErrln ( "Planning Constellation upgrades automatically is not supported in the OSS build of the Constellation CLI. Consult the documentation for instructions on where to download the enterprise version." )
return errors . New ( "upgrade check is not supported" )
}
2023-01-31 06:12:19 -05:00
// get current image version of the cluster
csp := conf . GetProvider ( )
2023-05-22 08:59:28 -04:00
attestationVariant := conf . GetAttestationConfig ( ) . GetVariant ( )
u . log . Debugf ( "Using provider %s with attestation variant %s" , csp . String ( ) , attestationVariant . String ( ) )
2023-01-31 06:12:19 -05:00
2023-04-03 08:31:17 -04:00
current , err := u . collect . currentVersions ( cmd . Context ( ) )
2023-01-31 06:12:19 -05:00
if err != nil {
return err
}
2023-04-03 08:31:17 -04:00
supported , err := u . collect . supportedVersions ( cmd . Context ( ) , current . image , current . k8s )
2023-01-31 06:12:19 -05:00
if err != nil {
return err
}
2023-04-03 08:31:17 -04:00
u . log . Debugf ( "Current cli version: %s" , current . cli )
u . log . Debugf ( "Supported cli version(s): %s" , supported . cli )
u . log . Debugf ( "Current service version: %s" , current . service )
u . log . Debugf ( "Supported service version: %s" , supported . service )
u . log . Debugf ( "Current k8s version: %s" , current . k8s )
u . log . Debugf ( "Supported k8s version(s): %s" , supported . k8s )
2023-01-31 06:12:19 -05:00
// Filter versions to only include upgrades
2023-04-03 08:31:17 -04:00
newServices := supported . service
2023-07-25 08:20:25 -04:00
if err := supported . service . IsUpgradeTo ( current . service ) ; err != nil {
newServices = consemver . Semver { }
u . log . Debugf ( "No valid service upgrades are available from %q to %q. The minor version can only drift by 1.\n" , current . service . String ( ) , supported . service . String ( ) )
2023-01-31 06:12:19 -05:00
}
2023-04-03 08:31:17 -04:00
newKubernetes := filterK8sUpgrades ( current . k8s , supported . k8s )
semver . Sort ( newKubernetes )
2023-01-31 06:12:19 -05:00
2023-04-03 08:31:17 -04:00
supported . image = filterImageUpgrades ( current . image , supported . image )
2023-05-22 08:59:28 -04:00
newImages , err := u . collect . newMeasurements ( cmd . Context ( ) , csp , attestationVariant , supported . image )
2023-01-31 06:12:19 -05:00
if err != nil {
return err
}
2023-06-21 03:22:32 -04:00
u . log . Debugf ( "Planning Terraform migrations" )
2023-08-09 10:04:32 -04:00
// Add manual migrations here if required
//
// var manualMigrations []terraform.StateMigration
// for _, migration := range manualMigrations {
// u.log.Debugf("Adding manual Terraform migration: %s", migration.DisplayName)
// u.upgrader.AddManualStateMigration(migration)
// }
2023-06-27 07:12:50 -04:00
2023-08-16 03:59:32 -04:00
if err := u . terraformChecker . CheckTerraformMigrations ( constants . UpgradeDir ) ; err != nil {
2023-06-21 03:22:32 -04:00
return fmt . Errorf ( "checking workspace: %w" , err )
}
2023-08-14 09:16:07 -04:00
vars , err := cloudcmd . TerraformUpgradeVars ( conf )
2023-06-21 03:22:32 -04:00
if err != nil {
return fmt . Errorf ( "parsing upgrade variables: %w" , err )
}
u . log . Debugf ( "Using Terraform variables:\n%v" , vars )
opts := upgrade . TerraformUpgradeOptions {
2023-08-04 07:53:51 -04:00
LogLevel : flags . terraformLogLevel ,
CSP : conf . GetProvider ( ) ,
Vars : vars ,
TFWorkspace : constants . TerraformWorkingDir ,
UpgradeWorkspace : constants . UpgradeDir ,
2023-06-21 03:22:32 -04:00
}
2023-07-18 03:33:42 -04:00
cmd . Println ( "The following Terraform migrations are available with this CLI:" )
2023-06-21 03:22:32 -04:00
// Check if there are any Terraform migrations
2023-08-16 03:59:32 -04:00
hasDiff , err := u . terraformChecker . PlanTerraformMigrations ( cmd . Context ( ) , opts )
2023-06-21 03:22:32 -04:00
if err != nil {
return fmt . Errorf ( "planning terraform migrations: %w" , err )
}
defer func ( ) {
2023-08-16 03:59:32 -04:00
if err := u . terraformChecker . CleanUpTerraformMigrations ( constants . UpgradeDir ) ; err != nil {
2023-06-21 03:22:32 -04:00
u . log . Debugf ( "Failed to clean up Terraform migrations: %v" , err )
}
} ( )
if ! hasDiff {
cmd . Println ( " No Terraform migrations are available." )
}
2023-01-31 06:12:19 -05:00
upgrade := versionUpgrade {
newServices : newServices ,
newImages : newImages ,
newKubernetes : newKubernetes ,
2023-04-03 08:31:17 -04:00
newCLI : supported . cli ,
newCompatibleCLI : supported . compatibleCLI ,
currentServices : current . service ,
currentImage : current . image ,
currentKubernetes : current . k8s ,
currentCLI : current . cli ,
2023-01-31 06:12:19 -05:00
}
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 )
2023-06-28 08:47:44 -04:00
if flags . updateConfig {
2023-08-04 07:53:51 -04:00
if err := upgrade . writeConfig ( conf , fileHandler , constants . ConfigFilename ) ; err != nil {
2023-01-31 06:12:19 -05:00
return fmt . Errorf ( "writing config: %w" , err )
}
2023-03-03 10:50:25 -05:00
cmd . Println ( "Config updated successfully." )
2023-01-31 06:12:19 -05:00
}
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
}
2023-04-03 08:31:17 -04:00
// filterImageUpgrades filters out image versions that are not valid upgrades.
2023-01-31 06:12:19 -05:00
func filterImageUpgrades ( currentVersion string , newVersions [ ] versionsapi . Version ) [ ] versionsapi . Version {
newImages := [ ] versionsapi . Version { }
for i := range newVersions {
2023-08-01 10:48:13 -04:00
if err := compatibility . IsValidUpgrade ( currentVersion , newVersions [ i ] . Version ( ) ) ; err != nil {
2023-01-31 06:12:19 -05:00
continue
}
newImages = append ( newImages , newVersions [ i ] )
}
return newImages
}
2023-04-03 08:31:17 -04:00
// filterK8sUpgrades filters out K8s versions that are not valid upgrades.
2023-01-31 06:12:19 -05:00
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 {
2023-04-03 08:31:17 -04:00
currentVersions ( ctx context . Context ) ( currentVersionInfo , error )
supportedVersions ( ctx context . Context , version , currentK8sVersion string ) ( supportedVersionInfo , error )
2023-03-20 06:03:36 -04:00
newImages ( ctx context . Context , version string ) ( [ ] versionsapi . Version , error )
2023-05-22 08:59:28 -04:00
newMeasurements ( ctx context . Context , csp cloudprovider . Provider , attestationVariant variant . Variant , images [ ] versionsapi . Version ) ( map [ string ] measurements . M , error )
2023-03-20 06:03:36 -04:00
newerVersions ( ctx context . Context , allowedVersions [ ] string ) ( [ ] versionsapi . Version , error )
2023-07-25 08:20:25 -04:00
newCLIVersions ( ctx context . Context ) ( [ ] consemver . Semver , error )
filterCompatibleCLIVersions ( ctx context . Context , cliPatchVersions [ ] consemver . Semver , currentK8sVersion string ) ( [ ] consemver . Semver , error )
2023-01-31 06:12:19 -05:00
}
type versionCollector struct {
writer io . Writer
2023-08-16 03:59:32 -04:00
kubeChecker kubernetesChecker
2023-01-31 06:12:19 -05:00
verListFetcher versionListFetcher
fileHandler file . Handler
client * http . Client
rekor rekorVerifier
flags upgradeCheckFlags
2023-04-03 08:31:17 -04:00
versionsapi versionFetcher
2023-07-25 08:20:25 -04:00
cliVersion consemver . Semver
2023-01-31 06:12:19 -05:00
log debugLog
}
2023-08-01 10:48:13 -04:00
func ( v * versionCollector ) newMeasurements ( ctx context . Context , csp cloudprovider . Provider , attestationVariant variant . Variant , versions [ ] versionsapi . Version ) ( map [ string ] measurements . M , error ) {
2023-01-31 06:12:19 -05:00
// get expected measurements for each image
2023-08-01 10:48:13 -04:00
upgrades := make ( map [ string ] measurements . M )
for _ , version := range versions {
v . log . Debugf ( "Fetching measurements for image: %s" , version )
shortPath := version . ShortPath ( )
publicKey , err := keyselect . CosignPublicKeyForVersion ( version )
if err != nil {
return nil , fmt . Errorf ( "getting public key: %w" , err )
}
cosign , err := sigstore . NewCosignVerifier ( publicKey )
if err != nil {
return nil , fmt . Errorf ( "setting public key: %w" , err )
}
measurements , err := getCompatibleImageMeasurements ( ctx , v . writer , v . client , cosign , v . rekor , csp , attestationVariant , version , v . log )
if err != nil {
if _ , err := fmt . Fprintf ( v . writer , "Skipping compatible image %q: %s\n" , shortPath , err ) ; err != nil {
return nil , fmt . Errorf ( "writing to buffer: %w" , err )
}
continue
}
upgrades [ shortPath ] = measurements
2023-01-31 06:12:19 -05:00
}
v . log . Debugf ( "Compatible image measurements are %v" , upgrades )
return upgrades , nil
}
2023-04-03 08:31:17 -04:00
type currentVersionInfo struct {
2023-07-25 08:20:25 -04:00
service consemver . Semver
2023-04-03 08:31:17 -04:00
image string
k8s string
2023-07-25 08:20:25 -04:00
cli consemver . Semver
2023-04-03 08:31:17 -04:00
}
func ( v * versionCollector ) currentVersions ( ctx context . Context ) ( currentVersionInfo , error ) {
2023-08-04 07:53:51 -04:00
helmClient , err := helm . NewUpgradeClient ( kubectl . New ( ) , constants . UpgradeDir , constants . AdminConfFilename , constants . HelmNamespace , v . log )
2023-01-31 06:12:19 -05:00
if err != nil {
2023-04-03 08:31:17 -04:00
return currentVersionInfo { } , fmt . Errorf ( "setting up helm client: %w" , err )
2023-01-31 06:12:19 -05:00
}
2023-03-24 06:51:18 -04:00
serviceVersions , err := helmClient . Versions ( )
2023-01-31 06:12:19 -05:00
if err != nil {
2023-04-03 08:31:17 -04:00
return currentVersionInfo { } , fmt . Errorf ( "getting service versions: %w" , err )
2023-01-31 06:12:19 -05:00
}
2023-08-16 03:59:32 -04:00
imageVersion , err := getCurrentImageVersion ( ctx , v . kubeChecker )
2023-01-31 06:12:19 -05:00
if err != nil {
2023-04-03 08:31:17 -04:00
return currentVersionInfo { } , fmt . Errorf ( "getting image version: %w" , err )
2023-01-31 06:12:19 -05:00
}
2023-08-16 03:59:32 -04:00
k8sVersion , err := getCurrentKubernetesVersion ( ctx , v . kubeChecker )
2023-01-31 06:12:19 -05:00
if err != nil {
2023-04-03 08:31:17 -04:00
return currentVersionInfo { } , fmt . Errorf ( "getting Kubernetes version: %w" , err )
2023-01-31 06:12:19 -05:00
}
2023-04-03 08:31:17 -04:00
return currentVersionInfo {
service : serviceVersions . ConstellationServices ( ) ,
image : imageVersion ,
k8s : k8sVersion ,
cli : v . cliVersion ,
} , nil
}
type supportedVersionInfo struct {
2023-07-25 08:20:25 -04:00
service consemver . Semver
2023-04-03 08:31:17 -04:00
image [ ] versionsapi . Version
k8s [ ] string
// CLI versions including those incompatible with the current Kubernetes version.
2023-07-25 08:20:25 -04:00
cli [ ] consemver . Semver
2023-04-03 08:31:17 -04:00
// CLI versions compatible with the current Kubernetes version.
2023-07-25 08:20:25 -04:00
compatibleCLI [ ] consemver . Semver
2023-01-31 06:12:19 -05:00
}
// supportedVersions returns slices of supported versions.
2023-04-03 08:31:17 -04:00
func ( v * versionCollector ) supportedVersions ( ctx context . Context , version , currentK8sVersion string ) ( supportedVersionInfo , error ) {
k8sVersions := versions . SupportedK8sVersions ( )
2023-05-16 09:21:15 -04:00
2023-04-03 08:31:17 -04:00
imageVersions , err := v . newImages ( ctx , version )
if err != nil {
return supportedVersionInfo { } , fmt . Errorf ( "loading image versions: %w" , err )
}
cliVersions , err := v . newCLIVersions ( ctx )
2023-01-31 06:12:19 -05:00
if err != nil {
2023-04-03 08:31:17 -04:00
return supportedVersionInfo { } , fmt . Errorf ( "loading cli versions: %w" , err )
2023-01-31 06:12:19 -05:00
}
2023-04-03 08:31:17 -04:00
compatibleCLIVersions , err := v . filterCompatibleCLIVersions ( ctx , cliVersions , currentK8sVersion )
2023-01-31 06:12:19 -05:00
if err != nil {
2023-04-03 08:31:17 -04:00
return supportedVersionInfo { } , fmt . Errorf ( "filtering cli versions: %w" , err )
2023-01-31 06:12:19 -05:00
}
2023-04-03 08:31:17 -04:00
return supportedVersionInfo {
2023-07-25 08:20:25 -04:00
// Each CLI comes with a set of services that have the same version as the CLI.
service : constants . BinaryVersion ( ) ,
2023-04-03 08:31:17 -04:00
image : imageVersions ,
k8s : k8sVersions ,
cli : cliVersions ,
compatibleCLI : compatibleCLIVersions ,
} , nil
2023-01-31 06:12:19 -05:00
}
2023-03-20 06:03:36 -04:00
func ( v * versionCollector ) newImages ( ctx context . Context , version string ) ( [ ] versionsapi . Version , error ) {
2023-01-31 06:12:19 -05:00
// 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 )
2023-07-25 08:20:25 -04:00
currentCLIMinorVer := semver . MajorMinor ( v . cliVersion . String ( ) )
2023-01-31 06:12:19 -05:00
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 )
}
2023-02-14 08:46:30 -05:00
if _ , err := fmt . Fprintln ( v . writer , "WARNING: CLI version is older than cluster image version. Continuing due to force flag." ) ; err != nil {
2023-01-31 06:12:19 -05:00
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 )
2023-03-20 06:03:36 -04:00
newerImages , err := v . newerVersions ( ctx , allowedMinorVersions )
2023-01-31 06:12:19 -05:00
if err != nil {
return nil , fmt . Errorf ( "newer versions: %w" , err )
}
return newerImages , nil
}
2023-03-20 06:03:36 -04:00
func ( v * versionCollector ) newerVersions ( ctx context . Context , allowedVersions [ ] string ) ( [ ] versionsapi . Version , error ) {
2023-01-31 06:12:19 -05:00
var updateCandidates [ ] versionsapi . Version
for _ , minorVer := range allowedVersions {
patchList := versionsapi . List {
Ref : v . flags . ref ,
Stream : v . flags . stream ,
Base : minorVer ,
Granularity : versionsapi . GranularityMinor ,
Kind : versionsapi . VersionKindImage ,
}
patchList , err := v . verListFetcher . FetchVersionList ( ctx , patchList )
var notFound * fetcher . NotFoundError
if errors . As ( err , & notFound ) {
v . log . Debugf ( "Skipping version: %s" , err )
continue
}
if err != nil {
return nil , fmt . Errorf ( "fetching version list: %w" , err )
}
updateCandidates = append ( updateCandidates , patchList . StructuredVersions ( ) ... )
}
v . log . Debugf ( "Update candidates are %v" , updateCandidates )
return updateCandidates , nil
}
type versionUpgrade struct {
2023-07-25 08:20:25 -04:00
newServices consemver . Semver
2023-01-31 06:12:19 -05:00
newImages map [ string ] measurements . M
newKubernetes [ ] string
2023-07-25 08:20:25 -04:00
newCLI [ ] consemver . Semver
newCompatibleCLI [ ] consemver . Semver
currentServices consemver . Semver
2023-01-31 06:12:19 -05:00
currentImage string
currentKubernetes string
2023-07-25 08:20:25 -04:00
currentCLI consemver . Semver
2023-01-31 06:12:19 -05:00
}
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 , "" )
}
2023-07-25 08:20:25 -04:00
if v . newServices != ( consemver . Semver { } ) {
2023-01-31 06:12:19 -05:00
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
}
2023-04-03 08:31:17 -04:00
// no upgrades available
2023-07-25 08:20:25 -04:00
if v . newServices == ( consemver . Semver { } ) && len ( v . newImages ) == 0 {
2023-04-03 08:31:17 -04:00
if len ( v . newCompatibleCLI ) > 0 {
2023-07-25 08:20:25 -04:00
result . WriteString ( fmt . Sprintf ( "Newer CLI versions that are compatible with your cluster are: %s\n" , strings . Join ( consemver . ToStrings ( v . newCompatibleCLI ) , " " ) ) )
2023-04-03 08:31:17 -04:00
return result . String ( ) , nil
} else if len ( v . newCLI ) > 0 {
2023-07-25 08:20:25 -04:00
result . WriteString ( fmt . Sprintf ( "There are newer CLIs available (%s), however, you need to upgrade your cluster's Kubernetes version first.\n" , strings . Join ( consemver . ToStrings ( v . newCLI ) , " " ) ) )
2023-04-03 08:31:17 -04:00
return result . String ( ) , nil
}
}
2023-01-31 06:12:19 -05:00
2023-04-03 08:31:17 -04:00
result . WriteString ( "You are up to date.\n" )
2023-01-31 06:12:19 -05:00
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.
2023-07-25 08:20:25 -04:00
if v . newServices != ( consemver . Semver { } ) {
2023-01-31 06:12:19 -05:00
conf . MicroserviceVersion = v . newServices
}
2023-06-02 11:19:41 -04:00
if len ( v . newKubernetes ) > 0 {
2023-01-31 06:12:19 -05:00
conf . KubernetesVersion = v . newKubernetes [ 0 ]
}
if len ( v . newImages ) > 0 {
imageUpgrade := sortedMapKeys ( v . newImages ) [ 0 ]
conf . Image = imageUpgrade
conf . UpdateMeasurements ( v . newImages [ imageUpgrade ] )
}
2023-03-20 06:06:51 -04:00
return fileHandler . WriteYAML ( configPath , conf , file . OptOverwrite )
2023-01-31 06:12:19 -05:00
}
// 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.
2023-08-16 03:59:32 -04:00
func getCurrentImageVersion ( ctx context . Context , checker kubernetesChecker ) ( string , error ) {
2023-02-09 09:54:12 -05:00
imageVersion , err := checker . CurrentImage ( ctx )
2023-01-31 06:12:19 -05:00
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.
2023-08-16 03:59:32 -04:00
func getCurrentKubernetesVersion ( ctx context . Context , checker kubernetesChecker ) ( string , error ) {
2023-02-09 09:54:12 -05:00
k8sVersion , err := checker . CurrentKubernetesVersion ( ctx )
2023-01-31 06:12:19 -05:00
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.
2023-08-01 10:48:13 -04:00
func getCompatibleImageMeasurements ( ctx context . Context , writer io . Writer , client * http . Client , cosign sigstore . Verifier , rekor rekorVerifier ,
csp cloudprovider . Provider , attestationVariant variant . Variant , version versionsapi . Version , log debugLog ,
) ( measurements . M , error ) {
measurementsURL , signatureURL , err := versionsapi . MeasurementURL ( version )
if err != nil {
return nil , err
}
2023-01-31 06:12:19 -05:00
2023-08-01 10:48:13 -04:00
var fetchedMeasurements measurements . M
log . Debugf ( "Fetching for measurement url: %s" , measurementsURL )
2023-01-31 06:12:19 -05:00
2023-08-01 10:48:13 -04:00
hash , err := fetchedMeasurements . FetchAndVerify (
ctx , client , cosign ,
measurementsURL ,
signatureURL ,
version ,
csp ,
attestationVariant ,
)
if err != nil {
return nil , fmt . Errorf ( "fetching measurements: %w" , err )
}
2023-01-31 06:12:19 -05:00
2023-08-01 10:48:13 -04:00
pubkey , err := keyselect . CosignPublicKeyForVersion ( version )
if err != nil {
return nil , fmt . Errorf ( "getting public key: %w" , err )
}
2023-01-31 06:12:19 -05:00
2023-08-01 10:48:13 -04:00
if err = sigstore . VerifyWithRekor ( ctx , pubkey , rekor , hash ) ; err != nil {
if _ , err := fmt . Fprintf ( writer , "Warning: Unable to verify '%s' in Rekor.\nMake sure measurements are correct.\n" , hash ) ; err != nil {
return nil , fmt . Errorf ( "writing to buffer: %w" , err )
}
2023-01-31 06:12:19 -05:00
}
2023-08-01 10:48:13 -04:00
return fetchedMeasurements , nil
2023-01-31 06:12:19 -05:00
}
2023-04-03 08:31:17 -04:00
type versionFetcher interface {
FetchVersionList ( ctx context . Context , list versionsapi . List ) ( versionsapi . List , error )
FetchCLIInfo ( ctx context . Context , cliInfo versionsapi . CLIInfo ) ( versionsapi . CLIInfo , error )
}
// newCLIVersions returns a list of versions of the CLI which are a valid upgrade.
2023-07-25 08:20:25 -04:00
func ( v * versionCollector ) newCLIVersions ( ctx context . Context ) ( [ ] consemver . Semver , error ) {
2023-04-03 08:31:17 -04:00
list := versionsapi . List {
Ref : v . flags . ref ,
Stream : v . flags . stream ,
Granularity : versionsapi . GranularityMajor ,
2023-07-25 08:20:25 -04:00
Base : fmt . Sprintf ( "v%d" , constants . BinaryVersion ( ) . Major ( ) ) ,
2023-04-03 08:31:17 -04:00
Kind : versionsapi . VersionKindCLI ,
}
minorList , err := v . versionsapi . FetchVersionList ( ctx , list )
if err != nil {
return nil , fmt . Errorf ( "listing major versions: %w" , err )
}
var patchVersions [ ] string
for _ , version := range minorList . Versions {
2023-07-25 08:20:25 -04:00
target , err := consemver . New ( version )
if err != nil {
return nil , fmt . Errorf ( "parsing version %s: %w" , version , err )
}
if err := target . IsUpgradeTo ( v . cliVersion ) ; err != nil {
2023-04-03 08:31:17 -04:00
v . log . Debugf ( "Skipping incompatible minor version %q: %s" , version , err )
continue
}
list := versionsapi . List {
Ref : v . flags . ref ,
Stream : v . flags . stream ,
Granularity : versionsapi . GranularityMinor ,
Base : version ,
Kind : versionsapi . VersionKindCLI ,
}
patchList , err := v . versionsapi . FetchVersionList ( ctx , list )
if err != nil {
return nil , fmt . Errorf ( "listing minor versions for major version %s: %w" , version , err )
}
patchVersions = append ( patchVersions , patchList . Versions ... )
}
2023-07-25 08:20:25 -04:00
out , err := consemver . NewSlice ( patchVersions )
if err != nil {
return nil , fmt . Errorf ( "parsing versions: %w" , err )
}
consemver . Sort ( out )
2023-04-03 08:31:17 -04:00
2023-07-25 08:20:25 -04:00
return out , nil
2023-04-03 08:31:17 -04:00
}
// filterCompatibleCLIVersions filters a list of CLI versions which are compatible with the current Kubernetes version.
2023-07-25 08:20:25 -04:00
func ( v * versionCollector ) filterCompatibleCLIVersions ( ctx context . Context , cliPatchVersions [ ] consemver . Semver , currentK8sVersion string ) ( [ ] consemver . Semver , error ) {
2023-04-03 08:31:17 -04:00
// filter out invalid upgrades and versions which are not compatible with the current Kubernetes version
2023-07-25 08:20:25 -04:00
var compatibleVersions [ ] consemver . Semver
2023-04-03 08:31:17 -04:00
for _ , version := range cliPatchVersions {
2023-07-25 08:20:25 -04:00
if err := version . IsUpgradeTo ( v . cliVersion ) ; err != nil {
2023-04-03 08:31:17 -04:00
v . log . Debugf ( "Skipping incompatible patch version %q: %s" , version , err )
continue
}
req := versionsapi . CLIInfo {
Ref : v . flags . ref ,
Stream : v . flags . stream ,
2023-07-25 08:20:25 -04:00
Version : version . String ( ) ,
2023-04-03 08:31:17 -04:00
}
info , err := v . versionsapi . FetchCLIInfo ( ctx , req )
if err != nil {
return nil , fmt . Errorf ( "fetching CLI info: %w" , err )
}
for _ , k8sVersion := range info . Kubernetes {
if k8sVersion == currentK8sVersion {
compatibleVersions = append ( compatibleVersions , version )
}
}
}
2023-07-25 08:20:25 -04:00
consemver . Sort ( compatibleVersions )
2023-04-03 08:31:17 -04:00
return compatibleVersions , nil
}
2023-01-31 06:12:19 -05:00
type upgradeCheckFlags struct {
2023-06-21 03:22:32 -04:00
force bool
2023-06-28 08:47:44 -04:00
updateConfig bool
2023-06-21 03:22:32 -04:00
ref string
stream string
terraformLogLevel terraform . LogLevel
2023-01-31 06:12:19 -05:00
}
2023-08-16 03:59:32 -04:00
type kubernetesChecker interface {
2023-02-09 09:54:12 -05:00
CurrentImage ( ctx context . Context ) ( string , error )
CurrentKubernetesVersion ( ctx context . Context ) ( string , error )
2023-08-16 03:59:32 -04:00
}
type terraformChecker interface {
2023-06-21 03:22:32 -04:00
PlanTerraformMigrations ( ctx context . Context , opts upgrade . TerraformUpgradeOptions ) ( bool , error )
2023-08-04 07:53:51 -04:00
CheckTerraformMigrations ( upgradeWorkspace string ) error
CleanUpTerraformMigrations ( upgradeWorkspace string ) error
2023-01-31 06:12:19 -05:00
}
type versionListFetcher interface {
FetchVersionList ( ctx context . Context , list versionsapi . List ) ( versionsapi . List , error )
}