mirror of
https://github.com/Luzifer/go-latestver.git
synced 2024-12-20 10:31:16 +00:00
[#6] Implement version constraints / downgrade protection
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
718a32222f
commit
333a8ffe9b
9 changed files with 286 additions and 7 deletions
|
@ -18,6 +18,11 @@ catalog:
|
|||
name: 'Website'
|
||||
url: 'https://alpinelinux.org'
|
||||
|
||||
version_constraint:
|
||||
allow_downgrade: false
|
||||
allow_prerelease: false
|
||||
type: semver
|
||||
|
||||
check_interval: 1h
|
||||
|
||||
...
|
||||
|
@ -29,6 +34,8 @@ Additionally you will configure a `fetcher` with its corresponding `fetcher_conf
|
|||
|
||||
You can provide your own `links` for each catalog entry which will be added to or override the links returned from the fetcher. If you provide the same `name` as the fetcher uses the link of the fetcher will be overridden. The `icon_class` should consist of `fas` or `fab` and an icon (for example `fa-globe`). You can use all **solid** (`fas`) or **breand** (`fab`) icons within [Font Awesome v5 Free](https://fontawesome.com/v5.15/icons?d=gallery&s=brands,solid&m=free).
|
||||
|
||||
For the `version_constraint`s you must specify a `type` to parse the version returned by the fetcher (currently `semver` and `numeric_dot` (`104.0.5112.79`) are supported) and then can allow downgrades and pre-releases in the version. If no constraint is present, versions are neither parsed nor checked for downgrade / pre-release. If only the `type` is specified, downgrades and pre-releases are forbidden.
|
||||
|
||||
## Available Fetchers
|
||||
|
||||
## Fetcher: `atlassian`
|
||||
|
|
|
@ -18,6 +18,11 @@ catalog:
|
|||
name: 'Website'
|
||||
url: 'https://alpinelinux.org'
|
||||
|
||||
version_constraint:
|
||||
allow_downgrade: false
|
||||
allow_prerelease: false
|
||||
type: semver
|
||||
|
||||
check_interval: 1h
|
||||
|
||||
...
|
||||
|
@ -29,6 +34,8 @@ Additionally you will configure a `fetcher` with its corresponding `fetcher_conf
|
|||
|
||||
You can provide your own `links` for each catalog entry which will be added to or override the links returned from the fetcher. If you provide the same `name` as the fetcher uses the link of the fetcher will be overridden. The `icon_class` should consist of `fas` or `fab` and an icon (for example `fa-globe`). You can use all **solid** (`fas`) or **breand** (`fab`) icons within [Font Awesome v5 Free](https://fontawesome.com/v5.15/icons?d=gallery&s=brands,solid&m=free).
|
||||
|
||||
For the `version_constraint`s you must specify a `type` to parse the version returned by the fetcher (currently `semver` and `numeric_dot` (`104.0.5112.79`) are supported) and then can allow downgrades and pre-releases in the version. If no constraint is present, versions are neither parsed nor checked for downgrade / pre-release. If only the `type` is specified, downgrades and pre-releases are forbidden.
|
||||
|
||||
## Available Fetchers
|
||||
|
||||
{% for module in modules -%}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/Luzifer/go-latestver/internal/version"
|
||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||
)
|
||||
|
||||
|
@ -19,6 +20,8 @@ type (
|
|||
Fetcher string `json:"-" yaml:"fetcher"`
|
||||
FetcherConfig *fieldcollection.FieldCollection `json:"-" yaml:"fetcher_config"`
|
||||
|
||||
VersionConstraint *version.Constraint `json:"-" yaml:"version_constraint"`
|
||||
|
||||
Links []CatalogLink `json:"links" yaml:"links"`
|
||||
}
|
||||
|
||||
|
|
68
internal/version/constraint.go
Normal file
68
internal/version/constraint.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package version
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
|
||||
type (
|
||||
Constraint struct {
|
||||
AllowDowngrade bool `yaml:"allow_downgrade"`
|
||||
AllowPrerelease bool `yaml:"allow_prerelease"`
|
||||
|
||||
Type string `yaml:"type"`
|
||||
}
|
||||
|
||||
comparer interface {
|
||||
Compare(oldVersion, newVersion string) (compareResult, error)
|
||||
IsPrerelease(newVersion string) (bool, error)
|
||||
}
|
||||
|
||||
compareResult uint
|
||||
)
|
||||
|
||||
const (
|
||||
compareResultInvalid compareResult = iota
|
||||
compareResultEqual
|
||||
compareResultDowngrade
|
||||
compareResultUpgrade
|
||||
)
|
||||
|
||||
func (c Constraint) ShouldApply(oldVersion, newVersion string) (bool, error) {
|
||||
comp := c.getComparer()
|
||||
if comp == nil {
|
||||
return false, errors.New("invalid version type specified")
|
||||
}
|
||||
|
||||
// Compare versions and check for UpgradeOnly flag
|
||||
compResult, err := comp.Compare(oldVersion, newVersion)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "comparing versions")
|
||||
}
|
||||
|
||||
if !c.AllowDowngrade && compResult != compareResultUpgrade {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check for forbidden pre-releases
|
||||
isPreR, err := comp.IsPrerelease(newVersion)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "checking pre-release")
|
||||
}
|
||||
|
||||
if !c.AllowPrerelease && isPreR {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c Constraint) getComparer() comparer {
|
||||
switch c.Type {
|
||||
case "numeric_dot":
|
||||
return numericDotSeparatedComparer{}
|
||||
|
||||
case "semver":
|
||||
return semVerComparer{}
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
68
internal/version/numeric_dot.go
Normal file
68
internal/version/numeric_dot.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
numericDotSeparatedComparer struct{}
|
||||
)
|
||||
|
||||
var _ comparer = numericDotSeparatedComparer{}
|
||||
|
||||
func (n numericDotSeparatedComparer) Compare(oldVersion, newVersion string) (compareResult, error) {
|
||||
oldV, err := n.parse(oldVersion)
|
||||
if err != nil {
|
||||
return compareResultInvalid, errors.Wrap(err, "parsing old version")
|
||||
}
|
||||
|
||||
newV, err := n.parse(newVersion)
|
||||
if err != nil {
|
||||
return compareResultInvalid, errors.Wrap(err, "parsing old version")
|
||||
}
|
||||
|
||||
getSeg := func(v []int, i int) int {
|
||||
if i >= len(v) {
|
||||
return 0
|
||||
}
|
||||
return v[i]
|
||||
}
|
||||
|
||||
for i := 0; i < int(math.Max(float64(len(oldV)), float64(len(newV)))); i++ {
|
||||
switch {
|
||||
case getSeg(oldV, i) < getSeg(newV, i):
|
||||
return compareResultUpgrade, nil
|
||||
|
||||
case getSeg(oldV, i) > getSeg(newV, i):
|
||||
return compareResultDowngrade, nil
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return compareResultEqual, nil
|
||||
}
|
||||
|
||||
func (numericDotSeparatedComparer) IsPrerelease(string) (bool, error) {
|
||||
// Numeric Dot has no marker for Pre-Releases
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (numericDotSeparatedComparer) parse(ver string) ([]int, error) {
|
||||
var out []int
|
||||
|
||||
for _, seg := range strings.Split(ver, ".") {
|
||||
segI, err := strconv.Atoi(seg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parsing segment")
|
||||
}
|
||||
out = append(out, segI)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
27
internal/version/numeric_dot_test.go
Normal file
27
internal/version/numeric_dot_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package version
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNumericDotSeparatedCompareFunc(t *testing.T) {
|
||||
comp := numericDotSeparatedComparer{}
|
||||
|
||||
for _, tc := range []struct {
|
||||
v1, v2 string
|
||||
res compareResult
|
||||
}{
|
||||
{"2.1.0", "2.1.1", compareResultUpgrade},
|
||||
{"2.1.1", "2.1.0", compareResultDowngrade},
|
||||
{"2.1.1", "2.1.1", compareResultEqual},
|
||||
{"103.0.5060.134", "104.0.5112.79", compareResultUpgrade},
|
||||
{"2022.7.7", "2022.8.0", compareResultUpgrade},
|
||||
} {
|
||||
res, err := comp.Compare(tc.v1, tc.v2)
|
||||
if err != nil {
|
||||
t.Errorf("Comparing %q to %q: %s", tc.v1, tc.v2, err)
|
||||
}
|
||||
|
||||
if res != tc.res {
|
||||
t.Errorf("Comparing %q to %q: expected %v, got %v", tc.v1, tc.v2, tc.res, res)
|
||||
}
|
||||
}
|
||||
}
|
51
internal/version/semver.go
Normal file
51
internal/version/semver.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package version
|
||||
|
||||
import (
|
||||
"github.com/blang/semver/v4"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
semVerComparer struct{}
|
||||
)
|
||||
|
||||
var _ comparer = semVerComparer{}
|
||||
|
||||
func (semVerComparer) Compare(oldVersion, newVersion string) (compareResult, error) {
|
||||
oldS, err := semver.Make(oldVersion)
|
||||
if err != nil {
|
||||
return compareResultInvalid, errors.Wrap(err, "parsing old version")
|
||||
}
|
||||
|
||||
newS, err := semver.Make(newVersion)
|
||||
if err != nil {
|
||||
return compareResultInvalid, errors.Wrap(err, "parsing new version")
|
||||
}
|
||||
|
||||
switch oldS.Compare(newS) {
|
||||
case -1:
|
||||
// oldS < newS
|
||||
return compareResultUpgrade, nil
|
||||
|
||||
case 0:
|
||||
// oldS == newS
|
||||
return compareResultEqual, nil
|
||||
|
||||
case 1:
|
||||
// oldS > newS
|
||||
return compareResultDowngrade, nil
|
||||
|
||||
default:
|
||||
// WTF, that does not exist according to lib docs
|
||||
return compareResultInvalid, errors.New("invalid compare result")
|
||||
}
|
||||
}
|
||||
|
||||
func (semVerComparer) IsPrerelease(newVersion string) (bool, error) {
|
||||
newS, err := semver.Make(newVersion)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "parsing version")
|
||||
}
|
||||
|
||||
return newS.Pre != nil, nil
|
||||
}
|
27
internal/version/semver_test.go
Normal file
27
internal/version/semver_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package version
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSemVerCompareFunc(t *testing.T) {
|
||||
comp := semVerComparer{}
|
||||
|
||||
for _, tc := range []struct {
|
||||
v1, v2 string
|
||||
res compareResult
|
||||
}{
|
||||
{"1.0.0", "2.0.0", compareResultUpgrade},
|
||||
{"2.0.0", "2.1.0", compareResultUpgrade},
|
||||
{"2.1.0", "2.1.1", compareResultUpgrade},
|
||||
{"2.1.1", "2.1.0", compareResultDowngrade},
|
||||
{"2.1.1", "2.1.1", compareResultEqual},
|
||||
} {
|
||||
res, err := comp.Compare(tc.v1, tc.v2)
|
||||
if err != nil {
|
||||
t.Errorf("Comparing %q to %q: %s", tc.v1, tc.v2, err)
|
||||
}
|
||||
|
||||
if res != tc.res {
|
||||
t.Errorf("Comparing %q to %q: expected %v, got %v", tc.v1, tc.v2, tc.res, res)
|
||||
}
|
||||
}
|
||||
}
|
35
scheduler.go
35
scheduler.go
|
@ -59,18 +59,35 @@ func checkForUpdates(ce *database.CatalogEntry) error {
|
|||
ver = strings.TrimPrefix(ver, "v")
|
||||
vertime = vertime.Truncate(time.Second).UTC()
|
||||
|
||||
logger = logger.WithFields(log.Fields{
|
||||
"from": cm.CurrentVersion,
|
||||
"to": ver,
|
||||
})
|
||||
|
||||
var (
|
||||
compareErr error
|
||||
shouldUpdate = true
|
||||
)
|
||||
if ce.VersionConstraint != nil {
|
||||
shouldUpdate, compareErr = ce.VersionConstraint.ShouldApply(cm.CurrentVersion, ver)
|
||||
}
|
||||
|
||||
switch {
|
||||
|
||||
case err != nil:
|
||||
log.WithField("entry", ce.Key()).WithError(err).Error("Fetcher caused error, error is stored in entry")
|
||||
logger.WithError(err).Error("Fetcher caused error, error is stored in entry")
|
||||
cm.Error = err.Error()
|
||||
|
||||
case cm.CurrentVersion != ver:
|
||||
case compareErr != nil:
|
||||
logger.WithError(err).Error("Version compare caused error, error is stored in entry")
|
||||
cm.Error = compareErr.Error()
|
||||
|
||||
logger.WithFields(log.Fields{
|
||||
"from": cm.CurrentVersion,
|
||||
"to": ver,
|
||||
}).Info("Entry had version update")
|
||||
case cm.CurrentVersion != ver && !shouldUpdate:
|
||||
logger.Info("Version-updated prevented by constraints")
|
||||
cm.Error = ""
|
||||
|
||||
case cm.CurrentVersion != ver && shouldUpdate:
|
||||
logger.Info("Entry had version update")
|
||||
|
||||
if err = storage.Logs.Add(&database.LogEntry{
|
||||
CatalogName: ce.Name,
|
||||
|
@ -84,7 +101,11 @@ func checkForUpdates(ce *database.CatalogEntry) error {
|
|||
|
||||
cm.VersionTime = ptrTime(vertime)
|
||||
cm.CurrentVersion = ver
|
||||
fallthrough
|
||||
cm.Error = ""
|
||||
|
||||
case cm.CurrentVersion == ver:
|
||||
logger.Debug("Version did not change")
|
||||
cm.Error = ""
|
||||
|
||||
default:
|
||||
cm.Error = ""
|
||||
|
|
Loading…
Reference in a new issue