Skip to content

Commit 9128c3e

Browse files
committed
fix: normalise customer desired to a major.minor version
This cater for the fact that the customer desired could be a full x.y.z version e.g candidate,fast or nightly versions
1 parent 0e58ab3 commit 9128c3e

File tree

2 files changed

+63
-11
lines changed

2 files changed

+63
-11
lines changed

backend/pkg/controllers/upgradecontrollers/control_plane_version_controller.go

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -279,18 +279,15 @@ func (c *controlPlaneVersionSyncer) desiredControlPlaneZVersion(
279279

280280
actualMinorVersion := semver.MustParse(fmt.Sprintf("%d.%d.0", actualLatestVersion.Major, actualLatestVersion.Minor))
281281

282-
// ParseTolerant handles both "4.19" and "4.19.0" formats (validated at API level, should never fail)
283-
desiredMinorVersion := api.Must(semver.ParseTolerant(customerDesiredMinor))
284-
285-
if len(channelGroup) == 0 {
286-
logger.Info("No channel group specified. Terminating upgrade resolution.")
287-
return nil, nil
288-
}
282+
// ParseTolerant handles both "4.19", "4.19.0" and full versions like "4.20.15". Normalize to major.minor.0
283+
// so that same-minor z-stream (e.g. 4.20.0 -> 4.20.15) is not mistaken for a y-stream upgrade.
284+
parsedDesired := api.Must(semver.ParseTolerant(customerDesiredMinor))
285+
desiredMinorVersion := semver.MustParse(fmt.Sprintf("%d.%d.0", parsedDesired.Major, parsedDesired.Minor))
289286

290287
if desiredMinorVersion.LT(actualMinorVersion) {
291288
return nil, utils.TrackError(fmt.Errorf(
292289
"invalid next y-stream upgrade path from %s to %s: only upgrades to the next minor version are allowed, no downgrades",
293-
actualMinorVersion.String(), customerDesiredMinor,
290+
actualMinorVersion.String(), desiredMinorVersion.String(),
294291
))
295292
}
296293

@@ -299,13 +296,13 @@ func (c *controlPlaneVersionSyncer) desiredControlPlaneZVersion(
299296
if desiredMinorVersion.Major != actualMinorVersion.Major {
300297
return nil, utils.TrackError(fmt.Errorf(
301298
"invalid next y-stream upgrade path from %s to %s: major version changes are not supported",
302-
actualMinorVersion.String(), customerDesiredMinor,
299+
actualMinorVersion.String(), desiredMinorVersion.String(),
303300
))
304301
}
305302
if desiredMinorVersion.Minor != actualMinorVersion.Minor+1 {
306303
return nil, utils.TrackError(fmt.Errorf(
307304
"invalid next y-stream upgrade path from %s to %s: only upgrades to the next minor version are allowed, no skipping minor versions",
308-
actualMinorVersion.String(), customerDesiredMinor,
305+
actualMinorVersion.String(), desiredMinorVersion.String(),
309306
))
310307
}
311308
}
@@ -319,7 +316,7 @@ func (c *controlPlaneVersionSyncer) desiredControlPlaneZVersion(
319316

320317
if desiredMinorVersion.Minor == actualMinorVersion.Minor+1 {
321318
logger.Info("Resolving next Y-stream upgrade", "actualMinor", actualMinorVersion.String(), "activeVersions", activeVersions, "channelGroup", channelGroup,
322-
"targetMinor", customerDesiredMinor)
319+
"targetMinor", desiredMinorVersion.String())
323320

324321
latestVersion, err := c.findLatestVersionInMinor(ctx, cincinnatiClient, channelGroup, desiredMinorVersion, activeVersionList)
325322
if err != nil {

backend/pkg/controllers/upgradecontrollers/control_plane_version_controller_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,61 @@ func TestDesiredControlPlaneZVersion_ZStreamManagedUpgrade(t *testing.T) {
207207
expectedVersion: nil,
208208
expectedError: false,
209209
},
210+
{
211+
name: "Z-stream upgrade - candidate channel, customer desired full version (4.20.15) normalized to same minor",
212+
activeVersions: []api.HCPClusterActiveVersion{{Version: ptr.To(semver.MustParse("4.20.10"))}},
213+
customerDesiredMinor: "4.20.15",
214+
channelGroup: "candidate",
215+
mockSetup: func(mc *cincinatti.MockClient) {
216+
mc.EXPECT().GetUpdates(gomock.AssignableToTypeOf(context.Background()), api.Must(cincinatti.GetCincinnatiURI("candidate")), "multi", "multi", "candidate-4.20", semver.MustParse("4.20.10")).Return(
217+
configv1.Release{Version: "4.20.10"},
218+
[]configv1.Release{{Version: "4.20.15"}, {Version: "4.20.12"}},
219+
[]configv1.ConditionalUpdate{},
220+
nil,
221+
)
222+
// Check if next minor (4.21) exists using latest candidate (4.20.15)
223+
mc.EXPECT().GetUpdates(gomock.AssignableToTypeOf(context.Background()), api.Must(cincinatti.GetCincinnatiURI("candidate")), "multi", "multi", "candidate-4.21", semver.MustParse("4.20.15")).Return(
224+
configv1.Release{Version: "4.20.15"},
225+
[]configv1.Release{{Version: "4.21.0"}},
226+
[]configv1.ConditionalUpdate{},
227+
nil,
228+
)
229+
// isGatewayToNextMinor(4.20.15) - has path to 4.21, so 4.20.15 is selected
230+
mc.EXPECT().GetUpdates(gomock.AssignableToTypeOf(context.Background()), api.Must(cincinatti.GetCincinnatiURI("candidate")), "multi", "multi", "candidate-4.21", semver.MustParse("4.20.15")).Return(
231+
configv1.Release{Version: "4.20.15"},
232+
[]configv1.Release{{Version: "4.21.0"}},
233+
[]configv1.ConditionalUpdate{},
234+
nil,
235+
)
236+
},
237+
expectedVersion: ptr.To(semver.MustParse("4.20.15")),
238+
expectedError: false,
239+
},
240+
{
241+
name: "Z-stream upgrade - nightly channel, customer desired full version (4.19.0-0.nightly-multi-...) normalized to same minor",
242+
activeVersions: []api.HCPClusterActiveVersion{{Version: ptr.To(api.Must(semver.ParseTolerant("4.19.0-0.nightly-multi-2026-01-10-204154")))}},
243+
customerDesiredMinor: "4.19.0-0.nightly-multi-2026-01-12-061259",
244+
channelGroup: "nightly",
245+
mockSetup: func(mc *cincinatti.MockClient) {
246+
activeVer := api.Must(semver.ParseTolerant("4.19.0-0.nightly-multi-2026-01-10-204154"))
247+
latestVer := api.Must(semver.ParseTolerant("4.19.0-0.nightly-multi-2026-01-12-061259"))
248+
mc.EXPECT().GetUpdates(gomock.AssignableToTypeOf(context.Background()), api.Must(cincinatti.GetCincinnatiURI("nightly")), "multi", "multi", "nightly-4.19", activeVer).Return(
249+
configv1.Release{Version: "4.19.0-0.nightly-multi-2026-01-10-204154"},
250+
[]configv1.Release{{Version: "4.19.0-0.nightly-multi-2026-01-12-061259"}},
251+
[]configv1.ConditionalUpdate{},
252+
nil,
253+
)
254+
// Check if next minor (4.20) exists using latest candidate - it doesn't; return latest
255+
mc.EXPECT().GetUpdates(gomock.AssignableToTypeOf(context.Background()), api.Must(cincinatti.GetCincinnatiURI("nightly")), "multi", "multi", "nightly-4.20", latestVer).Return(
256+
configv1.Release{},
257+
nil,
258+
nil,
259+
&cincinnati.Error{Reason: "VersionNotFound"},
260+
)
261+
},
262+
expectedVersion: ptr.To(api.Must(semver.ParseTolerant("4.19.0-0.nightly-multi-2026-01-12-061259"))),
263+
expectedError: false,
264+
},
210265
}
211266

212267
for _, tt := range tests {

0 commit comments

Comments
 (0)