Skip to content

Commit bc45dde

Browse files
JGAntunesajp-io
andauthored
fix(upgrade): validate current release isn't required if it has failed (#3253)
* fix(upgrade): validate current required failed releases * Update pkg-new/validation/errors.go Co-authored-by: Alex Parker <7272359+ajp-io@users.noreply.github.com> --------- Co-authored-by: Alex Parker <7272359+ajp-io@users.noreply.github.com>
1 parent 03e1fea commit bc45dde

File tree

6 files changed

+150
-42
lines changed

6 files changed

+150
-42
lines changed

cmd/installer/cli/upgrade.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,7 @@ func validateIsReleaseUpgradable(ctx context.Context, upgradeConfig *upgradeConf
655655
if upgradeConfig.currentAppVersion != nil {
656656
opts.CurrentAppVersion = upgradeConfig.currentAppVersion.VersionLabel
657657
opts.CurrentAppSequence = upgradeConfig.currentAppVersion.ChannelSequence
658+
opts.CurrentAppStatus = upgradeConfig.currentAppVersion.Status
658659
}
659660

660661
// Add target app version info

pkg-new/replicatedapi/client.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,12 @@ func basicAuth(username, password string) string {
177177
}
178178

179179
// GetPendingReleases fetches pending releases from the Replicated API
180-
func (c *client) GetPendingReleases(ctx context.Context, channelID string, currentSequence int64, opts *PendingReleasesOptions) (*PendingReleasesResponse, error) {
180+
func (c *client) GetPendingReleases(ctx context.Context, channelID string, channelSequence int64, opts *PendingReleasesOptions) (*PendingReleasesResponse, error) {
181181
u := fmt.Sprintf("%s/release/%s/pending", c.replicatedAppURL, c.license.Spec.AppSlug)
182182

183183
params := url.Values{}
184184
params.Set("selectedChannelId", channelID)
185-
params.Set("channelSequence", fmt.Sprintf("%d", currentSequence))
185+
params.Set("channelSequence", fmt.Sprintf("%d", channelSequence))
186186
params.Set("isSemverSupported", fmt.Sprintf("%t", opts.IsSemverSupported))
187187
if opts.SortOrder != "" {
188188
params.Set("sortOrder", string(opts.SortOrder))
@@ -196,6 +196,11 @@ func (c *client) GetPendingReleases(ctx context.Context, channelID string, curre
196196

197197
req.Header.Set("Accept", "application/json")
198198

199+
// If we provide the current channel sequence, set it as an header since that's the way market API currently expects it
200+
if opts.CurrentChannelSequence != 0 {
201+
req.Header.Set("X-Replicated-CurrentChannelSequence", fmt.Sprintf("%d", opts.CurrentChannelSequence))
202+
}
203+
199204
resp, err := c.httpClient.Do(req)
200205
if err != nil {
201206
return nil, fmt.Errorf("execute request: %w", err)

pkg-new/replicatedapi/types.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ const SortOrderDescending SortOrder = "desc"
1515

1616
// PendingReleasesOptions represents options for fetching pending releases
1717
type PendingReleasesOptions struct {
18-
IsSemverSupported bool
19-
SortOrder SortOrder
18+
IsSemverSupported bool
19+
SortOrder SortOrder
20+
CurrentChannelSequence int64
2021
}
2122

2223
// ChannelRelease represents a single release in a channel

pkg-new/validation/errors.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ func NewRequiredReleasesError(requiredVersions []string, targetVersion string) *
2626
}
2727
}
2828

29+
// NewCurrentReleaseFailedError creates a ValidationError indicating that the current
30+
// installed release is required and it's in a failed state
31+
func NewCurrentReleaseFailedError(currentVersion string, targetVersion string) *ValidationError {
32+
return &ValidationError{
33+
Message: fmt.Sprintf("Cannot upgrade to version %s, because the required version %s did not install successfully. Version %s must be fully installed before upgrading to version %s.",
34+
targetVersion, currentVersion, currentVersion, targetVersion),
35+
}
36+
}
37+
2938
// NewAppVersionDowngradeError creates a ValidationError indicating that the target
3039
// app version is older than the current version
3140
func NewAppVersionDowngradeError(currentVersion, targetVersion string) *ValidationError {

pkg-new/validation/upgradable.go

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ var k8sBuildRegex = regexp.MustCompile(`k8s-(\d+\.\d+)`)
1717

1818
// UpgradableOptions holds configuration for validating release deployability
1919
type UpgradableOptions struct {
20-
CurrentAppVersion string
21-
CurrentAppSequence int64
22-
CurrentECVersion string
23-
TargetAppVersion string
24-
TargetAppSequence int64
25-
TargetECVersion string
26-
License *kotsv1beta1.License
27-
requiredReleases []string
20+
CurrentAppVersion string
21+
CurrentAppSequence int64
22+
CurrentECVersion string
23+
CurrentAppStatus string
24+
TargetAppVersion string
25+
TargetAppSequence int64
26+
TargetECVersion string
27+
License *kotsv1beta1.License
28+
currentReleaseIsRequired bool
29+
requiredReleases []string
2830
}
2931

3032
// WithAirgapRequiredReleases extracts the required releases from airgap metadata to be used for validation
@@ -42,8 +44,13 @@ func (opts *UpgradableOptions) WithAirgapRequiredReleases(metadata *airgap.Airga
4244
if err != nil {
4345
return fmt.Errorf("failed to parse airgap spec required release update cursor %s: %w", release.UpdateCursor, err)
4446
}
45-
// We've hit a release that is less than or equal to the current installed release, we can stop
46-
if sequence <= opts.CurrentAppSequence {
47+
// We've hit the current app sequence, mark that it's required and return
48+
if sequence == opts.CurrentAppSequence {
49+
opts.currentReleaseIsRequired = true
50+
return nil
51+
}
52+
// We've hit a release that is less than the current installed release, we can stop
53+
if sequence < opts.CurrentAppSequence {
4754
return nil
4855
}
4956
opts.requiredReleases = append(opts.requiredReleases, release.VersionLabel)
@@ -52,17 +59,19 @@ func (opts *UpgradableOptions) WithAirgapRequiredReleases(metadata *airgap.Airga
5259
return nil
5360
}
5461

55-
// WithOnlineRequiredReleases fetches the pending releases from the current app sequence and extracts the required releases until the target app sequence
62+
// WithOnlineRequiredReleases fetches all the releases from the current app sequence (including it) and extracts the required releases until the target app sequence
5663
func (opts *UpgradableOptions) WithOnlineRequiredReleases(ctx context.Context, replAPIClient replicatedapi.Client) error {
5764
if opts.License == nil {
5865
return fmt.Errorf("license is required to check online upgrade required releases")
5966
}
67+
// We want to get current app sequence inclusive. In oder for us to do that in a way that works for both channel sequences and semver we need to set the CurrentChannelSequence to current app sequence and the channel sequence provided to the method to current app sequence - 1
6068
options := &replicatedapi.PendingReleasesOptions{
61-
IsSemverSupported: opts.License.Spec.IsSemverRequired,
62-
SortOrder: replicatedapi.SortOrderAscending,
69+
IsSemverSupported: opts.License.Spec.IsSemverRequired,
70+
SortOrder: replicatedapi.SortOrderAscending,
71+
CurrentChannelSequence: opts.CurrentAppSequence,
6372
}
6473
// Get pending releases from the current app sequence in asceding order
65-
pendingReleases, err := replAPIClient.GetPendingReleases(ctx, opts.License.Spec.ChannelID, opts.CurrentAppSequence, options)
74+
pendingReleases, err := replAPIClient.GetPendingReleases(ctx, opts.License.Spec.ChannelID, opts.CurrentAppSequence-1, options)
6675
if err != nil {
6776
return fmt.Errorf("failed to get pending releases while checking required releases for upgrade: %w", err)
6877
}
@@ -81,7 +90,13 @@ func (opts *UpgradableOptions) handlePendingReleases(pendingReleases []replicate
8190
break
8291
}
8392
if release.IsRequired {
84-
opts.requiredReleases = append(opts.requiredReleases, release.VersionLabel)
93+
// Mark that the current app release is required if channel sequence matches
94+
if release.ChannelSequence == opts.CurrentAppSequence {
95+
opts.currentReleaseIsRequired = true
96+
// Else append to required releases
97+
} else {
98+
opts.requiredReleases = append(opts.requiredReleases, release.VersionLabel)
99+
}
85100
}
86101
}
87102
}
@@ -94,7 +109,7 @@ func ValidateIsReleaseUpgradable(ctx context.Context, opts UpgradableOptions) er
94109
}
95110

96111
// Check 2: Required releases
97-
if err := validateRequiredReleases(ctx, opts); err != nil {
112+
if err := validateRequiredReleases(opts); err != nil {
98113
return err
99114
}
100115

@@ -111,8 +126,13 @@ func ValidateIsReleaseUpgradable(ctx context.Context, opts UpgradableOptions) er
111126
return nil
112127
}
113128

114-
// validateRequiredReleases checks if any required releases are being skipped
115-
func validateRequiredReleases(ctx context.Context, opts UpgradableOptions) error {
129+
// validateRequiredReleases checks if:
130+
// - any required releases are being skipped
131+
// - current app status is failed and it's a required version
132+
func validateRequiredReleases(opts UpgradableOptions) error {
133+
if opts.CurrentAppStatus == "failed" && opts.currentReleaseIsRequired {
134+
return NewCurrentReleaseFailedError(opts.CurrentAppVersion, opts.TargetAppVersion)
135+
}
116136
if len(opts.requiredReleases) > 0 {
117137
return NewRequiredReleasesError(opts.requiredReleases, opts.TargetAppVersion)
118138
}

pkg-new/validation/upgradable_test.go

Lines changed: 92 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,13 @@ func newTestAirgapMetadataWithSequences(releases []airgapReleaseData) *airgap.Ai
6464

6565
func TestWithAirgapRequiredReleases(t *testing.T) {
6666
tests := []struct {
67-
name string
68-
metadata *airgap.AirgapMetadata
69-
currentSequence int64
70-
expectedReleases []string
71-
expectError bool
72-
errorContains string
67+
name string
68+
metadata *airgap.AirgapMetadata
69+
currentSequence int64
70+
expectedReleases []string
71+
expectedCurrentReleaseIsRequired bool
72+
expectError bool
73+
errorContains string
7374
}{
7475
{
7576
name: "no required releases",
@@ -102,25 +103,36 @@ func TestWithAirgapRequiredReleases(t *testing.T) {
102103
expectError: false,
103104
},
104105
{
105-
name: "stops at current sequence",
106+
name: "all releases older than current",
106107
metadata: newTestAirgapMetadataWithSequences([]airgapReleaseData{
107-
{versionLabel: "1.4.0", updateCursor: "400"},
108-
{versionLabel: "1.3.0", updateCursor: "300"},
109108
{versionLabel: "1.2.0", updateCursor: "200"},
109+
{versionLabel: "1.1.0", updateCursor: "100"},
110110
}),
111111
currentSequence: 300,
112-
expectedReleases: []string{"1.4.0"},
112+
expectedReleases: []string{},
113113
expectError: false,
114114
},
115115
{
116-
name: "all releases older than current",
116+
name: "current release is required",
117+
metadata: newTestAirgapMetadataWithSequences([]airgapReleaseData{
118+
{versionLabel: "1.0.0", updateCursor: "300"},
119+
}),
120+
currentSequence: 300,
121+
expectedReleases: []string{},
122+
expectError: false,
123+
expectedCurrentReleaseIsRequired: true,
124+
},
125+
{
126+
name: "current release is required and there's other required releases",
117127
metadata: newTestAirgapMetadataWithSequences([]airgapReleaseData{
128+
{versionLabel: "1.4.0", updateCursor: "400"},
129+
{versionLabel: "1.3.0", updateCursor: "300"},
118130
{versionLabel: "1.2.0", updateCursor: "200"},
119-
{versionLabel: "1.1.0", updateCursor: "100"},
120131
}),
121-
currentSequence: 300,
122-
expectedReleases: []string{},
123-
expectError: false,
132+
currentSequence: 300,
133+
expectedReleases: []string{"1.4.0"},
134+
expectedCurrentReleaseIsRequired: true,
135+
expectError: false,
124136
},
125137
{
126138
name: "nil metadata",
@@ -167,18 +179,20 @@ func TestWithAirgapRequiredReleases(t *testing.T) {
167179
} else {
168180
assert.Equal(t, tt.expectedReleases, opts.requiredReleases)
169181
}
182+
assert.Equal(t, tt.expectedCurrentReleaseIsRequired, opts.currentReleaseIsRequired)
170183
}
171184
})
172185
}
173186
}
174187

175188
func TestHandlePendingReleases(t *testing.T) {
176189
tests := []struct {
177-
name string
178-
pendingReleases []replicatedapi.ChannelRelease
179-
currentSequence int64
180-
targetSequence int64
181-
expectedReleases []string
190+
name string
191+
pendingReleases []replicatedapi.ChannelRelease
192+
currentSequence int64
193+
targetSequence int64
194+
expectedReleases []string
195+
expectedCurrentReleaseIsRequired bool
182196
}{
183197
{
184198
name: "no required releases",
@@ -252,6 +266,29 @@ func TestHandlePendingReleases(t *testing.T) {
252266
targetSequence: 100,
253267
expectedReleases: []string{},
254268
},
269+
{
270+
name: "current sequence is required",
271+
pendingReleases: []replicatedapi.ChannelRelease{
272+
{ChannelSequence: 99, VersionLabel: "1.0.0", IsRequired: true},
273+
},
274+
currentSequence: 99,
275+
targetSequence: 100,
276+
expectedReleases: []string{},
277+
expectedCurrentReleaseIsRequired: true,
278+
},
279+
{
280+
name: "current sequence is required and there's other required releases",
281+
pendingReleases: []replicatedapi.ChannelRelease{
282+
{ChannelSequence: 99, VersionLabel: "1.0.0", IsRequired: true},
283+
{ChannelSequence: 101, VersionLabel: "1.1.0", IsRequired: true},
284+
{ChannelSequence: 102, VersionLabel: "1.2.0", IsRequired: false},
285+
{ChannelSequence: 103, VersionLabel: "1.3.0", IsRequired: true},
286+
},
287+
currentSequence: 99,
288+
targetSequence: 104,
289+
expectedReleases: []string{"1.1.0", "1.3.0"},
290+
expectedCurrentReleaseIsRequired: true,
291+
},
255292
}
256293

257294
for _, tt := range tests {
@@ -268,6 +305,7 @@ func TestHandlePendingReleases(t *testing.T) {
268305
} else {
269306
assert.Equal(t, tt.expectedReleases, opts.requiredReleases)
270307
}
308+
assert.Equal(t, tt.expectedCurrentReleaseIsRequired, opts.currentReleaseIsRequired)
271309
})
272310
}
273311
}
@@ -399,6 +437,40 @@ func TestValidateIsReleaseUpgradable(t *testing.T) {
399437
expectError: true,
400438
expectValidationErr: true,
401439
},
440+
{
441+
name: "current release failed and is required",
442+
opts: UpgradableOptions{
443+
CurrentAppVersion: "1.0.0",
444+
CurrentAppSequence: 100,
445+
CurrentAppStatus: "failed",
446+
CurrentECVersion: "2.0.0+k8s-1.29",
447+
TargetAppVersion: "1.5.0",
448+
TargetAppSequence: 500,
449+
TargetECVersion: "2.1.0+k8s-1.29",
450+
License: newTestLicense(true),
451+
requiredReleases: []string{},
452+
currentReleaseIsRequired: true,
453+
},
454+
expectError: true,
455+
expectValidationErr: true,
456+
},
457+
{
458+
name: "current release failed and is deployed",
459+
opts: UpgradableOptions{
460+
CurrentAppVersion: "1.0.0",
461+
CurrentAppSequence: 100,
462+
CurrentAppStatus: "deployed",
463+
CurrentECVersion: "2.0.0+k8s-1.29",
464+
TargetAppVersion: "1.5.0",
465+
TargetAppSequence: 500,
466+
TargetECVersion: "2.1.0+k8s-1.29",
467+
License: newTestLicense(true),
468+
requiredReleases: []string{},
469+
currentReleaseIsRequired: true,
470+
},
471+
expectError: false,
472+
expectValidationErr: false,
473+
},
402474
{
403475
name: "ec version downgrade",
404476
opts: UpgradableOptions{

0 commit comments

Comments
 (0)