mirror of
https://github.com/Luzifer/go-latestver.git
synced 2024-12-20 10:31:16 +00:00
Add fetcher documentation
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
2d8f360a56
commit
3a3aedf998
12 changed files with 233 additions and 1 deletions
10
Makefile
10
Makefile
|
@ -15,3 +15,13 @@ node_modules:
|
||||||
go_test:
|
go_test:
|
||||||
go test -cover -v ./...
|
go test -cover -v ./...
|
||||||
golangci-lint run
|
golangci-lint run
|
||||||
|
|
||||||
|
# --- Documentation
|
||||||
|
|
||||||
|
gendoc: .venv
|
||||||
|
.venv/bin/python3 ci/gendoc.py $(shell grep -l '@module ' internal/fetcher/*.go) >docs/config.md
|
||||||
|
git add docs/config.md
|
||||||
|
|
||||||
|
.venv:
|
||||||
|
python -m venv .venv
|
||||||
|
.venv/bin/pip install -r ci/requirements.txt
|
||||||
|
|
|
@ -23,6 +23,8 @@ Usage of go-latestver:
|
||||||
--version Prints current version and exits
|
--version Prints current version and exits
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The documentation for the format of the `config` file can be found in the [`docs/config.md`](docs/config.md) file.
|
||||||
|
|
||||||
To use the `github_release` fetcher without hitting the API limits quite fast provide `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` of an [OAuth App](https://github.com/settings/developers) in environment variables.
|
To use the `github_release` fetcher without hitting the API limits quite fast provide `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` of an [OAuth App](https://github.com/settings/developers) in environment variables.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
52
ci/gendoc.py
Normal file
52
ci/gendoc.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import jinja2
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def main(args):
|
||||||
|
modules = []
|
||||||
|
|
||||||
|
for filename in args:
|
||||||
|
with open(filename, 'r') as codefile:
|
||||||
|
mod = {
|
||||||
|
'attributes': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for line in codefile:
|
||||||
|
match = re.search(r'@module (.*)', line)
|
||||||
|
if match is not None:
|
||||||
|
mod['type'] = match[1]
|
||||||
|
|
||||||
|
match = re.search(r'@module_desc (.*)', line)
|
||||||
|
if match is not None:
|
||||||
|
mod['description'] = match[1]
|
||||||
|
|
||||||
|
match = re.search(
|
||||||
|
r'@attr ([^\s]+) ([^\s]+) ([^\s]+) "([^"]*)" (.*)', line)
|
||||||
|
if match is not None:
|
||||||
|
mod['attributes'].append({
|
||||||
|
'name': match[1],
|
||||||
|
'required': match[2],
|
||||||
|
'type': match[3],
|
||||||
|
'default': match[4],
|
||||||
|
'description': match[5],
|
||||||
|
})
|
||||||
|
|
||||||
|
mod['attributes'] = sorted(
|
||||||
|
mod['attributes'], key=lambda a: ('0' if a['required'] == 'required' else '1') + ':' + a['name'])
|
||||||
|
|
||||||
|
modules.append(mod)
|
||||||
|
|
||||||
|
modules = sorted(modules, key=lambda m: m['type'])
|
||||||
|
|
||||||
|
with open('docs/config.md.tpl', 'r') as f:
|
||||||
|
tpl = jinja2.Template(f.read())
|
||||||
|
print(tpl.render(modules=modules))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
1
ci/requirements.txt
Normal file
1
ci/requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
jinja2
|
84
docs/config.md
Normal file
84
docs/config.md
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
# Configuration file format
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
|
||||||
|
catalog:
|
||||||
|
|
||||||
|
- name: alpine
|
||||||
|
tag: stable
|
||||||
|
|
||||||
|
fetcher: html
|
||||||
|
fetcher_config:
|
||||||
|
url: https://alpinelinux.org/downloads/
|
||||||
|
xpath: '//div[@class="l-box"]/p/strong'
|
||||||
|
|
||||||
|
check_interval: 1h
|
||||||
|
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Each catalog entry contains a `name` and a `tag` representing the entry. You can choose those freely but they should be URL-safe. Some examples I'm using are: `alpine:stable`, `google-chrome:dev`, `google-chrome:stable`, `factorio:latest`, …
|
||||||
|
|
||||||
|
Additionally you will configure a `fetcher` with its corresponding `fetcher_config` for the catalog entry. In the example above the `html` fetcher is used with two attributes configured. The attributes for each fetcher can be found below.
|
||||||
|
|
||||||
|
## Available Fetchers
|
||||||
|
|
||||||
|
## Fetcher: `atlassian`
|
||||||
|
|
||||||
|
Fetches latest version of an Atlassian product
|
||||||
|
|
||||||
|
| Attribute | Req. | Type | Default Value | Description |
|
||||||
|
| --------- | :--: | ---- | ------------- | ----------- |
|
||||||
|
| `product` | ✅ | string | | Lowercase name of the product to fetch (e.g. confluence, crowd, jira-software, ...) |
|
||||||
|
| `edition` | | string | | Filter down the versions according to its edition (e.g. "Enterprise" or "Standard" for Confluence) |
|
||||||
|
| `search` | | string | `TAR.GZ` | What to search in the download description: default is to search for the standalone .tar.gz file |
|
||||||
|
|
||||||
|
## Fetcher: `git_tag`
|
||||||
|
|
||||||
|
Reads git tags (annotated and leightweight) from a remote repository and returns the newest one
|
||||||
|
|
||||||
|
| Attribute | Req. | Type | Default Value | Description |
|
||||||
|
| --------- | :--: | ---- | ------------- | ----------- |
|
||||||
|
| `remote` | ✅ | string | | Repository remote to fetch the tags from (should accept everything you can use in `git remote set-url` command) |
|
||||||
|
|
||||||
|
## Fetcher: `github_release`
|
||||||
|
|
||||||
|
Fetches the latest release from Github for a given repository not marked as pre-release
|
||||||
|
|
||||||
|
| Attribute | Req. | Type | Default Value | Description |
|
||||||
|
| --------- | :--: | ---- | ------------- | ----------- |
|
||||||
|
| `repository` | ✅ | string | | Repository to fetch in form `owner/repo` |
|
||||||
|
|
||||||
|
## Fetcher: `html`
|
||||||
|
|
||||||
|
Downloads website, selects text-node using XPath and optionally applies custom regular expression
|
||||||
|
|
||||||
|
| Attribute | Req. | Type | Default Value | Description |
|
||||||
|
| --------- | :--: | ---- | ------------- | ----------- |
|
||||||
|
| `url` | ✅ | string | | URL to fetch the HTML from |
|
||||||
|
| `xpath` | ✅ | string | | XPath expression leading to the text-node containing the version |
|
||||||
|
| `regex` | | string | `(v?(?:[0-9]+\.?){2,})` | Regular expression to apply to the text from the XPath expression |
|
||||||
|
|
||||||
|
## Fetcher: `json`
|
||||||
|
|
||||||
|
Fetches a JSON / JSONP file from remote source and traverses it using XPath expression
|
||||||
|
|
||||||
|
| Attribute | Req. | Type | Default Value | Description |
|
||||||
|
| --------- | :--: | ---- | ------------- | ----------- |
|
||||||
|
| `url` | ✅ | string | | URL to fetch the HTML from |
|
||||||
|
| `xpath` | ✅ | string | | XPath expression leading to the text-node containing the version |
|
||||||
|
| `jsonp` | | boolean | `false` | File contains JSONP function, strip it to get the raw JSON |
|
||||||
|
|
||||||
|
## Fetcher: `regex`
|
||||||
|
|
||||||
|
Fetches URL and applies a regular expression to extract a version from it
|
||||||
|
|
||||||
|
| Attribute | Req. | Type | Default Value | Description |
|
||||||
|
| --------- | :--: | ---- | ------------- | ----------- |
|
||||||
|
| `regex` | ✅ | string | | Regular expression (RE2) to apply to the text fetched from the URL. The regex MUST have exactly one submatch containing the version. |
|
||||||
|
| `url` | ✅ | string | | URL to fetch the content from |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- vim: set ft=markdown : -->
|
40
docs/config.md.tpl
Normal file
40
docs/config.md.tpl
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# Configuration file format
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
|
||||||
|
catalog:
|
||||||
|
|
||||||
|
- name: alpine
|
||||||
|
tag: stable
|
||||||
|
|
||||||
|
fetcher: html
|
||||||
|
fetcher_config:
|
||||||
|
url: https://alpinelinux.org/downloads/
|
||||||
|
xpath: '//div[@class="l-box"]/p/strong'
|
||||||
|
|
||||||
|
check_interval: 1h
|
||||||
|
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Each catalog entry contains a `name` and a `tag` representing the entry. You can choose those freely but they should be URL-safe. Some examples I'm using are: `alpine:stable`, `google-chrome:dev`, `google-chrome:stable`, `factorio:latest`, …
|
||||||
|
|
||||||
|
Additionally you will configure a `fetcher` with its corresponding `fetcher_config` for the catalog entry. In the example above the `html` fetcher is used with two attributes configured. The attributes for each fetcher can be found below.
|
||||||
|
|
||||||
|
## Available Fetchers
|
||||||
|
|
||||||
|
{% for module in modules -%}
|
||||||
|
## Fetcher: `{{ module.type }}`
|
||||||
|
|
||||||
|
{{ module.description }}
|
||||||
|
|
||||||
|
| Attribute | Req. | Type | Default Value | Description |
|
||||||
|
| --------- | :--: | ---- | ------------- | ----------- |
|
||||||
|
{%- for attr in module.attributes %}
|
||||||
|
| `{{ attr.name }}` | {% if attr.required == 'required' %}✅{% endif %} | {{ attr.type }} | {% if attr.default != "" %}`{{ attr.default }}`{% endif %} | {{ attr.description }} |
|
||||||
|
{%- endfor %}
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<!-- vim: set ft=markdown : -->
|
|
@ -16,6 +16,11 @@ import (
|
||||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @module atlassian
|
||||||
|
* @module_desc Fetches latest version of an Atlassian product
|
||||||
|
*/
|
||||||
|
|
||||||
var (
|
var (
|
||||||
atlassianDefaultEdition = ""
|
atlassianDefaultEdition = ""
|
||||||
atlassianDefaultSearch = "TAR.GZ"
|
atlassianDefaultSearch = "TAR.GZ"
|
||||||
|
@ -76,8 +81,10 @@ func (a AtlassianFetcher) FetchVersion(ctx context.Context, attrs *fieldcollecti
|
||||||
})
|
})
|
||||||
|
|
||||||
var (
|
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)
|
edition = attrs.MustString("edition", &atlassianDefaultEdition)
|
||||||
search = attrs.MustString("search", &atlassianDefaultSearch)
|
// @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 {
|
for _, r := range payload {
|
||||||
|
@ -101,6 +108,7 @@ func (AtlassianFetcher) Links(attrs *fieldcollection.FieldCollection) []database
|
||||||
}
|
}
|
||||||
|
|
||||||
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, ...)
|
||||||
if v, err := attrs.String("product"); err != nil || v == "" {
|
if v, err := attrs.String("product"); err != nil || v == "" {
|
||||||
return errors.New("product is expected to be non-empty string")
|
return errors.New("product is expected to be non-empty string")
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,11 @@ import (
|
||||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @module git_tag
|
||||||
|
* @module_desc Reads git tags (annotated and leightweight) from a remote repository and returns the newest one
|
||||||
|
*/
|
||||||
|
|
||||||
type (
|
type (
|
||||||
GitTagFetcher struct{}
|
GitTagFetcher struct{}
|
||||||
)
|
)
|
||||||
|
@ -79,6 +84,7 @@ func (g GitTagFetcher) Links(attrs *fieldcollection.FieldCollection) []database.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g GitTagFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
func (g 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 == "" {
|
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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,11 @@ import (
|
||||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @module github_release
|
||||||
|
* @module_desc Fetches the latest release from Github for a given repository not marked as pre-release
|
||||||
|
*/
|
||||||
|
|
||||||
const githubHTTPTimeout = 2 * time.Second
|
const githubHTTPTimeout = 2 * time.Second
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -91,6 +96,7 @@ func (g GithubReleaseFetcher) Links(attrs *fieldcollection.FieldCollection) []da
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g GithubReleaseFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
func (g 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 == "" {
|
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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,11 @@ import (
|
||||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @module html
|
||||||
|
* @module_desc Downloads website, selects text-node using XPath and optionally applies custom regular expression
|
||||||
|
*/
|
||||||
|
|
||||||
var htmlFetcherDefaultRegex = `(v?(?:[0-9]+\.?){2,})`
|
var htmlFetcherDefaultRegex = `(v?(?:[0-9]+\.?){2,})`
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -64,10 +69,12 @@ func (h HTMLFetcher) Links(attrs *fieldcollection.FieldCollection) []database.Ca
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h HTMLFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
func (h 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 == "" {
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @attr xpath required string "" XPath expression leading to the text-node containing the version
|
||||||
if v, err := attrs.String("xpath"); err != nil || v == "" {
|
if v, err := attrs.String("xpath"); err != nil || v == "" {
|
||||||
return errors.New("xpath is expected to be non-empty string")
|
return errors.New("xpath is expected to be non-empty string")
|
||||||
}
|
}
|
||||||
|
@ -76,6 +83,7 @@ func (h HTMLFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
||||||
return errors.Wrap(err, "compiling xpath expression")
|
return errors.Wrap(err, "compiling xpath expression")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @attr regex optional string "(v?(?:[0-9]+\.?){2,})" Regular expression to apply to the text from the XPath expression
|
||||||
if attrs.CanString("regex") {
|
if attrs.CanString("regex") {
|
||||||
if _, err := regexp.Compile(attrs.MustString("regex", nil)); err != nil {
|
if _, err := regexp.Compile(attrs.MustString("regex", nil)); err != nil {
|
||||||
return errors.Wrap(err, "invalid regex given")
|
return errors.Wrap(err, "invalid regex given")
|
||||||
|
|
|
@ -16,6 +16,11 @@ import (
|
||||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @module json
|
||||||
|
* @module_desc Fetches a JSON / JSONP file from remote source and traverses it using XPath expression
|
||||||
|
*/
|
||||||
|
|
||||||
var (
|
var (
|
||||||
jsonFetcherDefaultRegex = `(v?(?:[0-9]+\.?){2,})`
|
jsonFetcherDefaultRegex = `(v?(?:[0-9]+\.?){2,})`
|
||||||
jsonpStripRegex = regexp.MustCompile(`(?m)^[^\(]+\((.*)\)$`)
|
jsonpStripRegex = regexp.MustCompile(`(?m)^[^\(]+\((.*)\)$`)
|
||||||
|
@ -34,6 +39,7 @@ func (JSONFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.Fiel
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @attr jsonp optional boolean "false" File contains JSONP function, strip it to get the raw JSON
|
||||||
if attrs.MustBool("jsonp", ptrBoolFalse) {
|
if attrs.MustBool("jsonp", ptrBoolFalse) {
|
||||||
var (
|
var (
|
||||||
body []byte
|
body []byte
|
||||||
|
@ -98,10 +104,12 @@ func (JSONFetcher) FetchVersion(ctx context.Context, attrs *fieldcollection.Fiel
|
||||||
func (JSONFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink { return nil }
|
func (JSONFetcher) Links(attrs *fieldcollection.FieldCollection) []database.CatalogLink { return nil }
|
||||||
|
|
||||||
func (JSONFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
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 == "" {
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @attr xpath required string "" XPath expression leading to the text-node containing the version
|
||||||
if v, err := attrs.String("xpath"); err != nil || v == "" {
|
if v, err := attrs.String("xpath"); err != nil || v == "" {
|
||||||
return errors.New("xpath is expected to be non-empty string")
|
return errors.New("xpath is expected to be non-empty string")
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,11 @@ import (
|
||||||
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
"github.com/Luzifer/go_helpers/v2/fieldcollection"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @module regex
|
||||||
|
* @module_desc Fetches URL and applies a regular expression to extract a version from it
|
||||||
|
*/
|
||||||
|
|
||||||
const (
|
const (
|
||||||
httpStatus3xx = 300
|
httpStatus3xx = 300
|
||||||
regexpFetcherExpectedLength = 2
|
regexpFetcherExpectedLength = 2
|
||||||
|
@ -68,10 +73,12 @@ func (h RegexFetcher) Links(attrs *fieldcollection.FieldCollection) []database.C
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h RegexFetcher) Validate(attrs *fieldcollection.FieldCollection) error {
|
func (h 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 == "" {
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @attr regex required string "" Regular expression (RE2) to apply to the text fetched from the URL. The regex MUST have exactly one submatch containing the version.
|
||||||
if v, err := attrs.String("regex"); err != nil || v == "" {
|
if v, err := attrs.String("regex"); err != nil || v == "" {
|
||||||
return errors.New("regex is expected to be non-empty string")
|
return errors.New("regex is expected to be non-empty string")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue