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-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"
2023-12-08 10:27:04 -05:00
"github.com/edgelesssys/constellation/v2/internal/constellation/featureset"
2023-12-06 04:01:39 -05:00
"github.com/edgelesssys/constellation/v2/internal/constellation/helm"
2023-12-05 10:23:31 -05:00
"github.com/edgelesssys/constellation/v2/internal/constellation/kubecmd"
2023-01-31 06:12:19 -05:00
"github.com/edgelesssys/constellation/v2/internal/file"
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"
2023-10-16 09:05:29 -04:00
"github.com/spf13/pflag"
2023-01-31 06:12:19 -05:00
"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-10-16 09:05:29 -04:00
type upgradeCheckFlags struct {
rootFlags
updateConfig bool
ref string
stream string
}
func ( f * upgradeCheckFlags ) parse ( flags * pflag . FlagSet ) error {
if err := f . rootFlags . parse ( flags ) ; err != nil {
return err
}
updateConfig , err := flags . GetBool ( "update-config" )
if err != nil {
return fmt . Errorf ( "getting 'update-config' flag: %w" , err )
}
f . updateConfig = updateConfig
f . ref , err = flags . GetString ( "ref" )
if err != nil {
return fmt . Errorf ( "getting 'ref' flag: %w" , err )
}
f . stream , err = flags . GetString ( "stream" )
if err != nil {
return fmt . Errorf ( "getting 'stream' flag: %w" , err )
}
return nil
}
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 )
}
2023-08-04 07:53:51 -04:00
2023-10-16 09:05:29 -04:00
var flags upgradeCheckFlags
if err := flags . parse ( cmd . Flags ( ) ) ; err != nil {
2023-01-31 06:12:19 -05:00
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 )
2023-08-23 04:35:42 -04:00
upgradeDir := filepath . Join ( constants . UpgradeDir , upgradeID )
2023-10-31 07:46:40 -04:00
tfClient , cleanUp , err := cloudcmd . NewApplier (
2023-08-23 04:35:42 -04:00
cmd . Context ( ) ,
2023-10-31 07:46:40 -04:00
cmd . OutOrStdout ( ) ,
2023-08-23 04:35:42 -04:00
constants . TerraformWorkingDir ,
upgradeDir ,
2023-10-16 09:05:29 -04:00
flags . tfLogLevel ,
2023-08-23 04:35:42 -04:00
fileHandler ,
)
2023-08-16 03:59:32 -04:00
if err != nil {
2023-08-23 04:35:42 -04:00
return fmt . Errorf ( "setting up Terraform upgrader: %w" , err )
2023-08-16 03:59:32 -04:00
}
2023-10-31 07:46:40 -04:00
defer cleanUp ( )
2023-08-16 03:59:32 -04:00
2023-12-01 03:00:44 -05:00
kubeConfig , err := fileHandler . Read ( constants . AdminConfFilename )
if err != nil {
return fmt . Errorf ( "reading kubeconfig: %w" , err )
}
2023-12-05 10:23:31 -05:00
kubeChecker , err := kubecmd . New ( kubeConfig , 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-12-01 03:00:44 -05:00
helmClient , err := helm . NewReleaseVersionClient ( kubeConfig , log )
if err != nil {
return fmt . Errorf ( "setting up helm client: %w" , err )
}
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-12-01 03:00:44 -05:00
helmClient : helmClient ,
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-09-14 05:51:20 -04:00
upgradeDir : upgradeDir ,
2023-08-23 04:35:42 -04:00
terraformChecker : tfClient ,
fileHandler : fileHandler ,
2023-10-16 09:05:29 -04:00
flags : flags ,
2023-08-16 03:59:32 -04:00
log : log ,
2023-01-31 06:12:19 -05:00
}
2023-10-16 09:05:29 -04:00
return up . upgradeCheck ( cmd , attestationconfigapi . NewFetcher ( ) )
2023-01-31 06:12:19 -05:00
}
type upgradeCheckCmd struct {
2023-08-16 03:59:32 -04:00
canUpgradeCheck bool
2023-09-14 05:51:20 -04:00
upgradeDir string
2023-08-16 03:59:32 -04:00
collect collector
terraformChecker terraformChecker
2023-08-23 04:35:42 -04:00
fileHandler file . Handler
2023-10-16 09:05:29 -04:00
flags upgradeCheckFlags
2023-08-16 03:59:32 -04:00
log debugLog
2023-01-31 06:12:19 -05:00
}
// upgradePlan plans an upgrade of a Constellation cluster.
2023-10-16 09:05:29 -04:00
func ( u * upgradeCheckCmd ) upgradeCheck ( cmd * cobra . Command , fetcher attestationconfigapi . Fetcher ) error {
conf , err := config . New ( u . fileHandler , constants . ConfigFilename , fetcher , u . 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 ( )
2024-04-03 09:49:03 -04:00
u . log . Debug ( fmt . Sprintf ( "Using provider %q with attestation variant %q" , 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
}
2024-04-03 09:49:03 -04:00
u . log . Debug ( fmt . Sprintf ( "Current cli version: %q" , current . cli ) )
u . log . Debug ( fmt . Sprintf ( "Supported cli version(s): %q" , supported . cli ) )
u . log . Debug ( fmt . Sprintf ( "Current service version: %q" , current . service ) )
u . log . Debug ( fmt . Sprintf ( "Supported service version: %q" , supported . service ) )
u . log . Debug ( fmt . Sprintf ( "Current k8s version: %q" , current . k8s ) )
u . log . Debug ( fmt . Sprintf ( "Supported k8s version(s): %q" , 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 { }
2024-02-08 09:20:01 -05:00
u . log . Debug ( fmt . Sprintf ( "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
}
2024-02-08 09:20:01 -05:00
u . log . Debug ( "Planning Terraform migrations" )
2023-06-21 03:22:32 -04:00
2023-08-09 10:04:32 -04:00
// Add manual migrations here if required
//
// var manualMigrations []terraform.StateMigration
// for _, migration := range manualMigrations {
2024-02-08 09:20:01 -05:00
// u.log.Debug("Adding manual Terraform migration: %s", migration.DisplayName)
2023-10-31 07:46:40 -04:00
// u.terraformChecker.AddManualStateMigration(migration)
2023-08-09 10:04:32 -04:00
// }
2023-07-18 03:33:42 -04:00
cmd . Println ( "The following Terraform migrations are available with this CLI:" )
2023-10-31 07:46:40 -04:00
hasDiff , err := u . terraformChecker . Plan ( cmd . Context ( ) , conf )
2023-06-21 03:22:32 -04:00
if err != nil {
return fmt . Errorf ( "planning terraform migrations: %w" , err )
}
defer func ( ) {
2023-09-14 05:51:20 -04:00
// User doesn't expect to see any changes in his workspace after an "upgrade plan",
// therefore, roll back to the backed up state.
2023-10-31 07:46:40 -04:00
if err := u . terraformChecker . RestoreWorkspace ( ) ; err != nil {
2023-09-14 05:51:20 -04:00
cmd . PrintErrf (
"restoring Terraform workspace: %s, restore the Terraform workspace manually from %s " ,
err ,
filepath . Join ( u . upgradeDir , constants . TerraformUpgradeBackupDir ) ,
)
2023-06-21 03:22:32 -04:00
}
} ( )
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-10-16 09:05:29 -04:00
if u . flags . updateConfig {
2023-08-23 04:35:42 -04:00
if err := upgrade . writeConfig ( conf , u . 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-08-23 08:37:53 -04:00
func filterImageUpgrades ( currentVersion consemver . Semver , newVersions [ ] versionsapi . Version ) [ ] versionsapi . Version {
2023-01-31 06:12:19 -05:00
newImages := [ ] versionsapi . Version { }
for i := range newVersions {
2023-08-23 08:37:53 -04:00
if err := compatibility . IsValidUpgrade ( currentVersion . String ( ) , 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-08-23 08:37:53 -04:00
func filterK8sUpgrades ( currentVersion consemver . Semver , newVersions [ ] string ) [ ] string {
2023-01-31 06:12:19 -05:00
result := [ ] string { }
for i := range newVersions {
2023-08-23 08:37:53 -04:00
if err := compatibility . IsValidUpgrade ( currentVersion . String ( ) , newVersions [ i ] ) ; err != nil {
2023-01-31 06:12:19 -05:00
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 )
2023-08-23 08:37:53 -04:00
supportedVersions ( ctx context . Context , currentImageVersion , currentK8sVersion consemver . Semver ) ( supportedVersionInfo , error )
newImages ( ctx context . Context , currentImageVersion consemver . Semver ) ( [ ] 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 )
2023-08-23 08:37:53 -04:00
filterCompatibleCLIVersions ( ctx context . Context , cliPatchVersions [ ] consemver . Semver , currentK8sVersion consemver . Semver ) ( [ ] 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-12-01 03:00:44 -05:00
helmClient * helm . ReleaseVersionClient
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 {
2024-04-03 09:49:03 -04:00
v . log . Debug ( fmt . Sprintf ( "Fetching measurements for image: %q" , version . Version ( ) ) )
2023-08-01 10:48:13 -04:00
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
2024-04-03 09:49:03 -04:00
v . log . Debug ( "Compatible image measurement found" , shortPath , measurements . String ( ) )
2023-01-31 06:12:19 -05:00
}
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-08-23 08:37:53 -04:00
image consemver . Semver
k8s consemver . Semver
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-12-01 03:00:44 -05:00
serviceVersions , err := v . 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-21 10:15:32 -04:00
clusterVersions , err := v . kubeChecker . GetConstellationVersion ( ctx )
2023-01-31 06:12:19 -05:00
if err != nil {
2023-08-21 10:15:32 -04:00
return currentVersionInfo { } , fmt . Errorf ( "getting cluster versions: %w" , err )
2023-01-31 06:12:19 -05:00
}
2023-08-23 08:37:53 -04:00
imageVersion , err := consemver . New ( clusterVersions . ImageVersion ( ) )
if err != nil {
return currentVersionInfo { } , fmt . Errorf ( "parsing image semantic version: %w" , err )
2023-08-21 10:15:32 -04:00
}
2023-08-23 08:37:53 -04:00
k8sVersion , err := consemver . New ( clusterVersions . KubernetesVersion ( ) )
if err != nil {
return currentVersionInfo { } , fmt . Errorf ( "parsing Kubernetes semantic version: %w" , err )
2023-01-31 06:12:19 -05:00
}
2023-04-03 08:31:17 -04:00
return currentVersionInfo {
service : serviceVersions . ConstellationServices ( ) ,
2023-08-23 08:37:53 -04:00
image : imageVersion ,
k8s : k8sVersion ,
2023-04-03 08:31:17 -04:00
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-08-23 08:37:53 -04:00
func ( v * versionCollector ) supportedVersions ( ctx context . Context , currentImageVersion , currentK8sVersion consemver . Semver ) ( supportedVersionInfo , error ) {
2023-04-03 08:31:17 -04:00
k8sVersions := versions . SupportedK8sVersions ( )
2023-05-16 09:21:15 -04:00
2023-08-21 10:15:32 -04:00
imageVersions , err := v . newImages ( ctx , currentImageVersion )
2023-04-03 08:31:17 -04:00
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-08-23 08:37:53 -04:00
func ( v * versionCollector ) newImages ( ctx context . Context , currentImageVersion consemver . Semver ) ( [ ] 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
2023-08-23 08:37:53 -04:00
currentImageMinorVer := semver . MajorMinor ( currentImageVersion . String ( ) )
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 )
}
2024-04-03 09:49:03 -04:00
v . log . Debug ( fmt . Sprintf ( "Current image minor version is %q" , currentImageMinorVer ) )
v . log . Debug ( fmt . Sprintf ( "Current CLI minor version is %q" , currentCLIMinorVer ) )
v . log . Debug ( fmt . Sprintf ( "Next image minor version is %q" , nextImageMinorVer ) )
2023-01-31 06:12:19 -05:00
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 }
}
2024-02-08 09:20:01 -05:00
v . log . Debug ( fmt . Sprintf ( "Allowed minor versions are %#v" , allowedMinorVersions ) )
2023-01-31 06:12:19 -05:00
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 ) {
2024-04-03 09:49:03 -04:00
v . log . Debug ( fmt . Sprintf ( "Skipping version: %q" , err ) )
2023-01-31 06:12:19 -05:00
continue
}
if err != nil {
return nil , fmt . Errorf ( "fetching version list: %w" , err )
}
updateCandidates = append ( updateCandidates , patchList . StructuredVersions ( ) ... )
}
2024-02-08 09:20:01 -05:00
v . log . Debug ( fmt . Sprintf ( "Update candidates are %v" , updateCandidates ) )
2023-01-31 06:12:19 -05:00
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-08-23 08:37:53 -04:00
currentImage consemver . Semver
currentKubernetes consemver . Semver
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-09-19 07:50:00 -04:00
var err error
conf . KubernetesVersion , err = versions . NewValidK8sVersion ( v . newKubernetes [ 0 ] , true )
if err != nil {
return fmt . Errorf ( "parsing Kubernetes version: %w" , err )
}
2023-01-31 06:12:19 -05:00
}
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
}
// 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
2024-04-03 09:49:03 -04:00
log . Debug ( fmt . Sprintf ( "Fetching for measurement url: %q" , 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 {
2024-04-03 09:49:03 -04:00
v . log . Debug ( fmt . Sprintf ( "Skipping incompatible minor version %q: %q" , version , err ) )
2023-04-03 08:31:17 -04:00
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-08-23 08:37:53 -04:00
func ( v * versionCollector ) filterCompatibleCLIVersions ( ctx context . Context , cliPatchVersions [ ] consemver . Semver , currentK8sVersion consemver . Semver ,
2023-08-21 10:15:32 -04:00
) ( [ ] 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 {
2024-04-03 09:49:03 -04:00
v . log . Debug ( fmt . Sprintf ( "Skipping incompatible patch version %q: %q" , version , err ) )
2023-04-03 08:31:17 -04:00
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 {
2023-08-23 08:37:53 -04:00
k8sVersionSem , err := consemver . New ( k8sVersion )
if err != nil {
return nil , fmt . Errorf ( "parsing Kubernetes version %s: %w" , k8sVersion , err )
}
if k8sVersionSem . Compare ( currentK8sVersion ) == 0 {
2023-04-03 08:31:17 -04:00
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-08-16 03:59:32 -04:00
type kubernetesChecker interface {
2023-08-21 10:15:32 -04:00
GetConstellationVersion ( ctx context . Context ) ( kubecmd . NodeVersion , error )
2023-08-16 03:59:32 -04:00
}
type terraformChecker interface {
2023-10-31 07:46:40 -04:00
Plan ( context . Context , * config . Config ) ( bool , error )
RestoreWorkspace ( ) error
2023-01-31 06:12:19 -05:00
}
type versionListFetcher interface {
FetchVersionList ( ctx context . Context , list versionsapi . List ) ( versionsapi . List , error )
}