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'
|
name: 'Website'
|
||||||
url: 'https://alpinelinux.org'
|
url: 'https://alpinelinux.org'
|
||||||
|
|
||||||
|
version_constraint:
|
||||||
|
allow_downgrade: false
|
||||||
|
allow_prerelease: false
|
||||||
|
type: semver
|
||||||
|
|
||||||
check_interval: 1h
|
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).
|
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
|
## Available Fetchers
|
||||||
|
|
||||||
## Fetcher: `atlassian`
|
## Fetcher: `atlassian`
|
||||||
|
|
|
@ -18,6 +18,11 @@ catalog:
|
||||||
name: 'Website'
|
name: 'Website'
|
||||||
url: 'https://alpinelinux.org'
|
url: 'https://alpinelinux.org'
|
||||||
|
|
||||||
|
version_constraint:
|
||||||
|
allow_downgrade: false
|
||||||
|
allow_prerelease: false
|
||||||
|
type: semver
|
||||||
|
|
||||||
check_interval: 1h
|
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).
|
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
|
## Available Fetchers
|
||||||
|
|
||||||
{% for module in modules -%}
|
{% for module in modules -%}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go-latestver/internal/version"
|
||||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,6 +20,8 @@ type (
|
||||||
Fetcher string `json:"-" yaml:"fetcher"`
|
Fetcher string `json:"-" yaml:"fetcher"`
|
||||||
FetcherConfig *fieldcollection.FieldCollection `json:"-" yaml:"fetcher_config"`
|
FetcherConfig *fieldcollection.FieldCollection `json:"-" yaml:"fetcher_config"`
|
||||||
|
|
||||||
|
VersionConstraint *version.Constraint `json:"-" yaml:"version_constraint"`
|
||||||
|
|
||||||
Links []CatalogLink `json:"links" yaml:"links"`
|
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")
|
ver = strings.TrimPrefix(ver, "v")
|
||||||
vertime = vertime.Truncate(time.Second).UTC()
|
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 {
|
switch {
|
||||||
|
|
||||||
case err != nil:
|
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()
|
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{
|
case cm.CurrentVersion != ver && !shouldUpdate:
|
||||||
"from": cm.CurrentVersion,
|
logger.Info("Version-updated prevented by constraints")
|
||||||
"to": ver,
|
cm.Error = ""
|
||||||
}).Info("Entry had version update")
|
|
||||||
|
case cm.CurrentVersion != ver && shouldUpdate:
|
||||||
|
logger.Info("Entry had version update")
|
||||||
|
|
||||||
if err = storage.Logs.Add(&database.LogEntry{
|
if err = storage.Logs.Add(&database.LogEntry{
|
||||||
CatalogName: ce.Name,
|
CatalogName: ce.Name,
|
||||||
|
@ -84,7 +101,11 @@ func checkForUpdates(ce *database.CatalogEntry) error {
|
||||||
|
|
||||||
cm.VersionTime = ptrTime(vertime)
|
cm.VersionTime = ptrTime(vertime)
|
||||||
cm.CurrentVersion = ver
|
cm.CurrentVersion = ver
|
||||||
fallthrough
|
cm.Error = ""
|
||||||
|
|
||||||
|
case cm.CurrentVersion == ver:
|
||||||
|
logger.Debug("Version did not change")
|
||||||
|
cm.Error = ""
|
||||||
|
|
||||||
default:
|
default:
|
||||||
cm.Error = ""
|
cm.Error = ""
|
||||||
|
|
Loading…
Reference in a new issue