Skip to content

Commit b5a9dfa

Browse files
authored
fix: prevent deploying updates when current required version is failed to be deployed (#5790)
* wip * check for airgap
1 parent 826bc99 commit b5a9dfa

3 files changed

Lines changed: 215 additions & 0 deletions

File tree

pkg/handlers/upgrade_service.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/replicatedhq/kots/pkg/replicatedapp"
1515
"github.com/replicatedhq/kots/pkg/reporting"
1616
"github.com/replicatedhq/kots/pkg/store"
17+
storetypes "github.com/replicatedhq/kots/pkg/store/types"
1718
"github.com/replicatedhq/kots/pkg/tasks"
1819
"github.com/replicatedhq/kots/pkg/update"
1920
"github.com/replicatedhq/kots/pkg/upgradeservice"
@@ -125,6 +126,20 @@ func canStartUpgradeService(a *apptypes.App, r StartUpgradeServiceRequest) (bool
125126
if r.ChannelID != airgap.Spec.ChannelID {
126127
return false, "channel mismatch", nil
127128
}
129+
downstreams, err := store.GetStore().ListDownstreamsForApp(a.ID)
130+
if err != nil {
131+
return false, "", errors.Wrap(err, "failed to list downstreams for app")
132+
}
133+
if len(downstreams) > 0 {
134+
currentVersion, err := store.GetStore().GetCurrentDownstreamVersion(a.ID, downstreams[0].ClusterID)
135+
if err != nil {
136+
return false, "", errors.Wrap(err, "failed to get current downstream version")
137+
}
138+
if currentVersion != nil && currentVersion.Status == storetypes.VersionFailed && currentVersion.IsRequired {
139+
return false, fmt.Sprintf("Cannot deploy this version because required version %s failed to deploy. Please retry deploying version %s or check for new updates.", currentVersion.VersionLabel, currentVersion.VersionLabel), nil
140+
}
141+
}
142+
128143
currentECVersion := util.EmbeddedClusterVersion()
129144
isDeployable, nonDeployableCause, err := update.IsAirgapUpdateDeployable(a, airgap, currentECVersion)
130145
if err != nil {

pkg/update/update.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/replicatedhq/kots/pkg/logger"
1212
"github.com/replicatedhq/kots/pkg/reporting"
1313
storepkg "github.com/replicatedhq/kots/pkg/store"
14+
storetypes "github.com/replicatedhq/kots/pkg/store/types"
1415
"github.com/replicatedhq/kots/pkg/update/types"
1516
upstreampkg "github.com/replicatedhq/kots/pkg/upstream"
1617
upstreamtypes "github.com/replicatedhq/kots/pkg/upstream/types"
@@ -62,6 +63,29 @@ func GetAvailableUpdates(kotsStore storepkg.Store, app *apptypes.App, license *l
6263

6364
availableUpdates := getAvailableUpdates(updates.Updates, currentECVersion)
6465

66+
// additional deployable checks against current version
67+
downstreams, err := kotsStore.ListDownstreamsForApp(app.ID)
68+
if err != nil {
69+
return nil, errors.Wrap(err, "failed to list downstreams for app")
70+
}
71+
if len(downstreams) == 0 {
72+
return availableUpdates, nil
73+
}
74+
75+
currentVersion, err := kotsStore.GetCurrentDownstreamVersion(app.ID, downstreams[0].ClusterID)
76+
if err != nil {
77+
return nil, errors.Wrap(err, "failed to get current downstream version")
78+
}
79+
if currentVersion != nil &&
80+
currentVersion.Status == storetypes.VersionFailed &&
81+
currentVersion.IsRequired {
82+
// none of the upstream available updates are deployable if current version is required and failed to deploy
83+
for i := range availableUpdates {
84+
availableUpdates[i].IsDeployable = false
85+
availableUpdates[i].NonDeployableCause = fmt.Sprintf("Cannot deploy this version because required version %s failed to deploy. Please retry deploying version %s or check for new updates.", currentVersion.VersionLabel, currentVersion.VersionLabel)
86+
}
87+
}
88+
6589
return availableUpdates, nil
6690
}
6791

@@ -114,6 +138,29 @@ func GetAvailableAirgapUpdates(app *apptypes.App, license *licensewrapper.Licens
114138
return nil, errors.Wrap(err, "failed to walk airgap root dir")
115139
}
116140

141+
// additional deployable checks against current version
142+
downstreams, err := storepkg.GetStore().ListDownstreamsForApp(app.ID)
143+
if err != nil {
144+
return nil, errors.Wrap(err, "failed to list downstreams for app")
145+
}
146+
if len(downstreams) == 0 {
147+
return updates, nil
148+
}
149+
150+
currentVersion, err := storepkg.GetStore().GetCurrentDownstreamVersion(app.ID, downstreams[0].ClusterID)
151+
if err != nil {
152+
return nil, errors.Wrap(err, "failed to get current downstream version")
153+
}
154+
if currentVersion != nil &&
155+
currentVersion.Status == storetypes.VersionFailed &&
156+
currentVersion.IsRequired {
157+
// none of the airgap available updates are deployable if current version is required and failed to deploy
158+
for i := range updates {
159+
updates[i].IsDeployable = false
160+
updates[i].NonDeployableCause = fmt.Sprintf("Cannot deploy this version because required version %s failed to deploy. Please retry deploying version %s or check for new updates.", currentVersion.VersionLabel, currentVersion.VersionLabel)
161+
}
162+
}
163+
117164
return updates, nil
118165
}
119166

pkg/update/update_test.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import (
99

1010
"github.com/golang/mock/gomock"
1111
apptypes "github.com/replicatedhq/kots/pkg/app/types"
12+
downstreamtypes "github.com/replicatedhq/kots/pkg/api/downstream/types"
1213
storepkg "github.com/replicatedhq/kots/pkg/store"
1314
mock_store "github.com/replicatedhq/kots/pkg/store/mock"
15+
storetypes "github.com/replicatedhq/kots/pkg/store/types"
1416
"github.com/replicatedhq/kots/pkg/update/types"
1517
"github.com/replicatedhq/kots/pkg/upstream"
1618
upstreamtypes "github.com/replicatedhq/kots/pkg/upstream/types"
@@ -62,6 +64,7 @@ func TestGetAvailableUpdates(t *testing.T) {
6264
t.Setenv("USE_MOCK_REPORTING", "1")
6365
args.license.Spec.Endpoint = licenseEndpoint
6466
mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil)
67+
mockStore.EXPECT().ListDownstreamsForApp(args.app.ID).Return(nil, nil)
6568
},
6669
want: []types.AvailableUpdate{},
6770
wantErr: false,
@@ -128,6 +131,7 @@ func TestGetAvailableUpdates(t *testing.T) {
128131
t.Setenv("EMBEDDED_CLUSTER_VERSION", "2.4.0+k8s-1.30")
129132
args.license.Spec.Endpoint = licenseEndpoint
130133
mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("1", nil)
134+
mockStore.EXPECT().ListDownstreamsForApp(args.app.ID).Return(nil, nil)
131135
},
132136
want: []types.AvailableUpdate{
133137
{
@@ -263,6 +267,7 @@ func TestGetAvailableUpdates(t *testing.T) {
263267
t.Setenv("USE_MOCK_REPORTING", "1")
264268
args.license.Spec.Endpoint = licenseEndpoint
265269
mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.Channels[1].ChannelID).Return("1", nil)
270+
mockStore.EXPECT().ListDownstreamsForApp(args.app.ID).Return(nil, nil)
266271
},
267272
want: []types.AvailableUpdate{
268273
{
@@ -278,6 +283,154 @@ func TestGetAvailableUpdates(t *testing.T) {
278283
wantErr: false,
279284
expectedSelectedChannelId: "channel-id2",
280285
},
286+
{
287+
name: "current version is failed and required - all updates blocked",
288+
args: args{
289+
kotsStore: mockStore,
290+
app: &apptypes.App{
291+
ID: "app-id",
292+
SelectedChannelID: "channel-id",
293+
},
294+
license: &kotsv1beta1.License{
295+
Spec: kotsv1beta1.LicenseSpec{
296+
ChannelID: "channel-id",
297+
ChannelName: "channel-name",
298+
AppSlug: "app-slug",
299+
LicenseID: "license-id",
300+
},
301+
},
302+
},
303+
perChannelReleases: map[string][]upstream.ChannelRelease{
304+
"channel-id": {
305+
{
306+
ChannelSequence: 2,
307+
ReleaseSequence: 2,
308+
VersionLabel: "0.0.2",
309+
IsRequired: false,
310+
CreatedAt: testTime.Format(time.RFC3339),
311+
ReleaseNotes: "release notes",
312+
},
313+
{
314+
ChannelSequence: 1,
315+
ReleaseSequence: 1,
316+
VersionLabel: "0.0.1",
317+
IsRequired: false,
318+
CreatedAt: testTime.Format(time.RFC3339),
319+
ReleaseNotes: "release notes",
320+
},
321+
},
322+
},
323+
setup: func(t *testing.T, args args, licenseEndpoint string) {
324+
t.Setenv("USE_MOCK_REPORTING", "1")
325+
args.license.Spec.Endpoint = licenseEndpoint
326+
mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("0", nil)
327+
mockStore.EXPECT().ListDownstreamsForApp(args.app.ID).Return([]downstreamtypes.Downstream{
328+
{ClusterID: "cluster-id"},
329+
}, nil)
330+
mockStore.EXPECT().GetCurrentDownstreamVersion(args.app.ID, "cluster-id").Return(&downstreamtypes.DownstreamVersion{
331+
VersionLabel: "0.0.0",
332+
Status: storetypes.VersionFailed,
333+
IsRequired: true,
334+
}, nil)
335+
},
336+
want: []types.AvailableUpdate{
337+
{
338+
VersionLabel: "0.0.2",
339+
UpdateCursor: "2",
340+
ChannelID: "channel-id",
341+
IsRequired: false,
342+
UpstreamReleasedAt: &testTime,
343+
ReleaseNotes: "release notes",
344+
IsDeployable: false,
345+
NonDeployableCause: "Cannot deploy this version because required version 0.0.0 failed to deploy. Please retry deploying version 0.0.0 or check for new updates.",
346+
},
347+
{
348+
VersionLabel: "0.0.1",
349+
UpdateCursor: "1",
350+
ChannelID: "channel-id",
351+
IsRequired: false,
352+
UpstreamReleasedAt: &testTime,
353+
ReleaseNotes: "release notes",
354+
IsDeployable: false,
355+
NonDeployableCause: "Cannot deploy this version because required version 0.0.0 failed to deploy. Please retry deploying version 0.0.0 or check for new updates.",
356+
},
357+
},
358+
wantErr: false,
359+
expectedSelectedChannelId: "channel-id",
360+
},
361+
{
362+
name: "current version is failed but not required - updates unaffected",
363+
args: args{
364+
kotsStore: mockStore,
365+
app: &apptypes.App{
366+
ID: "app-id",
367+
SelectedChannelID: "channel-id",
368+
},
369+
license: &kotsv1beta1.License{
370+
Spec: kotsv1beta1.LicenseSpec{
371+
ChannelID: "channel-id",
372+
ChannelName: "channel-name",
373+
AppSlug: "app-slug",
374+
LicenseID: "license-id",
375+
},
376+
},
377+
},
378+
perChannelReleases: map[string][]upstream.ChannelRelease{
379+
"channel-id": {
380+
{
381+
ChannelSequence: 2,
382+
ReleaseSequence: 2,
383+
VersionLabel: "0.0.2",
384+
IsRequired: false,
385+
CreatedAt: testTime.Format(time.RFC3339),
386+
ReleaseNotes: "release notes",
387+
},
388+
{
389+
ChannelSequence: 1,
390+
ReleaseSequence: 1,
391+
VersionLabel: "0.0.1",
392+
IsRequired: false,
393+
CreatedAt: testTime.Format(time.RFC3339),
394+
ReleaseNotes: "release notes",
395+
},
396+
},
397+
},
398+
setup: func(t *testing.T, args args, licenseEndpoint string) {
399+
t.Setenv("USE_MOCK_REPORTING", "1")
400+
args.license.Spec.Endpoint = licenseEndpoint
401+
mockStore.EXPECT().GetCurrentUpdateCursor(args.app.ID, args.license.Spec.ChannelID).Return("0", nil)
402+
mockStore.EXPECT().ListDownstreamsForApp(args.app.ID).Return([]downstreamtypes.Downstream{
403+
{ClusterID: "cluster-id"},
404+
}, nil)
405+
mockStore.EXPECT().GetCurrentDownstreamVersion(args.app.ID, "cluster-id").Return(&downstreamtypes.DownstreamVersion{
406+
VersionLabel: "0.0.0",
407+
Status: storetypes.VersionFailed,
408+
IsRequired: false,
409+
}, nil)
410+
},
411+
want: []types.AvailableUpdate{
412+
{
413+
VersionLabel: "0.0.2",
414+
UpdateCursor: "2",
415+
ChannelID: "channel-id",
416+
IsRequired: false,
417+
UpstreamReleasedAt: &testTime,
418+
ReleaseNotes: "release notes",
419+
IsDeployable: true,
420+
},
421+
{
422+
VersionLabel: "0.0.1",
423+
UpdateCursor: "1",
424+
ChannelID: "channel-id",
425+
IsRequired: false,
426+
UpstreamReleasedAt: &testTime,
427+
ReleaseNotes: "release notes",
428+
IsDeployable: true,
429+
},
430+
},
431+
wantErr: false,
432+
expectedSelectedChannelId: "channel-id",
433+
},
281434
}
282435
for _, tt := range tests {
283436
t.Run(tt.name, func(t *testing.T) {

0 commit comments

Comments
 (0)