Skip to content

Commit 012b892

Browse files
committed
feat(upgrade): add verification release is deployable
1 parent 030261b commit 012b892

File tree

5 files changed

+411
-2
lines changed

5 files changed

+411
-2
lines changed

cmd/installer/cli/upgrade.go

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ 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"
1920
"github.com/replicatedhq/embedded-cluster/pkg/airgap"
2021
"github.com/replicatedhq/embedded-cluster/pkg/helpers"
2122
"github.com/replicatedhq/embedded-cluster/pkg/kubeutils"
@@ -24,6 +25,7 @@ import (
2425
"github.com/replicatedhq/embedded-cluster/pkg/release"
2526
"github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig"
2627
rcutil "github.com/replicatedhq/embedded-cluster/pkg/runtimeconfig/util"
28+
"github.com/replicatedhq/embedded-cluster/pkg/validation"
2729
"github.com/replicatedhq/embedded-cluster/pkg/versions"
2830
"github.com/replicatedhq/embedded-cluster/web"
2931
kotsv1beta1 "github.com/replicatedhq/kotskinds/apis/kots/v1beta1"
@@ -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, appSlug); err != nil {
114118
return err
115119
}
116120

@@ -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, appSlug string) error {
344356
isAirgap := flags.airgapBundle != ""
345357

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

370+
// Validate release deployability
371+
if err := validateReleaseDeployability(ctx, upgradeConfig, kcli, appSlug, isAirgap); err != nil {
372+
return fmt.Errorf("upgrade validation failed: %w", err)
373+
}
374+
358375
if !isAirgap {
359376
if err := maybePromptForAppUpdate(ctx, prompt, upgradeConfig.license, flags.assumeYes); err != nil {
360377
if errors.As(err, &ErrorNothingElseToAdd{}) {
@@ -554,3 +571,64 @@ func checkRequiresInfraUpgrade(ctx context.Context) (bool, error) {
554571

555572
return !bytes.Equal(currentJSON, targetJSON), nil
556573
}
574+
575+
// validateReleaseDeployability validates that the target release can be safely deployed
576+
func validateReleaseDeployability(ctx context.Context, upgradeConfig upgradeConfig, kcli client.Client, appSlug string, isAirgap bool) error {
577+
// Get current installation for version information
578+
currentInstallation, err := kubeutils.GetLatestInstallation(ctx, kcli)
579+
if err != nil {
580+
return fmt.Errorf("get current installation: %w", err)
581+
}
582+
583+
// Get target release data
584+
releaseData := release.GetReleaseData()
585+
if releaseData == nil {
586+
return fmt.Errorf("release data not found")
587+
}
588+
589+
// Get channel release info
590+
channelRelease := releaseData.ChannelRelease
591+
if channelRelease == nil {
592+
return fmt.Errorf("channel release not found in release data")
593+
}
594+
595+
// Get current and target EC/K8s versions
596+
var currentECVersion string
597+
if currentInstallation.Spec.Config != nil {
598+
currentECVersion = currentInstallation.Spec.Config.Version
599+
}
600+
601+
targetECVersion := versions.Version
602+
603+
// Build validation options
604+
opts := validation.UpgradableOptions{
605+
IsAirgap: isAirgap,
606+
CurrentECVersion: currentECVersion,
607+
TargetECVersion: targetECVersion,
608+
License: upgradeConfig.license,
609+
AirgapMetadata: upgradeConfig.airgapMetadata,
610+
}
611+
612+
// Add current app version info if available
613+
if upgradeConfig.currentAppVersion != nil {
614+
opts.CurrentAppVersion = upgradeConfig.currentAppVersion.VersionLabel
615+
opts.CurrentAppSequence = upgradeConfig.currentAppVersion.ChannelSequence
616+
}
617+
618+
// Add target app version info
619+
opts.TargetAppVersion = channelRelease.VersionLabel
620+
opts.TargetAppSequence = channelRelease.ChannelSequence
621+
622+
// For online upgrades, add the replicated API client and channel ID
623+
if !isAirgap && upgradeConfig.replicatedAPIClient != nil {
624+
opts.ReplicatedAPI = upgradeConfig.replicatedAPIClient
625+
opts.ChannelID = channelRelease.ChannelID
626+
}
627+
628+
// Perform validation
629+
if err := validation.ValidateIsReleaseUpgradable(ctx, opts); err != nil {
630+
return err
631+
}
632+
633+
return nil
634+
}

cmd/installer/kotscli/kotscli.go

Lines changed: 51 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,56 @@ 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+
// Find the deployed version
482+
for _, v := range versions {
483+
if v.Status == "deployed" {
484+
return &v, nil
485+
}
486+
}
487+
488+
return nil, fmt.Errorf("no deployed version found for app %s", appSlug)
489+
}
490+
440491
// GetConfigValuesOptions holds options for getting config values
441492
type GetConfigValuesOptions struct {
442493
AppSlug string

pkg-new/replicatedapi/client.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
kyaml "sigs.k8s.io/yaml"
1616
)
1717

18+
var _ Client = (*client)(nil)
19+
1820
var defaultHTTPClient = newRetryableHTTPClient()
1921

2022
// ClientFactory is a function type for creating replicatedapi clients
@@ -29,6 +31,7 @@ func SetClientFactory(factory ClientFactory) {
2931

3032
type Client interface {
3133
SyncLicense(ctx context.Context) (*kotsv1beta1.License, []byte, error)
34+
GetPendingReleases(ctx context.Context, channelID string, currentSequence int64, opts *PendingReleasesOptions) (*PendingReleasesResponse, error)
3235
}
3336

3437
type client struct {
@@ -172,3 +175,47 @@ func basicAuth(username, password string) string {
172175
auth := username + ":" + password
173176
return base64.StdEncoding.EncodeToString([]byte(auth))
174177
}
178+
179+
// GetPendingReleases fetches pending releases from the Replicated API
180+
func (c *client) GetPendingReleases(ctx context.Context, channelID string, currentSequence int64, opts *PendingReleasesOptions) (*PendingReleasesResponse, error) {
181+
u := fmt.Sprintf("%s/release/%s/pending", c.replicatedAppURL, c.license.Spec.AppSlug)
182+
183+
params := url.Values{}
184+
params.Set("selectedChannelId", channelID)
185+
params.Set("currentSequence", fmt.Sprintf("%d", currentSequence))
186+
params.Set("isSemverSupported", fmt.Sprintf("%t", opts.IsSemverSupported))
187+
if opts.SortOrder != "" {
188+
params.Set("sortOrder", string(opts.SortOrder))
189+
}
190+
u = fmt.Sprintf("%s?%s", u, params.Encode())
191+
192+
req, err := c.newRetryableRequest(ctx, http.MethodGet, u, nil)
193+
if err != nil {
194+
return nil, fmt.Errorf("create request: %w", err)
195+
}
196+
197+
req.Header.Set("Accept", "application/json")
198+
199+
resp, err := c.httpClient.Do(req)
200+
if err != nil {
201+
return nil, fmt.Errorf("execute request: %w", err)
202+
}
203+
defer resp.Body.Close()
204+
205+
if resp.StatusCode != http.StatusOK {
206+
body, _ := io.ReadAll(resp.Body)
207+
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body))
208+
}
209+
210+
body, err := io.ReadAll(resp.Body)
211+
if err != nil {
212+
return nil, fmt.Errorf("read response body: %w", err)
213+
}
214+
215+
var pendingReleases PendingReleasesResponse
216+
if err := kyaml.Unmarshal(body, &pendingReleases); err != nil {
217+
return nil, fmt.Errorf("unmarshal pending releases response: %w", err)
218+
}
219+
220+
return &pendingReleases, nil
221+
}

pkg-new/replicatedapi/types.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package replicatedapi
2+
3+
import "time"
4+
5+
// PendingReleasesResponse represents the response from the /release/{appSlug}/pending API endpoint
6+
type PendingReleasesResponse struct {
7+
ChannelReleases []ChannelRelease `json:"channelReleases"`
8+
}
9+
10+
// SortOrder represents the order in which to sort releases
11+
type SortOrder string
12+
13+
const SortOrderAscending SortOrder = "asc"
14+
const SortOrderDescending SortOrder = "desc"
15+
16+
// PendingReleasesOptions represents options for fetching pending releases
17+
type PendingReleasesOptions struct {
18+
IsSemverSupported bool
19+
SortOrder SortOrder
20+
}
21+
22+
// ChannelRelease represents a single release in a channel
23+
type ChannelRelease struct {
24+
ChannelID string `json:"channelId"`
25+
ChannelSequence int64 `json:"channelSequence"`
26+
ReleaseSequence int64 `json:"releaseSequence"`
27+
VersionLabel string `json:"versionLabel"`
28+
IsRequired bool `json:"isRequired"`
29+
CreatedAt time.Time `json:"createdAt"`
30+
}

0 commit comments

Comments
 (0)