mirror of
https://github.com/Luzifer/go-latestver.git
synced 2024-12-20 10:31:16 +00:00
Lint: Update linter config, fix all linter issues
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
f00c91767a
commit
9255fc6898
18 changed files with 308 additions and 96 deletions
182
.golangci.yml
182
.golangci.yml
|
@ -11,6 +11,61 @@ run:
|
||||||
output:
|
output:
|
||||||
format: tab
|
format: tab
|
||||||
|
|
||||||
|
issues:
|
||||||
|
# This disables the included exclude-list in golangci-lint as that
|
||||||
|
# list for example fully hides G304 gosec rule, errcheck, exported
|
||||||
|
# rule of revive and other errors one really wants to see.
|
||||||
|
# Smme detail: https://github.com/golangci/golangci-lint/issues/456
|
||||||
|
exclude-use-default: false
|
||||||
|
# Don't limit the number of shown issues: Report ALL of them
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
|
|
||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false]
|
||||||
|
- bidichk # Checks for dangerous unicode character sequences [fast: true, auto-fix: false]
|
||||||
|
- bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false]
|
||||||
|
- containedctx # containedctx is a linter that detects struct contained context.Context field [fast: true, auto-fix: false]
|
||||||
|
- contextcheck # check the function whether use a non-inherited context [fast: false, auto-fix: false]
|
||||||
|
- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
|
||||||
|
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
|
||||||
|
- durationcheck # check for two durations multiplied together [fast: false, auto-fix: false]
|
||||||
|
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false]
|
||||||
|
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted. [fast: false, auto-fix: false]
|
||||||
|
- exportloopref # checks for pointers to enclosing loop variables [fast: true, auto-fix: false]
|
||||||
|
- forbidigo # Forbids identifiers [fast: true, auto-fix: false]
|
||||||
|
- funlen # Tool for detection of long functions [fast: true, auto-fix: false]
|
||||||
|
- gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
|
||||||
|
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
|
||||||
|
- gocritic # The most opinionated Go source code linter [fast: true, auto-fix: false]
|
||||||
|
- gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false]
|
||||||
|
- godox # Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false]
|
||||||
|
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true]
|
||||||
|
- gofumpt # Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true]
|
||||||
|
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true]
|
||||||
|
- gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false]
|
||||||
|
- gosec # Inspects source code for security problems [fast: true, auto-fix: false]
|
||||||
|
- gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false]
|
||||||
|
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false]
|
||||||
|
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
|
||||||
|
- misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true]
|
||||||
|
- nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false]
|
||||||
|
- nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false]
|
||||||
|
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. [fast: false, auto-fix: false]
|
||||||
|
- noctx # noctx finds sending http request without context.Context [fast: true, auto-fix: false]
|
||||||
|
- nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false]
|
||||||
|
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false]
|
||||||
|
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false]
|
||||||
|
- stylecheck # Stylecheck is a replacement for golint [fast: true, auto-fix: false]
|
||||||
|
- tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 [fast: false, auto-fix: false]
|
||||||
|
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false]
|
||||||
|
- unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false]
|
||||||
|
- unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
|
||||||
|
- wastedassign # wastedassign finds wasted assignment statements. [fast: false, auto-fix: false]
|
||||||
|
- wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false]
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
forbidigo:
|
forbidigo:
|
||||||
forbid:
|
forbid:
|
||||||
|
@ -29,41 +84,96 @@ linters-settings:
|
||||||
mnd:
|
mnd:
|
||||||
ignored-functions: 'strconv.(?:Format|Parse)\B+'
|
ignored-functions: 'strconv.(?:Format|Parse)\B+'
|
||||||
|
|
||||||
linters:
|
revive:
|
||||||
disable-all: true
|
rules:
|
||||||
enable:
|
#- name: add-constant # Suggests using constant for magic numbers and string literals
|
||||||
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false]
|
# Opinion: Makes sense for strings, not for numbers but checks numbers
|
||||||
- bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false]
|
#- name: argument-limit # Specifies the maximum number of arguments a function can receive | Opinion: Don't need this
|
||||||
- deadcode # Finds unused code [fast: true, auto-fix: false]
|
- name: atomic # Check for common mistaken usages of the `sync/atomic` package
|
||||||
- depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false]
|
- name: banned-characters # Checks banned characters in identifiers
|
||||||
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false]
|
arguments:
|
||||||
- exportloopref # checks for pointers to enclosing loop variables [fast: true, auto-fix: false]
|
- ';' # Greek question mark
|
||||||
- forbidigo # Forbids identifiers [fast: true, auto-fix: false]
|
- name: bare-return # Warns on bare returns
|
||||||
- funlen # Tool for detection of long functions [fast: true, auto-fix: false]
|
- name: blank-imports # Disallows blank imports
|
||||||
- gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false]
|
- name: bool-literal-in-expr # Suggests removing Boolean literals from logic expressions
|
||||||
- goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false]
|
- name: call-to-gc # Warns on explicit call to the garbage collector
|
||||||
- gocritic # The most opinionated Go source code linter [fast: true, auto-fix: false]
|
#- name: cognitive-complexity # Sets restriction for maximum Cognitive complexity.
|
||||||
- gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false]
|
# There is a dedicated linter for this
|
||||||
- godox # Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false]
|
- name: confusing-naming # Warns on methods with names that differ only by capitalization
|
||||||
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true]
|
- name: confusing-results # Suggests to name potentially confusing function results
|
||||||
- gofumpt # Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true]
|
- name: constant-logical-expr # Warns on constant logical expressions
|
||||||
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true]
|
- name: context-as-argument # `context.Context` should be the first argument of a function.
|
||||||
- gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false]
|
- name: context-keys-type # Disallows the usage of basic types in `context.WithValue`.
|
||||||
- gosec # Inspects source code for security problems [fast: true, auto-fix: false]
|
#- name: cyclomatic # Sets restriction for maximum Cyclomatic complexity.
|
||||||
- gosimple # Linter for Go source code that specializes in simplifying a code [fast: true, auto-fix: false]
|
# There is a dedicated linter for this
|
||||||
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true, auto-fix: false]
|
#- name: datarace # Spots potential dataraces
|
||||||
- ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
|
# Is not (yet) available?
|
||||||
- misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true]
|
- name: deep-exit # Looks for program exits in funcs other than `main()` or `init()`
|
||||||
- nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false]
|
- name: defer # Warns on some [defer gotchas](https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-iii-36a1ab3d6ef1)
|
||||||
- noctx # noctx finds sending http request without context.Context [fast: true, auto-fix: false]
|
- name: dot-imports # Forbids `.` imports.
|
||||||
- nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false]
|
- name: duplicated-imports # Looks for packages that are imported two or more times
|
||||||
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false]
|
- name: early-return # Spots if-then-else statements that can be refactored to simplify code reading
|
||||||
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: true, auto-fix: false]
|
- name: empty-block # Warns on empty code blocks
|
||||||
- structcheck # Finds unused struct fields [fast: true, auto-fix: false]
|
- name: empty-lines # Warns when there are heading or trailing newlines in a block
|
||||||
- stylecheck # Stylecheck is a replacement for golint [fast: true, auto-fix: false]
|
- name: errorf # Should replace `errors.New(fmt.Sprintf())` with `fmt.Errorf()`
|
||||||
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: true, auto-fix: false]
|
- name: error-naming # Naming of error variables.
|
||||||
- unconvert # Remove unnecessary type conversions [fast: true, auto-fix: false]
|
- name: error-return # The error return parameter should be last.
|
||||||
- unused # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]
|
- name: error-strings # Conventions around error strings.
|
||||||
- varcheck # Finds unused global variables and constants [fast: true, auto-fix: false]
|
- name: exported # Naming and commenting conventions on exported symbols.
|
||||||
|
arguments: ['sayRepetitiveInsteadOfStutters']
|
||||||
|
#- name: file-header # Header which each file should have.
|
||||||
|
# Useless without config, have no config for it
|
||||||
|
- name: flag-parameter # Warns on boolean parameters that create a control coupling
|
||||||
|
#- name: function-length # Warns on functions exceeding the statements or lines max
|
||||||
|
# There is a dedicated linter for this
|
||||||
|
#- name: function-result-limit # Specifies the maximum number of results a function can return
|
||||||
|
# Opinion: Don't need this
|
||||||
|
- name: get-return # Warns on getters that do not yield any result
|
||||||
|
- name: identical-branches # Spots if-then-else statements with identical `then` and `else` branches
|
||||||
|
- name: if-return # Redundant if when returning an error.
|
||||||
|
#- name: imports-blacklist # Disallows importing the specified packages
|
||||||
|
# Useless without config, have no config for it
|
||||||
|
- name: import-shadowing # Spots identifiers that shadow an import
|
||||||
|
- name: increment-decrement # Use `i++` and `i--` instead of `i += 1` and `i -= 1`.
|
||||||
|
- name: indent-error-flow # Prevents redundant else statements.
|
||||||
|
#- name: line-length-limit # Specifies the maximum number of characters in a lined
|
||||||
|
# There is a dedicated linter for this
|
||||||
|
#- name: max-public-structs # The maximum number of public structs in a file.
|
||||||
|
# Opinion: Don't need this
|
||||||
|
- name: modifies-parameter # Warns on assignments to function parameters
|
||||||
|
- name: modifies-value-receiver # Warns on assignments to value-passed method receivers
|
||||||
|
#- name: nested-structs # Warns on structs within structs
|
||||||
|
# Opinion: Don't need this
|
||||||
|
- name: optimize-operands-order # Checks inefficient conditional expressions
|
||||||
|
#- name: package-comments # Package commenting conventions.
|
||||||
|
# Opinion: Don't need this
|
||||||
|
- name: range # Prevents redundant variables when iterating over a collection.
|
||||||
|
- name: range-val-address # Warns if address of range value is used dangerously
|
||||||
|
- name: range-val-in-closure # Warns if range value is used in a closure dispatched as goroutine
|
||||||
|
- name: receiver-naming # Conventions around the naming of receivers.
|
||||||
|
- name: redefines-builtin-id # Warns on redefinitions of builtin identifiers
|
||||||
|
#- name: string-format # Warns on specific string literals that fail one or more user-configured regular expressions
|
||||||
|
# Useless without config, have no config for it
|
||||||
|
- name: string-of-int # Warns on suspicious casts from int to string
|
||||||
|
- name: struct-tag # Checks common struct tags like `json`,`xml`,`yaml`
|
||||||
|
- name: superfluous-else # Prevents redundant else statements (extends indent-error-flow)
|
||||||
|
- name: time-equal # Suggests to use `time.Time.Equal` instead of `==` and `!=` for equality check time.
|
||||||
|
- name: time-naming # Conventions around the naming of time variables.
|
||||||
|
- name: unconditional-recursion # Warns on function calls that will lead to (direct) infinite recursion
|
||||||
|
- name: unexported-naming # Warns on wrongly named un-exported symbols
|
||||||
|
- name: unexported-return # Warns when a public return is from unexported type.
|
||||||
|
- name: unhandled-error # Warns on unhandled errors returned by funcion calls
|
||||||
|
arguments:
|
||||||
|
- "fmt.(Fp|P)rint(f|ln|)"
|
||||||
|
- name: unnecessary-stmt # Suggests removing or simplifying unnecessary statements
|
||||||
|
- name: unreachable-code # Warns on unreachable code
|
||||||
|
- name: unused-parameter # Suggests to rename or remove unused function parameters
|
||||||
|
- name: unused-receiver # Suggests to rename or remove unused method receivers
|
||||||
|
#- name: use-any # Proposes to replace `interface{}` with its alias `any`
|
||||||
|
# Is not (yet) available?
|
||||||
|
- name: useless-break # Warns on useless `break` statements in case clauses
|
||||||
|
- name: var-declaration # Reduces redundancies around variable declaration.
|
||||||
|
- name: var-naming # Naming rules.
|
||||||
|
- name: waitgroup-by-value # Warns on functions taking sync.WaitGroup as a by-value parameter
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
12
api.go
12
api.go
|
@ -22,7 +22,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
APICatalogEntry struct {
|
apiCatalogEntry struct {
|
||||||
database.CatalogEntry
|
database.CatalogEntry
|
||||||
database.CatalogMeta
|
database.CatalogMeta
|
||||||
}
|
}
|
||||||
|
@ -35,10 +35,10 @@ func buildFullURL(u *url.URL, _ error) string {
|
||||||
}, "/")
|
}, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func catalogEntryToAPICatalogEntry(ce database.CatalogEntry) (APICatalogEntry, error) {
|
func catalogEntryToAPICatalogEntry(ce database.CatalogEntry) (apiCatalogEntry, error) {
|
||||||
cm, err := storage.Catalog.GetMeta(&ce)
|
cm, err := storage.Catalog.GetMeta(&ce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return APICatalogEntry{}, errors.Wrap(err, "fetching catalog meta")
|
return apiCatalogEntry{}, errors.Wrap(err, "fetching catalog meta")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, l := range fetcher.Get(ce.Fetcher).Links(ce.FetcherConfig) {
|
for _, l := range fetcher.Get(ce.Fetcher).Links(ce.FetcherConfig) {
|
||||||
|
@ -55,7 +55,7 @@ func catalogEntryToAPICatalogEntry(ce database.CatalogEntry) (APICatalogEntry, e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return APICatalogEntry{CatalogEntry: ce, CatalogMeta: *cm}, nil
|
return apiCatalogEntry{CatalogEntry: ce, CatalogMeta: *cm}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleBadgeRedirect(w http.ResponseWriter, r *http.Request) {
|
func handleBadgeRedirect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -142,8 +142,8 @@ func handleCatalogGetVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprint(w, cm.CurrentVersion)
|
fmt.Fprint(w, cm.CurrentVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCatalogList(w http.ResponseWriter, r *http.Request) {
|
func handleCatalogList(w http.ResponseWriter, _ *http.Request) {
|
||||||
out := make([]APICatalogEntry, len(configFile.Catalog))
|
out := make([]apiCatalogEntry, len(configFile.Catalog))
|
||||||
|
|
||||||
for i := range configFile.Catalog {
|
for i := range configFile.Catalog {
|
||||||
ce := configFile.Catalog[i]
|
ce := configFile.Catalog[i]
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/Luzifer/go-latestver/internal/helpers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleSinglePage(w http.ResponseWriter, r *http.Request) {
|
func handleSinglePage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -19,16 +21,15 @@ func handleSinglePage(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range []string{
|
for _, frontendPath := range []string{
|
||||||
filepath.Join("frontend", path.Base(urlPath)),
|
filepath.Join("frontend", path.Base(urlPath)),
|
||||||
filepath.Join("frontend", "index.html"),
|
filepath.Join("frontend", "index.html"),
|
||||||
} {
|
} {
|
||||||
f, err := frontendFS.Open(path)
|
f, err := frontendFS.Open(frontendPath)
|
||||||
switch {
|
switch {
|
||||||
|
|
||||||
case err == nil:
|
case err == nil:
|
||||||
// file is opened, serve it
|
// file is opened, serve it
|
||||||
defer f.Close()
|
defer func() { helpers.LogIfErr(f.Close(), "closing frontend file after serve") }() //revive:disable-line:defer Fine here as it will only open one file
|
||||||
|
|
||||||
stat, err := f.Stat()
|
stat, err := f.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package config holds the definition of the configuration file and
|
||||||
|
// some methods to load and validate it
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -9,23 +11,30 @@ import (
|
||||||
|
|
||||||
"github.com/Luzifer/go-latestver/internal/database"
|
"github.com/Luzifer/go-latestver/internal/database"
|
||||||
"github.com/Luzifer/go-latestver/internal/fetcher"
|
"github.com/Luzifer/go-latestver/internal/fetcher"
|
||||||
|
"github.com/Luzifer/go-latestver/internal/helpers"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrCatalogEntryNotFound signalizes a catalog entry with the given
|
||||||
|
// name was not found
|
||||||
var ErrCatalogEntryNotFound = errors.New("catalog entry not found")
|
var ErrCatalogEntryNotFound = errors.New("catalog entry not found")
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// File represents the configuration file content
|
||||||
File struct {
|
File struct {
|
||||||
Catalog []database.CatalogEntry `yaml:"catalog"`
|
Catalog []database.CatalogEntry `yaml:"catalog"`
|
||||||
CheckInterval time.Duration `yaml:"check_interval"`
|
CheckInterval time.Duration `yaml:"check_interval"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// New creates a new empty File object with defaults
|
||||||
func New() *File {
|
func New() *File {
|
||||||
return &File{
|
return &File{
|
||||||
CheckInterval: time.Hour,
|
CheckInterval: time.Hour,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CatalogEntryByTag retrieves a catalog entry by its name or returns
|
||||||
|
// and ErrCatalogEntryNotFound if it does not exist
|
||||||
func (f File) CatalogEntryByTag(name, tag string) (database.CatalogEntry, error) {
|
func (f File) CatalogEntryByTag(name, tag string) (database.CatalogEntry, error) {
|
||||||
for i := range f.Catalog {
|
for i := range f.Catalog {
|
||||||
ce := f.Catalog[i]
|
ce := f.Catalog[i]
|
||||||
|
@ -37,12 +46,14 @@ func (f File) CatalogEntryByTag(name, tag string) (database.CatalogEntry, error)
|
||||||
return database.CatalogEntry{}, ErrCatalogEntryNotFound
|
return database.CatalogEntry{}, ErrCatalogEntryNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load loads the contents of the configuration file in the filesystem
|
||||||
|
// into the File object
|
||||||
func (f *File) Load(filepath string) error {
|
func (f *File) Load(filepath string) error {
|
||||||
fh, err := os.Open(filepath)
|
fh, err := os.Open(filepath) //#nosec:G304 // As this is the config file and needs a location this is fine
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "opening config file")
|
return errors.Wrap(err, "opening config file")
|
||||||
}
|
}
|
||||||
defer fh.Close()
|
defer func() { helpers.LogIfErr(fh.Close(), "closing config after load") }()
|
||||||
|
|
||||||
dec := yaml.NewDecoder(fh)
|
dec := yaml.NewDecoder(fh)
|
||||||
dec.KnownFields(true)
|
dec.KnownFields(true)
|
||||||
|
@ -50,14 +61,16 @@ func (f *File) Load(filepath string) error {
|
||||||
return errors.Wrap(dec.Decode(f), "decoding config")
|
return errors.Wrap(dec.Decode(f), "decoding config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidateCatalog checks whether invalid fetchers are used or the
|
||||||
|
// configuration of the fetcher is not suitable for the given fetcher
|
||||||
func (f File) ValidateCatalog() error {
|
func (f File) ValidateCatalog() error {
|
||||||
for i, ce := range f.Catalog {
|
for i, ce := range f.Catalog {
|
||||||
f := fetcher.Get(ce.Fetcher)
|
fi := fetcher.Get(ce.Fetcher)
|
||||||
if f == nil {
|
if fi == nil {
|
||||||
return errors.Errorf("catalog entry %d has unknown fetcher", i)
|
return errors.Errorf("catalog entry %d has unknown fetcher", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := f.Validate(ce.FetcherConfig); err != nil {
|
if err := fi.Validate(ce.FetcherConfig); err != nil {
|
||||||
return errors.Wrapf(err, "catalog entry %d has invalid fetcher config", i)
|
return errors.Wrapf(err, "catalog entry %d has invalid fetcher config", i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package database implements a wrapper around the real database
|
||||||
|
// with some helper functions to store catalog / log entries
|
||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -14,6 +16,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// Client represents a database client
|
||||||
Client struct {
|
Client struct {
|
||||||
Catalog CatalogMetaStore
|
Catalog CatalogMetaStore
|
||||||
Logs LogStore
|
Logs LogStore
|
||||||
|
@ -30,6 +33,9 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// NewClient creates a new Client and connects to the database using
|
||||||
|
// some default configurations. The database is automatically
|
||||||
|
// initialized with required tables.
|
||||||
func NewClient(dbtype, dsn string) (*Client, error) {
|
func NewClient(dbtype, dsn string) (*Client, error) {
|
||||||
c := &Client{}
|
c := &Client{}
|
||||||
c.Catalog = CatalogMetaStore{c}
|
c.Catalog = CatalogMetaStore{c}
|
||||||
|
@ -75,6 +81,7 @@ func NewClient(dbtype, dsn string) (*Client, error) {
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migrate executes database migrations for all required types
|
||||||
func (c Client) Migrate(dest *Client) error {
|
func (c Client) Migrate(dest *Client) error {
|
||||||
for _, m := range []migrator{
|
for _, m := range []migrator{
|
||||||
c.Catalog,
|
c.Catalog,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// CatalogEntry represents the entry in the config file
|
||||||
CatalogEntry struct {
|
CatalogEntry struct {
|
||||||
Name string `json:"name" yaml:"name"`
|
Name string `json:"name" yaml:"name"`
|
||||||
Tag string `json:"tag" yaml:"tag"`
|
Tag string `json:"tag" yaml:"tag"`
|
||||||
|
@ -25,12 +26,14 @@ type (
|
||||||
Links []CatalogLink `json:"links" yaml:"links"`
|
Links []CatalogLink `json:"links" yaml:"links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CatalogLink represents a link assigned to a CatalogEntry
|
||||||
CatalogLink struct {
|
CatalogLink struct {
|
||||||
IconClass string `json:"icon_class" yaml:"icon_class"`
|
IconClass string `json:"icon_class" yaml:"icon_class"`
|
||||||
Name string `json:"name" yaml:"name"`
|
Name string `json:"name" yaml:"name"`
|
||||||
URL string `json:"url" yaml:"url"`
|
URL string `json:"url" yaml:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CatalogMeta contains meta-information about the catalog entry
|
||||||
CatalogMeta struct {
|
CatalogMeta struct {
|
||||||
CatalogName string `gorm:"primaryKey" json:"-"`
|
CatalogName string `gorm:"primaryKey" json:"-"`
|
||||||
CatalogTag string `gorm:"primaryKey" json:"-"`
|
CatalogTag string `gorm:"primaryKey" json:"-"`
|
||||||
|
@ -40,6 +43,7 @@ type (
|
||||||
VersionTime *time.Time `json:"version_time,omitempty"`
|
VersionTime *time.Time `json:"version_time,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogEntry represents a single version change for a given catalog entry
|
||||||
LogEntry struct {
|
LogEntry struct {
|
||||||
CatalogName string `gorm:"index:catalog_key" json:"catalog_name"`
|
CatalogName string `gorm:"index:catalog_key" json:"catalog_name"`
|
||||||
CatalogTag string `gorm:"index:catalog_key" json:"catalog_tag"`
|
CatalogTag string `gorm:"index:catalog_key" json:"catalog_tag"`
|
||||||
|
@ -48,17 +52,21 @@ type (
|
||||||
VersionFrom string `json:"version_from"`
|
VersionFrom string `json:"version_from"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CatalogMetaStore is an accessor for the meta store and wraps a Client
|
||||||
CatalogMetaStore struct {
|
CatalogMetaStore struct {
|
||||||
c *Client
|
c *Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogStore is an accessor for the log store and wraps a Client
|
||||||
LogStore struct {
|
LogStore struct {
|
||||||
c *Client
|
c *Client
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Key returns the name / tag combination as a single key
|
||||||
func (c CatalogEntry) Key() string { return strings.Join([]string{c.Name, c.Tag}, ":") }
|
func (c CatalogEntry) Key() string { return strings.Join([]string{c.Name, c.Tag}, ":") }
|
||||||
|
|
||||||
|
// GetMeta fetches the current database stored CatalogMeta for the CatalogEntry
|
||||||
func (c CatalogMetaStore) GetMeta(ce *CatalogEntry) (*CatalogMeta, error) {
|
func (c CatalogMetaStore) GetMeta(ce *CatalogEntry) (*CatalogMeta, error) {
|
||||||
out := &CatalogMeta{
|
out := &CatalogMeta{
|
||||||
CatalogName: ce.Name,
|
CatalogName: ce.Name,
|
||||||
|
@ -77,6 +85,7 @@ func (c CatalogMetaStore) GetMeta(ce *CatalogEntry) (*CatalogMeta, error) {
|
||||||
return out, errors.Wrap(err, "querying metadata")
|
return out, errors.Wrap(err, "querying metadata")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migrate applies the updated database schema for the CatalogMetaStore
|
||||||
func (c CatalogMetaStore) Migrate(dest *Client) error {
|
func (c CatalogMetaStore) Migrate(dest *Client) error {
|
||||||
var metas []*CatalogMeta
|
var metas []*CatalogMeta
|
||||||
if err := c.c.db.Find(&metas).Error; err != nil {
|
if err := c.c.db.Find(&metas).Error; err != nil {
|
||||||
|
@ -92,6 +101,7 @@ func (c CatalogMetaStore) Migrate(dest *Client) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PutMeta stores the updated CatalogMeta
|
||||||
func (c CatalogMetaStore) PutMeta(cm *CatalogMeta) error {
|
func (c CatalogMetaStore) PutMeta(cm *CatalogMeta) error {
|
||||||
return errors.Wrap(
|
return errors.Wrap(
|
||||||
c.c.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(cm).Error,
|
c.c.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(cm).Error,
|
||||||
|
@ -103,6 +113,7 @@ func (c CatalogMetaStore) ensureTable() error {
|
||||||
return errors.Wrap(c.c.db.AutoMigrate(&CatalogMeta{}), "applying migration")
|
return errors.Wrap(c.c.db.AutoMigrate(&CatalogMeta{}), "applying migration")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add creates a new LogEntry inside the LogStore
|
||||||
func (l LogStore) Add(le *LogEntry) error {
|
func (l LogStore) Add(le *LogEntry) error {
|
||||||
return errors.Wrap(
|
return errors.Wrap(
|
||||||
l.c.db.Create(le).Error,
|
l.c.db.Create(le).Error,
|
||||||
|
@ -110,14 +121,17 @@ func (l LogStore) Add(le *LogEntry) error {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List retrieves unfiltered log entries by page
|
||||||
func (l LogStore) List(num, page int) ([]LogEntry, error) {
|
func (l LogStore) List(num, page int) ([]LogEntry, error) {
|
||||||
return l.listWithFilter(l.c.db, num, page)
|
return l.listWithFilter(l.c.db, num, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListForCatalogEntry retrieves filered log entries by page
|
||||||
func (l LogStore) ListForCatalogEntry(ce *CatalogEntry, num, page int) ([]LogEntry, error) {
|
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)
|
return l.listWithFilter(l.c.db.Where(&LogEntry{CatalogName: ce.Name, CatalogTag: ce.Tag}), num, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migrate applies the updated database schema for the LogStore
|
||||||
func (l LogStore) Migrate(dest *Client) error {
|
func (l LogStore) Migrate(dest *Client) error {
|
||||||
var logs []*LogEntry
|
var logs []*LogEntry
|
||||||
if err := l.c.db.Find(&logs).Error; err != nil {
|
if err := l.c.db.Find(&logs).Error; err != nil {
|
||||||
|
@ -133,7 +147,7 @@ func (l LogStore) Migrate(dest *Client) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l LogStore) listWithFilter(filter *gorm.DB, num, page int) ([]LogEntry, error) {
|
func (LogStore) listWithFilter(filter *gorm.DB, num, page int) ([]LogEntry, error) {
|
||||||
var out []LogEntry
|
var out []LogEntry
|
||||||
return out, errors.Wrap(
|
return out, errors.Wrap(
|
||||||
filter.
|
filter.
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/Luzifer/go-latestver/internal/database"
|
"github.com/Luzifer/go-latestver/internal/database"
|
||||||
|
"github.com/Luzifer/go-latestver/internal/helpers"
|
||||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,12 +28,14 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// AtlassianFetcher implements the fetcher interface to monitor Atlassian products
|
||||||
AtlassianFetcher struct{}
|
AtlassianFetcher struct{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { registerFetcher("atlassian", func() Fetcher { return &AtlassianFetcher{} }) }
|
func init() { registerFetcher("atlassian", func() Fetcher { return &AtlassianFetcher{} }) }
|
||||||
|
|
||||||
func (a AtlassianFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
// FetchVersion retrieves the latest version for the catalog entry
|
||||||
|
func (AtlassianFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
||||||
url := fmt.Sprintf("https://my.atlassian.com/download/feeds/current/%s.json", attrs.MustString("product", nil))
|
url := fmt.Sprintf("https://my.atlassian.com/download/feeds/current/%s.json", attrs.MustString("product", nil))
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
@ -44,9 +47,9 @@ func (a AtlassianFetcher) FetchVersion(ctx context.Context, attrs *fieldcollecti
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", time.Time{}, errors.Wrap(err, "executing request")
|
return "", time.Time{}, errors.Wrap(err, "executing request")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { helpers.LogIfErr(resp.Body.Close(), "closing response body after read") }()
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", time.Time{}, errors.Wrap(err, "reading response body")
|
return "", time.Time{}, errors.Wrap(err, "reading response body")
|
||||||
}
|
}
|
||||||
|
@ -103,10 +106,12 @@ func (a AtlassianFetcher) FetchVersion(ctx context.Context, attrs *fieldcollecti
|
||||||
return "", time.Time{}, ErrNoVersionFound
|
return "", time.Time{}, ErrNoVersionFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AtlassianFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink {
|
// Links retrieves a collection of links for the fetcher
|
||||||
|
func (AtlassianFetcher) Links(_ *fieldcollection.FieldCollection) []database.CatalogLink {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate validates the configuration given to the fetcher
|
||||||
func (AtlassianFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
func (AtlassianFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
||||||
// @attr product required string "" Lowercase name of the product to fetch (e.g. confluence, crowd, jira-software, ...)
|
// @attr product required string "" Lowercase name of the product to fetch (e.g. confluence, crowd, jira-software, ...)
|
||||||
if v, err := attrs.String("product"); err != nil || v == "" {
|
if v, err := attrs.String("product"); err != nil || v == "" {
|
||||||
|
|
|
@ -20,12 +20,14 @@ import (
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// GitTagFetcher implements the fetcher interface to monitor tags in a git repository
|
||||||
GitTagFetcher struct{}
|
GitTagFetcher struct{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { registerFetcher("git_tag", func() Fetcher { return &GitTagFetcher{} }) }
|
func init() { registerFetcher("git_tag", func() Fetcher { return &GitTagFetcher{} }) }
|
||||||
|
|
||||||
func (g GitTagFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
// FetchVersion retrieves the latest version for the catalog entry
|
||||||
|
func (g GitTagFetcher) FetchVersion(_ context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
||||||
repo, err := git.Init(memory.NewStorage(), nil)
|
repo, err := git.Init(memory.NewStorage(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", time.Time{}, errors.Wrap(err, "opening in-mem repo")
|
return "", time.Time{}, errors.Wrap(err, "opening in-mem repo")
|
||||||
|
@ -79,11 +81,13 @@ func (g GitTagFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.
|
||||||
return latestTag.Name().Short(), latestTagTime, nil
|
return latestTag.Name().Short(), latestTagTime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g GitTagFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink {
|
// Links retrieves a collection of links for the fetcher
|
||||||
|
func (GitTagFetcher) Links(_ *fieldcollection.FieldCollection) []database.CatalogLink {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g GitTagFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
// Validate validates the configuration given to the fetcher
|
||||||
|
func (GitTagFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
||||||
// @attr remote required string "" Repository remote to fetch the tags from (should accept everything you can use in `git remote set-url` command)
|
// @attr remote required string "" Repository remote to fetch the tags from (should accept everything you can use in `git remote set-url` command)
|
||||||
if v, err := attrs.String("remote"); err != nil || v == "" {
|
if v, err := attrs.String("remote"); err != nil || v == "" {
|
||||||
return errors.New("remote is expected to be non-empty string")
|
return errors.New("remote is expected to be non-empty string")
|
||||||
|
@ -92,7 +96,7 @@ func (g GitTagFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g GitTagFetcher) tagRefToTime(repo *git.Repository, tag *plumbing.Reference) (time.Time, error) {
|
func (GitTagFetcher) tagRefToTime(repo *git.Repository, tag *plumbing.Reference) (time.Time, error) {
|
||||||
tagObj, err := repo.TagObject(tag.Hash())
|
tagObj, err := repo.TagObject(tag.Hash())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Annotated tag: Take the time of the tag
|
// Annotated tag: Take the time of the tag
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/Luzifer/go-latestver/internal/database"
|
"github.com/Luzifer/go-latestver/internal/database"
|
||||||
|
"github.com/Luzifer/go-latestver/internal/helpers"
|
||||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,6 +23,7 @@ import (
|
||||||
const githubHTTPTimeout = 2 * time.Second
|
const githubHTTPTimeout = 2 * time.Second
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// GithubReleaseFetcher implements the fetcher interface to monitor releases in a Github repository
|
||||||
GithubReleaseFetcher struct{}
|
GithubReleaseFetcher struct{}
|
||||||
|
|
||||||
githubRelease struct {
|
githubRelease struct {
|
||||||
|
@ -33,7 +35,8 @@ type (
|
||||||
|
|
||||||
func init() { registerFetcher("github_release", func() Fetcher { return &GithubReleaseFetcher{} }) }
|
func init() { registerFetcher("github_release", func() Fetcher { return &GithubReleaseFetcher{} }) }
|
||||||
|
|
||||||
func (g GithubReleaseFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
// FetchVersion retrieves the latest version for the catalog entry
|
||||||
|
func (GithubReleaseFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, githubHTTPTimeout)
|
ctx, cancel := context.WithTimeout(ctx, githubHTTPTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
@ -56,11 +59,11 @@ func (g GithubReleaseFetcher) FetchVersion(ctx context.Context, attrs *fieldcoll
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", time.Time{}, errors.Wrap(err, "executing request")
|
return "", time.Time{}, errors.Wrap(err, "executing request")
|
||||||
}
|
}
|
||||||
|
defer func() { helpers.LogIfErr(resp.Body.Close(), "closing response body after read") }()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return "", time.Time{}, errors.Errorf("unexpected HTTP status %d", resp.StatusCode)
|
return "", time.Time{}, errors.Errorf("unexpected HTTP status %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var payload []githubRelease
|
var payload []githubRelease
|
||||||
if err = json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
if err = json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||||
|
@ -85,7 +88,8 @@ func (g GithubReleaseFetcher) FetchVersion(ctx context.Context, attrs *fieldcoll
|
||||||
return release.TagName, release.PublishedAt, nil
|
return release.TagName, release.PublishedAt, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g GithubReleaseFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink {
|
// Links retrieves a collection of links for the fetcher
|
||||||
|
func (GithubReleaseFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink {
|
||||||
return []database.CatalogLink{
|
return []database.CatalogLink{
|
||||||
{
|
{
|
||||||
IconClass: "fab fa-github",
|
IconClass: "fab fa-github",
|
||||||
|
@ -95,7 +99,8 @@ func (g GithubReleaseFetcher) Links(attrs *fieldcollection.FieldCollection) []da
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g GithubReleaseFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
// Validate validates the configuration given to the fetcher
|
||||||
|
func (GithubReleaseFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
||||||
// @attr repository required string "" Repository to fetch in form `owner/repo`
|
// @attr repository required string "" Repository to fetch in form `owner/repo`
|
||||||
if v, err := attrs.String("repository"); err != nil || v == "" {
|
if v, err := attrs.String("repository"); err != nil || v == "" {
|
||||||
return errors.New("repository is expected to be non-empty string")
|
return errors.New("repository is expected to be non-empty string")
|
||||||
|
|
|
@ -22,12 +22,14 @@ import (
|
||||||
var htmlFetcherDefaultRegex = `(v?(?:[0-9]+\.?){2,})`
|
var htmlFetcherDefaultRegex = `(v?(?:[0-9]+\.?){2,})`
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// HTMLFetcher implements the fetcher interface to monitor versions on websites by xpath queries
|
||||||
HTMLFetcher struct{}
|
HTMLFetcher struct{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { registerFetcher("html", func() Fetcher { return &HTMLFetcher{} }) }
|
func init() { registerFetcher("html", func() Fetcher { return &HTMLFetcher{} }) }
|
||||||
|
|
||||||
func (h HTMLFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
// FetchVersion retrieves the latest version for the catalog entry
|
||||||
|
func (HTMLFetcher) FetchVersion(_ context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
||||||
doc, err := htmlquery.LoadURL(attrs.MustString("url", nil))
|
doc, err := htmlquery.LoadURL(attrs.MustString("url", nil))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", time.Time{}, errors.Wrap(err, "loading URL")
|
return "", time.Time{}, errors.Wrap(err, "loading URL")
|
||||||
|
@ -58,7 +60,8 @@ func (h HTMLFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.Fi
|
||||||
return match[1], time.Now(), nil
|
return match[1], time.Now(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h HTMLFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink {
|
// Links retrieves a collection of links for the fetcher
|
||||||
|
func (HTMLFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink {
|
||||||
return []database.CatalogLink{
|
return []database.CatalogLink{
|
||||||
{
|
{
|
||||||
IconClass: "fas fa-globe",
|
IconClass: "fas fa-globe",
|
||||||
|
@ -68,7 +71,8 @@ func (h HTMLFetcher) Links(attrs *fieldcollection.FieldCollection) []database.Ca
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h HTMLFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
// Validate validates the configuration given to the fetcher
|
||||||
|
func (HTMLFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
||||||
// @attr url required string "" URL to fetch the HTML from
|
// @attr url required string "" URL to fetch the HTML from
|
||||||
if v, err := attrs.String("url"); err != nil || v == "" {
|
if v, err := attrs.String("url"); err != nil || v == "" {
|
||||||
return errors.New("url is expected to be non-empty string")
|
return errors.New("url is expected to be non-empty string")
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Package fetcher contains the implementations of fetchers to retrieve
|
||||||
|
// current versions for the catalog entries
|
||||||
package fetcher
|
package fetcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -12,15 +14,21 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// Fetcher defines the interface all fetchers have to follow
|
||||||
Fetcher interface {
|
Fetcher interface {
|
||||||
|
// FetchVersion retrieves the latest version for the catalog entry
|
||||||
FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error)
|
FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error)
|
||||||
|
// Links retrieves a collection of links for the fetcher
|
||||||
Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink
|
Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink
|
||||||
|
// Validate validates the configuration given to the fetcher
|
||||||
Validate(attrs *fieldcollection.FieldCollection) error
|
Validate(attrs *fieldcollection.FieldCollection) error
|
||||||
}
|
}
|
||||||
|
// Create represents a function to instantiate a Fetcher
|
||||||
Create func() Fetcher
|
Create func() Fetcher
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// ErrNoVersionFound signalizes the fetcher was not able to retrieve a version
|
||||||
ErrNoVersionFound = errors.New("no version found")
|
ErrNoVersionFound = errors.New("no version found")
|
||||||
|
|
||||||
availableFetchers = map[string]Create{}
|
availableFetchers = map[string]Create{}
|
||||||
|
@ -34,6 +42,7 @@ func registerFetcher(name string, fn Create) {
|
||||||
availableFetchers[name] = fn
|
availableFetchers[name] = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get retrieves an creation function for the given fetcher name
|
||||||
func Get(name string) Fetcher {
|
func Get(name string) Fetcher {
|
||||||
availableFetchersLock.RLock()
|
availableFetchersLock.RLock()
|
||||||
defer availableFetchersLock.RUnlock()
|
defer availableFetchersLock.RUnlock()
|
||||||
|
|
|
@ -3,7 +3,7 @@ package fetcher
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/Luzifer/go-latestver/internal/database"
|
"github.com/Luzifer/go-latestver/internal/database"
|
||||||
|
"github.com/Luzifer/go-latestver/internal/helpers"
|
||||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,11 +29,13 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// JSONFetcher implements the fetcher interface to retrieve a version from a JSON document
|
||||||
JSONFetcher struct{}
|
JSONFetcher struct{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { registerFetcher("json", func() Fetcher { return &JSONFetcher{} }) }
|
func init() { registerFetcher("json", func() Fetcher { return &JSONFetcher{} }) }
|
||||||
|
|
||||||
|
// FetchVersion retrieves the latest version for the catalog entry
|
||||||
func (JSONFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
func (JSONFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
||||||
var (
|
var (
|
||||||
doc *jsonquery.Node
|
doc *jsonquery.Node
|
||||||
|
@ -55,9 +58,9 @@ func (JSONFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.Fiel
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", time.Time{}, errors.Wrap(err, "executing request")
|
return "", time.Time{}, errors.Wrap(err, "executing request")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { helpers.LogIfErr(resp.Body.Close(), "closing response body after read") }()
|
||||||
|
|
||||||
body, err = ioutil.ReadAll(resp.Body)
|
body, err = io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", time.Time{}, errors.Wrap(err, "reading response body")
|
return "", time.Time{}, errors.Wrap(err, "reading response body")
|
||||||
}
|
}
|
||||||
|
@ -101,8 +104,10 @@ func (JSONFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.Fiel
|
||||||
return match[1], time.Now(), nil
|
return match[1], time.Now(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (JSONFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink { return nil }
|
// Links retrieves a collection of links for the fetcher
|
||||||
|
func (JSONFetcher) Links(_ *fieldcollection.FieldCollection) []database.CatalogLink { return nil }
|
||||||
|
|
||||||
|
// Validate validates the configuration given to the fetcher
|
||||||
func (JSONFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
func (JSONFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
||||||
// @attr url required string "" URL to fetch the HTML from
|
// @attr url required string "" URL to fetch the HTML from
|
||||||
if v, err := attrs.String("url"); err != nil || v == "" {
|
if v, err := attrs.String("url"); err != nil || v == "" {
|
||||||
|
|
|
@ -2,7 +2,7 @@ package fetcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/Luzifer/go-latestver/internal/database"
|
"github.com/Luzifer/go-latestver/internal/database"
|
||||||
|
"github.com/Luzifer/go-latestver/internal/helpers"
|
||||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,12 +25,14 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// RegexFetcher implements the fetcher interface to monitor versions on a web request by regex
|
||||||
RegexFetcher struct{}
|
RegexFetcher struct{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() { registerFetcher("regex", func() Fetcher { return &RegexFetcher{} }) }
|
func init() { registerFetcher("regex", func() Fetcher { return &RegexFetcher{} }) }
|
||||||
|
|
||||||
func (h RegexFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
// FetchVersion retrieves the latest version for the catalog entry
|
||||||
|
func (RegexFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.FieldCollection) (string, time.Time, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, attrs.MustString("url", nil), nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, attrs.MustString("url", nil), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", time.Time{}, errors.Wrap(err, "creating request")
|
return "", time.Time{}, errors.Wrap(err, "creating request")
|
||||||
|
@ -39,13 +42,13 @@ func (h RegexFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.F
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", time.Time{}, errors.Wrap(err, "executing request")
|
return "", time.Time{}, errors.Wrap(err, "executing request")
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer func() { helpers.LogIfErr(resp.Body.Close(), "closing response body after read") }()
|
||||||
|
|
||||||
if resp.StatusCode >= httpStatus3xx {
|
if resp.StatusCode >= httpStatus3xx {
|
||||||
return "", time.Time{}, errors.Errorf("HTTP status %d", resp.StatusCode)
|
return "", time.Time{}, errors.Errorf("HTTP status %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(resp.Body)
|
data, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", time.Time{}, errors.Wrap(err, "reading response body")
|
return "", time.Time{}, errors.Wrap(err, "reading response body")
|
||||||
}
|
}
|
||||||
|
@ -62,7 +65,8 @@ func (h RegexFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.F
|
||||||
return matches[1], time.Now(), nil
|
return matches[1], time.Now(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h RegexFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink {
|
// Links retrieves a collection of links for the fetcher
|
||||||
|
func (RegexFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink {
|
||||||
return []database.CatalogLink{
|
return []database.CatalogLink{
|
||||||
{
|
{
|
||||||
IconClass: "fas fa-globe",
|
IconClass: "fas fa-globe",
|
||||||
|
@ -72,7 +76,8 @@ func (h RegexFetcher) Links(attrs *fieldcollection.FieldCollection) []database.C
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h RegexFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
// Validate validates the configuration given to the fetcher
|
||||||
|
func (RegexFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
||||||
// @attr url required string "" URL to fetch the content from
|
// @attr url required string "" URL to fetch the content from
|
||||||
if v, err := attrs.String("url"); err != nil || v == "" {
|
if v, err := attrs.String("url"); err != nil || v == "" {
|
||||||
return errors.New("url is expected to be non-empty string")
|
return errors.New("url is expected to be non-empty string")
|
||||||
|
|
14
internal/helpers/log.go
Normal file
14
internal/helpers/log.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// Package helpers contains helper functions to avoid code duplication
|
||||||
|
package helpers
|
||||||
|
|
||||||
|
import "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
// LogIfErr yields a logrus error log line when the given error is
|
||||||
|
// not nil
|
||||||
|
func LogIfErr(err error, msgTpl string, params ...any) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.WithError(err).Errorf(msgTpl, params...)
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package version
|
||||||
import "github.com/pkg/errors"
|
import "github.com/pkg/errors"
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
// Constraint document how a version update should be handled
|
||||||
Constraint struct {
|
Constraint struct {
|
||||||
AllowDowngrade bool `yaml:"allow_downgrade"`
|
AllowDowngrade bool `yaml:"allow_downgrade"`
|
||||||
AllowPrerelease bool `yaml:"allow_prerelease"`
|
AllowPrerelease bool `yaml:"allow_prerelease"`
|
||||||
|
@ -25,6 +26,8 @@ const (
|
||||||
compareResultUpgrade
|
compareResultUpgrade
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ShouldApply checks whether a new version should overwrite the old
|
||||||
|
// one given the parameters inside the Constraint
|
||||||
func (c Constraint) ShouldApply(oldVersion, newVersion string) (bool, error) {
|
func (c Constraint) ShouldApply(oldVersion, newVersion string) (bool, error) {
|
||||||
if oldVersion == "" && newVersion != "" {
|
if oldVersion == "" && newVersion != "" {
|
||||||
// The old version does not exist, the new one does, update it!
|
// The old version does not exist, the new one does, update it!
|
||||||
|
|
3
internal/version/doc.go
Normal file
3
internal/version/doc.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// Package version contains parsers and methods to validate
|
||||||
|
// versions and version updates
|
||||||
|
package version
|
44
main.go
44
main.go
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
@ -38,10 +39,25 @@ var (
|
||||||
version = "dev"
|
version = "dev"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initApp() {
|
func initApp() error {
|
||||||
rconfig.AutoEnv(true)
|
rconfig.AutoEnv(true)
|
||||||
if err := rconfig.ParseAndValidate(&cfg); err != nil {
|
if err := rconfig.ParseAndValidate(&cfg); err != nil {
|
||||||
log.Fatalf("Unable to parse commandline options: %s", err)
|
return errors.Wrap(err, "parsing commandline options")
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := log.ParseLevel(cfg.LogLevel)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "parsing log-level")
|
||||||
|
}
|
||||||
|
log.SetLevel(l)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var err error
|
||||||
|
if err = initApp(); err != nil {
|
||||||
|
log.WithError(err).Fatal("initializing app")
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.VersionAndExit {
|
if cfg.VersionAndExit {
|
||||||
|
@ -49,18 +65,6 @@ func initApp() {
|
||||||
os.Exit(0)
|
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 {
|
if err = configFile.Load(cfg.Config); err != nil {
|
||||||
log.WithError(err).Fatal("Unable to load configuration")
|
log.WithError(err).Fatal("Unable to load configuration")
|
||||||
}
|
}
|
||||||
|
@ -83,7 +87,9 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
scheduler := cron.New()
|
scheduler := cron.New()
|
||||||
scheduler.AddFunc(fmt.Sprintf("@every %s", schedulerInterval), schedulerRun)
|
if _, err = scheduler.AddFunc(fmt.Sprintf("@every %s", schedulerInterval), schedulerRun); err != nil {
|
||||||
|
log.WithError(err).Fatal("registering cron entry")
|
||||||
|
}
|
||||||
scheduler.Start()
|
scheduler.Start()
|
||||||
|
|
||||||
router = mux.NewRouter()
|
router = mux.NewRouter()
|
||||||
|
@ -105,7 +111,13 @@ func main() {
|
||||||
handler = httpHelper.GzipHandler(handler)
|
handler = httpHelper.GzipHandler(handler)
|
||||||
handler = httpHelper.NewHTTPLogHandler(handler)
|
handler = httpHelper.NewHTTPLogHandler(handler)
|
||||||
|
|
||||||
if err := http.ListenAndServe(cfg.Listen, handler); err != nil {
|
server := &http.Server{
|
||||||
|
Addr: cfg.Listen,
|
||||||
|
Handler: handler,
|
||||||
|
ReadHeaderTimeout: time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := server.ListenAndServe(); err != nil {
|
||||||
log.WithError(err).Fatal("HTTP server exited unclean")
|
log.WithError(err).Fatal("HTTP server exited unclean")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,6 @@ func checkForUpdates(ce *database.CatalogEntry) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
|
||||||
case err != nil:
|
case err != nil:
|
||||||
logger.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()
|
||||||
|
@ -109,7 +108,6 @@ func checkForUpdates(ce *database.CatalogEntry) error {
|
||||||
|
|
||||||
default:
|
default:
|
||||||
cm.Error = ""
|
cm.Error = ""
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cm.LastChecked = ptrTime(time.Now().Truncate(time.Second).UTC())
|
cm.LastChecked = ptrTime(time.Now().Truncate(time.Second).UTC())
|
||||||
|
|
Loading…
Reference in a new issue