functionaltests/internal/ecclient/stack_version.go (209 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package ecclient import ( "cmp" "errors" "fmt" "regexp" "slices" "strconv" "strings" ) type StackVersionInfo struct { Version StackVersion UpgradableTo []StackVersion } // CanUpgradeTo checks if the current stack version can upgrade to the provided `version`. func (info StackVersionInfo) CanUpgradeTo(version StackVersion) bool { for _, upgrade := range info.UpgradableTo { if upgrade == version { return true } } return false } type StackVersionInfos []StackVersionInfo // Sort sorts the stack versions in ascending order based on // major, minor, patch, suffix in order of importance. func (infos StackVersionInfos) Sort() { cmpFn := func(a, b StackVersionInfo) int { return a.Version.Compare(b.Version) } if slices.IsSortedFunc(infos, cmpFn) { return } slices.SortFunc(infos, cmpFn) } // Last returns the last version in the list. func (infos StackVersionInfos) Last() (StackVersionInfo, bool) { if len(infos) == 0 { return StackVersionInfo{}, false } return infos[len(infos)-1], true } // LatestFor retrieves the latest stack version for that prefix. // The prefix must loosely follow semantic versioning in the form of: // - X.Y.Z // - X.Y // - X // // Invalid prefix will cause this function to panic. // // Note: This assumes that StackVersionInfos is already sorted in ascending order. func (infos StackVersionInfos) LatestFor(prefix string) (StackVersionInfo, bool) { lv, err := parseVersionPrefix(prefix) if err != nil { panic(err) } for i := len(infos) - 1; i >= 0; i-- { if ok := infos[i].Version.looseMatch(lv); ok { return infos[i], true } } return StackVersionInfo{}, false } // LatestForMajor retrieves the latest stack version for that major. // // Note: This assumes that StackVersionInfos is already sorted in ascending order. func (infos StackVersionInfos) LatestForMajor(major uint64) (StackVersionInfo, bool) { for i := len(infos) - 1; i >= 0; i-- { if infos[i].Version.IsMajor(major) { return infos[i], true } } return StackVersionInfo{}, false } // LatestForMinor retrieves the latest stack version for that minor. // // Note: This assumes that StackVersionInfos is already sorted in ascending order. func (infos StackVersionInfos) LatestForMinor(major, minor uint64) (StackVersionInfo, bool) { for i := len(infos) - 1; i >= 0; i-- { if infos[i].Version.IsMinor(major, minor) { return infos[i], true } } return StackVersionInfo{}, false } // PreviousMinorLatest retrieves the latest stack version from the previous // minor of the provided `version`. // If the minor of `version` is 0, the latest version for previous major is // returned instead. // // Note: This assumes that StackVersionInfos is already sorted in ascending order. func (infos StackVersionInfos) PreviousMinorLatest(version StackVersion) (StackVersionInfo, bool) { if version.Minor == 0 { // When the minor is 0, we want the latest of the previous major return infos.LatestForMajor(version.Major - 1) } return infos.LatestForMinor(version.Major, version.Minor-1) } // PreviousPatch retrieves the previous patch version info from the provided `version`. // // Note: This assumes that StackVersionInfos is already sorted in ascending order. func (infos StackVersionInfos) PreviousPatch(version StackVersion) (StackVersionInfo, bool) { if version.Patch == 0 { // When the patch is 0, we want the latest of the previous minor return infos.LatestForMinor(version.Major, version.Minor-1) } prevPatch := version prevPatch.Patch = version.Patch - 1 return infos.GetByVersion(prevPatch) } // GetByVersion returns the version info if the provided `version` exists in the list. func (infos StackVersionInfos) GetByVersion(version StackVersion) (StackVersionInfo, bool) { for i := len(infos) - 1; i >= 0; i-- { if infos[i].Version == version { return infos[i], true } } return StackVersionInfo{}, false } type StackVersion struct { Major uint64 Minor uint64 Patch uint64 Suffix string // Optional } func NewStackVersion(major, minor, patch uint64, suffix string) StackVersion { return StackVersion{ Major: major, Minor: minor, Patch: patch, Suffix: suffix, } } func NewStackVersionFromStr(versionStr string) (StackVersion, error) { splits := strings.SplitN(versionStr, ".", 3) if len(splits) != 3 { return StackVersion{}, errors.New("invalid format") } major, err := strconv.ParseUint(splits[0], 10, 64) if err != nil { return StackVersion{}, fmt.Errorf("invalid major version: %w", err) } minor, err := strconv.ParseUint(splits[1], 10, 64) if err != nil { return StackVersion{}, fmt.Errorf("invalid minor version: %w", err) } splits = strings.SplitN(splits[2], "-", 2) patch, err := strconv.ParseUint(splits[0], 10, 64) if err != nil { return StackVersion{}, fmt.Errorf("invalid patch version: %w", err) } suffix := "" if len(splits) > 1 { suffix = splits[1] } return NewStackVersion(major, minor, patch, suffix), nil } func (v StackVersion) String() string { var suffix string if v.Suffix != "" { suffix = "-" + v.Suffix } return fmt.Sprintf("%d.%d.%d%s", v.Major, v.Minor, v.Patch, suffix) } func (v StackVersion) MajorMinor() string { return fmt.Sprintf("%d.%d", v.Major, v.Minor) } func (v StackVersion) IsMajor(major uint64) bool { return v.Major == major } func (v StackVersion) IsMinor(major, minor uint64) bool { return v.Major == major && v.Minor == minor } func (v StackVersion) IsPatch(major, minor, patch uint64) bool { return v.Major == major && v.Minor == minor && v.Patch == patch } func (v StackVersion) Compare(other StackVersion) int { res := cmp.Compare(v.Major, other.Major) if res != 0 { return res } res = cmp.Compare(v.Minor, other.Minor) if res != 0 { return res } res = cmp.Compare(v.Patch, other.Patch) if res != 0 { return res } return cmp.Compare(v.Suffix, other.Suffix) } // HasPrefix checks if the stack version contains the prefix. // The prefix must loosely follow semantic versioning in the form of: // - X.Y.Z // - X.Y // - X func (v StackVersion) HasPrefix(prefix string) (bool, error) { lv, err := parseVersionPrefix(prefix) if err != nil { return false, err } return v.looseMatch(lv), nil } type looseVersion struct { major, minor, patch *uint64 } func (v StackVersion) looseMatch(lv looseVersion) bool { // Only major if lv.minor == nil { return v.IsMajor(*lv.major) } // Only major minor if lv.patch == nil { return v.IsMinor(*lv.major, *lv.minor) } // Major, minor, patch return v.IsPatch(*lv.major, *lv.minor, *lv.patch) } var looseVersionRegex = regexp.MustCompile(`^(\d*)(?:\.(\d*))?(?:\.(\d*))?(?:-(\w*))?$`) func parseVersionPrefix(prefix string) (looseVersion, error) { lv := looseVersion{} // First match is the whole string, last match is the suffix, total 5 matches := looseVersionRegex.FindStringSubmatch(prefix) if len(matches) == 0 || len(matches) > 5 { return looseVersion{}, errors.New("invalid prefix format") } major, err := strconv.ParseUint(matches[1], 10, 64) if err != nil { return looseVersion{}, fmt.Errorf("invalid major version: %w", err) } // Only major lv.major = &major if matches[2] == "" { return lv, nil } minor, err := strconv.ParseUint(matches[2], 10, 64) if err != nil { return looseVersion{}, fmt.Errorf("invalid minor version: %w", err) } // Only major minor lv.minor = &minor if matches[3] == "" { return lv, nil } // Major, minor, patch patch, err := strconv.ParseUint(matches[3], 10, 64) if err != nil { return looseVersion{}, fmt.Errorf("invalid patch version: %w", err) } lv.patch = &patch return lv, nil }