mirror of
https://github.com/Luzifer/go-latestver.git
synced 2024-11-08 23:20:03 +00:00
Initial running API version
This commit is contained in:
commit
b364743a0b
17 changed files with 1489 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
config.yaml
|
||||||
|
.env
|
||||||
|
latestver.db
|
142
api.go
Normal file
142
api.go
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go-latestver/internal/config"
|
||||||
|
"github.com/Luzifer/go-latestver/internal/database"
|
||||||
|
"github.com/Luzifer/go-latestver/internal/fetcher"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleCatalogGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
vars = mux.Vars(r)
|
||||||
|
name, tag = vars["name"], vars["tag"]
|
||||||
|
)
|
||||||
|
|
||||||
|
ce, err := configFile.CatalogEntryByTag(name, tag)
|
||||||
|
if errors.Is(err, config.ErrCatalogEntryNotFound) {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cm, err := storage.Catalog.GetMeta(&ce)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("Unable to fetch catalog meta")
|
||||||
|
http.Error(w, "Unable to fetch catalog meta", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ce.Links = append(
|
||||||
|
ce.Links,
|
||||||
|
fetcher.Get(ce.Fetcher).Links(&ce.FetcherConfig)...,
|
||||||
|
)
|
||||||
|
ce.CatalogMeta = *cm
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err = json.NewEncoder(w).Encode(ce); err != nil {
|
||||||
|
log.WithError(err).Error("Unable to encode catalog entry")
|
||||||
|
http.Error(w, "Unable to encode catalog meta", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCatalogGetVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
vars = mux.Vars(r)
|
||||||
|
name, tag = vars["name"], vars["tag"]
|
||||||
|
)
|
||||||
|
|
||||||
|
ce, err := configFile.CatalogEntryByTag(name, tag)
|
||||||
|
if errors.Is(err, config.ErrCatalogEntryNotFound) {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cm, err := storage.Catalog.GetMeta(&ce)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("Unable to fetch catalog meta")
|
||||||
|
http.Error(w, "Unable to fetch catalog meta", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
|
fmt.Fprint(w, cm.CurrentVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCatalogList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
out := make([]database.CatalogEntry, len(configFile.Catalog))
|
||||||
|
|
||||||
|
for i := range configFile.Catalog {
|
||||||
|
ce := configFile.Catalog[i]
|
||||||
|
|
||||||
|
cm, err := storage.Catalog.GetMeta(&ce)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Error("Unable to fetch catalog meta")
|
||||||
|
http.Error(w, "Unable to fetch catalog meta", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ce.Links = append(
|
||||||
|
ce.Links,
|
||||||
|
fetcher.Get(ce.Fetcher).Links(&ce.FetcherConfig)...,
|
||||||
|
)
|
||||||
|
ce.CatalogMeta = *cm
|
||||||
|
|
||||||
|
out[i] = ce
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := json.NewEncoder(w).Encode(out); err != nil {
|
||||||
|
log.WithError(err).Error("Unable to encode catalog entry list")
|
||||||
|
http.Error(w, "Unable to encode catalog meta", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleLog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
vars = mux.Vars(r)
|
||||||
|
name, tag = vars["name"], vars["tag"]
|
||||||
|
|
||||||
|
num, page = 25, 0
|
||||||
|
|
||||||
|
err error
|
||||||
|
logs []database.LogEntry
|
||||||
|
)
|
||||||
|
|
||||||
|
if v, err := strconv.Atoi(r.FormValue("num")); err == nil && v > 0 && v < 100 {
|
||||||
|
num = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := strconv.Atoi(r.FormValue("page")); err == nil && v >= 0 {
|
||||||
|
page = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "" && tag == "" {
|
||||||
|
logs, err = storage.Logs.List(num, page)
|
||||||
|
} else {
|
||||||
|
ce, err := configFile.CatalogEntryByTag(name, tag)
|
||||||
|
if errors.Is(err, config.ErrCatalogEntryNotFound) {
|
||||||
|
http.Error(w, "Not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err = storage.Logs.ListForCatalogEntry(&ce, num, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err = json.NewEncoder(w).Encode(logs); err != nil {
|
||||||
|
log.WithError(err).Error("Unable to encode logs")
|
||||||
|
http.Error(w, "Unable to encode logs", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleLogFeed(w http.ResponseWriter, r *http.Request) {} // FIXME
|
51
go.mod
Normal file
51
go.mod
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
module github.com/Luzifer/go-latestver
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Luzifer/go_helpers/v2 v2.13.0
|
||||||
|
github.com/Luzifer/rconfig/v2 v2.4.0
|
||||||
|
github.com/gorilla/mux v1.8.0
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
github.com/sirupsen/logrus v1.8.1
|
||||||
|
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||||
|
gorm.io/driver/mysql v1.2.0
|
||||||
|
gorm.io/driver/sqlite v1.2.4
|
||||||
|
gorm.io/gorm v1.22.3
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.4.16 // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 // indirect
|
||||||
|
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||||
|
github.com/antchfx/htmlquery v1.2.4 // indirect
|
||||||
|
github.com/antchfx/xpath v1.2.0 // indirect
|
||||||
|
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||||
|
github.com/emirpasic/gods v1.12.0 // indirect
|
||||||
|
github.com/go-git/gcfg v1.5.0 // indirect
|
||||||
|
github.com/go-git/go-billy/v5 v5.3.1 // indirect
|
||||||
|
github.com/go-git/go-git/v5 v5.4.2 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||||
|
github.com/imdario/mergo v0.3.12 // indirect
|
||||||
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.2 // indirect
|
||||||
|
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.9 // indirect
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
|
github.com/sergi/go-diff v1.1.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/src-d/gcfg v1.4.0 // indirect
|
||||||
|
github.com/xanzy/ssh-agent v0.3.0 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
|
||||||
|
golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79 // indirect
|
||||||
|
golang.org/x/text v0.3.5 // indirect
|
||||||
|
gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect
|
||||||
|
gopkg.in/validator.v2 v2.0.0-20210331031555-b37d688a7fb0 // indirect
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
)
|
269
go.sum
Normal file
269
go.sum
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/Luzifer/go_helpers v1.4.0 h1:Pmm058SbYewfnpP1CHda/zERoAqYoZFiBHF4l8k03Ko=
|
||||||
|
github.com/Luzifer/go_helpers v1.4.0/go.mod h1:5yUSe0FS7lIx1Uzmt0R3tdPFrSSaPfiCqaIA6u0Zn4Y=
|
||||||
|
github.com/Luzifer/go_helpers/v2 v2.13.0 h1:R/U4qEyf8J3kwBbOzFrE/ongBc4purK/gq2uwDgsDaI=
|
||||||
|
github.com/Luzifer/go_helpers/v2 v2.13.0/go.mod h1:HVMy8b4LwntHF8AtCf0LrAlYGRAumstLus8oK67e0hU=
|
||||||
|
github.com/Luzifer/rconfig/v2 v2.4.0 h1:MAdymTlExAZ8mx5VG8xOFAtFQSpWBipKYQHPOmYTn9o=
|
||||||
|
github.com/Luzifer/rconfig/v2 v2.4.0/go.mod h1:hWF3ZVSusbYlg5bEvCwalEyUSY+0JPJWUiIu7rBmav8=
|
||||||
|
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||||
|
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
|
||||||
|
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
|
||||||
|
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ=
|
||||||
|
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||||
|
github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk=
|
||||||
|
github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
|
||||||
|
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||||
|
github.com/antchfx/htmlquery v1.2.4 h1:qLteofCMe/KGovBI6SQgmou2QNyedFUW+pE+BpeZ494=
|
||||||
|
github.com/antchfx/htmlquery v1.2.4/go.mod h1:2xO6iu3EVWs7R2JYqBbp8YzG50gj/ofqs5/0VZoDZLc=
|
||||||
|
github.com/antchfx/xpath v1.2.0 h1:mbwv7co+x0RwgeGAOHdrKy89GvHaGvxxBtPK0uF9Zr8=
|
||||||
|
github.com/antchfx/xpath v1.2.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
|
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||||
|
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||||
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||||
|
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||||
|
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
|
||||||
|
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
|
||||||
|
github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||||
|
github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34=
|
||||||
|
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
|
||||||
|
github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0=
|
||||||
|
github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4=
|
||||||
|
github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||||
|
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||||
|
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
|
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
||||||
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
|
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
||||||
|
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
|
||||||
|
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
|
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck=
|
||||||
|
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leekchan/gtf v0.0.0-20190214083521-5fba33c5b00b/go.mod h1:thNruaSwydMhkQ8dXzapABF9Sc1Tz08ZBcDdgott9RA=
|
||||||
|
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
github.com/nxadm/tail v1.4.6/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
|
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
|
||||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
|
||||||
|
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
|
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||||
|
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
|
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||||
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||||
|
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
|
||||||
|
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
|
||||||
|
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
|
||||||
|
github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI=
|
||||||
|
github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
|
||||||
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||||
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs=
|
||||||
|
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
||||||
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79 h1:RX8C8PRZc2hTIod4ds8ij+/4RQX3AqhYj3uOHmyaz4E=
|
||||||
|
golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
|
||||||
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
|
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
|
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
|
||||||
|
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||||
|
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
|
||||||
|
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
|
||||||
|
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/validator.v2 v2.0.0-20210331031555-b37d688a7fb0 h1:EFLtLCwd8tGN+r/ePz3cvRtdsfYNhDEdt/vp6qsT+0A=
|
||||||
|
gopkg.in/validator.v2 v2.0.0-20210331031555-b37d688a7fb0/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc=
|
||||||
|
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||||
|
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.2.0 h1:l8+9VwjjyzEkw0PNPBOr2JHhLOGVk7XEnl5hk42bcvs=
|
||||||
|
gorm.io/driver/mysql v1.2.0/go.mod h1:4RQmTg4okPghdt+kbe6e1bTXIQp7Ny1NnBn/3Z6ghjk=
|
||||||
|
gorm.io/driver/sqlite v1.2.4 h1:jx16ESo1WzNjgBJNSbhEDoMKJnlhkU8BuBR2C0GC7D8=
|
||||||
|
gorm.io/driver/sqlite v1.2.4/go.mod h1:n8/CTEIEmo7lKrehQI4pd+rz6O514tMkBeCAR5UTXLs=
|
||||||
|
gorm.io/gorm v1.22.2/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||||
|
gorm.io/gorm v1.22.3 h1:/JS6z+GStEQvJNW3t1FTwJwG/gZ+A7crFdRqtvG5ehA=
|
||||||
|
gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
65
internal/config/config.go
Normal file
65
internal/config/config.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go-latestver/internal/database"
|
||||||
|
"github.com/Luzifer/go-latestver/internal/fetcher"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrCatalogEntryNotFound = errors.New("catalog entry not found")
|
||||||
|
|
||||||
|
type (
|
||||||
|
File struct {
|
||||||
|
Catalog []database.CatalogEntry `yaml:"catalog"`
|
||||||
|
CheckInterval time.Duration `yaml:"check_interval"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func New() *File {
|
||||||
|
return &File{
|
||||||
|
CheckInterval: time.Hour,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) CatalogEntryByTag(name, tag string) (database.CatalogEntry, error) {
|
||||||
|
for i := range f.Catalog {
|
||||||
|
ce := f.Catalog[i]
|
||||||
|
if ce.Name == name && ce.Tag == tag {
|
||||||
|
return ce, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.CatalogEntry{}, ErrCatalogEntryNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Load(filepath string) error {
|
||||||
|
fh, err := os.Open(filepath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "opening config file")
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
dec := yaml.NewDecoder(fh)
|
||||||
|
dec.KnownFields(true)
|
||||||
|
|
||||||
|
return errors.Wrap(dec.Decode(f), "decoding config")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) ValidateCatalog() error {
|
||||||
|
for i, ce := range f.Catalog {
|
||||||
|
f := fetcher.Get(ce.Fetcher)
|
||||||
|
if f == nil {
|
||||||
|
return errors.Errorf("catalog entry %d has unknown fetcher", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Validate(&ce.FetcherConfig); err != nil {
|
||||||
|
return errors.Wrapf(err, "catalog entry %d has invalid fetcher config", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
87
internal/database/db.go
Normal file
87
internal/database/db.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Client struct {
|
||||||
|
Catalog CatalogMetaStore
|
||||||
|
Logs LogStore
|
||||||
|
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
logwrap struct {
|
||||||
|
l *io.PipeWriter
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewClient(dbtype, dsn string) (*Client, error) {
|
||||||
|
c := &Client{}
|
||||||
|
c.Catalog = CatalogMetaStore{c}
|
||||||
|
c.Logs = LogStore{c}
|
||||||
|
|
||||||
|
dbLogger := logger.New(
|
||||||
|
&logwrap{log.StandardLogger().WriterLevel(log.TraceLevel)},
|
||||||
|
logger.Config{
|
||||||
|
SlowThreshold: time.Second,
|
||||||
|
LogLevel: logger.Silent,
|
||||||
|
IgnoreRecordNotFoundError: true,
|
||||||
|
Colorful: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
switch dbtype {
|
||||||
|
case "mysql":
|
||||||
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||||
|
Logger: dbLogger,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "opening mysql database")
|
||||||
|
}
|
||||||
|
c.db = db
|
||||||
|
|
||||||
|
case "sqlite", "sqlite3":
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
|
||||||
|
Logger: dbLogger,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "opening sqlite3 database")
|
||||||
|
}
|
||||||
|
c.db = db
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, errors.Errorf("invalid db type: %s", dbtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.initDB(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "initializing database")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) initDB() error {
|
||||||
|
for name, fn := range map[string]func() error{
|
||||||
|
"catalogMeta": c.Catalog.ensureTable,
|
||||||
|
"log": c.Logs.ensureTable,
|
||||||
|
} {
|
||||||
|
if err := fn(); err != nil {
|
||||||
|
return errors.Wrapf(err, "ensuring tables: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l logwrap) Printf(f string, v ...interface{}) { fmt.Fprintf(l.l, f, v...) }
|
24
internal/database/db_test.go
Normal file
24
internal/database/db_test.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func Test_CreateInvalidDatabase(t *testing.T) {
|
||||||
|
_, err := NewClient("invalid", "")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("client creation for type 'invalid' did not cause error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_CreateInaccessibleSqlite(t *testing.T) {
|
||||||
|
_, err := NewClient("sqlite", "/this/path/should/really/not/exist.db")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("client creation with unavailable sqlite path did not cause error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_CreateInaccessibleMYSQL(t *testing.T) {
|
||||||
|
_, err := NewClient("mysql", "user:pass@tcp(127.0.0.1:70000)/dbname?charset=utf8mb4&parseTime=True&loc=Local")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("client creation with unavailable mysql did not cause error")
|
||||||
|
}
|
||||||
|
}
|
120
internal/database/store.go
Normal file
120
internal/database/store.go
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
CatalogEntry struct {
|
||||||
|
Name string `json:"name" yaml:"name"`
|
||||||
|
Tag string `json:"tag" yaml:"tag"`
|
||||||
|
|
||||||
|
Fetcher string `json:"-" yaml:"fetcher"`
|
||||||
|
FetcherConfig fieldcollection.FieldCollection `json:"-" yaml:"fetcher_config"`
|
||||||
|
|
||||||
|
CheckInterval time.Duration `json:"-" yaml:"check_interval"`
|
||||||
|
|
||||||
|
Links []CatalogLink `json:"links" yaml:"links"`
|
||||||
|
|
||||||
|
CatalogMeta `yaml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CatalogLink struct {
|
||||||
|
IconClass string `json:"icon_class" yaml:"icon_class"`
|
||||||
|
Name string `json:"name" yaml:"name"`
|
||||||
|
URL string `json:"url" yaml:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CatalogMeta struct {
|
||||||
|
CatalogName string `gorm:"primaryKey" json:"-"`
|
||||||
|
CatalogTag string `gorm:"primaryKey" json:"-"`
|
||||||
|
CurrentVersion string `json:"current_version,omitempty"`
|
||||||
|
LastChecked *time.Time `json:"last_checked,omitempty"`
|
||||||
|
VersionTime *time.Time `json:"version_time,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
LogEntry struct {
|
||||||
|
CatalogName string `gorm:"index:catalog_key" json:"catalog_name"`
|
||||||
|
CatalogTag string `gorm:"index:catalog_key" json:"catalog_tag"`
|
||||||
|
Timestamp time.Time `gorm:"index:,sort:desc" json:"timestamp"`
|
||||||
|
VersionTo string `json:"version_to"`
|
||||||
|
VersionFrom string `json:"version_from"`
|
||||||
|
}
|
||||||
|
|
||||||
|
CatalogMetaStore struct {
|
||||||
|
c *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
LogStore struct {
|
||||||
|
c *Client
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c CatalogEntry) Key() string { return strings.Join([]string{c.Name, c.Tag}, ":") }
|
||||||
|
|
||||||
|
func (c CatalogMetaStore) GetMeta(ce *CatalogEntry) (*CatalogMeta, error) {
|
||||||
|
out := &CatalogMeta{
|
||||||
|
CatalogName: ce.Name,
|
||||||
|
CatalogTag: ce.Tag,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.c.db.
|
||||||
|
Where(out).
|
||||||
|
First(out).Error
|
||||||
|
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
// If there is no meta yet we just return empty meta
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, errors.Wrap(err, "querying metadata")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CatalogMetaStore) PutMeta(cm *CatalogMeta) error {
|
||||||
|
return errors.Wrap(
|
||||||
|
c.c.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(cm).Error,
|
||||||
|
"writing catalog meta",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CatalogMetaStore) ensureTable() error {
|
||||||
|
return errors.Wrap(c.c.db.AutoMigrate(&CatalogMeta{}), "applying migration")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LogStore) Add(le *LogEntry) error {
|
||||||
|
return errors.Wrap(
|
||||||
|
l.c.db.Create(le).Error,
|
||||||
|
"writing log entry",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LogStore) List(num, page int) ([]LogEntry, error) {
|
||||||
|
return l.listWithFilter(l.c.db, num, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LogStore) ListForCatalogEntry(ce *CatalogEntry, num, page int) ([]LogEntry, error) {
|
||||||
|
return l.listWithFilter(l.c.db.Where(&LogEntry{CatalogName: ce.Name, CatalogTag: ce.Tag}), num, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LogStore) listWithFilter(filter *gorm.DB, num, page int) ([]LogEntry, error) {
|
||||||
|
var out []LogEntry
|
||||||
|
return out, errors.Wrap(
|
||||||
|
filter.
|
||||||
|
Order("timestamp desc").
|
||||||
|
Limit(num).Offset(num*page).
|
||||||
|
Find(&out).
|
||||||
|
Error,
|
||||||
|
"fetching log entries",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LogStore) ensureTable() error {
|
||||||
|
return errors.Wrap(l.c.db.AutoMigrate(&LogEntry{}), "applying migration")
|
||||||
|
}
|
130
internal/database/store_test.go
Normal file
130
internal/database/store_test.go
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const sqlliteMemoryDSN = "file::memory:?cache=shared"
|
||||||
|
|
||||||
|
func Test_CatalogMetaStorage(t *testing.T) {
|
||||||
|
ttp := func(v time.Time) *time.Time { return &v }
|
||||||
|
|
||||||
|
dbc, err := NewClient("sqlite3", sqlliteMemoryDSN)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create database client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ce = CatalogEntry{Name: "testapp", Tag: "latest"}
|
||||||
|
cm = CatalogMeta{CatalogName: ce.Name, CatalogTag: ce.Tag, CurrentVersion: "1.0.0", LastChecked: ttp(time.Now())}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Empty fetch
|
||||||
|
fetchedCM, err := dbc.Catalog.GetMeta(&ce)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to retrieve catalog meta: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, check := range map[string]bool{
|
||||||
|
"name match": fetchedCM.CatalogName == ce.Name,
|
||||||
|
"tag match": fetchedCM.CatalogTag == ce.Tag,
|
||||||
|
"version empty": fetchedCM.CurrentVersion == "",
|
||||||
|
"date nil": fetchedCM.LastChecked == nil,
|
||||||
|
} {
|
||||||
|
if !check {
|
||||||
|
t.Errorf("check failed: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial set
|
||||||
|
if err = dbc.Catalog.PutMeta(&cm); err != nil {
|
||||||
|
t.Fatalf("unable to store catalog meta: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchedCM, err = dbc.Catalog.GetMeta(&ce)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to retrieve catalog meta: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, check := range map[string]bool{
|
||||||
|
"name match": fetchedCM.CatalogName == ce.Name,
|
||||||
|
"tag match": fetchedCM.CatalogTag == ce.Tag,
|
||||||
|
"version match": fetchedCM.CurrentVersion == cm.CurrentVersion,
|
||||||
|
"date match": fetchedCM.LastChecked.Equal(*cm.LastChecked),
|
||||||
|
} {
|
||||||
|
if !check {
|
||||||
|
t.Errorf("check failed: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update
|
||||||
|
cm.LastChecked = ttp(time.Now().Add(time.Hour)) // Compensate test running quite fast
|
||||||
|
cm.CurrentVersion = "1.1.0"
|
||||||
|
|
||||||
|
if err = dbc.Catalog.PutMeta(&cm); err != nil {
|
||||||
|
t.Fatalf("unable to update catalog meta: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchedCM, err = dbc.Catalog.GetMeta(&ce)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to retrieve catalog meta: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, check := range map[string]bool{
|
||||||
|
"name match": fetchedCM.CatalogName == ce.Name,
|
||||||
|
"tag match": fetchedCM.CatalogTag == ce.Tag,
|
||||||
|
"version match": fetchedCM.CurrentVersion == cm.CurrentVersion,
|
||||||
|
"date match": fetchedCM.LastChecked.Equal(*cm.LastChecked),
|
||||||
|
} {
|
||||||
|
if !check {
|
||||||
|
t.Errorf("check failed: %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_LogStorage(t *testing.T) {
|
||||||
|
dbc, err := NewClient("sqlite3", sqlliteMemoryDSN)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create database client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ce = CatalogEntry{Name: "testapp", Tag: "latest"}
|
||||||
|
rt = time.Now()
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, le := range []LogEntry{
|
||||||
|
{CatalogName: ce.Name, CatalogTag: ce.Tag, Timestamp: rt.Add(-3 * time.Hour), VersionFrom: "1.0.0", VersionTo: "1.1.0"},
|
||||||
|
{CatalogName: ce.Name, CatalogTag: ce.Tag, Timestamp: rt.Add(-1 * time.Hour), VersionFrom: "1.2.0", VersionTo: "1.3.0"},
|
||||||
|
{CatalogName: ce.Name, CatalogTag: ce.Tag, Timestamp: rt.Add(-2 * time.Hour), VersionFrom: "1.1.0", VersionTo: "1.2.0"},
|
||||||
|
{CatalogName: "anotherapp", CatalogTag: ce.Tag, Timestamp: rt.Add(-2 * time.Hour), VersionFrom: "5.2.0", VersionTo: "5.2.1"},
|
||||||
|
{CatalogName: "anotherapp", CatalogTag: ce.Tag, Timestamp: rt.Add(-1 * time.Hour), VersionFrom: "5.2.1", VersionTo: "6.0.0"},
|
||||||
|
} {
|
||||||
|
if err = dbc.Logs.Add(&le); err != nil {
|
||||||
|
t.Fatalf("unable to add log entry: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := dbc.Logs.ListForCatalogEntry(&ce, 100, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to fetch log entries for entry: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c := len(logs); c != 3 {
|
||||||
|
t.Errorf("got unexpected number of logs for entry: %d != 3", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !logs[2].Timestamp.Before(logs[1].Timestamp) || !logs[1].Timestamp.Before(logs[0].Timestamp) {
|
||||||
|
t.Error("log entries are not sorted descending")
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err = dbc.Logs.List(100, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to fetch log entries: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c := len(logs); c != 5 {
|
||||||
|
t.Errorf("got unexpected number of logs: %d != 5", c)
|
||||||
|
}
|
||||||
|
}
|
102
internal/fetcher/git.go
Normal file
102
internal/fetcher/git.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package fetcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go-latestver/internal/database"
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/go-git/go-git/v5"
|
||||||
|
"github.com/go-git/go-git/v5/config"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
|
"github.com/go-git/go-git/v5/storage/memory"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
GitTagFetcher struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { registerFetcher("git_tag", func() Fetcher { return &GitTagFetcher{} }) }
|
||||||
|
|
||||||
|
func (g GitTagFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
||||||
|
repo, err := git.Init(memory.NewStorage(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, errors.Wrap(err, "opening in-mem repo")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = repo.CreateRemote(&config.RemoteConfig{
|
||||||
|
Name: "origin",
|
||||||
|
URLs: []string{attrs.MustString("remote", nil)},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, errors.Wrap(err, "adding remote")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = repo.Fetch(&git.FetchOptions{
|
||||||
|
Depth: 1,
|
||||||
|
RefSpecs: []config.RefSpec{"+refs/tags/*:refs/remotes/origin/tags/*"},
|
||||||
|
RemoteName: "origin",
|
||||||
|
}); err != nil {
|
||||||
|
return "", time.Time{}, errors.Wrap(err, "fetching remote")
|
||||||
|
}
|
||||||
|
|
||||||
|
tags, err := repo.Tags()
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, errors.Wrap(err, "listing tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
latestTag *plumbing.Reference
|
||||||
|
latestTagTime time.Time
|
||||||
|
)
|
||||||
|
if err = tags.ForEach(func(ref *plumbing.Reference) error {
|
||||||
|
tt, err := g.tagRefToTime(repo, ref)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "fetching time for tag")
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestTag == nil || tt.After(latestTagTime) {
|
||||||
|
latestTag = ref
|
||||||
|
latestTagTime = tt
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return "", time.Time{}, errors.Wrap(err, "iterating tags")
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestTag == nil {
|
||||||
|
return "", time.Time{}, ErrNoVersionFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestTag.Name().Short(), latestTagTime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GitTagFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GitTagFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
||||||
|
if v, err := attrs.String("remote"); err != nil || v == "" {
|
||||||
|
return errors.New("remote is expected to be non-empty string")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GitTagFetcher) tagRefToTime(repo *git.Repository, tag *plumbing.Reference) (time.Time, error) {
|
||||||
|
tagObj, err := repo.TagObject(tag.Hash())
|
||||||
|
if err == nil {
|
||||||
|
// Annotated tag: Take the time of the tag
|
||||||
|
return tagObj.Tagger.When, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
commitObj, err := repo.CommitObject(tag.Hash())
|
||||||
|
if err == nil {
|
||||||
|
// Lightweight tag: Take the time of the commit
|
||||||
|
return commitObj.Committer.When, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Time{}, errors.New("reference points neither to tag nor to commit")
|
||||||
|
}
|
45
internal/fetcher/git_test.go
Normal file
45
internal/fetcher/git_test.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package fetcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_GitTagFetcher(t *testing.T) {
|
||||||
|
attrs := fieldcollection.FieldCollectionFromData(map[string]interface{}{
|
||||||
|
"remote": "https://github.com/WordPress/WordPress.git", // This is a huge repo and serves as benchmark too
|
||||||
|
})
|
||||||
|
|
||||||
|
f := Get("git_tag")
|
||||||
|
|
||||||
|
if err := f.Validate(attrs); err != nil {
|
||||||
|
t.Fatalf("validating attributes: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ver, _, err := f.FetchVersion(context.Background(), attrs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("fetching version: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uses tag format: 1.0.0
|
||||||
|
if len(ver) < 5 {
|
||||||
|
t.Errorf("version has unexpected format: %s != X.X.X", ver)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("found tag: %s", ver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GitTagFetcherInvalid(t *testing.T) {
|
||||||
|
attrs := fieldcollection.FieldCollectionFromData(map[string]interface{}{
|
||||||
|
"remote": "https://example.com/example.git",
|
||||||
|
})
|
||||||
|
|
||||||
|
f := Get("git_tag")
|
||||||
|
|
||||||
|
_, _, err := f.FetchVersion(context.Background(), attrs)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("fetching version dit not cause error")
|
||||||
|
}
|
||||||
|
}
|
98
internal/fetcher/github.go
Normal file
98
internal/fetcher/github.go
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
package fetcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go-latestver/internal/database"
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const githubHTTPTimeout = 2 * time.Second
|
||||||
|
|
||||||
|
type (
|
||||||
|
GithubReleaseFetcher struct{}
|
||||||
|
|
||||||
|
githubRelease struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
PublishedAt time.Time `json:"published_at"`
|
||||||
|
Prerelease bool `json:"prerelease"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { registerFetcher("github_release", func() Fetcher { return &GithubReleaseFetcher{} }) }
|
||||||
|
|
||||||
|
func (g GithubReleaseFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, githubHTTPTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
fmt.Sprintf("https://api.github.com/repos/%s/releases", attrs.MustString("repository", nil)),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, errors.Wrap(err, "creating http request")
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "Luzifer/go-latestver GithubReleaseFetcher")
|
||||||
|
|
||||||
|
if os.Getenv("GITHUB_CLIENT_ID") != "" && os.Getenv("GITHUB_CLIENT_SECRET") != "" {
|
||||||
|
req.SetBasicAuth(os.Getenv("GITHUB_CLIENT_ID"), os.Getenv("GITHUB_CLIENT_SECRET"))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, errors.Wrap(err, "executing request")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", time.Time{}, errors.Errorf("unexpected HTTP status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var payload []githubRelease
|
||||||
|
if err = json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||||
|
return "", time.Time{}, errors.Wrap(err, "decoding response")
|
||||||
|
}
|
||||||
|
|
||||||
|
var release *githubRelease
|
||||||
|
for i := range payload {
|
||||||
|
if payload[i].Prerelease {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if release == nil || release.PublishedAt.Before(payload[i].PublishedAt) {
|
||||||
|
release = &payload[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if release == nil {
|
||||||
|
return "", time.Time{}, ErrNoVersionFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return release.TagName, release.PublishedAt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GithubReleaseFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink {
|
||||||
|
return []database.CatalogLink{
|
||||||
|
{
|
||||||
|
IconClass: "fab fa-github",
|
||||||
|
Name: "Repository",
|
||||||
|
URL: fmt.Sprintf("https://github.com/%s", attrs.MustString("repository", nil)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g GithubReleaseFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
||||||
|
if v, err := attrs.String("repository"); err != nil || v == "" {
|
||||||
|
return errors.New("repository is expected to be non-empty string")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
45
internal/fetcher/github_test.go
Normal file
45
internal/fetcher/github_test.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package fetcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_GithubReleaseFetcher(t *testing.T) {
|
||||||
|
attrs := fieldcollection.FieldCollectionFromData(map[string]interface{}{
|
||||||
|
"repository": "Luzifer/korvike",
|
||||||
|
})
|
||||||
|
|
||||||
|
f := Get("github_release")
|
||||||
|
|
||||||
|
if err := f.Validate(attrs); err != nil {
|
||||||
|
t.Fatalf("validating attributes: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ver, _, err := f.FetchVersion(context.Background(), attrs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("fetching version: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uses tag format: v1.0.0
|
||||||
|
if len(ver) < 6 || ver[0] != 'v' {
|
||||||
|
t.Errorf("version has unexpected format: %s != vX.X.X", ver)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("found release: %s", ver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_GithubReleaseFetcherInvalid(t *testing.T) {
|
||||||
|
attrs := fieldcollection.FieldCollectionFromData(map[string]interface{}{
|
||||||
|
"repository": "Luzifer/thiswillneverexist",
|
||||||
|
})
|
||||||
|
|
||||||
|
f := Get("github_release")
|
||||||
|
|
||||||
|
_, _, err := f.FetchVersion(context.Background(), attrs)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("fetching version dit not cause error")
|
||||||
|
}
|
||||||
|
}
|
81
internal/fetcher/html.go
Normal file
81
internal/fetcher/html.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package fetcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go-latestver/internal/database"
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/antchfx/htmlquery"
|
||||||
|
"github.com/antchfx/xpath"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/net/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
var htmlFetcherDefaultRegex = `(v?(?:[0-9]+\.?){2,})`
|
||||||
|
|
||||||
|
type (
|
||||||
|
HTMLFetcher struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() { registerFetcher("html", func() Fetcher { return &HTMLFetcher{} }) }
|
||||||
|
|
||||||
|
func (h HTMLFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
||||||
|
doc, err := htmlquery.LoadURL(attrs.MustString("url", nil))
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, errors.Wrap(err, "loading URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
node, err := htmlquery.Query(doc, attrs.MustString("xpath", nil))
|
||||||
|
if err != nil {
|
||||||
|
return "", time.Time{}, errors.Wrap(err, "querying xpath")
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Type == html.ElementNode && node.FirstChild.Type == html.TextNode {
|
||||||
|
node = node.FirstChild
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Type != html.TextNode {
|
||||||
|
return "", time.Time{}, errors.Errorf("xpath expression lead to unexpected node type: %d", node.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
match := regexp.MustCompile(attrs.MustString("regex", &htmlFetcherDefaultRegex)).FindStringSubmatch(node.Data)
|
||||||
|
if len(match) < 2 {
|
||||||
|
return "", time.Time{}, errors.New("regular expression did not yield version")
|
||||||
|
}
|
||||||
|
|
||||||
|
return match[1], time.Now(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HTMLFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink {
|
||||||
|
return []database.CatalogLink{
|
||||||
|
{
|
||||||
|
IconClass: "fas fa-globe",
|
||||||
|
Name: "Website",
|
||||||
|
URL: attrs.MustString("url", nil),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HTMLFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
||||||
|
if v, err := attrs.String("url"); err != nil || v == "" {
|
||||||
|
return errors.New("url is expected to be non-empty string")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, err := attrs.String("xpath"); err != nil || v == "" {
|
||||||
|
return errors.New("xpath is expected to be non-empty string")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := xpath.Compile(attrs.MustString("xpath", nil)); err != nil {
|
||||||
|
return errors.Wrap(err, "compiling xpath expression")
|
||||||
|
}
|
||||||
|
|
||||||
|
if attrs.CanString("regex") {
|
||||||
|
if _, err := regexp.Compile(attrs.MustString("regex", nil)); err != nil {
|
||||||
|
return errors.Wrap(err, "invalid regex given")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
46
internal/fetcher/interface.go
Normal file
46
internal/fetcher/interface.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package fetcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go-latestver/internal/database"
|
||||||
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Fetcher interface {
|
||||||
|
FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error)
|
||||||
|
Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink
|
||||||
|
Validate(attrs *fieldcollection.FieldCollection) error
|
||||||
|
}
|
||||||
|
FetcherCreate func() Fetcher
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNoVersionFound = errors.New("no version found")
|
||||||
|
|
||||||
|
availableFetchers = map[string]FetcherCreate{}
|
||||||
|
availableFetchersLock sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerFetcher(name string, fn FetcherCreate) {
|
||||||
|
availableFetchersLock.Lock()
|
||||||
|
defer availableFetchersLock.Unlock()
|
||||||
|
|
||||||
|
availableFetchers[name] = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(name string) Fetcher {
|
||||||
|
availableFetchersLock.RLock()
|
||||||
|
defer availableFetchersLock.RUnlock()
|
||||||
|
|
||||||
|
fn, ok := availableFetchers[name]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn()
|
||||||
|
}
|
92
main.go
Normal file
92
main.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go-latestver/internal/config"
|
||||||
|
"github.com/Luzifer/go-latestver/internal/database"
|
||||||
|
httpHelper "github.com/Luzifer/go_helpers/v2/http"
|
||||||
|
"github.com/Luzifer/rconfig/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cfg = struct {
|
||||||
|
Config string `flag:"config,c" default:"config.yaml" description:"Configuration file with catalog entries"`
|
||||||
|
Listen string `flag:"listen" default:":3000" description:"Port/IP to listen on"`
|
||||||
|
LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"`
|
||||||
|
MaxJitter time.Duration `flag:"max-jitter" default:"30m" description:"Maximum jitter to add to the check interval for load balancing"`
|
||||||
|
Storage string `flag:"storage" default:"sqlite" description:"Storage adapter to use (mysql, sqlite)"`
|
||||||
|
StorageDSN string `flag:"storage-dsn" default:"file::memory:?cache=shared" description:"DSN to connect to the database"`
|
||||||
|
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
configFile = config.New()
|
||||||
|
storage *database.Client
|
||||||
|
|
||||||
|
version = "dev"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initApp() {
|
||||||
|
rconfig.AutoEnv(true)
|
||||||
|
if err := rconfig.ParseAndValidate(&cfg); err != nil {
|
||||||
|
log.Fatalf("Unable to parse commandline options: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.VersionAndExit {
|
||||||
|
fmt.Printf("go-latestver %s\n", version)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if l, err := log.ParseLevel(cfg.LogLevel); err != nil {
|
||||||
|
log.WithError(err).Fatal("Unable to parse log level")
|
||||||
|
} else {
|
||||||
|
log.SetLevel(l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
initApp()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if err = configFile.Load(cfg.Config); err != nil {
|
||||||
|
log.WithError(err).Fatal("Unable to load configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = configFile.ValidateCatalog(); err != nil {
|
||||||
|
log.WithError(err).Fatal("Configuration is not valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
storage, err = database.NewClient(cfg.Storage, cfg.StorageDSN)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Fatal("Unable to connect to database")
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler := cron.New()
|
||||||
|
scheduler.AddFunc("@every 1m", schedulerRun)
|
||||||
|
scheduler.Start()
|
||||||
|
|
||||||
|
router := mux.NewRouter()
|
||||||
|
router.HandleFunc("/v1/catalog", handleCatalogList).Methods(http.MethodGet)
|
||||||
|
router.HandleFunc("/v1/catalog/{name}/{tag}", handleCatalogGet).Methods(http.MethodGet)
|
||||||
|
router.HandleFunc("/v1/catalog/{name}/{tag}/log", handleLog).Methods(http.MethodGet)
|
||||||
|
router.HandleFunc("/v1/catalog/{name}/{tag}/log.rss", handleLogFeed).Methods(http.MethodGet)
|
||||||
|
router.HandleFunc("/v1/catalog/{name}/{tag}/version", handleCatalogGetVersion).Methods(http.MethodGet)
|
||||||
|
router.HandleFunc("/v1/log", handleLog).Methods(http.MethodGet)
|
||||||
|
router.HandleFunc("/v1/log.rss", handleLogFeed).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
var handler http.Handler = router
|
||||||
|
handler = httpHelper.GzipHandler(handler)
|
||||||
|
handler = httpHelper.NewHTTPLogHandler(handler)
|
||||||
|
|
||||||
|
if err := http.ListenAndServe(cfg.Listen, handler); err != nil {
|
||||||
|
log.WithError(err).Fatal("HTTP server exited unclean")
|
||||||
|
}
|
||||||
|
}
|
89
scheduler.go
Normal file
89
scheduler.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go-latestver/internal/database"
|
||||||
|
"github.com/Luzifer/go-latestver/internal/fetcher"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var schedulerRunActive bool
|
||||||
|
|
||||||
|
func schedulerRun() {
|
||||||
|
if schedulerRunActive {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
schedulerRunActive = true
|
||||||
|
defer func() { schedulerRunActive = false }()
|
||||||
|
|
||||||
|
for _, ce := range configFile.Catalog {
|
||||||
|
if err := checkForUpdates(&ce); err != nil {
|
||||||
|
log.WithField("entry", ce.Key()).WithError(err).Error("Unable to update entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForUpdates(ce *database.CatalogEntry) error {
|
||||||
|
logger := log.WithField("entry", ce.Key())
|
||||||
|
|
||||||
|
cm, err := storage.Catalog.GetMeta(ce)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "getting catalog meta")
|
||||||
|
}
|
||||||
|
|
||||||
|
checkTime := time.Now()
|
||||||
|
if cm.LastChecked != nil {
|
||||||
|
checkTime = *cm.LastChecked
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case ce.CheckInterval > 0:
|
||||||
|
checkTime = checkTime.Add(ce.CheckInterval)
|
||||||
|
|
||||||
|
case configFile.CheckInterval > 0:
|
||||||
|
checkTime = checkTime.Add(configFile.CheckInterval)
|
||||||
|
|
||||||
|
default:
|
||||||
|
checkTime = checkTime.Add(time.Hour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkTime.After(time.Now()) {
|
||||||
|
// Not yet ready to check
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug("Checking for updates")
|
||||||
|
|
||||||
|
ver, vertime, err := fetcher.Get(ce.Fetcher).FetchVersion(context.Background(), &ce.FetcherConfig)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "fetching version")
|
||||||
|
}
|
||||||
|
|
||||||
|
ver = strings.TrimPrefix(ver, "v")
|
||||||
|
|
||||||
|
if cm.CurrentVersion != ver {
|
||||||
|
if err = storage.Logs.Add(&database.LogEntry{
|
||||||
|
CatalogName: ce.Name,
|
||||||
|
CatalogTag: ce.Tag,
|
||||||
|
Timestamp: vertime,
|
||||||
|
VersionTo: ver,
|
||||||
|
VersionFrom: cm.CurrentVersion,
|
||||||
|
}); err != nil {
|
||||||
|
return errors.Wrap(err, "adding log entry")
|
||||||
|
}
|
||||||
|
logger.WithFields(log.Fields{
|
||||||
|
"from": cm.CurrentVersion,
|
||||||
|
"to": ver,
|
||||||
|
}).Info("Entry had version update")
|
||||||
|
cm.VersionTime = func(v time.Time) *time.Time { return &v }(vertime)
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.CurrentVersion = ver
|
||||||
|
cm.LastChecked = func(v time.Time) *time.Time { return &v }(time.Now())
|
||||||
|
|
||||||
|
return errors.Wrap(storage.Catalog.PutMeta(cm), "updating meta entry")
|
||||||
|
}
|
Loading…
Reference in a new issue