Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions e2e/upgrades/upgrades_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,53 @@ func TestUpgradesMajorVersion20_1To20_2(t *testing.T) {
steps.Run(t)
}

func TestUpgradesMajorVersionSkippingInnovativeRelease24_3To25_1(t *testing.T) {

if testing.Short() {
t.Skip("skipping test in short mode.")
}

testLog := zapr.NewLogger(zaptest.NewLogger(t))

e := testenv.CreateActiveEnvForTest()
env := e.Start()
defer e.Stop()

sb := testenv.NewDiffingSandbox(t, env)
sb.StartManager(t, controller.InitClusterReconcilerWithLogger(testLog))

builder := testutil.NewBuilder("crdb").WithNodeCount(3).WithTLS().
WithImage("cockroachdb/cockroach:v24.3.4").
WithPVDataStore("1Gi").WithResources(resRequirements)

steps := testutil.Steps{
{
Name: "creates a 3-node secure cluster",
Test: func(t *testing.T) {
require.NoError(t, sb.Create(builder.Cr()))
testutil.RequireClusterToBeReadyEventuallyTimeout(t, sb, builder, e2e.CreateClusterTimeout)
},
},
{
Name: "upgrades the cluster to the next major version skipping innovative release",
Test: func(t *testing.T) {
current := builder.Cr()
require.NoError(t, sb.Get(current))

updated := current.DeepCopy()
updated.Spec.Image.Name = "cockroachdb/cockroach:v25.1.0"
require.NoError(t, sb.Patch(updated, client.MergeFrom(current)))
// we wait 10 min because we will be waiting 3 min for each pod.
testutil.RequireClusterToBeReadyEventuallyTimeout(t, sb, builder, e2e.CreateClusterTimeout)
testutil.RequireDbContainersToUseImage(t, sb, updated)
t.Log("Done with major upgrade")
},
},
}

steps.Run(t)
}

