From b2e84cfd218157561c0c769a2f008b1868d3a6c1 Mon Sep 17 00:00:00 2001 From: dkeven <82354774+dkeven@users.noreply.github.com> Date: Wed, 16 Jul 2025 00:05:09 +0800 Subject: [PATCH] cli(refactor): new structure for upgrade (#1546) --- cli/cmd/ctl/os/upgrade.go | 46 ++++++- cli/pkg/pipelines/upgrade.go | 30 +++-- cli/pkg/upgrade/1_12_0_20250702.go | 94 ++++++++++++++ cli/pkg/upgrade/{task.go => base.go} | 171 ++++++++++++++----------- cli/pkg/upgrade/interfaces.go | 22 ++++ cli/pkg/upgrade/modules.go | 51 ++++++++ cli/pkg/upgrade/upgrade.go | 181 --------------------------- cli/pkg/upgrade/version.go | 175 ++++++++++++++++++++------ 8 files changed, 466 insertions(+), 304 deletions(-) create mode 100644 cli/pkg/upgrade/1_12_0_20250702.go rename cli/pkg/upgrade/{task.go => base.go} (74%) create mode 100644 cli/pkg/upgrade/interfaces.go create mode 100644 cli/pkg/upgrade/modules.go delete mode 100644 cli/pkg/upgrade/upgrade.go diff --git a/cli/cmd/ctl/os/upgrade.go b/cli/cmd/ctl/os/upgrade.go index ab622de81..4d59fd98f 100644 --- a/cli/cmd/ctl/os/upgrade.go +++ b/cli/cmd/ctl/os/upgrade.go @@ -1,11 +1,17 @@ package os import ( - "log" - + "encoding/json" + "fmt" + "github.com/Masterminds/semver/v3" "github.com/beclab/Olares/cli/cmd/ctl/options" + "github.com/beclab/Olares/cli/pkg/phase" "github.com/beclab/Olares/cli/pkg/pipelines" + "github.com/beclab/Olares/cli/pkg/upgrade" + "github.com/pkg/errors" "github.com/spf13/cobra" + "log" + "os" ) type UpgradeOsOptions struct { @@ -31,6 +37,42 @@ func NewCmdUpgradeOs() *cobra.Command { } o.UpgradeOptions.AddFlags(cmd) cmd.AddCommand(NewCmdUpgradePrecheck()) + cmd.AddCommand(NewCmdGetUpgradePath()) + return cmd +} + +func NewCmdGetUpgradePath() *cobra.Command { + var baseVersionStr string + cmd := &cobra.Command{ + Use: "path", + Short: "Get the upgrade path (required intermediate versions) from base version to the latest upgradable version (as known to this release of olares-cli)", + RunE: func(cmd *cobra.Command, args []string) error { + var baseVersion *semver.Version + var err error + if baseVersionStr == "" { + baseVersionStr, err = phase.GetOlaresVersion() + if err != nil { + return errors.New("failed to get current Olares version, please specify the base version explicitly") + } + } + baseVersion, err = semver.NewVersion(baseVersionStr) + if err != nil { + return fmt.Errorf("invalid base version: %v", err) + } + + path, err := upgrade.GetUpgradePathFor(baseVersion, nil) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + encoder := json.NewEncoder(cmd.OutOrStdout()) + encoder.SetIndent("", " ") + return encoder.Encode(path) + }, + } + + cmd.Flags().StringVarP(&baseVersionStr, "base-version", "b", baseVersionStr, "base version to be upgraded, defaults to the current Olares version if inside Olares cluster") + return cmd } diff --git a/cli/pkg/pipelines/upgrade.go b/cli/pkg/pipelines/upgrade.go index 2a9ace233..f1e54da9d 100644 --- a/cli/pkg/pipelines/upgrade.go +++ b/cli/pkg/pipelines/upgrade.go @@ -2,11 +2,10 @@ package pipelines import ( "fmt" - "os" - "path" - "github.com/beclab/Olares/cli/pkg/upgrade" "github.com/beclab/Olares/cli/pkg/utils" + "os" + "path" "github.com/beclab/Olares/cli/cmd/ctl/options" "github.com/beclab/Olares/cli/pkg/common" @@ -40,9 +39,23 @@ func UpgradeOlaresPipeline(opts *options.UpgradeOptions) error { return fmt.Errorf("error parsing target Olares version: %v", err) } - if !targetVersion.GreaterThan(currentVersion) { - fmt.Printf("current version is: %s, no need to upgrade to %s\n", currentVersion.String(), opts.Version) - os.Exit(0) + upgradePath, err := upgrade.GetUpgradePathFor(currentVersion, targetVersion) + if err != nil { + return err + } + if len(upgradePath) > 1 { + fmt.Printf("unable to upgrade from %s to %s directly,\n", currentVersion, targetVersion) + if len(upgradePath) == 2 { + fmt.Printf("please upgrade to %s first!\n", upgradePath[0]) + } else { + line := "please upgrade sequentially to:" + for _, u := range upgradePath[:len(upgradePath)-1] { + line += fmt.Sprintf(" %s", u) + } + line += " first!" + fmt.Println(line) + } + os.Exit(1) } arg := common.NewArgument() @@ -59,9 +72,8 @@ func UpgradeOlaresPipeline(opts *options.UpgradeOptions) error { manifest := path.Join(runtime.GetInstallerDir(), "installation.manifest") runtime.Arg.SetManifest(manifest) - upgradeModule := &upgrade.UpgradeModule{ - CurrentVersion: currentVersion, - TargetVersion: targetVersion, + upgradeModule := &upgrade.Module{ + TargetVersion: targetVersion, } p := &pipeline.Pipeline{ diff --git a/cli/pkg/upgrade/1_12_0_20250702.go b/cli/pkg/upgrade/1_12_0_20250702.go new file mode 100644 index 000000000..423913244 --- /dev/null +++ b/cli/pkg/upgrade/1_12_0_20250702.go @@ -0,0 +1,94 @@ +package upgrade + +import ( + "fmt" + "github.com/Masterminds/semver/v3" + "github.com/beclab/Olares/cli/pkg/common" + "github.com/beclab/Olares/cli/pkg/core/connector" + "github.com/beclab/Olares/cli/pkg/core/logger" + "github.com/beclab/Olares/cli/pkg/core/task" + "os" + "strings" +) + +type upgrader_1_12_0_20250702 struct { + upgraderBase +} + +func (u upgrader_1_12_0_20250702) Version() *semver.Version { + return semver.MustParse("1.12.0-20250702") +} + +func (u upgrader_1_12_0_20250702) PrepareForUpgrade() []task.Interface { + preTasks := []task.Interface{ + &task.LocalTask{ + Name: "UpdateSysctlReservedPorts", + Action: new(updateSysctlReservedPorts), + }, + } + return append(preTasks, u.upgraderBase.PrepareForUpgrade()...) +} + +type updateSysctlReservedPorts struct { + common.KubeAction +} + +func (u *updateSysctlReservedPorts) Execute(runtime connector.Runtime) error { + const sysctlFile = "/etc/sysctl.conf" + const reservedPortsKey = "net.ipv4.ip_local_reserved_ports" + const expectedValue = "30000-32767,46800-50000" + + content, err := os.ReadFile(sysctlFile) + if err != nil { + return fmt.Errorf("failed to read sysctl.conf: %v", err) + } + + lines := strings.Split(string(content), "\n") + var foundKey bool + var needUpdate bool + var updatedLines []string + + for _, line := range lines { + trimmedLine := strings.TrimSpace(line) + if strings.HasPrefix(trimmedLine, reservedPortsKey) { + foundKey = true + parts := strings.SplitN(trimmedLine, "=", 2) + if len(parts) == 2 { + currentValue := strings.TrimSpace(parts[1]) + if currentValue != expectedValue { + logger.Infof("updating %s from %s to %s", reservedPortsKey, currentValue, expectedValue) + updatedLines = append(updatedLines, fmt.Sprintf("%s=%s", reservedPortsKey, expectedValue)) + needUpdate = true + } else { + updatedLines = append(updatedLines, line) + } + } else { + updatedLines = append(updatedLines, line) + } + } else { + updatedLines = append(updatedLines, line) + } + } + + if !foundKey { + logger.Infof("key %s not found in sysctl.conf, adding it", reservedPortsKey) + updatedLines = append(updatedLines, fmt.Sprintf("%s=%s", reservedPortsKey, expectedValue)) + needUpdate = true + } + + if needUpdate { + updatedContent := strings.Join(updatedLines, "\n") + if err := os.WriteFile(sysctlFile, []byte(updatedContent), 0644); err != nil { + return fmt.Errorf("failed to write updated sysctl.conf: %v", err) + } + + if _, err := runtime.GetRunner().SudoCmd("sysctl -p", false, false); err != nil { + return fmt.Errorf("failed to reload sysctl: %v", err) + } + logger.Infof("updated and reloaded sysctl configuration") + } else { + logger.Debugf("%s already has the expected value: %s", reservedPortsKey, expectedValue) + } + + return nil +} diff --git a/cli/pkg/upgrade/task.go b/cli/pkg/upgrade/base.go similarity index 74% rename from cli/pkg/upgrade/task.go rename to cli/pkg/upgrade/base.go index b05e51e95..08dba0f57 100644 --- a/cli/pkg/upgrade/task.go +++ b/cli/pkg/upgrade/base.go @@ -3,9 +3,10 @@ package upgrade import ( "context" "fmt" + "github.com/beclab/Olares/cli/pkg/core/task" + "github.com/beclab/Olares/cli/pkg/terminus" "os" "path" - "strings" "time" "github.com/beclab/Olares/cli/pkg/common" @@ -20,11 +21,101 @@ import ( ctrl "sigs.k8s.io/controller-runtime" ) -type PrepareUserInfoForUpgrade struct { +// upgraderBase is the general-purpose upgrader implementation +// for upgrading across versions without any breaking changes. +// Other implementations of breakingUpgrader, +// targeted for versions with breaking changes, +// should use this as a base for injecting and/or rewriting specific tasks as needed +type upgraderBase struct{} + +func (u upgraderBase) PrepareForUpgrade() []task.Interface { + return []task.Interface{ + &task.LocalTask{ + Name: "prepareUserInfoForUpgrade", + Action: new(prepareUserInfoForUpgrade), + Retry: 5, + }, + } +} + +func (u upgraderBase) ClearAppChartValues() []task.Interface { + return []task.Interface{ + &task.LocalTask{ + Name: "ClearAppChartValues", + Action: new(terminus.ClearAppValues), + }, + } +} + +func (u upgraderBase) ClearBFLChartValues() []task.Interface { + return []task.Interface{ + &task.LocalTask{ + Name: "ClearBFLChartValues", + Action: new(terminus.ClearBFLValues), + }, + } +} + +func (u upgraderBase) UpdateChartsInAppService() []task.Interface { + return []task.Interface{ + &task.LocalTask{ + Name: "UpdateChartsInAppService", + Action: new(terminus.CopyAppServiceHelmFiles), + Retry: 5, + }, + } +} + +func (u upgraderBase) UpgradeUserComponents() []task.Interface { + return []task.Interface{ + &task.LocalTask{ + Name: "upgradeUserComponents", + Action: new(upgradeUserComponents), + Retry: 5, + Delay: 15 * time.Second, + }, + } +} + +func (u upgraderBase) UpdateReleaseFile() []task.Interface { + return []task.Interface{ + &task.LocalTask{ + Name: "UpdateReleaseFile", + Action: new(terminus.WriteReleaseFile), + }, + } +} + +func (u upgraderBase) UpgradeSystemComponents() []task.Interface { + // this task updates the version in the CR + // so put this at last to make the whole pipeline + // reentrant + return []task.Interface{ + &task.LocalTask{ + Name: "upgradeSystemComponents", + Action: new(upgradeSystemComponents), + Retry: 10, + Delay: 15 * time.Second, + }, + } +} + +func (u upgraderBase) PostUpgrade() []task.Interface { + return []task.Interface{ + &task.LocalTask{ + Name: "EnsurePodsUpAndRunningAgain", + Action: new(terminus.CheckKeyPodsRunning), + Delay: 15 * time.Second, + Retry: 60, + }, + } +} + +type prepareUserInfoForUpgrade struct { common.KubeAction } -func (p *PrepareUserInfoForUpgrade) Execute(runtime connector.Runtime) error { +func (p *prepareUserInfoForUpgrade) Execute(runtime connector.Runtime) error { config, err := ctrl.GetConfig() if err != nil { return fmt.Errorf("failed to get rest config: %s", err) @@ -63,7 +154,7 @@ func (p *PrepareUserInfoForUpgrade) Execute(runtime connector.Runtime) error { return fmt.Errorf("failed to get user-space-%x: %v", user.Name, err) } usersToUpgrade = append(usersToUpgrade, user) - if role, ok := user.Annotations["bytetrade.io/owner-role"]; ok && role == "platform-admin" { + if role, ok := user.Annotations["bytetrade.io/owner-role"]; ok && role == "owner" { adminUser = user.Name } } @@ -79,11 +170,11 @@ func (p *PrepareUserInfoForUpgrade) Execute(runtime connector.Runtime) error { return nil } -type UpgradeUserComponents struct { +type upgradeUserComponents struct { common.KubeAction } -func (u *UpgradeUserComponents) Execute(runtime connector.Runtime) error { +func (u *upgradeUserComponents) Execute(runtime connector.Runtime) error { config, err := ctrl.GetConfig() if err != nil { return fmt.Errorf("failed to get rest config: %s", err) @@ -178,11 +269,11 @@ func (u *UpgradeUserComponents) Execute(runtime connector.Runtime) error { return nil } -type UpgradeSystemComponents struct { +type upgradeSystemComponents struct { common.KubeAction } -func (u *UpgradeSystemComponents) Execute(runtime connector.Runtime) error { +func (u *upgradeSystemComponents) Execute(runtime connector.Runtime) error { config, err := ctrl.GetConfig() if err != nil { return fmt.Errorf("failed to get rest config: %s", err) @@ -222,67 +313,3 @@ func (u *UpgradeSystemComponents) Execute(runtime connector.Runtime) error { } return nil } - -type UpdateSysctlReservedPorts struct { - common.KubeAction -} - -func (u *UpdateSysctlReservedPorts) Execute(runtime connector.Runtime) error { - const sysctlFile = "/etc/sysctl.conf" - const reservedPortsKey = "net.ipv4.ip_local_reserved_ports" - const expectedValue = "30000-32767,46800-50000" - - content, err := os.ReadFile(sysctlFile) - if err != nil { - return fmt.Errorf("failed to read sysctl.conf: %v", err) - } - - lines := strings.Split(string(content), "\n") - var foundKey bool - var needUpdate bool - var updatedLines []string - - for _, line := range lines { - trimmedLine := strings.TrimSpace(line) - if strings.HasPrefix(trimmedLine, reservedPortsKey) { - foundKey = true - parts := strings.SplitN(trimmedLine, "=", 2) - if len(parts) == 2 { - currentValue := strings.TrimSpace(parts[1]) - if currentValue != expectedValue { - logger.Infof("updating %s from %s to %s", reservedPortsKey, currentValue, expectedValue) - updatedLines = append(updatedLines, fmt.Sprintf("%s=%s", reservedPortsKey, expectedValue)) - needUpdate = true - } else { - updatedLines = append(updatedLines, line) - } - } else { - updatedLines = append(updatedLines, line) - } - } else { - updatedLines = append(updatedLines, line) - } - } - - if !foundKey { - logger.Infof("key %s not found in sysctl.conf, adding it", reservedPortsKey) - updatedLines = append(updatedLines, fmt.Sprintf("%s=%s", reservedPortsKey, expectedValue)) - needUpdate = true - } - - if needUpdate { - updatedContent := strings.Join(updatedLines, "\n") - if err := os.WriteFile(sysctlFile, []byte(updatedContent), 0644); err != nil { - return fmt.Errorf("failed to write updated sysctl.conf: %v", err) - } - - if _, err := runtime.GetRunner().SudoCmd("sysctl -p", false, false); err != nil { - return fmt.Errorf("failed to reload sysctl: %v", err) - } - logger.Infof("updated and reloaded sysctl configuration") - } else { - logger.Debugf("%s already has the expected value: %s", reservedPortsKey, expectedValue) - } - - return nil -} diff --git a/cli/pkg/upgrade/interfaces.go b/cli/pkg/upgrade/interfaces.go new file mode 100644 index 000000000..be0b6148a --- /dev/null +++ b/cli/pkg/upgrade/interfaces.go @@ -0,0 +1,22 @@ +package upgrade + +import ( + "github.com/Masterminds/semver/v3" + "github.com/beclab/Olares/cli/pkg/core/task" +) + +type upgrader interface { + PrepareForUpgrade() []task.Interface + ClearAppChartValues() []task.Interface + ClearBFLChartValues() []task.Interface + UpdateChartsInAppService() []task.Interface + UpgradeUserComponents() []task.Interface + UpdateReleaseFile() []task.Interface + UpgradeSystemComponents() []task.Interface + PostUpgrade() []task.Interface +} + +type breakingUpgrader interface { + upgrader + Version() *semver.Version +} diff --git a/cli/pkg/upgrade/modules.go b/cli/pkg/upgrade/modules.go new file mode 100644 index 000000000..09522dcd5 --- /dev/null +++ b/cli/pkg/upgrade/modules.go @@ -0,0 +1,51 @@ +package upgrade + +import ( + "github.com/beclab/Olares/cli/pkg/bootstrap/precheck" + + "github.com/Masterminds/semver/v3" + "github.com/beclab/Olares/cli/pkg/common" + "github.com/beclab/Olares/cli/pkg/core/task" +) + +type Module struct { + common.KubeModule + TargetVersion *semver.Version +} + +func (m *Module) Init() { + m.Name = "UpgradeOlares" + + u := getUpgraderByVersion(m.TargetVersion) + m.Tasks = append(m.Tasks, u.PrepareForUpgrade()...) + m.Tasks = append(m.Tasks, u.ClearAppChartValues()...) + m.Tasks = append(m.Tasks, u.ClearBFLChartValues()...) + m.Tasks = append(m.Tasks, u.UpdateChartsInAppService()...) + m.Tasks = append(m.Tasks, u.UpgradeUserComponents()...) + m.Tasks = append(m.Tasks, u.UpdateReleaseFile()...) + m.Tasks = append(m.Tasks, u.UpgradeSystemComponents()...) + m.Tasks = append(m.Tasks, u.PostUpgrade()...) +} + +type PrecheckModule struct { + common.KubeModule +} + +func (m *PrecheckModule) Init() { + m.Name = "UpgradePrecheck" + + checkers := []precheck.Checker{ + new(precheck.MasterNodeReadyCheck), + new(precheck.RootPartitionAvailableSpaceCheck), + } + runPreChecks := &task.LocalTask{ + Name: "UpgradePrecheck", + Action: &precheck.RunChecks{ + Checkers: checkers, + }, + } + + m.Tasks = []task.Interface{ + runPreChecks, + } +} diff --git a/cli/pkg/upgrade/upgrade.go b/cli/pkg/upgrade/upgrade.go deleted file mode 100644 index 9939f1004..000000000 --- a/cli/pkg/upgrade/upgrade.go +++ /dev/null @@ -1,181 +0,0 @@ -package upgrade - -import ( - "time" - - "github.com/beclab/Olares/cli/pkg/bootstrap/precheck" - - "github.com/Masterminds/semver/v3" - "github.com/beclab/Olares/cli/pkg/common" - "github.com/beclab/Olares/cli/pkg/core/task" - "github.com/beclab/Olares/cli/pkg/terminus" -) - -type UpgradeModule struct { - common.KubeModule - CurrentVersion *semver.Version - TargetVersion *semver.Version -} - -var ( - preTasks = []*upgradeTask{ - { - Task: &task.LocalTask{ - Name: "UpdateSysctlReservedPorts", - Action: new(UpdateSysctlReservedPorts), - }, - Current: &explicitVersionMatcher{max: semver.New(1, 12, 0, "20250701", "")}, - Target: anyVersion, - }, - } - - coreTasks = []*upgradeTask{ - { - Task: &task.LocalTask{ - Name: "PrepareUserInfoForUpgrade", - Action: new(PrepareUserInfoForUpgrade), - Retry: 5, - }, - Current: atLeasVersion112, - Target: atLeasVersion112, - }, - { - Task: &task.LocalTask{ - Name: "ClearAppChartValues", - Action: new(terminus.ClearAppValues), - }, - Current: atLeasVersion112, - Target: atLeasVersion112, - }, - { - Task: &task.LocalTask{ - Name: "ClearBFLChartValues", - Action: new(terminus.ClearBFLValues), - }, - Current: atLeasVersion112, - Target: atLeasVersion112, - }, - { - Task: &task.LocalTask{ - Name: "UpdateChartsInAppService", - Action: new(terminus.CopyAppServiceHelmFiles), - Retry: 5, - }, - Current: atLeasVersion112, - Target: atLeasVersion112, - }, - { - Task: &task.LocalTask{ - Name: "UpgradeUserComponents", - Action: new(UpgradeUserComponents), - Retry: 5, - Delay: 15 * time.Second, - }, - Current: atLeasVersion112, - Target: atLeasVersion112, - }, - { - Task: &task.LocalTask{ - Name: "UpdateReleaseFile", - Action: new(terminus.WriteReleaseFile), - }, - Current: atLeasVersion112, - Target: atLeasVersion112, - }, - // this task updates the version in the CR - // so put this at last to make the whole pipeline - // reentrant - // maybe it should be put at the last of post tasks - // when post tasks are actually needed - { - Task: &task.LocalTask{ - Name: "UpgradeSystemComponents", - Action: new(UpgradeSystemComponents), - Retry: 10, - Delay: 15 * time.Second, - }, - Current: atLeasVersion112, - Target: atLeasVersion112, - }, - { - Task: &task.LocalTask{ - Name: "EnsurePodsUpAndRunningAgain", - Action: new(terminus.CheckKeyPodsRunning), - Delay: 15 * time.Second, - Retry: 60, - }, - Current: atLeasVersion112, - Target: atLeasVersion112, - }, - } - - postTasks []*upgradeTask -) - -func (m *UpgradeModule) Init() { - m.Name = "UpgradeOlares" - - // calculate tasks based on version difference - tasks := m.calculateUpgradeTasks() - - m.Tasks = tasks -} - -func (m *UpgradeModule) calculateUpgradeTasks() []task.Interface { - var tasks []task.Interface - - // for now, tasks are grouped into pre-upgrade/core-upgrade/post-upgrade tasks - // only for business logic compatibility - // they are still a normal sequence of tasks to be executed - // for the module layer - tasks = append(tasks, m.calculatePreUpgradeTasks()...) - tasks = append(tasks, m.calculateCoreUpgradeTasks()...) - tasks = append(tasks, m.calculatePostUpgradeTasks()...) - - return tasks -} - -func (m *UpgradeModule) getTasksToExecute(unfiltered []*upgradeTask) []task.Interface { - var filtered []task.Interface - for _, t := range unfiltered { - if t.Match(m.CurrentVersion, m.TargetVersion) { - filtered = append(filtered, t.Task) - } - } - return filtered -} - -func (m *UpgradeModule) calculatePreUpgradeTasks() []task.Interface { - return m.getTasksToExecute(preTasks) -} - -func (m *UpgradeModule) calculateCoreUpgradeTasks() []task.Interface { - return m.getTasksToExecute(coreTasks) -} - -func (m *UpgradeModule) calculatePostUpgradeTasks() []task.Interface { - return m.getTasksToExecute(postTasks) -} - -type PrecheckModule struct { - common.KubeModule -} - -func (m *PrecheckModule) Init() { - m.Name = "UpgradePrecheck" - - checkers := []precheck.Checker{ - new(precheck.MasterNodeReadyCheck), - new(precheck.RootPartitionAvailableSpaceCheck), - } - runPreChecks := &task.LocalTask{ - Name: "UpgradePrecheck", - Action: &precheck.RunChecks{ - Checkers: checkers, - }, - } - - m.Tasks = []task.Interface{ - runPreChecks, - } -} diff --git a/cli/pkg/upgrade/version.go b/cli/pkg/upgrade/version.go index 6ad95afd4..e57da00c5 100644 --- a/cli/pkg/upgrade/version.go +++ b/cli/pkg/upgrade/version.go @@ -1,61 +1,156 @@ package upgrade import ( + "fmt" "github.com/Masterminds/semver/v3" - "github.com/beclab/Olares/cli/pkg/core/task" + "github.com/beclab/Olares/cli/pkg/utils" + "github.com/beclab/Olares/cli/version" + "strings" ) -// versionMatcher checks if the specified version matches its condition -type versionMatcher interface { - Match(version *semver.Version) bool -} +type releaseLine string -// explicitVersionMatcher matches the specified version by a range of explicitly -// set version range by min/max version -// and additionally explicitly included/excluded versions -// if any type of condition is not set, that check is omitted -// i.e., if min is not set, there's no limit on the minimum version -// and if no condition is set, the matcher matches all non-nil versions -type explicitVersionMatcher struct { - min *semver.Version - max *semver.Version - include []*semver.Version - exclude []*semver.Version -} +var ( + mainLine = releaseLine("main") + dailyLine = releaseLine("daily") -func (m *explicitVersionMatcher) Match(version *semver.Version) bool { - if version == nil { - return false + dailyUpgraders = []breakingUpgrader{ + upgrader_1_12_0_20250702{}, } - for _, v := range m.include { - if v.Equal(version) { + mainUpgraders = []breakingUpgrader{} +) + +func getReleaseLineOfVersion(v *semver.Version) releaseLine { + preRelease := v.Prerelease() + if preRelease == "" || strings.HasPrefix(preRelease, "rc") { + return mainLine + } + return dailyLine +} + +func check(base *semver.Version, target *semver.Version) error { + if base == nil { + return fmt.Errorf("base version is nil") + } + + cliVersion, err := utils.ParseOlaresVersionString(version.VERSION) + if err != nil { + return fmt.Errorf("invalid olares-cli version :\"%s\"", version.VERSION) + } + + if target != nil { + if !target.GreaterThan(base) { + return fmt.Errorf("base version: %s, target version: %s, no need to upgrade", base, target) + } + + targetReleaseLine := getReleaseLineOfVersion(target) + baseReleaseLine := getReleaseLineOfVersion(base) + if targetReleaseLine != baseReleaseLine { + return fmt.Errorf("unable to upgrade to %s on %s release line from %s on %s release line", target, targetReleaseLine, base, baseReleaseLine) + } + switch baseReleaseLine { + case mainLine: + if !sameMajorLevelVersion(base, target) { + return fmt.Errorf("upgrade on %s rlease line can only be performed across same major level version", baseReleaseLine) + } + case dailyLine: + if !samePatchLevelVersion(base, target) { + return fmt.Errorf("upgrade on %s rlease line can only be performed across same patch version", baseReleaseLine) + } + } + + if target.GreaterThan(cliVersion) { + return fmt.Errorf("target version: %s, cli version: %s, please upgrade olares-cli first!", target, cliVersion) + } + } + + if base.GreaterThan(cliVersion) { + return fmt.Errorf("base version: %s, cli version: %s, please upgrade olares-cli first!", base, cliVersion) + } + + return nil +} + +func GetUpgradePathFor(base *semver.Version, target *semver.Version) ([]*semver.Version, error) { + if err := check(base, target); err != nil { + return nil, err + } + var path []*semver.Version + var releaseLineUpgraders []breakingUpgrader + var versionFilter func(v *semver.Version) bool + switch getReleaseLineOfVersion(base) { + case mainLine: + releaseLineUpgraders = mainUpgraders + versionFilter = func(v *semver.Version) bool { + if !v.GreaterThan(base) { + return false + } + if !sameMajorLevelVersion(v, base) { + return false + } + if target != nil && !v.LessThan(target) { + return false + } + return true + } + case dailyLine: + if target == nil { + cliVersion, err := utils.ParseOlaresVersionString(version.VERSION) + if err != nil { + return path, fmt.Errorf("invalid olares-cli version :\"%s\"", version.VERSION) + } + if getReleaseLineOfVersion(cliVersion) == dailyLine && samePatchLevelVersion(cliVersion, base) && cliVersion.GreaterThan(base) { + target = cliVersion + } + } + releaseLineUpgraders = dailyUpgraders + versionFilter = func(v *semver.Version) bool { + if !v.GreaterThan(base) { + return false + } + if !samePatchLevelVersion(v, base) { + return false + } + if target != nil && !v.LessThan(target) { + return false + } return true } } - for _, v := range m.exclude { - if v.Equal(version) { - return false + + for _, u := range releaseLineUpgraders { + v := u.Version() + if versionFilter(v) { + path = append(path, v) } } - if m.min != nil && version.LessThan(m.min) { - return false + + if target != nil { + path = append(path, target) } - if m.max != nil && version.GreaterThan(m.max) { - return false + + return path, nil +} + +func getUpgraderByVersion(target *semver.Version) upgrader { + for _, upgraders := range [][]breakingUpgrader{ + dailyUpgraders, + mainUpgraders, + } { + + for _, u := range upgraders { + if u.Version().Equal(target) { + return u + } + } } - return true + return upgraderBase{} } -// todo: do we need to check at least 1.12 in cli? -var anyVersion versionMatcher = &explicitVersionMatcher{} -var atLeasVersion112 versionMatcher = &explicitVersionMatcher{min: semver.New(1, 12, 0, "1", "")} - -type upgradeTask struct { - Task task.Interface - Current versionMatcher - Target versionMatcher +func samePatchLevelVersion(a, b *semver.Version) bool { + return a.Major() == b.Major() && a.Minor() == b.Minor() && a.Patch() == b.Patch() } -func (t *upgradeTask) Match(current, target *semver.Version) bool { - return t.Current.Match(current) && t.Target.Match(target) +func sameMajorLevelVersion(a, b *semver.Version) bool { + return a.Major() == b.Major() }