From 9255fc68981ec34c76f6e8733ca38180380d0303 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Sat, 18 Mar 2023 17:38:05 +0100 Subject: [PATCH] Lint: Update linter config, fix all linter issues Signed-off-by: Knut Ahlers --- .golangci.yml | 182 ++++++++++++++++++++++++++------- api.go | 12 +-- frontend.go | 9 +- internal/config/config.go | 23 ++++- internal/database/db.go | 7 ++ internal/database/store.go | 16 ++- internal/fetcher/atlassian.go | 15 ++- internal/fetcher/git.go | 12 ++- internal/fetcher/github.go | 13 ++- internal/fetcher/html.go | 10 +- internal/fetcher/interface.go | 9 ++ internal/fetcher/json.go | 13 ++- internal/fetcher/regex.go | 17 +-- internal/helpers/log.go | 14 +++ internal/version/constraint.go | 3 + internal/version/doc.go | 3 + main.go | 44 +++++--- scheduler.go | 2 - 18 files changed, 308 insertions(+), 96 deletions(-) create mode 100644 internal/helpers/log.go create mode 100644 internal/version/doc.go diff --git a/.golangci.yml b/.golangci.yml index 162bbf7..23e47f8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -11,6 +11,61 @@ run: output: 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: forbidigo: forbid: @@ -29,41 +84,96 @@ linters-settings: mnd: ignored-functions: 'strconv.(?:Format|Parse)\B+' -linters: - disable-all: true - enable: - - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false] - - bodyclose # checks whether HTTP response body is closed successfully [fast: true, auto-fix: false] - - deadcode # Finds unused code [fast: true, 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] - - 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] - - 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] - - structcheck # Finds unused struct fields [fast: true, auto-fix: false] - - stylecheck # Stylecheck is a replacement for golint [fast: true, 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] - - varcheck # Finds unused global variables and constants [fast: true, auto-fix: false] + revive: + rules: + #- name: add-constant # Suggests using constant for magic numbers and string literals + # Opinion: Makes sense for strings, not for numbers but checks numbers + #- name: argument-limit # Specifies the maximum number of arguments a function can receive | Opinion: Don't need this + - name: atomic # Check for common mistaken usages of the `sync/atomic` package + - name: banned-characters # Checks banned characters in identifiers + arguments: + - 'Íž' # Greek question mark + - name: bare-return # Warns on bare returns + - name: blank-imports # Disallows blank imports + - name: bool-literal-in-expr # Suggests removing Boolean literals from logic expressions + - name: call-to-gc # Warns on explicit call to the garbage collector + #- name: cognitive-complexity # Sets restriction for maximum Cognitive complexity. + # There is a dedicated linter for this + - name: confusing-naming # Warns on methods with names that differ only by capitalization + - name: confusing-results # Suggests to name potentially confusing function results + - name: constant-logical-expr # Warns on constant logical expressions + - name: context-as-argument # `context.Context` should be the first argument of a function. + - name: context-keys-type # Disallows the usage of basic types in `context.WithValue`. + #- name: cyclomatic # Sets restriction for maximum Cyclomatic complexity. + # There is a dedicated linter for this + #- name: datarace # Spots potential dataraces + # Is not (yet) available? + - name: deep-exit # Looks for program exits in funcs other than `main()` or `init()` + - name: defer # Warns on some [defer gotchas](https://blog.learngoprogramming.com/5-gotchas-of-defer-in-go-golang-part-iii-36a1ab3d6ef1) + - name: dot-imports # Forbids `.` imports. + - name: duplicated-imports # Looks for packages that are imported two or more times + - name: early-return # Spots if-then-else statements that can be refactored to simplify code reading + - name: empty-block # Warns on empty code blocks + - name: empty-lines # Warns when there are heading or trailing newlines in a block + - name: errorf # Should replace `errors.New(fmt.Sprintf())` with `fmt.Errorf()` + - name: error-naming # Naming of error variables. + - name: error-return # The error return parameter should be last. + - name: error-strings # Conventions around error strings. + - 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 ... diff --git a/api.go b/api.go index 581319a..fb47adc 100644 --- a/api.go +++ b/api.go @@ -22,7 +22,7 @@ import ( ) type ( - APICatalogEntry struct { + apiCatalogEntry struct { database.CatalogEntry 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) 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) { @@ -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) { @@ -142,8 +142,8 @@ func handleCatalogGetVersion(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, cm.CurrentVersion) } -func handleCatalogList(w http.ResponseWriter, r *http.Request) { - out := make([]APICatalogEntry, len(configFile.Catalog)) +func handleCatalogList(w http.ResponseWriter, _ *http.Request) { + out := make([]apiCatalogEntry, len(configFile.Catalog)) for i := range configFile.Catalog { ce := configFile.Catalog[i] diff --git a/frontend.go b/frontend.go index dde71dd..b89196d 100644 --- a/frontend.go +++ b/frontend.go @@ -7,6 +7,8 @@ import ( "path/filepath" "github.com/pkg/errors" + + "github.com/Luzifer/go-latestver/internal/helpers" ) func handleSinglePage(w http.ResponseWriter, r *http.Request) { @@ -19,16 +21,15 @@ func handleSinglePage(w http.ResponseWriter, r *http.Request) { return } - for _, path := range []string{ + for _, frontendPath := range []string{ filepath.Join("frontend", path.Base(urlPath)), filepath.Join("frontend", "index.html"), } { - f, err := frontendFS.Open(path) + f, err := frontendFS.Open(frontendPath) switch { - case err == nil: // 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() if err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index 0ab3dc8..a471dd4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,3 +1,5 @@ +// Package config holds the definition of the configuration file and +// some methods to load and validate it package config import ( @@ -9,23 +11,30 @@ import ( "github.com/Luzifer/go-latestver/internal/database" "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") type ( + // File represents the configuration file content File struct { Catalog []database.CatalogEntry `yaml:"catalog"` CheckInterval time.Duration `yaml:"check_interval"` } ) +// New creates a new empty File object with defaults func New() *File { return &File{ 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) { for i := range f.Catalog { ce := f.Catalog[i] @@ -37,12 +46,14 @@ func (f File) CatalogEntryByTag(name, tag string) (database.CatalogEntry, error) 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 { - 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 { 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.KnownFields(true) @@ -50,14 +61,16 @@ func (f *File) Load(filepath string) error { 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 { for i, ce := range f.Catalog { - f := fetcher.Get(ce.Fetcher) - if f == nil { + fi := fetcher.Get(ce.Fetcher) + if fi == nil { 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) } } diff --git a/internal/database/db.go b/internal/database/db.go index 62741db..fc602f2 100644 --- a/internal/database/db.go +++ b/internal/database/db.go @@ -1,3 +1,5 @@ +// Package database implements a wrapper around the real database +// with some helper functions to store catalog / log entries package database import ( @@ -14,6 +16,7 @@ import ( ) type ( + // Client represents a database client Client struct { Catalog CatalogMetaStore 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) { c := &Client{} c.Catalog = CatalogMetaStore{c} @@ -75,6 +81,7 @@ func NewClient(dbtype, dsn string) (*Client, error) { return c, nil } +// Migrate executes database migrations for all required types func (c Client) Migrate(dest *Client) error { for _, m := range []migrator{ c.Catalog, diff --git a/internal/database/store.go b/internal/database/store.go index bf23a62..3339de2 100644 --- a/internal/database/store.go +++ b/internal/database/store.go @@ -13,6 +13,7 @@ import ( ) type ( + // CatalogEntry represents the entry in the config file CatalogEntry struct { Name string `json:"name" yaml:"name"` Tag string `json:"tag" yaml:"tag"` @@ -25,12 +26,14 @@ type ( Links []CatalogLink `json:"links" yaml:"links"` } + // CatalogLink represents a link assigned to a CatalogEntry CatalogLink struct { IconClass string `json:"icon_class" yaml:"icon_class"` Name string `json:"name" yaml:"name"` URL string `json:"url" yaml:"url"` } + // CatalogMeta contains meta-information about the catalog entry CatalogMeta struct { CatalogName string `gorm:"primaryKey" json:"-"` CatalogTag string `gorm:"primaryKey" json:"-"` @@ -40,6 +43,7 @@ type ( VersionTime *time.Time `json:"version_time,omitempty"` } + // LogEntry represents a single version change for a given catalog entry LogEntry struct { CatalogName string `gorm:"index:catalog_key" json:"catalog_name"` CatalogTag string `gorm:"index:catalog_key" json:"catalog_tag"` @@ -48,17 +52,21 @@ type ( VersionFrom string `json:"version_from"` } + // CatalogMetaStore is an accessor for the meta store and wraps a Client CatalogMetaStore struct { c *Client } + // LogStore is an accessor for the log store and wraps a Client LogStore struct { 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}, ":") } +// GetMeta fetches the current database stored CatalogMeta for the CatalogEntry func (c CatalogMetaStore) GetMeta(ce *CatalogEntry) (*CatalogMeta, error) { out := &CatalogMeta{ CatalogName: ce.Name, @@ -77,6 +85,7 @@ func (c CatalogMetaStore) GetMeta(ce *CatalogEntry) (*CatalogMeta, error) { return out, errors.Wrap(err, "querying metadata") } +// Migrate applies the updated database schema for the CatalogMetaStore func (c CatalogMetaStore) Migrate(dest *Client) error { var metas []*CatalogMeta if err := c.c.db.Find(&metas).Error; err != nil { @@ -92,6 +101,7 @@ func (c CatalogMetaStore) Migrate(dest *Client) error { return nil } +// PutMeta stores the updated CatalogMeta func (c CatalogMetaStore) PutMeta(cm *CatalogMeta) error { return errors.Wrap( 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") } +// Add creates a new LogEntry inside the LogStore func (l LogStore) Add(le *LogEntry) error { return errors.Wrap( 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) { 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) { 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 { var logs []*LogEntry if err := l.c.db.Find(&logs).Error; err != nil { @@ -133,7 +147,7 @@ func (l LogStore) Migrate(dest *Client) error { 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 return out, errors.Wrap( filter. diff --git a/internal/fetcher/atlassian.go b/internal/fetcher/atlassian.go index 7dacb2f..ffd7b79 100644 --- a/internal/fetcher/atlassian.go +++ b/internal/fetcher/atlassian.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "sort" "strings" @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/Luzifer/go-latestver/internal/database" + "github.com/Luzifer/go-latestver/internal/helpers" "github.com/Luzifer/go_helpers/v2/fieldcollection" ) @@ -27,12 +28,14 @@ var ( ) type ( + // AtlassianFetcher implements the fetcher interface to monitor Atlassian products AtlassianFetcher struct{} ) 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)) 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 { 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 { 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 } -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 } +// Validate validates the configuration given to the fetcher func (AtlassianFetcher) Validate(attrs *fieldcollection.FieldCollection) error { // @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 == "" { diff --git a/internal/fetcher/git.go b/internal/fetcher/git.go index 019cdcd..ce498e5 100644 --- a/internal/fetcher/git.go +++ b/internal/fetcher/git.go @@ -20,12 +20,14 @@ import ( */ type ( + // GitTagFetcher implements the fetcher interface to monitor tags in a git repository 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) { +// 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) if err != nil { 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 } -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 } -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) if v, err := attrs.String("remote"); err != nil || v == "" { 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 } -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()) if err == nil { // Annotated tag: Take the time of the tag diff --git a/internal/fetcher/github.go b/internal/fetcher/github.go index 71b60ac..d9fed99 100644 --- a/internal/fetcher/github.go +++ b/internal/fetcher/github.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" "github.com/Luzifer/go-latestver/internal/database" + "github.com/Luzifer/go-latestver/internal/helpers" "github.com/Luzifer/go_helpers/v2/fieldcollection" ) @@ -22,6 +23,7 @@ import ( const githubHTTPTimeout = 2 * time.Second type ( + // GithubReleaseFetcher implements the fetcher interface to monitor releases in a Github repository GithubReleaseFetcher struct{} githubRelease struct { @@ -33,7 +35,8 @@ type ( 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) defer cancel() @@ -56,11 +59,11 @@ func (g GithubReleaseFetcher) FetchVersion(ctx context.Context, attrs *fieldcoll if err != nil { 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 { 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 { @@ -85,7 +88,8 @@ func (g GithubReleaseFetcher) FetchVersion(ctx context.Context, attrs *fieldcoll 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{ { 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` if v, err := attrs.String("repository"); err != nil || v == "" { return errors.New("repository is expected to be non-empty string") diff --git a/internal/fetcher/html.go b/internal/fetcher/html.go index 1f0ab2e..88eb0ad 100644 --- a/internal/fetcher/html.go +++ b/internal/fetcher/html.go @@ -22,12 +22,14 @@ import ( var htmlFetcherDefaultRegex = `(v?(?:[0-9]+\.?){2,})` type ( + // HTMLFetcher implements the fetcher interface to monitor versions on websites by xpath queries HTMLFetcher struct{} ) 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)) if err != nil { 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 } -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{ { 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 if v, err := attrs.String("url"); err != nil || v == "" { return errors.New("url is expected to be non-empty string") diff --git a/internal/fetcher/interface.go b/internal/fetcher/interface.go index 1370a2a..17015d1 100644 --- a/internal/fetcher/interface.go +++ b/internal/fetcher/interface.go @@ -1,3 +1,5 @@ +// Package fetcher contains the implementations of fetchers to retrieve +// current versions for the catalog entries package fetcher import ( @@ -12,15 +14,21 @@ import ( ) type ( + // Fetcher defines the interface all fetchers have to follow Fetcher interface { + // FetchVersion retrieves the latest version for the catalog entry 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 + // Validate validates the configuration given to the fetcher Validate(attrs *fieldcollection.FieldCollection) error } + // Create represents a function to instantiate a Fetcher Create func() Fetcher ) var ( + // ErrNoVersionFound signalizes the fetcher was not able to retrieve a version ErrNoVersionFound = errors.New("no version found") availableFetchers = map[string]Create{} @@ -34,6 +42,7 @@ func registerFetcher(name string, fn Create) { availableFetchers[name] = fn } +// Get retrieves an creation function for the given fetcher name func Get(name string) Fetcher { availableFetchersLock.RLock() defer availableFetchersLock.RUnlock() diff --git a/internal/fetcher/json.go b/internal/fetcher/json.go index 03fa928..206d4e1 100644 --- a/internal/fetcher/json.go +++ b/internal/fetcher/json.go @@ -3,7 +3,7 @@ package fetcher import ( "bytes" "context" - "io/ioutil" + "io" "net/http" "regexp" "time" @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/Luzifer/go-latestver/internal/database" + "github.com/Luzifer/go-latestver/internal/helpers" "github.com/Luzifer/go_helpers/v2/fieldcollection" ) @@ -28,11 +29,13 @@ var ( ) type ( + // JSONFetcher implements the fetcher interface to retrieve a version from a JSON document JSONFetcher struct{} ) 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) { var ( doc *jsonquery.Node @@ -55,9 +58,9 @@ func (JSONFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.Fiel if err != nil { 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 { 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 } -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 { // @attr url required string "" URL to fetch the HTML from if v, err := attrs.String("url"); err != nil || v == "" { diff --git a/internal/fetcher/regex.go b/internal/fetcher/regex.go index 8875557..69ddecc 100644 --- a/internal/fetcher/regex.go +++ b/internal/fetcher/regex.go @@ -2,7 +2,7 @@ package fetcher import ( "context" - "io/ioutil" + "io" "net/http" "regexp" "time" @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" "github.com/Luzifer/go-latestver/internal/database" + "github.com/Luzifer/go-latestver/internal/helpers" "github.com/Luzifer/go_helpers/v2/fieldcollection" ) @@ -24,12 +25,14 @@ const ( ) type ( + // RegexFetcher implements the fetcher interface to monitor versions on a web request by regex RegexFetcher struct{} ) 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) if err != nil { 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 { 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 { 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 { 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 } -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{ { 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 if v, err := attrs.String("url"); err != nil || v == "" { return errors.New("url is expected to be non-empty string") diff --git a/internal/helpers/log.go b/internal/helpers/log.go new file mode 100644 index 0000000..0d5f232 --- /dev/null +++ b/internal/helpers/log.go @@ -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...) +} diff --git a/internal/version/constraint.go b/internal/version/constraint.go index f10e04c..61fd288 100644 --- a/internal/version/constraint.go +++ b/internal/version/constraint.go @@ -3,6 +3,7 @@ package version import "github.com/pkg/errors" type ( + // Constraint document how a version update should be handled Constraint struct { AllowDowngrade bool `yaml:"allow_downgrade"` AllowPrerelease bool `yaml:"allow_prerelease"` @@ -25,6 +26,8 @@ const ( 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) { if oldVersion == "" && newVersion != "" { // The old version does not exist, the new one does, update it! diff --git a/internal/version/doc.go b/internal/version/doc.go new file mode 100644 index 0000000..e32fa3d --- /dev/null +++ b/internal/version/doc.go @@ -0,0 +1,3 @@ +// Package version contains parsers and methods to validate +// versions and version updates +package version diff --git a/main.go b/main.go index 20c0795..f951c18 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "time" "github.com/gorilla/mux" + "github.com/pkg/errors" "github.com/robfig/cron/v3" log "github.com/sirupsen/logrus" @@ -38,10 +39,25 @@ var ( version = "dev" ) -func initApp() { +func initApp() error { rconfig.AutoEnv(true) 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 { @@ -49,18 +65,6 @@ func initApp() { 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") } @@ -83,7 +87,9 @@ func main() { } 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() router = mux.NewRouter() @@ -105,7 +111,13 @@ func main() { handler = httpHelper.GzipHandler(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") } } diff --git a/scheduler.go b/scheduler.go index 1a601fd..9bfec7b 100644 --- a/scheduler.go +++ b/scheduler.go @@ -73,7 +73,6 @@ func checkForUpdates(ce *database.CatalogEntry) error { } switch { - case err != nil: logger.WithError(err).Error("Fetcher caused error, error is stored in entry") cm.Error = err.Error() @@ -109,7 +108,6 @@ func checkForUpdates(ce *database.CatalogEntry) error { default: cm.Error = "" - } cm.LastChecked = ptrTime(time.Now().Truncate(time.Second).UTC())