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"
"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"
2023-06-21 03:22:32 -04:00
"github.com/edgelesssys/constellation/v2/internal/imagefetcher"
2023-01-31 06:12:19 -05:00
"github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl"
2023-04-03 08:31:17 -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"
"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 ( )
flags , err := parseUpgradeCheckFlags ( cmd )
if err != nil {
return err
}
2023-07-24 04:30:53 -04:00
fileHandler := file . NewHandler ( afero . NewOsFs ( ) )
2023-07-18 03:33:42 -04:00
checker , err := kubernetes . NewUpgrader ( cmd . Context ( ) , cmd . OutOrStdout ( ) , fileHandler , log , kubernetes . UpgradeCmdKindCheck )
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-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 )
}
2023-07-24 04:30:53 -04:00
iamMigrateCmd , err := upgrade . NewIAMMigrateCmd ( cmd . Context ( ) , checker . GetUpgradeID ( ) , cloudprovider . AWS , terraform . LogLevelDebug )
if err != nil {
return fmt . Errorf ( "setting up IAM migration command: %w" , err )
}
2023-01-31 06:12:19 -05:00
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 ( ) ,
checker : checker ,
2023-06-07 10:16:32 -04:00
verListFetcher : versionfetcher ,
2023-01-31 06:12:19 -05:00
fileHandler : fileHandler ,
client : http . DefaultClient ,
2023-05-26 11:49:46 -04:00
cosign : sigstore . CosignVerifier { } ,
2023-01-31 06:12:19 -05:00
rekor : rekor ,
flags : flags ,
2023-03-01 05:55:12 -05:00
cliVersion : compatibility . EnsurePrefixV ( constants . VersionInfo ( ) ) ,
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-07-24 04:30:53 -04:00
checker : checker ,
imagefetcher : imagefetcher . New ( ) ,
log : log ,
iamMigrateCmd : iamMigrateCmd ,
planExecutor : & tfMigrationClient { 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 ) {
configPath , err := cmd . Flags ( ) . GetString ( "config" )
if err != nil {
2023-06-21 03:22:32 -04:00
return upgradeCheckFlags { } , fmt . Errorf ( "parsing config string: %w" , err )
2023-01-31 06:12:19 -05:00
}
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
configPath : configPath ,
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
}
2023-07-24 04:30:53 -04:00
type tfPlanner interface {
planMigration ( cmd * cobra . Command , file file . Handler , migrateCmd upgrade . TfMigrationCmd ) ( hasDiff bool , err error )
}
2023-01-31 06:12:19 -05:00
type upgradeCheckCmd struct {
2023-05-26 11:50:55 -04:00
canUpgradeCheck bool
collect collector
2023-06-21 03:22:32 -04:00
checker upgradeChecker
imagefetcher imageFetcher
2023-05-26 11:50:55 -04:00
log debugLog
2023-07-24 04:30:53 -04:00
iamMigrateCmd upgrade . TfMigrationCmd
planExecutor tfPlanner
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-06-01 07:55:46 -04:00
conf , err := config . New ( fileHandler , flags . configPath , 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
}
u . log . Debugf ( "Read configuration from %q" , flags . configPath )
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
if err := compatibility . IsValidUpgrade ( current . service , supported . service ) ; err != nil {
2023-01-31 06:12:19 -05:00
newServices = ""
2023-07-24 04:30:53 -04:00
u . log . Debugf ( "No valid service upgrades are available from %q to %q. The minor version can only drift by 1.\n" , current . service , supported . service )
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-07-24 04:30:53 -04:00
cmd . Println ( "The following IAM migrations are available with this CLI:" )
u . log . Debugf ( "Planning IAM migrations" )
if u . iamMigrateCmd != nil {
hasIAMDiff , err := u . planExecutor . planMigration ( cmd , fileHandler , u . iamMigrateCmd )
if err != nil {
return fmt . Errorf ( "planning IAM migration: %w" , err )
}
if ! hasIAMDiff {
cmd . Println ( " No IAM migrations are available." )
}
}
2023-06-21 03:22:32 -04:00
u . log . Debugf ( "Planning Terraform migrations" )
2023-07-24 04:30:53 -04:00
if err := u . checker . CheckTerraformMigrations ( ) ; err != nil {
return fmt . Errorf ( "checking workspace: %w" , err )
}
2023-06-21 03:22:32 -04:00
2023-06-27 07:12:50 -04:00
// TODO(AB#3248): Remove this migration after we can assume that all existing clusters have been migrated.
var awsZone string
if csp == cloudprovider . AWS {
awsZone = conf . Provider . AWS . Zone
}
manualMigrations := terraformMigrationAWSNodeGroups ( csp , awsZone )
for _ , migration := range manualMigrations {
u . log . Debugf ( "Adding manual Terraform migration: %s" , migration . DisplayName )
u . checker . AddManualStateMigration ( migration )
}
2023-07-18 03:33:42 -04:00
if err := u . checker . CheckTerraformMigrations ( ) ; err != nil {
2023-06-21 03:22:32 -04:00
return fmt . Errorf ( "checking workspace: %w" , err )
}
2023-07-21 04:04:29 -04:00
imageRef , err := getImage ( cmd . Context ( ) , conf , u . imagefetcher )
if err != nil {
return fmt . Errorf ( "fetching image reference: %w" , err )
}
vars , err := cloudcmd . TerraformUpgradeVars ( conf , imageRef )
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-07-18 03:33:42 -04:00
LogLevel : flags . terraformLogLevel ,
CSP : conf . GetProvider ( ) ,
Vars : vars ,
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
hasDiff , err := u . checker . PlanTerraformMigrations ( cmd . Context ( ) , opts )
if err != nil {
return fmt . Errorf ( "planning terraform migrations: %w" , err )
}
defer func ( ) {
2023-07-18 03:33:42 -04:00
if err := u . checker . CleanUpTerraformMigrations ( ) ; 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-01-31 06:12:19 -05:00
if err := upgrade . writeConfig ( conf , fileHandler , flags . configPath ) ; err != nil {
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 {
if err := compatibility . IsValidUpgrade ( currentVersion , newVersions [ i ] . Version ) ; err != nil {
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-04-03 08:31:17 -04:00
newCLIVersions ( ctx context . Context ) ( [ ] string , error )
filterCompatibleCLIVersions ( ctx context . Context , cliPatchVersions [ ] string , currentK8sVersion string ) ( [ ] string , error )
2023-01-31 06:12:19 -05:00
}
type versionCollector struct {
writer io . Writer
checker upgradeChecker
verListFetcher versionListFetcher
fileHandler file . Handler
client * http . Client
2023-05-26 11:49:46 -04:00
cosign cosignVerifier
2023-01-31 06:12:19 -05:00
rekor rekorVerifier
flags upgradeCheckFlags
2023-04-03 08:31:17 -04:00
versionsapi versionFetcher
2023-01-31 06:12:19 -05:00
cliVersion string
log debugLog
}
2023-05-22 08:59:28 -04:00
func ( v * versionCollector ) newMeasurements ( ctx context . Context , csp cloudprovider . Provider , attestationVariant variant . Variant , images [ ] versionsapi . Version ) ( map [ string ] measurements . M , error ) {
2023-01-31 06:12:19 -05:00
// get expected measurements for each image
2023-05-26 11:49:46 -04:00
upgrades , err := getCompatibleImageMeasurements ( ctx , v . writer , v . client , v . cosign , v . rekor , csp , attestationVariant , images , v . log )
2023-01-31 06:12:19 -05:00
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
}
2023-04-03 08:31:17 -04:00
type currentVersionInfo struct {
service string
image string
k8s string
cli string
}
func ( v * versionCollector ) currentVersions ( ctx context . Context ) ( currentVersionInfo , error ) {
2023-01-31 06:12:19 -05:00
helmClient , err := helm . NewClient ( kubectl . New ( ) , constants . AdminConfFilename , constants . HelmNamespace , v . log )
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-04-03 08:31:17 -04:00
imageVersion , err := getCurrentImageVersion ( ctx , v . checker )
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-04-03 08:31:17 -04:00
k8sVersion , err := getCurrentKubernetesVersion ( ctx , v . checker )
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 {
service string
image [ ] versionsapi . Version
k8s [ ] string
// CLI versions including those incompatible with the current Kubernetes version.
cli [ ] string
// CLI versions compatible with the current Kubernetes version.
compatibleCLI [ ] string
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
// Each CLI comes with a set of services that have the same version as the CLI.
serviceVersion := compatibility . EnsurePrefixV ( constants . VersionInfo ( ) )
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 {
service : serviceVersion ,
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 )
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 )
}
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 {
newServices string
newImages map [ string ] measurements . M
newKubernetes [ ] string
2023-04-03 08:31:17 -04:00
newCLI [ ] string
newCompatibleCLI [ ] string
2023-01-31 06:12:19 -05:00
currentServices string
currentImage string
currentKubernetes string
2023-04-03 08:31:17 -04:00
currentCLI string
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 , "" )
}
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
}
2023-04-03 08:31:17 -04:00
// no upgrades available
if v . newServices == "" && len ( v . newImages ) == 0 {
if len ( v . newCompatibleCLI ) > 0 {
result . WriteString ( fmt . Sprintf ( "Newer CLI versions that are compatible with your cluster are: %s\n" , strings . Join ( v . newCompatibleCLI , " " ) ) )
return result . String ( ) , nil
} else if len ( v . newCLI ) > 0 {
result . WriteString ( fmt . Sprintf ( "There are newer CLIs available (%s), however, you need to upgrade your cluster's Kubernetes version first.\n" , strings . Join ( v . newCLI , " " ) ) )
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.
if v . newServices != "" {
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.
func getCurrentImageVersion ( ctx context . Context , checker upgradeChecker ) ( 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.
func getCurrentKubernetesVersion ( ctx context . Context , checker upgradeChecker ) ( 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-05-26 11:49:46 -04:00
func getCompatibleImageMeasurements ( ctx context . Context , writer io . Writer , client * http . Client , cosign cosignVerifier , rekor rekorVerifier ,
2023-05-22 08:59:28 -04:00
csp cloudprovider . Provider , attestationVariant variant . Variant , versions [ ] versionsapi . Version , log debugLog ,
2023-01-31 06:12:19 -05:00
) ( 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 ( )
2023-05-22 08:59:28 -04:00
measurementsURL , signatureURL , err := versionsapi . MeasurementURL ( version )
2023-01-31 06:12:19 -05:00
if err != nil {
return nil , err
}
var fetchedMeasurements measurements . M
log . Debugf ( "Fetching for measurement url: %s" , measurementsURL )
hash , err := fetchedMeasurements . FetchAndVerify (
2023-05-26 11:49:46 -04:00
ctx , client , cosign ,
2023-01-31 06:12:19 -05:00
measurementsURL ,
signatureURL ,
2023-05-22 08:59:28 -04:00
version ,
csp ,
attestationVariant ,
2023-01-31 06:12:19 -05:00
)
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
}
2023-05-26 11:49:46 -04:00
if err = sigstore . VerifyWithRekor ( ctx , version , rekor , hash ) ; err != nil {
2023-01-31 06:12:19 -05:00
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
}
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.
func ( v * versionCollector ) newCLIVersions ( ctx context . Context ) ( [ ] string , error ) {
cliVersion , err := conSemver . New ( constants . VersionInfo ( ) )
if err != nil {
return nil , fmt . Errorf ( "parsing current CLI version: %w" , err )
}
list := versionsapi . List {
Ref : v . flags . ref ,
Stream : v . flags . stream ,
Granularity : versionsapi . GranularityMajor ,
Base : fmt . Sprintf ( "v%d" , cliVersion . Major ) ,
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 {
if err := compatibility . IsValidUpgrade ( v . cliVersion , version ) ; err != nil {
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 ... )
}
semver . Sort ( patchVersions )
return patchVersions , nil
}
// filterCompatibleCLIVersions filters a list of CLI versions which are compatible with the current Kubernetes version.
func ( v * versionCollector ) filterCompatibleCLIVersions ( ctx context . Context , cliPatchVersions [ ] string , currentK8sVersion string ) ( [ ] string , error ) {
// filter out invalid upgrades and versions which are not compatible with the current Kubernetes version
var compatibleVersions [ ] string
for _ , version := range cliPatchVersions {
if err := compatibility . IsValidUpgrade ( v . cliVersion , version ) ; err != nil {
v . log . Debugf ( "Skipping incompatible patch version %q: %s" , version , err )
continue
}
req := versionsapi . CLIInfo {
Ref : v . flags . ref ,
Stream : v . flags . stream ,
Version : version ,
}
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 )
}
}
}
semver . Sort ( compatibleVersions )
return compatibleVersions , nil
}
2023-01-31 06:12:19 -05:00
type upgradeCheckFlags struct {
2023-06-21 03:22:32 -04:00
configPath string
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
}
type upgradeChecker interface {
2023-02-09 09:54:12 -05:00
CurrentImage ( ctx context . Context ) ( string , error )
CurrentKubernetesVersion ( ctx context . Context ) ( string , error )
2023-06-21 03:22:32 -04:00
PlanTerraformMigrations ( ctx context . Context , opts upgrade . TerraformUpgradeOptions ) ( bool , error )
2023-07-18 03:33:42 -04:00
CheckTerraformMigrations ( ) error
CleanUpTerraformMigrations ( ) error
2023-06-27 07:12:50 -04:00
AddManualStateMigration ( migration terraform . StateMigration )
2023-01-31 06:12:19 -05:00
}
type versionListFetcher interface {
FetchVersionList ( ctx context . Context , list versionsapi . List ) ( versionsapi . List , error )
}