// TestUpgradesMinorVersionThenRollback tests a minor version bump
// then rollsback that upgrade
func TestUpgradesMinorVersionThenRollback(t *testing.T) {
Expand Down
10 changes: 10 additions & 0 deletions pkg/update/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ const (
AdminRole = "admin"
)

// ReleaseType represents the type of a release
type ReleaseType int

const (
// Regular releases are mandatory upgrades that cannot be skipped.
Regular ReleaseType = iota
// Innovative releases are optional upgrades that can be skipped.
Innovative
)

// internalUsers is a set of SQL users created as part of the managed service, not to be used
// by customers. This struct is used to hide specific users in the console.
var internalUsers = map[string]struct{}{
Expand Down
6 changes: 3 additions & 3 deletions pkg/update/update_cockroach_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func UpdateClusterCockroachVersion(

l.V(int(zapcore.InfoLevel)).Info("starting upgrade")

if isForwardOneMajorVersion(update.WantVersion, update.CurrentVersion) {
if isMajorUpgradeAllowed(update.WantVersion, update.CurrentVersion) {
if err := setDowngradeOption(ctx, update.WantVersion, update.CurrentVersion, update.Db, l); err != nil {
return errors.Wrapf(err, "setting downgrade option for major roll forward failed")
}
Expand Down Expand Up @@ -206,7 +206,7 @@ func kindAndCheckPreserveDowngradeSetting(
if isPatch(wantVersion, currentVersion) {
l.V(int(zapcore.DebugLevel)).Info("patch upgrade")
return "PATCH", nil
} else if isForwardOneMajorVersion(wantVersion, currentVersion) {
} else if isMajorUpgradeAllowed(wantVersion, currentVersion) {
l.V(int(zapcore.DebugLevel)).Info("major upgrade")
s := "MAJOR_UPGRADE"
preserve, err := preserveDowngradeSetting(ctx, db)
Expand All @@ -226,7 +226,7 @@ func kindAndCheckPreserveDowngradeSetting(
}
}
return s, nil
} else if isBackOneMajorVersion(wantVersion, currentVersion) {
} else if isMajorRollbackAllowed(wantVersion, currentVersion) {
l.V(int(zapcore.DebugLevel)).Info("major rollback")
return CheckDowngradeSetting(ctx, wantVersion, currentVersion, db)
}
Expand Down
188 changes: 186 additions & 2 deletions pkg/update/update_cockroach_version_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package update
import (
"errors"
"fmt"
"strconv"
"strings"
"time"

semver "github.com/Masterminds/semver/v3"
Expand Down Expand Up @@ -134,18 +136,200 @@ func isPatch(wantVersion *semver.Version, currentVersion *semver.Version) bool {
return currentVersion.Major() == wantVersion.Major() && currentVersion.Minor() == wantVersion.Minor()
}

func isForwardOneMajorVersion(wantVersion *semver.Version, currentVersion *semver.Version) bool {
// getReleaseType returns if a release is Innovative or Regular.
func getReleaseType(major, minor int) ReleaseType {
// Before 25.1, we define them explicitly
switch fmt.Sprintf("%d.%d", major, minor) {
case "24.1", "24.3":
return Regular
case "24.2":
return Innovative
}

// Post 25.1: Odd releases (1,3) are Innovative, Even (2,4) are Regular
if minor%2 == 1 {
return Innovative
}
return Regular
}

// generateReleases generates a list of release versions from a 24.1 to the specified year.
// The releases follow a "YY.Q" format, where YY represents the last two digits of the year, and Q represents the quarter.
//
// Parameters:
// - upToYear: The last two digits of the year up to which releases should be generated.
//
// Returns:
// - A slice of strings containing release versions from "24.1" onward, up to the specified year, with four
// releases per year (except 2024).
func generateReleases(upToYear int) []string {
var releases = []string{"24.1", "24.2", "24.3"}

for year := 25; year <= upToYear; year++ {
for quarter := 1; quarter <= 4; quarter++ {
releases = append(releases, fmt.Sprintf("%d.%d", year, quarter))
}
}

return releases
}

// getNextReleases returns a list of possible upgradable release versions following the given currentVersion.
// It iterates through the generated release versions for the current year and collects the next releases
// after finding the currentVersion. The function stops collecting releases once it encounters the next regular release.
//
// Parameters:
// - currentVersion: The current release version in the format "YY.Q" (e.g., "24.1").
//
// Returns:
// - A slice of strings containing the possible upgradable release versions, stopping at the next regular release.
func getNextReleases(currentVersion string) []string {
var nextReleases []string
var found bool

releases := generateReleases(time.Now().Year() % 100)
for _, release := range releases {
year, _ := strconv.Atoi(strings.Split(release, ".")[0])
quarter, _ := strconv.Atoi(strings.Split(release, ".")[1])

if found {
nextReleases = append(nextReleases, release)
if getReleaseType(year, quarter) == Regular {
break // Stop at the next regular release
}
}
if release == currentVersion {
found = true
}
}

return nextReleases
}

// getPreviousReleases returns a list of possible rollback release versions preceding the given currentVersion.
// It iterates through the generated release versions in reverse order and collects previous releases
// until it encounters the last regular release before the currentVersion.
//
// Parameters:
// - currentVersion: The current release version in the format "YY.Q" (e.g., "24.1").
//
// Returns:
// - A slice of strings containing possible rollback release versions, stopping at the last regular release before the currentVersion.
func getPreviousReleases(currentVersion string) []string {
var prevReleases []string
var found bool

releases := generateReleases(time.Now().Year() % 100)
for i := len(releases) - 1; i >= 0; i-- {
release := releases[i]
year, _ := strconv.Atoi(strings.Split(release, ".")[0])
quarter, _ := strconv.Atoi(strings.Split(release, ".")[1])
if found {
prevReleases = append(prevReleases, release)
if getReleaseType(year, quarter) == Regular {
break
}
}
if release == currentVersion {
found = true
}
}

return prevReleases
}

// isMajorUpgradeAllowed determines whether an upgrade from the current version to the desired version is allowed
// based on the release cycle and upgrade policies.
//
// Upgrade Rules:
// 1. For versions prior to 2024:
// - A major upgrade is allowed if
// - The major version remains the same and the minor version increments by 1 (e.g., 19.1 -> 19.2).
// - The major version increments by 1 and the minor version resets to 1. (e.g., 19.2 -> 20.1).
//
// 2. For version of 2024:
// - There are 3 releases (24.1, 24.2, 24.3) where 24.2 being innovative release
//
// 3. For versions from 2025 onwards:
// - Releases follow a quarterly cycle, with some releases designated as innovative.
// - Users are allowed to skip upgrading to an innovative release.
// - The upgrade is permitted if the desired version matches one of the next valid releases.
//
// Parameters:
// - wantVersion: The target version to which an upgrade is requested.
// - currentVersion: The currently installed version.
//
// Returns:
// - true if the upgrade is allowed based on the release rules; false otherwise.
func isMajorUpgradeAllowed(wantVersion *semver.Version, currentVersion *semver.Version) bool {
// Two cases:
// 19.1 to 19.2 -> same year
// 19.2 to 20.1 -> next year

// Since 2025, we have adopted a quarterly release cycle, with two of the four annual releases designated
// as innovative releases. Users have the option to skip upgrading to an innovative release.
// For 2024, we just had 3 releases overall, with 24.2 as the innovation release.
if currentVersion.Major() >= 24 {
// Four Cases:
// 24.1 to 24.2 -> Same year without skipping innovative release
// 24.1 to 24.3 -> Same year with skipping innovative release
// 24.3 to 25.1 -> Next year without skipping innovative release
// 24.3 to 25.2 -> Next year with skipping innovative release
nextPossibleRelease := getNextReleases(fmt.Sprintf("%d.%d", currentVersion.Major(), currentVersion.Minor()))
for _, version := range nextPossibleRelease {
if version == fmt.Sprintf("%d.%d", wantVersion.Major(), wantVersion.Minor()) {
return true
}
}

return false
}

return (currentVersion.Major() == wantVersion.Major() && currentVersion.Minor()+1 == wantVersion.Minor()) ||
(currentVersion.Major()+1 == wantVersion.Major() && currentVersion.Minor()-1 == wantVersion.Minor())
}

func isBackOneMajorVersion(wantVersion *semver.Version, currentVersion *semver.Version) bool {
// isMajorRollbackAllowed determines whether rolling back from the current version to the desired version is allowed
// based on the release cycle and rollback policies.
//
// Rollback Rules:
// 1. For versions prior to 2024:
// - A rollback is allowed if:
// - The major version remains the same and the minor version decrements by 1 (e.g., 19.2 -> 19.1).
// - The major version decrements by 1 and the minor version is the last release. (e.g., 20.1 -> 19.2).
//
// 2. For versions from 2024 onwards:
// - Users can skip rolling back to an innovative release.
// - The rollback is permitted if the desired version matches one of the valid previous releases.
//
// Parameters:
// - wantVersion: The target version to which a rollback is requested.
// - currentVersion: The currently installed version.
//
// Returns:
// - true if the rollback is allowed based on the rollback policies; false otherwise.
func isMajorRollbackAllowed(wantVersion *semver.Version, currentVersion *semver.Version) bool {
// Two cases:
// 19.2 to 19.1 -> same year
// 20.1 to 19.2 -> previous year

// Since 2024, users have the option to skip rollback to an innovative release.
if wantVersion.Major() >= 24 {
// Four cases:
// 24.2 -> 24.1 -> Same year without skipping innovative release
// 24.3 -> 24.1 -> Same year with skipping innovative release
// 25.1 -> 24.3 -> Previous year without skipping innovative release
// 25.2 -> 24.3 -> Previous year with skipping innovative release
rollbackReleases := getPreviousReleases(fmt.Sprintf("%d.%d", currentVersion.Major(), currentVersion.Minor()))
for _, version := range rollbackReleases {
if version == fmt.Sprintf("%d.%d", wantVersion.Major(), wantVersion.Minor()) {
return true
}
}

return false
}

return (currentVersion.Major() == wantVersion.Major() && currentVersion.Minor() == wantVersion.Minor()+1) ||
(currentVersion.Major() == wantVersion.Major()+1 && currentVersion.Minor() == wantVersion.Minor()-1)
}
Loading