go-latestver/internal/fetcher/atlassian.go

123 lines
3.7 KiB
Go
Raw Permalink Normal View History

package fetcher
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
"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"
)
/*
* @module atlassian
* @module_desc Fetches latest version of an Atlassian product
*/
var (
atlassianDefaultEdition = ""
atlassianDefaultSearch = "TAR.GZ"
)
type (
// AtlassianFetcher implements the fetcher interface to monitor Atlassian products
AtlassianFetcher struct{}
)
func init() { registerFetcher("atlassian", func() Fetcher { return &AtlassianFetcher{} }) }
// 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)
if err != nil {
return "", time.Time{}, errors.Wrap(err, "creating request")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", time.Time{}, errors.Wrap(err, "executing request")
}
defer func() { helpers.LogIfErr(resp.Body.Close(), "closing response body after read") }()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", time.Time{}, errors.Wrap(err, "reading response body")
}
matches := jsonpStripRegex.FindSubmatch(body)
if matches == nil {
return "", time.Time{}, errors.New("document does not match jsonp syntax")
}
var payload []struct {
Description string `json:"description"`
Edition string `json:"edition"`
Zipurl string `json:"zipUrl"`
Md5 string `json:"md5"`
Size string `json:"size"`
Released string `json:"released"`
Type string `json:"type"`
Platform string `json:"platform"`
Version string `json:"version"`
Releasenotes string `json:"releaseNotes"`
Upgradenotes string `json:"upgradeNotes"`
}
if err = json.Unmarshal(matches[1], &payload); err != nil {
return "", time.Time{}, errors.Wrap(err, "parsing response JSON")
}
sort.Slice(payload, func(j, i int) bool { // j, i -> Reverse sort, biggest date at the top
iRelease, _ := time.Parse("02-Jan-2006", payload[i].Released)
jRelease, _ := time.Parse("02-Jan-2006", payload[j].Released)
return iRelease.Before(jRelease)
})
var (
// @attr edition optional string "" Filter down the versions according to its edition (e.g. "Enterprise" or "Standard" for Confluence)
edition = attrs.MustString("edition", &atlassianDefaultEdition)
// @attr search optional string "TAR.GZ" What to search in the download description: default is to search for the standalone .tar.gz file
search = attrs.MustString("search", &atlassianDefaultSearch)
)
for _, r := range payload {
if edition != "" && !strings.Contains(r.Edition, edition) {
continue
}
if search != "" && !strings.Contains(r.Description, search) {
continue
}
rt, _ := time.Parse("02-Jan-2006", r.Released)
return r.Version, rt, nil
}
return "", time.Time{}, ErrNoVersionFound
}
// 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 == "" {
return errors.New("product is expected to be non-empty string")
}
return nil
}