Skip to content

Commit b4e73be

Browse files
JGAntunesemosbaugh
andauthored
feat(upgrade): add verification release is deployable (#3136)
* feat(upgrade): add verification release is deployable * chore: tests and cleaning up code * Apply suggestions from code review Co-authored-by: Ethan Mosbaugh <[email protected]> * chore: address feedback and fix airgap required version setup * Update pkg-new/validation/upgradable.go Co-authored-by: Ethan Mosbaugh <[email protected]> * chore: more comments and tests * chore: feedback * fix: client param naming * fix: client tests * fix: also validate kuberntes downgrades --------- Co-authored-by: Ethan Mosbaugh <[email protected]>
1 parent e50061d commit b4e73be

File tree

10 files changed

+1593
-6
lines changed

10 files changed

+1593
-6
lines changed

cmd/installer/cli/upgrade.go

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"github.com/replicatedhq/embedded-cluster/cmd/installer/goods"
1717
"github.com/replicatedhq/embedded-cluster/cmd/installer/kotscli"
1818
ecv1beta1 "github.com/replicatedhq/embedded-cluster/kinds/apis/v1beta1"
19+
"github.com/replicatedhq/embedded-cluster/pkg-new/replicatedapi"
20+
"github.com/replicatedhq/embedded-cluster/pkg-new/validation"
1921
"github.com/replicatedhq/embedded-cluster/pkg/airgap"
2022
"github.com/replicatedhq/embedded-cluster/pkg/helpers"
2123
"github.com/replicatedhq/embedded-cluster/pkg/kubeutils"
@@ -59,6 +61,8 @@ type upgradeConfig struct {
5961
managerPort int
6062
requiresInfraUpgrade bool
6163
kotsadmNamespace string
64+
currentAppVersion *kotscli.AppVersionInfo
65+
replicatedAPIClient replicatedapi.Client
6266
}
6367

6468
// UpgradeCmd returns a cobra command for upgrading the embedded cluster application.
@@ -110,7 +114,7 @@ func UpgradeCmd(ctx context.Context, appSlug, appTitle string) *cobra.Command {
110114
if err := preRunUpgrade(ctx, flags, &upgradeConfig, existingRC, kcli, appSlug); err != nil {
111115
return err
112116
}
113-
if err := verifyAndPromptUpgrade(ctx, flags, upgradeConfig, prompts.New()); err != nil {
117+
if err := verifyAndPromptUpgrade(ctx, flags, upgradeConfig, prompts.New(), kcli); err != nil {
114118
return err
115119
}
116120

@@ -254,8 +258,8 @@ func preRunUpgrade(ctx context.Context, flags UpgradeCmdFlags, upgradeConfig *up
254258
}
255259
upgradeConfig.license = l
256260

257-
// sync the license if a license is provided and we are not in airgap mode
258-
if upgradeConfig.license != nil && flags.airgapBundle == "" {
261+
// sync the license and initialize the replicated api client if we are not in airgap mode
262+
if flags.airgapBundle == "" {
259263
replicatedAPI, err := newReplicatedAPIClient(upgradeConfig.license, upgradeConfig.clusterID)
260264
if err != nil {
261265
return fmt.Errorf("failed to create replicated API client: %w", err)
@@ -267,6 +271,7 @@ func preRunUpgrade(ctx context.Context, flags UpgradeCmdFlags, upgradeConfig *up
267271
}
268272
upgradeConfig.license = updatedLicense
269273
upgradeConfig.licenseBytes = licenseBytes
274+
upgradeConfig.replicatedAPIClient = replicatedAPI
270275
}
271276

272277
// Continue using "kotsadm" namespace if it exists for backwards compatibility, otherwise use the appSlug
@@ -337,10 +342,17 @@ func preRunUpgrade(ctx context.Context, flags UpgradeCmdFlags, upgradeConfig *up
337342
}
338343
upgradeConfig.requiresInfraUpgrade = requiresInfraUpgrade
339344

345+
// Get current app version for deployability validation
346+
currentAppVersion, err := kotscli.GetCurrentAppVersion(appSlug, upgradeConfig.kotsadmNamespace)
347+
if err != nil {
348+
return fmt.Errorf("failed to get current app version: %w", err)
349+
}
350+
upgradeConfig.currentAppVersion = currentAppVersion
351+
340352
return nil
341353
}
342354

343-
func verifyAndPromptUpgrade(ctx context.Context, flags UpgradeCmdFlags, upgradeConfig upgradeConfig, prompt prompts.Prompt) error {
355+
func verifyAndPromptUpgrade(ctx context.Context, flags UpgradeCmdFlags, upgradeConfig upgradeConfig, prompt prompts.Prompt, kcli client.Client) error {
344356
isAirgap := flags.airgapBundle != ""
345357

346358
err := verifyChannelRelease("upgrade", isAirgap, flags.assumeYes)
@@ -355,6 +367,16 @@ func verifyAndPromptUpgrade(ctx context.Context, flags UpgradeCmdFlags, upgradeC
355367
}
356368
}
357369

370+
// Validate release upgradable
371+
if err := validateIsReleaseUpgradable(ctx, upgradeConfig, kcli, isAirgap); err != nil {
372+
var ve *validation.ValidationError
373+
if errors.As(err, &ve) {
374+
// This is a validation error that prevents the upgrade from proceeding, expose the error directly
375+
return ve
376+
}
377+
return fmt.Errorf("upgrade validation execution failed: %w", err)
378+
}
379+
358380
if !isAirgap {
359381
if err := maybePromptForAppUpdate(ctx, prompt, upgradeConfig.license, flags.assumeYes); err != nil {
360382
if errors.As(err, &ErrorNothingElseToAdd{}) {
@@ -554,3 +576,67 @@ func checkRequiresInfraUpgrade(ctx context.Context) (bool, error) {
554576

555577
return !bytes.Equal(currentJSON, targetJSON), nil
556578
}
579+
580+
// validateIsReleaseUpgradable validates that the target release can be safely deployed
581+
func validateIsReleaseUpgradable(ctx context.Context, upgradeConfig upgradeConfig, kcli client.Client, isAirgap bool) error {
582+
// Get current installation for version information
583+
currentInstallation, err := kubeutils.GetLatestInstallation(ctx, kcli)
584+
if err != nil {
585+
return fmt.Errorf("get current installation: %w", err)
586+
}
587+
588+
// Get target release data
589+
releaseData := release.GetReleaseData()
590+
if releaseData == nil {
591+
return fmt.Errorf("release data not found")
592+
}
593+
594+
// Get channel release info
595+
channelRelease := releaseData.ChannelRelease
596+
if channelRelease == nil {
597+
return fmt.Errorf("channel release not found in release data")
598+
}
599+
600+
// Get current and target EC/K8s versions
601+
var currentECVersion string
602+
if currentInstallation.Spec.Config != nil {
603+
currentECVersion = currentInstallation.Spec.Config.Version
604+
}
605+
606+
targetECVersion := versions.Version
607+
608+
// Build validation options
609+
opts := validation.UpgradableOptions{
610+
CurrentECVersion: currentECVersion,
611+
TargetECVersion: targetECVersion,
612+
License: upgradeConfig.license,
613+
}
614+
615+
// Add current app version info if available
616+
if upgradeConfig.currentAppVersion != nil {
617+
opts.CurrentAppVersion = upgradeConfig.currentAppVersion.VersionLabel
618+
opts.CurrentAppSequence = upgradeConfig.currentAppVersion.ChannelSequence
619+
}
620+
621+
// Add target app version info
622+
opts.TargetAppVersion = channelRelease.VersionLabel
623+
opts.TargetAppSequence = channelRelease.ChannelSequence
624+
625+
// Extract the required releases depending on if it's airgap or online
626+
if isAirgap {
627+
if err := opts.WithAirgapRequiredReleases(upgradeConfig.airgapMetadata); err != nil {
628+
return fmt.Errorf("failed to extract required releases from airgap metadata: %w", err)
629+
}
630+
} else {
631+
if err := opts.WithOnlineRequiredReleases(ctx, upgradeConfig.replicatedAPIClient); err != nil {
632+
return fmt.Errorf("failed to extract required releases from replicated API's pending release call: %w", err)
633+
}
634+
}
635+
636+
// Perform validation
637+
if err := validation.ValidateIsReleaseUpgradable(ctx, opts); err != nil {
638+
return err
639+
}
640+
641+
return nil
642+
}

cmd/installer/kotscli/kotscli.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package kotscli
33
import (
44
"bytes"
55
"context"
6+
"encoding/json"
67
"fmt"
78
"io"
89
"os"
@@ -437,6 +438,65 @@ func createLicenseFile(license []byte) (string, error) {
437438
return licenseFile.Name(), nil
438439
}
439440

441+
// AppVersionInfo holds information about a deployed app version
442+
type AppVersionInfo struct {
443+
VersionLabel string `json:"versionLabel"`
444+
ChannelSequence int64 `json:"channelSequence"`
445+
Sequence int64 `json:"sequence"`
446+
Status string `json:"status"`
447+
}
448+
449+
// GetCurrentAppVersion retrieves the currently deployed app version and sequence
450+
func GetCurrentAppVersion(appSlug string, namespace string) (*AppVersionInfo, error) {
451+
kotsBinPath, err := goods.InternalBinary("kubectl-kots")
452+
if err != nil {
453+
return nil, fmt.Errorf("materialize kubectl-kots binary: %w", err)
454+
}
455+
defer os.Remove(kotsBinPath)
456+
457+
// Build command arguments: kots get versions <appSlug> -n <namespace> -o json
458+
args := []string{
459+
"get", "versions",
460+
appSlug,
461+
"-n", namespace,
462+
"-o", "json",
463+
}
464+
465+
// Execute the command and capture output
466+
var outputBuffer bytes.Buffer
467+
runCommandOpts := helpers.RunCommandOptions{
468+
Stdout: &outputBuffer,
469+
}
470+
471+
if err := helpers.RunCommandWithOptions(runCommandOpts, kotsBinPath, args...); err != nil {
472+
return nil, fmt.Errorf("get versions from kots: %w", err)
473+
}
474+
475+
// Parse JSON output
476+
var versions []AppVersionInfo
477+
if err := json.Unmarshal(outputBuffer.Bytes(), &versions); err != nil {
478+
return nil, fmt.Errorf("unmarshal versions output: %w", err)
479+
}
480+
481+
version, err := getLastDeployedAppVersion(versions)
482+
if err != nil {
483+
return nil, fmt.Errorf("no deployed version found for app %s", appSlug)
484+
}
485+
return version, nil
486+
}
487+
488+
// getLastDeployedAppVersion finds the last deployed version from a slice of versions
489+
func getLastDeployedAppVersion(versions []AppVersionInfo) (*AppVersionInfo, error) {
490+
// Find the last deployed version. This can be either successful or failed deploys.
491+
for _, v := range versions {
492+
if v.Status == "deployed" || v.Status == "failed" {
493+
return &v, nil
494+
}
495+
}
496+
497+
return nil, fmt.Errorf("no deployed version found")
498+
}
499+
440500
// GetConfigValuesOptions holds options for getting config values
441501
type GetConfigValuesOptions struct {
442502
AppSlug string

0 commit comments

Comments
 (0)