mirror of
https://github.com/crazy-max/diun.git
synced 2025-12-21 13:23:09 +01:00
Configuration transposed into environment variables (#82)
Configuration file not required anymore DIUN_DB env var renamed DIUN_DB_PATH Only accept duration as timeout value (10 becomes 10s) Add getting started doc Enhanced documentation Add note about test notifications (#79) Improve configuration management Fix telegram init All fields in configuration now camelCased Improve configuration validation Update doc Update FAQ Co-authored-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
@@ -6,9 +6,15 @@ services:
|
|||||||
container_name: diun
|
container_name: diun
|
||||||
volumes:
|
volumes:
|
||||||
- "./data:/data"
|
- "./data:/data"
|
||||||
- "./diun.yml:/diun.yml:ro"
|
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
environment:
|
environment:
|
||||||
- "TZ=Europe/Paris"
|
- "TZ=Europe/Paris"
|
||||||
- "LOG_LEVEL=info"
|
- "LOG_LEVEL=info"
|
||||||
- "LOG_JSON=false"
|
- "LOG_JSON=false"
|
||||||
|
- "DIUN_PROVIDERS_DOCKER=true"
|
||||||
|
- "DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=false"
|
||||||
|
- "DIUN_PROVIDERS_DOCKER_WATCHSTOPPED=false"
|
||||||
|
labels:
|
||||||
|
- "diun.enable=true"
|
||||||
|
- "diun.watch_repo=true"
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ User=diun
|
|||||||
Group=diun
|
Group=diun
|
||||||
ExecStart=/usr/local/bin/diun --config /etc/diun/diun.yml --log-level info
|
ExecStart=/usr/local/bin/diun --config /etc/diun/diun.yml --log-level info
|
||||||
Restart=always
|
Restart=always
|
||||||
Environment=DIUN_DB=/var/lib/diun/diun.db
|
Environment=DIUN_DB_PATH=/var/lib/diun/diun.db
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ COPY --from=builder /app/diun /usr/local/bin/diun
|
|||||||
COPY --from=builder /usr/local/go/lib/time/zoneinfo.zip /usr/local/go/lib/time/zoneinfo.zip
|
COPY --from=builder /usr/local/go/lib/time/zoneinfo.zip /usr/local/go/lib/time/zoneinfo.zip
|
||||||
RUN diun --version
|
RUN diun --version
|
||||||
|
|
||||||
ENV DIUN_DB="/data/diun.db"
|
ENV DIUN_DB_PATH="/data/diun.db"
|
||||||
|
|
||||||
VOLUME [ "/data" ]
|
VOLUME [ "/data" ]
|
||||||
|
|
||||||
ENTRYPOINT [ "diun" ]
|
ENTRYPOINT [ "diun" ]
|
||||||
CMD [ "--config", "/diun.yml" ]
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
* [With Docker](doc/install/docker.md)
|
* [With Docker](doc/install/docker.md)
|
||||||
* [From binary](doc/install/binary.md)
|
* [From binary](doc/install/binary.md)
|
||||||
* [Linux service](doc/install/linux-service.md)
|
* [Linux service](doc/install/linux-service.md)
|
||||||
* [Usage](doc/usage.md)
|
* [Getting started](doc/getting-started.md)
|
||||||
* [Configuration](doc/configuration.md)
|
* [Configuration](doc/configuration.md)
|
||||||
* Providers
|
* Providers
|
||||||
* [Docker](doc/providers/docker.md)
|
* [Docker](doc/providers/docker.md)
|
||||||
|
|||||||
27
cmd/main.go
27
cmd/main.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -20,15 +21,25 @@ var (
|
|||||||
diun *app.Diun
|
diun *app.Diun
|
||||||
cli model.Cli
|
cli model.Cli
|
||||||
version = "dev"
|
version = "dev"
|
||||||
|
meta = model.Meta{
|
||||||
|
ID: "diun",
|
||||||
|
Name: "Diun",
|
||||||
|
Desc: "Docker image update notifier",
|
||||||
|
URL: "https://github.com/crazy-max/diun",
|
||||||
|
Logo: "https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png",
|
||||||
|
Author: "CrazyMax",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
meta.Version = version
|
||||||
|
meta.UserAgent = fmt.Sprintf("%s/%s go/%s %s", meta.ID, meta.Version, runtime.Version()[2:], strings.Title(runtime.GOOS))
|
||||||
|
|
||||||
// Parse command line
|
// Parse command line
|
||||||
_ = kong.Parse(&cli,
|
_ = kong.Parse(&cli,
|
||||||
kong.Name("diun"),
|
kong.Name(meta.ID),
|
||||||
kong.Description(`Docker image update notifier. More info: https://github.com/crazy-max/diun`),
|
kong.Description(fmt.Sprintf("%s. More info: %s", meta.Desc, meta.URL)),
|
||||||
kong.UsageOnError(),
|
kong.UsageOnError(),
|
||||||
kong.Vars{
|
kong.Vars{
|
||||||
"version": fmt.Sprintf("%s", version),
|
"version": fmt.Sprintf("%s", version),
|
||||||
@@ -46,7 +57,7 @@ func main() {
|
|||||||
|
|
||||||
// Init
|
// Init
|
||||||
logging.Configure(&cli, location)
|
logging.Configure(&cli, location)
|
||||||
log.Info().Msgf("Starting Diun %s", version)
|
log.Info().Str("version", version).Msgf("Starting %s", meta.Name)
|
||||||
|
|
||||||
// Handle os signals
|
// Handle os signals
|
||||||
channel := make(chan os.Signal)
|
channel := make(chan os.Signal)
|
||||||
@@ -59,15 +70,15 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Load configuration
|
// Load configuration
|
||||||
cfg, err := config.Load(cli, version)
|
cfg, err := config.Load(cli.Cfgfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal().Err(err).Msg("Cannot load configuration")
|
log.Fatal().Err(err).Msg("Cannot load configuration")
|
||||||
}
|
}
|
||||||
log.Debug().Msg(cfg.Display())
|
log.Debug().Msg(cfg.String())
|
||||||
|
|
||||||
// Init
|
// Init
|
||||||
if diun, err = app.New(cfg, location); err != nil {
|
if diun, err = app.New(meta, cfg, location); err != nil {
|
||||||
log.Fatal().Err(err).Msg("Cannot initialize Diun")
|
log.Fatal().Err(err).Msgf("Cannot initialize %s", meta.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test notif
|
// Test notif
|
||||||
@@ -78,6 +89,6 @@ func main() {
|
|||||||
|
|
||||||
// Start
|
// Start
|
||||||
if err = diun.Start(); err != nil {
|
if err = diun.Start(); err != nil {
|
||||||
log.Fatal().Err(err).Msg("Cannot start Diun")
|
log.Fatal().Err(err).Msgf("Cannot start %s", meta.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
* [Overview](#overview)
|
* [Overview](#overview)
|
||||||
|
* [Configuration file](#configuration-file)
|
||||||
* [Reference](#reference)
|
* [Reference](#reference)
|
||||||
* [db](#db)
|
* [db](#db)
|
||||||
* [watch](#watch)
|
* [watch](#watch)
|
||||||
@@ -10,6 +11,21 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
|
There are two different ways to define static configuration options in Diun:
|
||||||
|
|
||||||
|
* In a [configuration file](#configuration-file)
|
||||||
|
* As environment variables
|
||||||
|
|
||||||
|
These ways are evaluated in the order listed above.
|
||||||
|
|
||||||
|
If no value was provided for a given option, a default value applies. Moreover, if an option has sub-options, and any of these sub-options is not specified, a default value will apply as well.
|
||||||
|
|
||||||
|
For example, the `DIUN_PROVIDERS_DOCKER` environment variable is enough by itself to enable the docker provider, even though sub-options like `DIUN_PROVIDERS_DOCKER_ENDPOINT` exist. Once positioned, this option sets (and resets) all the default values of the sub-options of `DIUN_PROVIDERS_DOCKER`.
|
||||||
|
|
||||||
|
## Configuration file
|
||||||
|
|
||||||
|
You can define a configuration file through the option `--config` with the following content:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
db:
|
db:
|
||||||
path: diun.db
|
path: diun.db
|
||||||
@@ -17,7 +33,7 @@ db:
|
|||||||
watch:
|
watch:
|
||||||
workers: 10
|
workers: 10
|
||||||
schedule: "0 * * * *"
|
schedule: "0 * * * *"
|
||||||
first_check_notif: false
|
firstCheckNotif: false
|
||||||
|
|
||||||
notif:
|
notif:
|
||||||
amqp:
|
amqp:
|
||||||
@@ -25,65 +41,62 @@ notif:
|
|||||||
port: 5672
|
port: 5672
|
||||||
username: guest
|
username: guest
|
||||||
password: guest
|
password: guest
|
||||||
exchange:
|
|
||||||
queue: queue
|
queue: queue
|
||||||
gotify:
|
gotify:
|
||||||
endpoint: http://gotify.foo.com
|
endpoint: http://gotify.foo.com
|
||||||
token: Token123456
|
token: Token123456
|
||||||
priority: 1
|
priority: 1
|
||||||
timeout: 10
|
timeout: 10s
|
||||||
mail:
|
mail:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 25
|
port: 25
|
||||||
ssl: false
|
ssl: false
|
||||||
insecure_skip_verify: false
|
insecureSkipVerify: false
|
||||||
username:
|
from: diun@example.com
|
||||||
password:
|
to: webmaster@example.com
|
||||||
from:
|
|
||||||
to:
|
|
||||||
rocketchat:
|
rocketchat:
|
||||||
endpoint: http://rocket.foo.com:3000
|
endpoint: http://rocket.foo.com:3000
|
||||||
channel: "#general"
|
channel: "#general"
|
||||||
user_id: abcdEFGH012345678
|
userID: abcdEFGH012345678
|
||||||
token: Token123456
|
token: Token123456
|
||||||
timeout: 10
|
timeout: 10s
|
||||||
script:
|
script:
|
||||||
cmd: "myprogram"
|
cmd: "myprogram"
|
||||||
args:
|
args:
|
||||||
- "--anarg"
|
- "--anarg"
|
||||||
- "another"
|
- "another"
|
||||||
slack:
|
slack:
|
||||||
webhook_url: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij
|
webhookURL: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij
|
||||||
teams:
|
teams:
|
||||||
webhook_url: https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij
|
webhookURL: https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij
|
||||||
telegram:
|
telegram:
|
||||||
token: aabbccdd:11223344
|
token: aabbccdd:11223344
|
||||||
chat_ids:
|
chatIDs:
|
||||||
- 123456789
|
- 123456789
|
||||||
- 987654321
|
- 987654321
|
||||||
webhook:
|
webhook:
|
||||||
endpoint: http://webhook.foo.com/sd54qad89azd5a
|
endpoint: http://webhook.foo.com/sd54qad89azd5a
|
||||||
method: GET
|
method: GET
|
||||||
headers:
|
headers:
|
||||||
Content-Type: application/json
|
content-type: application/json
|
||||||
Authorization: Token123456
|
authorization: Token123456
|
||||||
timeout: 10
|
timeout: 10s
|
||||||
|
|
||||||
regopts:
|
regopts:
|
||||||
someregistryoptions:
|
someregistryoptions:
|
||||||
username: foo
|
username: foo
|
||||||
password: bar
|
password: bar
|
||||||
timeout: 20
|
timeout: 20s
|
||||||
onemore:
|
onemore:
|
||||||
username: foo2
|
username: foo2
|
||||||
password: bar2
|
password: bar2
|
||||||
insecure_tls: true
|
insecureTls: true
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
docker:
|
docker:
|
||||||
watch_stopped: true
|
watchStopped: true
|
||||||
swarm:
|
swarm:
|
||||||
watch_by_default: true
|
watchByDefault: true
|
||||||
file:
|
file:
|
||||||
directory: ./imagesdir
|
directory: ./imagesdir
|
||||||
```
|
```
|
||||||
@@ -92,13 +105,23 @@ providers:
|
|||||||
|
|
||||||
### db
|
### db
|
||||||
|
|
||||||
* `path`: Path to Bolt database file where images manifests are stored (default: `diun.db`). Environment var `DIUN_DB` override this value.
|
* `path`: Path to Bolt database file where images manifests are stored. (default `diun.db`)
|
||||||
|
|
||||||
|
You can also use the following environment variables:
|
||||||
|
|
||||||
|
* `DIUN_DB_PATH`
|
||||||
|
|
||||||
### watch
|
### watch
|
||||||
|
|
||||||
* `workers`: Maximum number of workers that will execute tasks concurrently (default: `10`).
|
* `workers`: Maximum number of workers that will execute tasks concurrently. (default `10`)
|
||||||
* `schedule`: [CRON expression](https://godoc.org/github.com/robfig/cron#hdr-CRON_Expression_Format) to schedule Diun watcher (default: `0 * * * *`).
|
* `schedule`: [CRON expression](https://godoc.org/github.com/robfig/cron#hdr-CRON_Expression_Format) to schedule Diun watcher. (default `0 * * * *`)
|
||||||
* `first_check_notif`: Send notification at the very first analysis of an image. (default: `false`).
|
* `firstCheckNotif`: Send notification at the very first analysis of an image. (default `false`)
|
||||||
|
|
||||||
|
You can also use the following environment variables:
|
||||||
|
|
||||||
|
* `DIUN_WATCH_WORKERS`
|
||||||
|
* `DIUN_WATCH_SCHEDULE`
|
||||||
|
* `DIUN_WATCH_FIRSTCHECKNOTIF`
|
||||||
|
|
||||||
### notif
|
### notif
|
||||||
|
|
||||||
@@ -107,7 +130,7 @@ providers:
|
|||||||
* [mail](notifications.md#mail)
|
* [mail](notifications.md#mail)
|
||||||
* [rocketchat](notifications.md#rocketchat)
|
* [rocketchat](notifications.md#rocketchat)
|
||||||
* [script](notifications.md#script)
|
* [script](notifications.md#script)
|
||||||
* [slack](notifications.md#slack)
|
* [slack](notifications.md#slack--mattermost)
|
||||||
* [teams](notifications.md#teams)
|
* [teams](notifications.md#teams)
|
||||||
* [telegram](notifications.md#telegram)
|
* [telegram](notifications.md#telegram)
|
||||||
* [webhook](notifications.md#webhook)
|
* [webhook](notifications.md#webhook)
|
||||||
@@ -115,11 +138,20 @@ providers:
|
|||||||
### regopts
|
### regopts
|
||||||
|
|
||||||
* `username`: Registry username.
|
* `username`: Registry username.
|
||||||
* `username_file`: Use content of secret file as registry username if `username` not defined.
|
* `usernameFile`: Use content of secret file as registry username if `username` not defined.
|
||||||
* `password`: Registry password.
|
* `password`: Registry password.
|
||||||
* `password_file`: Use content of secret file as registry password if `password` not defined.
|
* `passwordFile`: Use content of secret file as registry password if `password` not defined.
|
||||||
* `timeout`: Timeout is the maximum amount of time for the TCP connection to establish. 0 means no timeout (default: `10`).
|
* `timeout`: Timeout is the maximum amount of time for the TCP connection to establish. (default `10s`)
|
||||||
* `insecure_tls`: Allow contacting docker registry over HTTP, or HTTPS with failed TLS verification (default: `false`).
|
* `insecureTls`: Allow contacting docker registry over HTTP, or HTTPS with failed TLS verification. (default `false`)
|
||||||
|
|
||||||
|
You can also use the following environment variables:
|
||||||
|
|
||||||
|
* `DIUN_REGOPTS_<NAME>_USERNAME`
|
||||||
|
* `DIUN_REGOPTS_<NAME>_USERNAMEFILE`
|
||||||
|
* `DIUN_REGOPTS_<NAME>_PASSWORD`
|
||||||
|
* `DIUN_REGOPTS_<NAME>_PASSWORDFILE`
|
||||||
|
* `DIUN_REGOPTS_<NAME>_TIMEOUT`
|
||||||
|
* `DIUN_REGOPTS_<NAME>_INSECURETLS`
|
||||||
|
|
||||||
### providers
|
### providers
|
||||||
|
|
||||||
|
|||||||
67
doc/faq.md
67
doc/faq.md
@@ -1,7 +1,74 @@
|
|||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
|
* [Test notifications](#test-notifications)
|
||||||
|
* [Maximum supported API version is 1.39 error](#maximum-supported-api-version-is-139-error)
|
||||||
|
* [field docker|swarm uses unsupported type: invalid](#field-dockerswarm-uses-unsupported-type-invalid)
|
||||||
* [No image found in manifest list for architecture [], variant [], OS []](#no-image-found-in-manifest-list-for-architecture--variant--os-)
|
* [No image found in manifest list for architecture [], variant [], OS []](#no-image-found-in-manifest-list-for-architecture--variant--os-)
|
||||||
|
|
||||||
|
## Test notifications
|
||||||
|
|
||||||
|
Through the [command line](getting-started.md#diun-cli) with:
|
||||||
|
|
||||||
|
```
|
||||||
|
diun --config ./diun.yml --test-notif
|
||||||
|
```
|
||||||
|
|
||||||
|
Or within a container:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose exec diun --test-notif
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maximum supported API version is 1.39 error
|
||||||
|
|
||||||
|
The error `Error response from daemon: client version 1.40 is too new. Maximum supported API version is 1.39` indicates that you are using a fairly old version of Docker like:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker version
|
||||||
|
Client:
|
||||||
|
Version: 18.09.8
|
||||||
|
API version: 1.39
|
||||||
|
Go version: go1.11
|
||||||
|
Git commit: bfed4f5
|
||||||
|
Built: Fri Mar 13 06:46:11 2020
|
||||||
|
OS/Arch: linux/amd64
|
||||||
|
Experimental: false
|
||||||
|
|
||||||
|
Server:
|
||||||
|
Engine:
|
||||||
|
Version: 18.09.8
|
||||||
|
API version: 1.39 (minimum version 1.12)
|
||||||
|
Go version: go1.11
|
||||||
|
Git commit: 3a371f3
|
||||||
|
Built: Fri Mar 13 06:44:35 2020
|
||||||
|
OS/Arch: linux/amd64
|
||||||
|
Experimental: false
|
||||||
|
```
|
||||||
|
|
||||||
|
To solve this, you need to specify the API version in your docker or swarm provider (`apiVersion`) displayed in the error message. In this case:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
providers:
|
||||||
|
docker:
|
||||||
|
apiVersion: 1.39
|
||||||
|
```
|
||||||
|
|
||||||
|
## field docker|swarm uses unsupported type: invalid
|
||||||
|
|
||||||
|
If you have the error `failed to decode configuration from file: field docker uses unsupported type: invalid` that's because your `docker` or `swarm` provider is not initialized in your configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
providers:
|
||||||
|
docker:
|
||||||
|
```
|
||||||
|
|
||||||
|
should be:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
providers:
|
||||||
|
docker: {}
|
||||||
|
```
|
||||||
|
|
||||||
## No image found in manifest list for architecture [], variant [], OS []
|
## No image found in manifest list for architecture [], variant [], OS []
|
||||||
|
|
||||||
If you encounter this kind of error, you are probably using the [file provider](providers/file.md) containing an image with an erroneous or empty platform. If the platform is not filled in, it will be deduced automatically from the information of your operating system on which Diun is running.
|
If you encounter this kind of error, you are probably using the [file provider](providers/file.md) containing an image with an erroneous or empty platform. If the platform is not filled in, it will be deduced automatically from the information of your operating system on which Diun is running.
|
||||||
|
|||||||
95
doc/getting-started.md
Normal file
95
doc/getting-started.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# Getting started
|
||||||
|
|
||||||
|
* [Diun CLI](#diun-cli)
|
||||||
|
* [Run with the Docker provider](#run-with-the-docker-provider)
|
||||||
|
|
||||||
|
## Diun CLI
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ./diun --help
|
||||||
|
Usage: diun
|
||||||
|
|
||||||
|
Docker image update notifier. More info: https://github.com/crazy-max/diun
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--help Show context-sensitive help.
|
||||||
|
--version
|
||||||
|
--config=STRING Diun configuration file ($CONFIG).
|
||||||
|
--timezone="UTC" Timezone assigned to Diun ($TZ).
|
||||||
|
--log-level="info" Set log level ($LOG_LEVEL).
|
||||||
|
--log-json Enable JSON logging output ($LOG_JSON).
|
||||||
|
--log-caller Add file:line of the caller to log output ($LOG_CALLER).
|
||||||
|
--test-notif Test notification settings.
|
||||||
|
```
|
||||||
|
|
||||||
|
Following environment variables can be used in place of flags:
|
||||||
|
|
||||||
|
* `CONFIG`: Diun configuration file
|
||||||
|
* `TZ`: Timezone assigned (default `UTC`)
|
||||||
|
* `LOG_LEVEL`: Log level output (default `info`)
|
||||||
|
* `LOG_JSON`: Enable JSON logging output (default `false`)
|
||||||
|
* `LOG_CALLER`: Enable to add `file:line` of the caller (default `false`)
|
||||||
|
|
||||||
|
## Run with the Docker provider
|
||||||
|
|
||||||
|
Create a `docker-compose.yml` file that uses the official Diun image:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3.5"
|
||||||
|
|
||||||
|
services:
|
||||||
|
diun:
|
||||||
|
image: crazymax/diun:latest
|
||||||
|
volumes:
|
||||||
|
- "./data:/data"
|
||||||
|
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
|
environment:
|
||||||
|
- "TZ=Europe/Paris"
|
||||||
|
- "LOG_LEVEL=info"
|
||||||
|
- "LOG_JSON=false"
|
||||||
|
- "DIUN_WATCH_WORKERS=20"
|
||||||
|
- "DIUN_WATCH_SCHEDULE=*/30 * * * *"
|
||||||
|
- "DIUN_PROVIDERS_DOCKER=true"
|
||||||
|
- "DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT=true"
|
||||||
|
restart: always
|
||||||
|
```
|
||||||
|
|
||||||
|
Here we use a minimal configuration to analyze **all running containers** (watch by default enabled) of your **local Docker** instance **every 30 minutes**.
|
||||||
|
|
||||||
|
That's it. Now you can launch Diun with the following command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer to rely on the configuration file instead of environment variables:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3.5"
|
||||||
|
|
||||||
|
services:
|
||||||
|
diun:
|
||||||
|
image: crazymax/diun:latest
|
||||||
|
volumes:
|
||||||
|
- "./data:/data"
|
||||||
|
- "./diun.yml:/diun.yml:ro"
|
||||||
|
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
|
environment:
|
||||||
|
- "CONFIG=/diun.yml"
|
||||||
|
- "TZ=Europe/Paris"
|
||||||
|
- "LOG_LEVEL=info"
|
||||||
|
- "LOG_JSON=false"
|
||||||
|
restart: always
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ./diun.yml
|
||||||
|
|
||||||
|
watch:
|
||||||
|
workers: 20
|
||||||
|
schedule: "*/30 * * * *"
|
||||||
|
firstCheckNotif: false
|
||||||
|
|
||||||
|
providers:
|
||||||
|
docker: {}
|
||||||
|
```
|
||||||
@@ -7,29 +7,10 @@ Diun binaries are available in [releases](https://github.com/crazy-max/diun/rele
|
|||||||
Choose the archive matching the destination platform and extract diun:
|
Choose the archive matching the destination platform and extract diun:
|
||||||
|
|
||||||
```
|
```
|
||||||
wget -qO- https://github.com/crazy-max/diun/releases/download/v2.6.1/diun_2.6.1_linux_x86_64.tar.gz | tar -zxvf - diun
|
wget -qO- https://github.com/crazy-max/diun/releases/download/v3.0.0/diun_3.0.0_linux_x86_64.tar.gz | tar -zxvf - diun
|
||||||
```
|
```
|
||||||
|
|
||||||
## Test
|
After getting the binary, it can be tested with [`./diun --help`](../getting-started.md#diun-cli) command and moved to a permanent location.
|
||||||
|
|
||||||
After getting the binary, it can be tested with `./diun --help` or moved to a permanent location.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ ./diun --help
|
|
||||||
Usage: diun --config=STRING
|
|
||||||
|
|
||||||
Docker image update notifier. More info: https://github.com/crazy-max/diun
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
--help Show context-sensitive help.
|
|
||||||
--version
|
|
||||||
--config=STRING Diun configuration file ($CONFIG).
|
|
||||||
--timezone="UTC" Timezone assigned to Diun ($TZ).
|
|
||||||
--log-level="info" Set log level ($LOG_LEVEL).
|
|
||||||
--log-json Enable JSON logging output ($LOG_JSON).
|
|
||||||
--log-caller Add file:line of the caller to log output ($LOG_CALLER).
|
|
||||||
--test-notif Test notification settings.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Server configuration
|
## Server configuration
|
||||||
|
|
||||||
@@ -57,13 +38,15 @@ chmod 770 /etc/diun
|
|||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
You must create your first [configuration](../configuration.md) file in `/etc/diun/diun.yml` and type:
|
Create your first [configuration](../configuration.md) file in `/etc/diun/diun.yml` and type:
|
||||||
|
|
||||||
```
|
```
|
||||||
chown diun:diun /etc/diun/diun.yml
|
chown diun:diun /etc/diun/diun.yml
|
||||||
chmod 644 /etc/diun/diun.yml
|
chmod 644 /etc/diun/diun.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 💡 Not required if you want to only rely on environment variables
|
||||||
|
|
||||||
### Copy binary to global location
|
### Copy binary to global location
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -81,7 +64,7 @@ See how to create [Linux service](linux-service.md) to start Diun automatically.
|
|||||||
### 2. Running from command-line/terminal
|
### 2. Running from command-line/terminal
|
||||||
|
|
||||||
```
|
```
|
||||||
DIUN_DB=/var/lib/diun/diun.db /usr/local/bin/diun --config /etc/diun/diun.yml
|
DIUN_DB_PATH=/var/lib/diun/diun.db /usr/local/bin/diun --config /etc/diun/diun.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Updating to a new version
|
## Updating to a new version
|
||||||
|
|||||||
@@ -18,37 +18,32 @@ Image: crazymax/diun:latest
|
|||||||
- linux/s390x
|
- linux/s390x
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment variables
|
|
||||||
|
|
||||||
* `TZ` : Timezone assigned
|
|
||||||
* `LOG_LEVEL` : Log level output (default `info`)
|
|
||||||
* `LOG_JSON`: Enable JSON logging output (default `false`)
|
|
||||||
* `LOG_CALLER`: Enable to add file:line of the caller (default `false`)
|
|
||||||
|
|
||||||
## Volumes
|
## Volumes
|
||||||
|
|
||||||
* `/data` : Contains bbolt database which retains Docker images manifests
|
* `/data`: Contains bbolt database which retains Docker images manifests
|
||||||
|
|
||||||
> :warning: Note that the volume should be owned by uid `1000` and gid `1000`. If you don't give the volume correct permissions, the container may not start.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Docker compose is the recommended way to run this image. Copy the content of folder [.res/compose](../../.res/compose) in `/opt/diun/` on your host for example. Edit the compose and config file with your preferences and run the following commands:
|
Docker compose is the recommended way to run this image. Copy the content of folder [.res/compose](../../.res/compose) in `/opt/diun/` on your host for example. Edit the compose file with your preferences and run the following commands:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
docker-compose logs -f
|
docker-compose logs -f
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use the following command :
|
Or use the following command:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ docker run -d --name diun \
|
$ docker run -d --name diun \
|
||||||
-e "TZ=Europe/Paris" \
|
-e "TZ=Europe/Paris" \
|
||||||
-e "LOG_LEVEL=info" \
|
-e "LOG_LEVEL=info" \
|
||||||
-e "LOG_JSON=false" \
|
-e "LOG_JSON=false" \
|
||||||
|
-e "DIUN_WATCH_WORKERS=20" \
|
||||||
|
-e "DIUN_WATCH_SCHEDULE=*/30 * * * *" \
|
||||||
|
-e "DIUN_PROVIDERS_DOCKER=true" \
|
||||||
|
-e "DIUN_PROVIDERS_DOCKER_WATCHSTOPPED=true" \
|
||||||
-v "$(pwd)/data:/data" \
|
-v "$(pwd)/data:/data" \
|
||||||
-v "$(pwd)/diun.yml:/diun.yml:ro" \
|
-v "/var/run/docker.sock:/var/run/docker.sock" \
|
||||||
crazymax/diun:latest
|
crazymax/diun:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -12,18 +12,33 @@
|
|||||||
|
|
||||||
## Amqp
|
## Amqp
|
||||||
|
|
||||||
You can send notifications to any amqp compatible server with the following settings:
|
You can send notifications to any amqp compatible server with the following settings.
|
||||||
|
|
||||||
|
### Configuration file
|
||||||
|
|
||||||
* `amqp`
|
* `amqp`
|
||||||
* `host`: AMQP server host (default: `localhost`). **required**
|
* `host`: AMQP server host (default `localhost`). **required**
|
||||||
* `port`: AMQP server port (default: `5672`). **required**
|
* `port`: AMQP server port (default `5672`). **required**
|
||||||
* `username`: AMQP username. **required**
|
* `username`: AMQP username.
|
||||||
* `username_file`: Use content of secret file as AMQP username if `username` not defined.
|
* `usernameFile`: Use content of secret file as AMQP username if `username` not defined.
|
||||||
* `password`: AMQP password. **required**
|
* `password`: AMQP password.
|
||||||
* `password_file`: Use content of secret file as AMQP password if `password` not defined.
|
* `passwordFile`: Use content of secret file as AMQP password if `password` not defined.
|
||||||
* `exchange`: Name of the exchange the message will be sent to. (default: `empty`)
|
* `exchange`: Name of the exchange the message will be sent to.
|
||||||
* `queue`: Name of the queue the message will be sent to. **required**
|
* `queue`: Name of the queue the message will be sent to. **required**
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
* `DIUN_NOTIF_AMQP_HOST`
|
||||||
|
* `DIUN_NOTIF_AMQP_EXCHANGE`
|
||||||
|
* `DIUN_NOTIF_AMQP_PORT`
|
||||||
|
* `DIUN_NOTIF_AMQP_USERNAME`
|
||||||
|
* `DIUN_NOTIF_AMQP_USERNAMEFILE`
|
||||||
|
* `DIUN_NOTIF_AMQP_PASSWORD`
|
||||||
|
* `DIUN_NOTIF_AMQP_PASSWORDFILE`
|
||||||
|
* `DIUN_NOTIF_AMQP_QUEUE`
|
||||||
|
|
||||||
|
### Sample
|
||||||
|
|
||||||
The JSON response will look like this:
|
The JSON response will look like this:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -41,59 +56,92 @@ The JSON response will look like this:
|
|||||||
|
|
||||||
## Gotify
|
## Gotify
|
||||||
|
|
||||||
Notifications can be sent using a [Gotify](https://gotify.net/) instance:
|
Notifications can be sent using a [Gotify](https://gotify.net/) instance.
|
||||||
|
|
||||||
|
### Configuration file
|
||||||
|
|
||||||
* `gotify`
|
* `gotify`
|
||||||
* `endpoint`: Gotify base URL (e.g. `http://gotify.foo.com`). **required**
|
* `endpoint`: Gotify base URL (e.g. `http://gotify.foo.com`). **required**
|
||||||
* `token`: Application token. **required**
|
* `token`: Application token. **required**
|
||||||
* `priority`: The priority of the message.
|
* `priority`: The priority of the message (default `1`).
|
||||||
* `timeout`: Timeout specifies a time limit for the request to be made. (default: `10`).
|
* `timeout`: Timeout specifies a time limit for the request to be made. (default `10s`).
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
* `DIUN_NOTIF_GOTIFY_ENDPOINT`
|
||||||
|
* `DIUN_NOTIF_GOTIFY_TOKEN`
|
||||||
|
* `DIUN_NOTIF_GOTIFY_PRIORITY`
|
||||||
|
* `DIUN_NOTIF_GOTIFY_TIMEOUT`
|
||||||
|
|
||||||
|
### Sample
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Mail
|
## Mail
|
||||||
|
|
||||||
Notifications can be sent through SMTP:
|
Notifications can be sent through SMTP.
|
||||||
|
|
||||||
|
### Configuration file
|
||||||
|
|
||||||
* `mail`
|
* `mail`
|
||||||
* `host`: SMTP server host (default: `localhost`). **required**
|
* `host`: SMTP server host. (default `localhost`) **required**
|
||||||
* `port`: SMTP server port (default: `25`). **required**
|
* `port`: SMTP server port. (default `25`) **required**
|
||||||
* `ssl`: SSL defines whether an SSL connection is used. Should be false in most cases since the auth mechanism should use STARTTLS (default: `false`).
|
* `ssl`: SSL defines whether an SSL connection is used. Should be false in most cases since the auth mechanism should use STARTTLS. (default `false`)
|
||||||
* `insecure_skip_verify`: Controls whether a client verifies the server's certificate chain and hostname (default: `false`).
|
* `insecureSkipVerify`: Controls whether a client verifies the server's certificate chain and hostname. (default `false`)
|
||||||
* `username`: SMTP username.
|
* `username`: SMTP username.
|
||||||
* `username_file`: Use content of secret file as SMTP username if `username` not defined.
|
* `usernameFile`: Use content of secret file as SMTP username if `username` not defined.
|
||||||
* `password`: SMTP password.
|
* `password`: SMTP password.
|
||||||
* `password_file`: Use content of secret file as SMTP password if `password` not defined.
|
* `passwordFile`: Use content of secret file as SMTP password if `password` not defined.
|
||||||
* `from`: Sender email address. **required**
|
* `from`: Sender email address. **required**
|
||||||
* `to`: Recipient email address. **required**
|
* `to`: Recipient email address. **required**
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
* `DIUN_NOTIF_MAIL_HOST`
|
||||||
|
* `DIUN_NOTIF_MAIL_PORT`
|
||||||
|
* `DIUN_NOTIF_MAIL_SSL`
|
||||||
|
* `DIUN_NOTIF_MAIL_INSECURESKIPVERIFY`
|
||||||
|
* `DIUN_NOTIF_MAIL_USERNAME`
|
||||||
|
* `DIUN_NOTIF_MAIL_USERNAMEFILE`
|
||||||
|
* `DIUN_NOTIF_MAIL_PASSWORD`
|
||||||
|
* `DIUN_NOTIF_MAIL_PASSWORDFILE`
|
||||||
|
* `DIUN_NOTIF_MAIL_FROM`
|
||||||
|
* `DIUN_NOTIF_MAIL_TO`
|
||||||
|
|
||||||
|
### Sample
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Rocket.Chat
|
## Rocket.Chat
|
||||||
|
|
||||||
To be able to send notifications to your Rocket.Chat channel:
|
Allow to send notifications to your Rocket.Chat channel.
|
||||||
|
|
||||||
> You must first create a _Personal Access Token_ through your account settings on your RocketChat instance.
|
> You must first create a _Personal Access Token_ through your account settings on your RocketChat instance.
|
||||||
|
|
||||||
|
### Configuration file
|
||||||
|
|
||||||
* `rocketchat`
|
* `rocketchat`
|
||||||
* `endpoint`: Rocket.Chat base URL (e.g. `http://rocket.foo.com:3000`). **required**
|
* `endpoint`: Rocket.Chat base URL (e.g. `http://rocket.foo.com:3000`). **required**
|
||||||
* `channel`: Channel name with the prefix in front of it. **required**
|
* `channel`: Channel name with the prefix in front of it. **required**
|
||||||
* `user_id`: User ID. **required**
|
* `userID`: User ID. **required**
|
||||||
* `token`: Authentication token. **required**
|
* `token`: Authentication token. **required**
|
||||||
* `timeout`: Timeout specifies a time limit for the request to be made. (default: `10`).
|
* `timeout`: Timeout specifies a time limit for the request to be made. (default `10s`).
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
* `DIUN_NOTIF_ROCKETCHAT_ENDPOINT`
|
||||||
|
* `DIUN_NOTIF_ROCKETCHAT_CHANNEL`
|
||||||
|
* `DIUN_NOTIF_ROCKETCHAT_USERID`
|
||||||
|
* `DIUN_NOTIF_ROCKETCHAT_TOKEN`
|
||||||
|
* `DIUN_NOTIF_ROCKETCHAT_TIMEOUT`
|
||||||
|
|
||||||
|
### Sample
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Script
|
## Script
|
||||||
|
|
||||||
You can send script notifications with the following settings:
|
You can call a script when a notification occured. Following environment variables will be passed:
|
||||||
|
|
||||||
* `script`
|
|
||||||
* `cmd`: Command or script to execute. **required**
|
|
||||||
* `args`: List of args to pass to `cmd`.
|
|
||||||
* `dir`: Specifies the working directory of the command.
|
|
||||||
|
|
||||||
Following environment variables are passed to the process and will look like this:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
DIUN_VERSION=3.0.0
|
DIUN_VERSION=3.0.0
|
||||||
@@ -106,12 +154,33 @@ DIUN_ENTRY_CREATED=2020-03-26 12:23:56 +0000 UTC
|
|||||||
DIUN_ENTRY_PLATFORM=linux/adm64
|
DIUN_ENTRY_PLATFORM=linux/adm64
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Configuration file
|
||||||
|
|
||||||
|
* `script`
|
||||||
|
* `cmd`: Command or script to execute. **required**
|
||||||
|
* `args`: List of args to pass to `cmd`.
|
||||||
|
* `dir`: Specifies the working directory of the command.
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
* `DIUN_NOTIF_SCRIPT_CMD`
|
||||||
|
* `DIUN_NOTIF_SCRIPT_ARGS`
|
||||||
|
* `DIUN_NOTIF_SCRIPT_DIR`
|
||||||
|
|
||||||
## Slack / Mattermost
|
## Slack / Mattermost
|
||||||
|
|
||||||
You can send notifications to your Slack channel using an [incoming webhook URL](https://api.slack.com/messaging/webhooks):
|
You can send notifications to your Slack channel using an [incoming webhook URL](https://api.slack.com/messaging/webhooks).
|
||||||
|
|
||||||
|
### Configuration file
|
||||||
|
|
||||||
* `slack`
|
* `slack`
|
||||||
* `webhook_url`: Slack [incoming webhook URL](https://api.slack.com/messaging/webhooks). **required**
|
* `webhookURL`: Slack [incoming webhook URL](https://api.slack.com/messaging/webhooks). **required**
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
* `DIUN_NOTIF_SLACK_WEBHOOKURL`
|
||||||
|
|
||||||
|
### Sample
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -119,10 +188,18 @@ Mattermost webhooks are compatible with Slack notification without any special c
|
|||||||
|
|
||||||
## Teams
|
## Teams
|
||||||
|
|
||||||
You can send notifications to your Teams team-channel using an [incoming webhook URL](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors):
|
You can send notifications to your Teams team-channel using an [incoming webhook URL](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors).
|
||||||
|
|
||||||
* `teams`
|
### Configuration file
|
||||||
* `webhook_url`: Teams [incoming webhook URL](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors). **required**
|
|
||||||
|
* `teams`
|
||||||
|
* `webhookURL`: Teams [incoming webhook URL](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors). **required**
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
* `DIUN_NOTIF_TEAMS_WEBHOOKURL`
|
||||||
|
|
||||||
|
### Sample
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -135,21 +212,41 @@ Follow the [instructions](https://core.telegram.org/bots#6-botfather) to set up
|
|||||||
Message the [GetID bot](https://t.me/getidsbot) to find your chat ID.
|
Message the [GetID bot](https://t.me/getidsbot) to find your chat ID.
|
||||||
Multiple chat IDs can be provided in order to deliver notifications to multiple recipients.
|
Multiple chat IDs can be provided in order to deliver notifications to multiple recipients.
|
||||||
|
|
||||||
|
### Configuration file
|
||||||
|
|
||||||
* `telegram`
|
* `telegram`
|
||||||
* `token`: Telegram bot token. **required**
|
* `token`: Telegram bot token. **required**
|
||||||
* `chat_ids`: List of chat IDs to send notifications to. **required**
|
* `chatIDs`: List of chat IDs to send notifications to. **required**
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
* `DIUN_NOTIF_TELEGRAM_TOKEN`
|
||||||
|
* `DIUN_NOTIF_TELEGRAM_CHATIDS` (comma separated)
|
||||||
|
|
||||||
|
### Sample
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Webhook
|
## Webhook
|
||||||
|
|
||||||
You can send webhook notifications with the following settings:
|
You can send webhook notifications with the following settings.
|
||||||
|
|
||||||
|
### Configuration file
|
||||||
|
|
||||||
* `webhook`
|
* `webhook`
|
||||||
* `endpoint`: URL of the HTTP request. **required**
|
* `endpoint`: URL of the HTTP request. **required**
|
||||||
* `method`: HTTP method (default: `GET`). **required**
|
* `method`: HTTP method (default `GET`). **required**
|
||||||
* `headers`: Map of additional headers to be sent.
|
* `headers`: Map of additional headers to be sent (key is case insensitive).
|
||||||
* `timeout`: Timeout specifies a time limit for the request to be made. (default: `10`).
|
* `timeout`: Timeout specifies a time limit for the request to be made. (default `10s`)
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
* `DIUN_NOTIF_WEBHOOK_ENDPOINT`
|
||||||
|
* `DIUN_NOTIF_WEBHOOK_METHOD`
|
||||||
|
* `DIUN_NOTIF_WEBHOOK_HEADERS_<KEY>`
|
||||||
|
* `DIUN_NOTIF_WEBHOOK_TIMEOUT`
|
||||||
|
|
||||||
|
### Sample
|
||||||
|
|
||||||
The JSON response will look like this:
|
The JSON response will look like this:
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
* [About](#about)
|
* [About](#about)
|
||||||
* [Quick start](#quick-start)
|
* [Quick start](#quick-start)
|
||||||
* [Provider configuration](#provider-configuration)
|
* [Provider configuration](#provider-configuration)
|
||||||
|
* [Configuration file](#configuration-file)
|
||||||
|
* [Environment variables](#environment-variables)
|
||||||
* [Docker labels](#docker-labels)
|
* [Docker labels](#docker-labels)
|
||||||
|
|
||||||
## About
|
## About
|
||||||
@@ -13,18 +15,6 @@ The Docker provider allows you to analyze the containers of your Docker instance
|
|||||||
|
|
||||||
In this section we quickly go over a basic docker-compose file using your local docker provider.
|
In this section we quickly go over a basic docker-compose file using your local docker provider.
|
||||||
|
|
||||||
First of all, let's create a Diun configuration we named `diun.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
watch:
|
|
||||||
workers: 20
|
|
||||||
schedule: "*/30 * * * *"
|
|
||||||
|
|
||||||
providers:
|
|
||||||
docker:
|
|
||||||
watch_stopped: true
|
|
||||||
```
|
|
||||||
|
|
||||||
Here we use a single Docker provider with a minimum configuration to analyze labeled containers (watch by default disabled), even stopped ones, of your local Docker instance.
|
Here we use a single Docker provider with a minimum configuration to analyze labeled containers (watch by default disabled), even stopped ones, of your local Docker instance.
|
||||||
|
|
||||||
Now let's create a simple docker-compose file with Diun and some simple services:
|
Now let's create a simple docker-compose file with Diun and some simple services:
|
||||||
@@ -37,12 +27,15 @@ services:
|
|||||||
image: crazymax/diun:latest
|
image: crazymax/diun:latest
|
||||||
volumes:
|
volumes:
|
||||||
- "./data:/data"
|
- "./data:/data"
|
||||||
- "./diun.yml:/diun.yml:ro"
|
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
environment:
|
environment:
|
||||||
- "TZ=Europe/Paris"
|
- "TZ=Europe/Paris"
|
||||||
- "LOG_LEVEL=info"
|
- "LOG_LEVEL=info"
|
||||||
- "LOG_JSON=false"
|
- "LOG_JSON=false"
|
||||||
|
- "DIUN_WATCH_WORKERS=20"
|
||||||
|
- "DIUN_WATCH_SCHEDULE=*/30 * * * *"
|
||||||
|
- "DIUN_PROVIDERS_DOCKER=true"
|
||||||
|
- "DIUN_PROVIDERS_DOCKER_WATCHSTOPPED=true"
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
cloudflared:
|
cloudflared:
|
||||||
@@ -90,20 +83,32 @@ diun_1 | Sat, 14 Dec 2019 15:30:13 CET INF Next run in 29 minutes (2019-
|
|||||||
|
|
||||||
## Provider configuration
|
## Provider configuration
|
||||||
|
|
||||||
|
### Configuration file
|
||||||
|
|
||||||
* `endpoint`: Server address to connect to. Local if empty.
|
* `endpoint`: Server address to connect to. Local if empty.
|
||||||
* `api_version`: Overrides the client version with the specified one.
|
* `apiVersion`: Overrides the client version with the specified one.
|
||||||
* `tls_certs_path`: Path to load the TLS certificates from.
|
* `tlsCertsPath`: Path to load the TLS certificates from.
|
||||||
* `tls_verify`: Controls whether client verifies the server's certificate chain and hostname (default: `true`).
|
* `tlsVerify`: Controls whether client verifies the server's certificate chain and hostname (default `true`).
|
||||||
* `watch_by_default`: Enable watch by default. If false, containers that don't have `diun.enable=true` label will be ignored (default: `false`).
|
* `watchByDefault`: Enable watch by default. If false, containers that don't have `diun.enable=true` label will be ignored (default `false`).
|
||||||
* `watch_stopped`: Include created and exited containers too (default: `false`).
|
* `watchStopped`: Include created and exited containers too (default `false`).
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
* `DIUN_PROVIDERS_DOCKER`
|
||||||
|
* `DIUN_PROVIDERS_DOCKER_ENDPOINT`
|
||||||
|
* `DIUN_PROVIDERS_DOCKER_APIVERSION`
|
||||||
|
* `DIUN_PROVIDERS_DOCKER_TLSCERTSPATH`
|
||||||
|
* `DIUN_PROVIDERS_DOCKER_TLSVERIFY`
|
||||||
|
* `DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT`
|
||||||
|
* `DIUN_PROVIDERS_DOCKER_WATCHSTOPPED`
|
||||||
|
|
||||||
## Docker labels
|
## Docker labels
|
||||||
|
|
||||||
You can configure more finely the way to analyze the image of your container through Docker labels:
|
You can configure more finely the way to analyze the image of your container through Docker labels:
|
||||||
|
|
||||||
* `diun.enable`: Set to true to enable image analysis of this container. Required if `watch_by_default` is disabled for this provider.
|
* `diun.enable`: Set to true to enable image analysis of this container.
|
||||||
* `diun.regopts_id`: Registry options ID from [`regopts`](../configuration.md#regopts) to use.
|
* `diun.regopts_id`: Registry options ID from [`regopts`](../configuration.md#regopts) to use.
|
||||||
* `diun.watch_repo`: Watch all tags of this container image (default: `false`).
|
* `diun.watch_repo`: Watch all tags of this container image (default `false`).
|
||||||
* `diun.max_tags`: Maximum number of tags to watch if `diun.watch_repo` enabled. 0 means all of them (default: `0`).
|
* `diun.max_tags`: Maximum number of tags to watch if `diun.watch_repo` enabled. 0 means all of them (default `0`).
|
||||||
* `diun.include_tags`: Semi-colon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo`.
|
* `diun.include_tags`: Semi-colon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo`.
|
||||||
* `diun.exclude_tags`: Semi-colon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo`.
|
* `diun.exclude_tags`: Semi-colon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo`.
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
* [Example](#example)
|
* [Example](#example)
|
||||||
* [Quick start](#quick-start)
|
* [Quick start](#quick-start)
|
||||||
* [Provider configuration](#provider-configuration)
|
* [Provider configuration](#provider-configuration)
|
||||||
* [filename](#filename)
|
* [Configuration file](#configuration-file)
|
||||||
* [directory](#directory)
|
* [Environment variables](#environment-variables)
|
||||||
* [YAML configuration file](#yaml-configuration-file)
|
* [YAML configuration file](#yaml-configuration-file)
|
||||||
|
|
||||||
## About
|
## About
|
||||||
@@ -32,7 +32,7 @@ regopts:
|
|||||||
onemore:
|
onemore:
|
||||||
username: foo2
|
username: foo2
|
||||||
password: bar2
|
password: bar2
|
||||||
insecure_tls: true
|
insecureTls: true
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
file:
|
file:
|
||||||
@@ -131,7 +131,9 @@ Sat, 14 Dec 2019 15:32:28 UTC INF Next run in 31 seconds (2019-12-14 15:33:00 +0
|
|||||||
|
|
||||||
## Provider configuration
|
## Provider configuration
|
||||||
|
|
||||||
### filename
|
### Configuration file
|
||||||
|
|
||||||
|
#### filename
|
||||||
|
|
||||||
Defines the path to the [configuration file](#yaml-configuration-file).
|
Defines the path to the [configuration file](#yaml-configuration-file).
|
||||||
|
|
||||||
@@ -143,7 +145,7 @@ providers:
|
|||||||
filename: /path/to/config/conf.yml
|
filename: /path/to/config/conf.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
### directory
|
#### directory
|
||||||
|
|
||||||
Defines the path to the directory that contains the [configuration files](#yaml-configuration-file) (`*.yml` or `*.yaml`).
|
Defines the path to the directory that contains the [configuration files](#yaml-configuration-file) (`*.yml` or `*.yaml`).
|
||||||
|
|
||||||
@@ -155,14 +157,19 @@ providers:
|
|||||||
directory: /path/to/config
|
directory: /path/to/config
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
* `DIUN_PROVIDERS_FILE_DIRECTORY`
|
||||||
|
* `DIUN_PROVIDERS_FILE_FILENAME`
|
||||||
|
|
||||||
## YAML configuration file
|
## YAML configuration file
|
||||||
|
|
||||||
The configuration file(s) defines a slice of images to analyze with the following fields:
|
The configuration file(s) defines a slice of images to analyze with the following fields:
|
||||||
|
|
||||||
* `name`: Docker image name to watch using `registry/path:tag` format. If registry omitted, `docker.io` will be used and if tag omitted, `latest` will be used. **required**
|
* `name`: Docker image name to watch using `registry/path:tag` format. If registry omitted, `docker.io` will be used and if tag omitted, `latest` will be used. **required**
|
||||||
* `regopts_id`: Registry options ID from [`regopts`](../configuration.md#regopts) to use.
|
* `regopts_id`: Registry options ID from [`regopts`](../configuration.md#regopts) to use.
|
||||||
* `watch_repo`: Watch all tags of this `image` repository (default: `false`).
|
* `watch_repo`: Watch all tags of this `image` repository (default `false`).
|
||||||
* `max_tags`: Maximum number of tags to watch if `watch_repo` enabled. 0 means all of them (default: `0`).
|
* `max_tags`: Maximum number of tags to watch if `watch_repo` enabled. 0 means all of them (default `0`).
|
||||||
* `include_tags`: List of regular expressions to include tags. Can be useful if you enable `watch_repo`.
|
* `include_tags`: List of regular expressions to include tags. Can be useful if you enable `watch_repo`.
|
||||||
* `exclude_tags`: List of regular expressions to exclude tags. Can be useful if you enable `watch_repo`.
|
* `exclude_tags`: List of regular expressions to exclude tags. Can be useful if you enable `watch_repo`.
|
||||||
* `platform`: Check a custom platform. (default will retrieve platform dynamically based on your operating system).
|
* `platform`: Check a custom platform. (default will retrieve platform dynamically based on your operating system).
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
* [About](#about)
|
* [About](#about)
|
||||||
* [Quick start](#quick-start)
|
* [Quick start](#quick-start)
|
||||||
* [Provider configuration](#provider-configuration)
|
* [Provider configuration](#provider-configuration)
|
||||||
|
* [Configuration file](#configuration-file)
|
||||||
|
* [Environment variables](#environment-variables)
|
||||||
* [Docker labels](#docker-labels)
|
* [Docker labels](#docker-labels)
|
||||||
|
|
||||||
## About
|
## About
|
||||||
@@ -13,17 +15,6 @@ The Swarm provider allows you to analyze the services of your Swarm cluster to e
|
|||||||
|
|
||||||
In this section we quickly go over a basic stack using your local swarm cluster.
|
In this section we quickly go over a basic stack using your local swarm cluster.
|
||||||
|
|
||||||
First of all, let's create a Diun configuration we named `diun.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
watch:
|
|
||||||
workers: 20
|
|
||||||
schedule: "*/30 * * * *"
|
|
||||||
|
|
||||||
providers:
|
|
||||||
swarm:
|
|
||||||
```
|
|
||||||
|
|
||||||
Here we use our local Swarm provider with a minimum configuration to analyze labeled containers (watch by default disabled).
|
Here we use our local Swarm provider with a minimum configuration to analyze labeled containers (watch by default disabled).
|
||||||
|
|
||||||
Now let's create a simple stack for Diun:
|
Now let's create a simple stack for Diun:
|
||||||
@@ -36,12 +27,14 @@ services:
|
|||||||
image: crazymax/diun:latest
|
image: crazymax/diun:latest
|
||||||
volumes:
|
volumes:
|
||||||
- "./data:/data"
|
- "./data:/data"
|
||||||
- "./diun.yml:/diun.yml:ro"
|
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
environment:
|
environment:
|
||||||
- "TZ=Europe/Paris"
|
- "TZ=Europe/Paris"
|
||||||
- "LOG_LEVEL=info"
|
- "LOG_LEVEL=info"
|
||||||
- "LOG_JSON=false"
|
- "LOG_JSON=false"
|
||||||
|
- "DIUN_WATCH_WORKERS=20"
|
||||||
|
- "DIUN_WATCH_SCHEDULE=*/30 * * * *"
|
||||||
|
- "DIUN_PROVIDERS_SWARM=true"
|
||||||
deploy:
|
deploy:
|
||||||
placement:
|
placement:
|
||||||
constraints:
|
constraints:
|
||||||
@@ -75,7 +68,7 @@ docker stack deploy -c diun.yml diun
|
|||||||
docker stack deploy -c nginx.yml nginx
|
docker stack deploy -c nginx.yml nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
And watch logs of Diun service:
|
Now take a look at the logs:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ docker service logs -f diun_diun
|
$ docker service logs -f diun_diun
|
||||||
@@ -105,19 +98,30 @@ diun_diun.1.i1l4yuiafq6y@docker-desktop | Sat, 14 Dec 2019 16:20:02 CET INF N
|
|||||||
|
|
||||||
## Provider configuration
|
## Provider configuration
|
||||||
|
|
||||||
|
### Configuration file
|
||||||
|
|
||||||
* `endpoint`: Server address to connect to. Local if empty.
|
* `endpoint`: Server address to connect to. Local if empty.
|
||||||
* `api_version`: Overrides the client version with the specified one.
|
* `apiVersion`: Overrides the client version with the specified one.
|
||||||
* `tls_certs_path`: Path to load the TLS certificates from.
|
* `TLSCertsPath`: Path to load the TLS certificates from.
|
||||||
* `tls_verify`: Controls whether client verifies the server's certificate chain and hostname (default: `true`).
|
* `TLSVerify`: Controls whether client verifies the server's certificate chain and hostname (default `true`).
|
||||||
* `watch_by_default`: Enable watch by default. If false, services that don't have `diun.enable=true` label will be ignored (default: `false`).
|
* `watchByDefault`: Enable watch by default. If false, services that don't have `diun.enable=true` label will be ignored (default `false`).
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
* `DIUN_PROVIDERS_SWARM`
|
||||||
|
* `DIUN_PROVIDERS_SWARM_ENDPOINT`
|
||||||
|
* `DIUN_PROVIDERS_SWARM_APIVERSION`
|
||||||
|
* `DIUN_PROVIDERS_SWARM_TLSCERTSPATH`
|
||||||
|
* `DIUN_PROVIDERS_SWARM_TLSVERIFY`
|
||||||
|
* `DIUN_PROVIDERS_SWARM_WATCHBYDEFAULT`
|
||||||
|
|
||||||
## Docker labels
|
## Docker labels
|
||||||
|
|
||||||
You can configure more finely the way to analyze the image of your service through Docker labels:
|
You can configure more finely the way to analyze the image of your service through Docker labels:
|
||||||
|
|
||||||
* `diun.enable`: Set to true to enable image analysis of this container. Required if `watch_by_default` is disabled for this provider.
|
* `diun.enable`: Set to true to enable image analysis of this container.
|
||||||
* `diun.regopts_id`: Registry options ID from [`regopts`](../configuration.md#regopts) to use.
|
* `diun.regopts_id`: Registry options ID from [`regopts`](../configuration.md#regopts) to use.
|
||||||
* `diun.watch_repo`: Watch all tags of this container image (default: `false`).
|
* `diun.watch_repo`: Watch all tags of this container image (default `false`).
|
||||||
* `diun.max_tags`: Maximum number of tags to watch if `diun.watch_repo` enabled. 0 means all of them (default: `0`).
|
* `diun.max_tags`: Maximum number of tags to watch if `diun.watch_repo` enabled. 0 means all of them (default `0`).
|
||||||
* `diun.include_tags`: Semi-colon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo`.
|
* `diun.include_tags`: Semi-colon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo`.
|
||||||
* `diun.exclude_tags`: Semi-colon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo`.
|
* `diun.exclude_tags`: Semi-colon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo`.
|
||||||
|
|||||||
14
doc/usage.md
14
doc/usage.md
@@ -1,14 +0,0 @@
|
|||||||
# Usage
|
|
||||||
|
|
||||||
## Command line
|
|
||||||
|
|
||||||
`diun --config=STRING`
|
|
||||||
|
|
||||||
* `--help`: Show help text and exit.
|
|
||||||
* `--version`: Show version and exit.
|
|
||||||
* `--config <path>`: Diun YAML configuration file. **Required**. (e.g. `diun.yml`).
|
|
||||||
* `--timezone <timezone>`: Timezone assigned to Diun. (default `UTC`).
|
|
||||||
* `--log-level <level>`: Log level output. (default `info`).
|
|
||||||
* `--log-json`: Enable JSON logging output. (default `false`).
|
|
||||||
* `--log-caller`: Add file:line of the caller to log output. (default `false`).
|
|
||||||
* `--test-notif`: Enable to test notification settings.
|
|
||||||
1
go.mod
1
go.mod
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/docker/docker v1.4.2-0.20191219165747-a9416c67da9f
|
github.com/docker/docker v1.4.2-0.20191219165747-a9416c67da9f
|
||||||
github.com/docker/go-connections v0.4.0
|
github.com/docker/go-connections v0.4.0
|
||||||
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
|
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
|
||||||
|
github.com/go-playground/validator/v10 v10.3.0
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||||
github.com/hako/durafmt v0.0.0-20190612201238-650ed9f29a84
|
github.com/hako/durafmt v0.0.0-20190612201238-650ed9f29a84
|
||||||
github.com/imdario/mergo v0.3.9
|
github.com/imdario/mergo v0.3.9
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -73,6 +73,14 @@ github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCS
|
|||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/validator/v10 v10.3.0 h1:nZU+7q+yJoFmwvNgv/LnPUkwPal62+b2xXj0AU1Es7o=
|
||||||
|
github.com/go-playground/validator/v10 v10.3.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
|
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||||
@@ -133,6 +141,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
|||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||||
|
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||||
github.com/matcornic/hermes/v2 v2.1.0 h1:9TDYFBPFv6mcXanaDmRDEp/RTWj0dTTi+LpFnnnfNWc=
|
github.com/matcornic/hermes/v2 v2.1.0 h1:9TDYFBPFv6mcXanaDmRDEp/RTWj0dTTi+LpFnnnfNWc=
|
||||||
github.com/matcornic/hermes/v2 v2.1.0/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI=
|
github.com/matcornic/hermes/v2 v2.1.0/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI=
|
||||||
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
|
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -25,42 +22,39 @@ import (
|
|||||||
|
|
||||||
// Diun represents an active diun object
|
// Diun represents an active diun object
|
||||||
type Diun struct {
|
type Diun struct {
|
||||||
cfg *config.Config
|
meta model.Meta
|
||||||
cron *cron.Cron
|
cfg *config.Config
|
||||||
db *db.Client
|
cron *cron.Cron
|
||||||
notif *notif.Client
|
db *db.Client
|
||||||
userAgent string
|
notif *notif.Client
|
||||||
jobID cron.EntryID
|
jobID cron.EntryID
|
||||||
locker uint32
|
locker uint32
|
||||||
pool *ants.PoolWithFunc
|
pool *ants.PoolWithFunc
|
||||||
wg *sync.WaitGroup
|
wg *sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates new diun instance
|
// New creates new diun instance
|
||||||
func New(cfg *config.Config, location *time.Location) (*Diun, error) {
|
func New(meta model.Meta, cfg *config.Config, location *time.Location) (*Diun, error) {
|
||||||
// DB client
|
// DB client
|
||||||
dbcli, err := db.New(cfg.Db)
|
dbcli, err := db.New(*cfg.Db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// User-Agent
|
|
||||||
userAgent := fmt.Sprintf("diun/%s go/%s %s", cfg.App.Version, runtime.Version()[2:], strings.Title(runtime.GOOS))
|
|
||||||
|
|
||||||
// Notification client
|
// Notification client
|
||||||
notifcli, err := notif.New(cfg.Notif, cfg.App, userAgent)
|
notifcli, err := notif.New(cfg.Notif, meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Diun{
|
return &Diun{
|
||||||
cfg: cfg,
|
meta: meta,
|
||||||
|
cfg: cfg,
|
||||||
cron: cron.New(cron.WithLocation(location), cron.WithParser(cron.NewParser(
|
cron: cron.New(cron.WithLocation(location), cron.WithParser(cron.NewParser(
|
||||||
cron.SecondOptional|cron.Minute|cron.Hour|cron.Dom|cron.Month|cron.Dow|cron.Descriptor),
|
cron.SecondOptional|cron.Minute|cron.Hour|cron.Dom|cron.Month|cron.Dow|cron.Descriptor),
|
||||||
)),
|
)),
|
||||||
db: dbcli,
|
db: dbcli,
|
||||||
notif: notifcli,
|
notif: notifcli,
|
||||||
userAgent: userAgent,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package app
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/crazy-max/diun/v3/internal/model"
|
"github.com/crazy-max/diun/v3/internal/model"
|
||||||
"github.com/crazy-max/diun/v3/pkg/registry"
|
"github.com/crazy-max/diun/v3/pkg/registry"
|
||||||
@@ -35,7 +34,7 @@ func (di *Diun) createJob(job model.Job) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Registry options
|
// Registry options
|
||||||
regOpts, err := di.getRegOpts(job.Image.RegOptsID)
|
regOpts, err := di.cfg.GetRegOpts(job.Image.RegOptsID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sublog.Warn().Err(err).Msg("Registry options")
|
sublog.Warn().Err(err).Msg("Registry options")
|
||||||
}
|
}
|
||||||
@@ -76,9 +75,9 @@ func (di *Diun) createJob(job model.Job) {
|
|||||||
job.Registry, err = registry.New(registry.Options{
|
job.Registry, err = registry.New(registry.Options{
|
||||||
Username: regUser,
|
Username: regUser,
|
||||||
Password: regPassword,
|
Password: regPassword,
|
||||||
Timeout: time.Duration(regOpts.Timeout) * time.Second,
|
Timeout: *regOpts.Timeout,
|
||||||
InsecureTLS: regOpts.InsecureTLS,
|
InsecureTLS: *regOpts.InsecureTLS,
|
||||||
UserAgent: di.userAgent,
|
UserAgent: di.meta.UserAgent,
|
||||||
ImageOs: job.Image.Platform.Os,
|
ImageOs: job.Image.Platform.Os,
|
||||||
ImageArch: job.Image.Platform.Arch,
|
ImageArch: job.Image.Platform.Arch,
|
||||||
ImageVariant: job.Image.Platform.Variant,
|
ImageVariant: job.Image.Platform.Variant,
|
||||||
@@ -94,7 +93,7 @@ func (di *Diun) createJob(job model.Job) {
|
|||||||
sublog.Error().Err(err).Msgf("Invoking job")
|
sublog.Error().Err(err).Msgf("Invoking job")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !job.Image.WatchRepo || job.RegImage.Domain == "" {
|
if !job.Image.WatchRepo || len(job.RegImage.Domain) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +156,7 @@ func (di *Diun) runJob(job model.Job) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
status := model.ImageStatusUnchange
|
status := model.ImageStatusUnchange
|
||||||
if dbManifest.Name == "" {
|
if len(dbManifest.Name) == 0 {
|
||||||
status = model.ImageStatusNew
|
status = model.ImageStatusNew
|
||||||
sublog.Info().Msg("New image found")
|
sublog.Info().Msg("New image found")
|
||||||
} else if !liveManifest.Created.Equal(*dbManifest.Created) {
|
} else if !liveManifest.Created.Equal(*dbManifest.Created) {
|
||||||
@@ -187,13 +186,3 @@ func (di *Diun) runJob(job model.Job) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (di *Diun) getRegOpts(id string) (model.RegOpts, error) {
|
|
||||||
if id == "" {
|
|
||||||
return model.RegOpts{}, nil
|
|
||||||
}
|
|
||||||
if regopts, ok := di.cfg.RegOpts[id]; ok {
|
|
||||||
return regopts, nil
|
|
||||||
}
|
|
||||||
return model.RegOpts{}, fmt.Errorf("%s not found", id)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,92 +3,91 @@ package config
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/crazy-max/diun/v3/internal/model"
|
"github.com/crazy-max/diun/v3/internal/model"
|
||||||
"github.com/crazy-max/diun/v3/pkg/utl"
|
"github.com/crazy-max/diun/v3/third_party/traefik/config/env"
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/config/file"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds configuration details
|
// Config holds configuration details
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Cli model.Cli
|
Db *model.Db `yaml:"db,omitempty" json:"db,omitempty"`
|
||||||
App model.App
|
Watch *model.Watch `yaml:"watch,omitempty" json:"watch,omitempty"`
|
||||||
Db model.Db `yaml:"db,omitempty"`
|
Notif *model.Notif `yaml:"notif,omitempty" json:"notif,omitempty"`
|
||||||
Watch model.Watch `yaml:"watch,omitempty"`
|
RegOpts map[string]*model.RegOpts `yaml:"regopts,omitempty" json:"regopts,omitempty" validate:"unique"`
|
||||||
Notif *model.Notif `yaml:"notif,omitempty"`
|
Providers *model.Providers `yaml:"providers,omitempty" json:"providers,omitempty" validate:"required"`
|
||||||
RegOpts map[string]model.RegOpts `yaml:"regopts,omitempty"`
|
|
||||||
Providers *model.Providers `yaml:"providers,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load returns Configuration struct
|
// Load returns Configuration struct
|
||||||
func Load(cli model.Cli, version string) (*Config, error) {
|
func Load(cfgfile string) (*Config, error) {
|
||||||
var err error
|
cfg := Config{
|
||||||
var cfg = Config{
|
Db: (&model.Db{}).GetDefaults(),
|
||||||
Cli: cli,
|
Watch: (&model.Watch{}).GetDefaults(),
|
||||||
App: model.App{
|
|
||||||
ID: "diun",
|
|
||||||
Name: "Diun",
|
|
||||||
Desc: "Docker image update notifier",
|
|
||||||
URL: "https://github.com/crazy-max/diun",
|
|
||||||
Author: "CrazyMax",
|
|
||||||
Version: version,
|
|
||||||
},
|
|
||||||
Db: model.Db{
|
|
||||||
Path: "diun.db",
|
|
||||||
},
|
|
||||||
Watch: model.Watch{
|
|
||||||
Workers: 10,
|
|
||||||
Schedule: "0 * * * *",
|
|
||||||
FirstCheckNotif: utl.NewFalse(),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = os.Lstat(cli.Cfgfile); err != nil {
|
if err := cfg.loadFile(cfgfile, &cfg); err != nil {
|
||||||
return nil, fmt.Errorf("unable to open config file, %s", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes, err := ioutil.ReadFile(cli.Cfgfile)
|
if err := cfg.loadEnv(&cfg); err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, fmt.Errorf("unable to read config file, %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := yaml.UnmarshalStrict(bytes, &cfg); err != nil {
|
validate := validator.New()
|
||||||
return nil, fmt.Errorf("unable to decode into struct, %v", err)
|
if err := validate.Struct(&cfg); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
if err := cfg.validate(); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) validate() error {
|
func (cfg *Config) loadFile(cfgfile string, out interface{}) error {
|
||||||
cfg.Db.Path = utl.GetEnv("DIUN_DB", cfg.Db.Path)
|
if len(cfgfile) == 0 {
|
||||||
if cfg.Db.Path == "" {
|
return nil
|
||||||
return errors.New("database path is required")
|
|
||||||
}
|
}
|
||||||
cfg.Db.Path = path.Clean(cfg.Db.Path)
|
|
||||||
|
|
||||||
if err := cfg.validateNotif(); err != nil {
|
if _, err := os.Lstat(cfgfile); os.IsNotExist(err) {
|
||||||
return err
|
return fmt.Errorf("config file %s not found", cfgfile)
|
||||||
}
|
}
|
||||||
if err := cfg.validateRegopts(); err != nil {
|
|
||||||
return err
|
if err := file.Decode(cfgfile, out); err != nil {
|
||||||
}
|
return errors.Wrap(err, "failed to decode configuration from file")
|
||||||
if err := cfg.validateProviders(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display configuration in a pretty JSON format
|
func (cfg *Config) loadEnv(out interface{}) error {
|
||||||
func (cfg *Config) Display() string {
|
var envvars []string
|
||||||
|
for _, envvar := range env.FindPrefixedEnvVars(os.Environ(), "DIUN_", out) {
|
||||||
|
envvars = append(envvars, envvar)
|
||||||
|
}
|
||||||
|
if len(envvars) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := env.Decode(envvars, "DIUN_", out); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to decode configuration from environment variables")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) GetRegOpts(id string) (*model.RegOpts, error) {
|
||||||
|
if len(id) == 0 {
|
||||||
|
return (&model.RegOpts{}).GetDefaults(), nil
|
||||||
|
}
|
||||||
|
if regopts, ok := cfg.RegOpts[id]; ok {
|
||||||
|
return regopts, nil
|
||||||
|
}
|
||||||
|
return (&model.RegOpts{}).GetDefaults(), fmt.Errorf("%s not found", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of configuration
|
||||||
|
func (cfg *Config) String() string {
|
||||||
b, _ := json.MarshalIndent(cfg, "", " ")
|
b, _ := json.MarshalIndent(cfg, "", " ")
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,46 @@
|
|||||||
package config_test
|
package config_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/crazy-max/diun/v3/internal/config"
|
"github.com/crazy-max/diun/v3/internal/config"
|
||||||
"github.com/crazy-max/diun/v3/internal/model"
|
"github.com/crazy-max/diun/v3/internal/model"
|
||||||
"github.com/crazy-max/diun/v3/pkg/utl"
|
"github.com/crazy-max/diun/v3/pkg/utl"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoad(t *testing.T) {
|
func TestLoadFile(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
cli model.Cli
|
cfgfile string
|
||||||
wantData *config.Config
|
wantData *config.Config
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Fail on non-existing file",
|
name: "Failed on non-existing file",
|
||||||
cli: model.Cli{},
|
cfgfile: "",
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Fail on wrong file format",
|
name: "Fail on wrong file format",
|
||||||
cli: model.Cli{
|
cfgfile: "./fixtures/config.invalid.yml",
|
||||||
Cfgfile: "./test/config.invalid.yml",
|
|
||||||
},
|
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Success",
|
name: "Success",
|
||||||
cli: model.Cli{
|
cfgfile: "./fixtures/config.test.yml",
|
||||||
Cfgfile: "./test/config.test.yml",
|
|
||||||
},
|
|
||||||
wantData: &config.Config{
|
wantData: &config.Config{
|
||||||
Cli: model.Cli{
|
Db: &model.Db{
|
||||||
Cfgfile: "./test/config.test.yml",
|
|
||||||
},
|
|
||||||
App: model.App{
|
|
||||||
ID: "diun",
|
|
||||||
Name: "Diun",
|
|
||||||
Desc: "Docker image update notifier",
|
|
||||||
URL: "https://github.com/crazy-max/diun",
|
|
||||||
Author: "CrazyMax",
|
|
||||||
Version: "test",
|
|
||||||
},
|
|
||||||
Db: model.Db{
|
|
||||||
Path: "diun.db",
|
Path: "diun.db",
|
||||||
},
|
},
|
||||||
Watch: model.Watch{
|
Watch: &model.Watch{
|
||||||
Workers: 100,
|
Workers: 100,
|
||||||
Schedule: "*/30 * * * *",
|
Schedule: "*/30 * * * *",
|
||||||
FirstCheckNotif: utl.NewFalse(),
|
FirstCheckNotif: utl.NewTrue(),
|
||||||
},
|
},
|
||||||
Notif: &model.Notif{
|
Notif: &model.Notif{
|
||||||
Amqp: &model.NotifAmqp{
|
Amqp: &model.NotifAmqp{
|
||||||
@@ -65,7 +54,7 @@ func TestLoad(t *testing.T) {
|
|||||||
Endpoint: "http://gotify.foo.com",
|
Endpoint: "http://gotify.foo.com",
|
||||||
Token: "Token123456",
|
Token: "Token123456",
|
||||||
Priority: 1,
|
Priority: 1,
|
||||||
Timeout: 10,
|
Timeout: utl.NewDuration(10 * time.Second),
|
||||||
},
|
},
|
||||||
Mail: &model.NotifMail{
|
Mail: &model.NotifMail{
|
||||||
Host: "localhost",
|
Host: "localhost",
|
||||||
@@ -80,12 +69,12 @@ func TestLoad(t *testing.T) {
|
|||||||
Channel: "#general",
|
Channel: "#general",
|
||||||
UserID: "abcdEFGH012345678",
|
UserID: "abcdEFGH012345678",
|
||||||
Token: "Token123456",
|
Token: "Token123456",
|
||||||
Timeout: 10,
|
Timeout: utl.NewDuration(10 * time.Second),
|
||||||
},
|
},
|
||||||
Script: &model.NotifScript{
|
Script: &model.NotifScript{
|
||||||
Cmd: "go",
|
Cmd: "uname",
|
||||||
Args: []string{
|
Args: []string{
|
||||||
"version",
|
"-a",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Slack: &model.NotifSlack{
|
Slack: &model.NotifSlack{
|
||||||
@@ -95,30 +84,35 @@ func TestLoad(t *testing.T) {
|
|||||||
WebhookURL: "https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij",
|
WebhookURL: "https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij",
|
||||||
},
|
},
|
||||||
Telegram: &model.NotifTelegram{
|
Telegram: &model.NotifTelegram{
|
||||||
BotToken: "abcdef123456",
|
Token: "abcdef123456",
|
||||||
ChatIDs: []int64{8547439, 1234567},
|
ChatIDs: []int64{8547439, 1234567},
|
||||||
},
|
},
|
||||||
Webhook: &model.NotifWebhook{
|
Webhook: &model.NotifWebhook{
|
||||||
Endpoint: "http://webhook.foo.com/sd54qad89azd5a",
|
Endpoint: "http://webhook.foo.com/sd54qad89azd5a",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
Headers: map[string]string{
|
Headers: map[string]string{
|
||||||
"Content-Type": "application/json",
|
"content-type": "application/json",
|
||||||
"Authorization": "Token123456",
|
"authorization": "Token123456",
|
||||||
},
|
},
|
||||||
Timeout: 10,
|
Timeout: utl.NewDuration(10 * time.Second),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
RegOpts: map[string]model.RegOpts{
|
RegOpts: map[string]*model.RegOpts{
|
||||||
"someregopts": {
|
"someregopts": {
|
||||||
Timeout: 5,
|
InsecureTLS: utl.NewFalse(),
|
||||||
|
Timeout: utl.NewDuration(5 * time.Second),
|
||||||
},
|
},
|
||||||
"bintrayoptions": {
|
"bintrayoptions": {
|
||||||
Username: "foo",
|
Username: "foo",
|
||||||
Password: "bar",
|
Password: "bar",
|
||||||
|
InsecureTLS: utl.NewFalse(),
|
||||||
|
Timeout: utl.NewDuration(10 * time.Second),
|
||||||
},
|
},
|
||||||
"sensitive": {
|
"sensitive": {
|
||||||
UsernameFile: "/run/secrets/username",
|
UsernameFile: "/run/secrets/username",
|
||||||
PasswordFile: "/run/secrets/password",
|
PasswordFile: "/run/secrets/password",
|
||||||
|
InsecureTLS: utl.NewFalse(),
|
||||||
|
Timeout: utl.NewDuration(10 * time.Second),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Providers: &model.Providers{
|
Providers: &model.Providers{
|
||||||
@@ -129,10 +123,10 @@ func TestLoad(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Swarm: &model.PrdSwarm{
|
Swarm: &model.PrdSwarm{
|
||||||
TLSVerify: utl.NewTrue(),
|
TLSVerify: utl.NewTrue(),
|
||||||
WatchByDefault: utl.NewTrue(),
|
WatchByDefault: utl.NewFalse(),
|
||||||
},
|
},
|
||||||
File: &model.PrdFile{
|
File: &model.PrdFile{
|
||||||
Filename: "./test/dummy.yml",
|
Filename: "./fixtures/dummy.yml",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -140,14 +134,341 @@ func TestLoad(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range cases {
|
for _, tt := range cases {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
cfg, err := config.Load(tt.cli, "test")
|
cfg, err := config.Load(tt.cfgfile)
|
||||||
if !tt.wantErr && err != nil {
|
if tt.wantErr {
|
||||||
t.Error(err)
|
require.Error(t, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
assert.Equal(t, tt.wantData, cfg)
|
assert.Equal(t, tt.wantData, cfg)
|
||||||
if !tt.wantErr && cfg != nil {
|
if cfg != nil {
|
||||||
assert.NotEmpty(t, cfg.Display())
|
assert.NotEmpty(t, cfg.String())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadEnv(t *testing.T) {
|
||||||
|
defer UnsetEnv("DIUN_")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
cfgfile string
|
||||||
|
environ []string
|
||||||
|
expected interface{}
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "no env vars",
|
||||||
|
environ: nil,
|
||||||
|
expected: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "docker provider",
|
||||||
|
environ: []string{
|
||||||
|
"DIUN_PROVIDERS_DOCKER=true",
|
||||||
|
},
|
||||||
|
expected: &config.Config{
|
||||||
|
Db: (&model.Db{}).GetDefaults(),
|
||||||
|
Watch: (&model.Watch{}).GetDefaults(),
|
||||||
|
Notif: nil,
|
||||||
|
RegOpts: nil,
|
||||||
|
Providers: &model.Providers{
|
||||||
|
Docker: &model.PrdDocker{
|
||||||
|
TLSVerify: utl.NewTrue(),
|
||||||
|
WatchByDefault: utl.NewFalse(),
|
||||||
|
WatchStopped: utl.NewFalse(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "docker provider and regopts",
|
||||||
|
environ: []string{
|
||||||
|
"DIUN_REGOPTS_SENSITIVE_USERNAMEFILE=/run/secrets/username",
|
||||||
|
"DIUN_REGOPTS_SENSITIVE_PASSWORDFILE=/run/secrets/password",
|
||||||
|
"DIUN_REGOPTS_SENSITIVE_TIMEOUT=30s",
|
||||||
|
"DIUN_PROVIDERS_DOCKER=true",
|
||||||
|
},
|
||||||
|
expected: &config.Config{
|
||||||
|
Db: (&model.Db{}).GetDefaults(),
|
||||||
|
Watch: (&model.Watch{}).GetDefaults(),
|
||||||
|
RegOpts: map[string]*model.RegOpts{
|
||||||
|
"sensitive": {
|
||||||
|
UsernameFile: "/run/secrets/username",
|
||||||
|
PasswordFile: "/run/secrets/password",
|
||||||
|
InsecureTLS: utl.NewFalse(),
|
||||||
|
Timeout: utl.NewDuration(30 * time.Second),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Providers: &model.Providers{
|
||||||
|
Docker: &model.PrdDocker{
|
||||||
|
TLSVerify: utl.NewTrue(),
|
||||||
|
WatchByDefault: utl.NewFalse(),
|
||||||
|
WatchStopped: utl.NewFalse(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "swarm provider and notif telegram",
|
||||||
|
environ: []string{
|
||||||
|
"DIUN_NOTIF_TELEGRAM_TOKEN=abcdef123456",
|
||||||
|
"DIUN_NOTIF_TELEGRAM_CHATIDS=8547439,1234567",
|
||||||
|
"DIUN_PROVIDERS_SWARM=true",
|
||||||
|
},
|
||||||
|
expected: &config.Config{
|
||||||
|
Db: (&model.Db{}).GetDefaults(),
|
||||||
|
Watch: (&model.Watch{}).GetDefaults(),
|
||||||
|
Notif: &model.Notif{
|
||||||
|
Telegram: &model.NotifTelegram{
|
||||||
|
Token: "abcdef123456",
|
||||||
|
ChatIDs: []int64{8547439, 1234567},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Providers: &model.Providers{
|
||||||
|
Swarm: &model.PrdSwarm{
|
||||||
|
TLSVerify: utl.NewTrue(),
|
||||||
|
WatchByDefault: utl.NewFalse(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "file provider and notif script",
|
||||||
|
environ: []string{
|
||||||
|
"DIUN_NOTIF_SCRIPT_CMD=uname",
|
||||||
|
"DIUN_NOTIF_SCRIPT_ARGS=-a",
|
||||||
|
"DIUN_PROVIDERS_FILE_DIRECTORY=./fixtures",
|
||||||
|
},
|
||||||
|
expected: &config.Config{
|
||||||
|
Db: (&model.Db{}).GetDefaults(),
|
||||||
|
Watch: (&model.Watch{}).GetDefaults(),
|
||||||
|
Notif: &model.Notif{
|
||||||
|
Script: &model.NotifScript{
|
||||||
|
Cmd: "uname",
|
||||||
|
Args: []string{
|
||||||
|
"-a",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Providers: &model.Providers{
|
||||||
|
File: &model.PrdFile{
|
||||||
|
Directory: "./fixtures",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
UnsetEnv("DIUN_")
|
||||||
|
|
||||||
|
if tt.environ != nil {
|
||||||
|
for _, environ := range tt.environ {
|
||||||
|
n := strings.SplitN(environ, "=", 2)
|
||||||
|
os.Setenv(n[0], n[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.Load(tt.cfgfile)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, cfg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadMixed(t *testing.T) {
|
||||||
|
defer UnsetEnv("DIUN_")
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
cfgfile string
|
||||||
|
environ []string
|
||||||
|
expected interface{}
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "env vars and invalid file",
|
||||||
|
cfgfile: "./fixtures/config.invalid.yml",
|
||||||
|
environ: []string{
|
||||||
|
"DIUN_PROVIDERS_DOCKER=true",
|
||||||
|
},
|
||||||
|
expected: nil,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "docker provider (file) and notif mails (envs)",
|
||||||
|
cfgfile: "./fixtures/config.docker.yml",
|
||||||
|
environ: []string{
|
||||||
|
"DIUN_NOTIF_MAIL_HOST=127.0.0.1",
|
||||||
|
"DIUN_NOTIF_MAIL_PORT=25",
|
||||||
|
"DIUN_NOTIF_MAIL_SSL=false",
|
||||||
|
"DIUN_NOTIF_MAIL_INSECURESKIPVERIFY=true",
|
||||||
|
"DIUN_NOTIF_MAIL_FROM=diun@foo.com",
|
||||||
|
"DIUN_NOTIF_MAIL_TO=webmaster@foo.com",
|
||||||
|
},
|
||||||
|
expected: &config.Config{
|
||||||
|
Db: (&model.Db{}).GetDefaults(),
|
||||||
|
Watch: (&model.Watch{}).GetDefaults(),
|
||||||
|
Notif: &model.Notif{
|
||||||
|
Mail: &model.NotifMail{
|
||||||
|
Host: "127.0.0.1",
|
||||||
|
Port: 25,
|
||||||
|
SSL: utl.NewFalse(),
|
||||||
|
InsecureSkipVerify: utl.NewTrue(),
|
||||||
|
From: "diun@foo.com",
|
||||||
|
To: "webmaster@foo.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RegOpts: nil,
|
||||||
|
Providers: &model.Providers{
|
||||||
|
Docker: &model.PrdDocker{
|
||||||
|
TLSVerify: utl.NewTrue(),
|
||||||
|
WatchByDefault: utl.NewFalse(),
|
||||||
|
WatchStopped: utl.NewFalse(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "file provider, regopts (file) and notif webhook env override",
|
||||||
|
cfgfile: "./fixtures/config.file-regopts.yml",
|
||||||
|
environ: []string{
|
||||||
|
"DIUN_NOTIF_WEBHOOK_ENDPOINT=http://webhook.foo.com/sd54qad89azd5a",
|
||||||
|
"DIUN_NOTIF_WEBHOOK_HEADERS_AUTHORIZATION=Token78910",
|
||||||
|
"DIUN_NOTIF_WEBHOOK_HEADERS_CONTENT-TYPE=text/plain",
|
||||||
|
"DIUN_NOTIF_WEBHOOK_METHOD=GET",
|
||||||
|
"DIUN_NOTIF_WEBHOOK_TIMEOUT=1m",
|
||||||
|
},
|
||||||
|
expected: &config.Config{
|
||||||
|
Db: (&model.Db{}).GetDefaults(),
|
||||||
|
Watch: (&model.Watch{}).GetDefaults(),
|
||||||
|
Notif: &model.Notif{
|
||||||
|
Webhook: &model.NotifWebhook{
|
||||||
|
Endpoint: "http://webhook.foo.com/sd54qad89azd5a",
|
||||||
|
Method: "GET",
|
||||||
|
Headers: map[string]string{
|
||||||
|
"content-type": "text/plain",
|
||||||
|
"authorization": "Token78910",
|
||||||
|
},
|
||||||
|
Timeout: utl.NewDuration(1 * time.Minute),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RegOpts: nil,
|
||||||
|
Providers: &model.Providers{
|
||||||
|
File: &model.PrdFile{
|
||||||
|
Filename: "./fixtures/dummy.yml",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range testCases {
|
||||||
|
t.Run(tt.desc, func(t *testing.T) {
|
||||||
|
UnsetEnv("DIUN_")
|
||||||
|
|
||||||
|
if tt.environ != nil {
|
||||||
|
for _, environ := range tt.environ {
|
||||||
|
n := strings.SplitN(environ, "=", 2)
|
||||||
|
os.Setenv(n[0], n[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := config.Load(tt.cfgfile)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.expected, cfg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidation(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
cfgfile string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Success",
|
||||||
|
cfgfile: "./fixtures/config.validate.yml",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range cases {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
_, err := config.Load(tt.cfgfile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
//dec, err := env.Encode(cfg)
|
||||||
|
//for _, value := range dec {
|
||||||
|
// fmt.Println(fmt.Sprintf(`%s=%s`, strings.Replace(value.Name, "TRAEFIK_", "DIUN_", 1), value.Default))
|
||||||
|
//}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnsetEnv(prefix string) (restore func()) {
|
||||||
|
before := map[string]string{}
|
||||||
|
|
||||||
|
for _, e := range os.Environ() {
|
||||||
|
if !strings.HasPrefix(e, prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(e, "=", 2)
|
||||||
|
before[parts[0]] = parts[1]
|
||||||
|
|
||||||
|
os.Unsetenv(parts[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
after := map[string]string{}
|
||||||
|
|
||||||
|
for _, e := range os.Environ() {
|
||||||
|
if !strings.HasPrefix(e, prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(e, "=", 2)
|
||||||
|
after[parts[0]] = parts[1]
|
||||||
|
|
||||||
|
// Check if the envar previously existed
|
||||||
|
v, ok := before[parts[0]]
|
||||||
|
if !ok {
|
||||||
|
// This is a newly added envar with prefix, zap it
|
||||||
|
os.Unsetenv(parts[0])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if parts[1] != v {
|
||||||
|
// If the envar value has changed, set it back
|
||||||
|
os.Setenv(parts[0], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still need to check if there have been any deleted envars
|
||||||
|
for k, v := range before {
|
||||||
|
if _, ok := after[k]; !ok {
|
||||||
|
// k is not present in after, so we set it.
|
||||||
|
os.Setenv(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
11
internal/config/fixtures/config.docker.yml
Normal file
11
internal/config/fixtures/config.docker.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
notif:
|
||||||
|
mail:
|
||||||
|
host: localhost
|
||||||
|
port: 25
|
||||||
|
ssl: false
|
||||||
|
insecureSkipVerify: false
|
||||||
|
from: diun@example.com
|
||||||
|
to: webmaster@example.com
|
||||||
|
|
||||||
|
providers:
|
||||||
|
docker: {}
|
||||||
11
internal/config/fixtures/config.file-regopts.yml
Normal file
11
internal/config/fixtures/config.file-regopts.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
notif:
|
||||||
|
webhook:
|
||||||
|
endpoint: http://webhook.foo.com/sd54qad89azd5a
|
||||||
|
headers:
|
||||||
|
content-type: application/json
|
||||||
|
authorization: Token123456
|
||||||
|
timeout: 20s
|
||||||
|
|
||||||
|
providers:
|
||||||
|
file:
|
||||||
|
filename: "./fixtures/dummy.yml"
|
||||||
@@ -4,7 +4,7 @@ db:
|
|||||||
watch:
|
watch:
|
||||||
workers: 100
|
workers: 100
|
||||||
schedule: "*/30 * * * *"
|
schedule: "*/30 * * * *"
|
||||||
first_check_notif: false
|
firstCheckNotif: true
|
||||||
|
|
||||||
notif:
|
notif:
|
||||||
amqp:
|
amqp:
|
||||||
@@ -17,60 +17,55 @@ notif:
|
|||||||
endpoint: http://gotify.foo.com
|
endpoint: http://gotify.foo.com
|
||||||
token: Token123456
|
token: Token123456
|
||||||
priority: 1
|
priority: 1
|
||||||
timeout: 10
|
timeout: 10s
|
||||||
mail:
|
mail:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 25
|
port: 25
|
||||||
ssl: false
|
ssl: false
|
||||||
insecure_skip_verify: false
|
insecureSkipVerify: false
|
||||||
username:
|
|
||||||
username_file:
|
|
||||||
password:
|
|
||||||
password_file:
|
|
||||||
from: diun@example.com
|
from: diun@example.com
|
||||||
to: webmaster@example.com
|
to: webmaster@example.com
|
||||||
rocketchat:
|
rocketchat:
|
||||||
endpoint: http://rocket.foo.com:3000
|
endpoint: http://rocket.foo.com:3000
|
||||||
channel: "#general"
|
channel: "#general"
|
||||||
user_id: abcdEFGH012345678
|
userID: abcdEFGH012345678
|
||||||
token: Token123456
|
token: Token123456
|
||||||
timeout: 10
|
timeout: 10s
|
||||||
script:
|
script:
|
||||||
cmd: "go"
|
cmd: "uname"
|
||||||
args:
|
args:
|
||||||
- "version"
|
- "-a"
|
||||||
slack:
|
slack:
|
||||||
webhook_url: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij
|
webhookURL: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij
|
||||||
teams:
|
teams:
|
||||||
webhook_url: https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij
|
webhookURL: https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij
|
||||||
telegram:
|
telegram:
|
||||||
token: abcdef123456
|
token: abcdef123456
|
||||||
chat_ids:
|
chatIDs:
|
||||||
- 8547439
|
- 8547439
|
||||||
- 1234567
|
- 1234567
|
||||||
webhook:
|
webhook:
|
||||||
endpoint: http://webhook.foo.com/sd54qad89azd5a
|
endpoint: http://webhook.foo.com/sd54qad89azd5a
|
||||||
method: GET
|
method: GET
|
||||||
headers:
|
headers:
|
||||||
Content-Type: application/json
|
content-type: application/json
|
||||||
Authorization: Token123456
|
authorization: Token123456
|
||||||
timeout: 10
|
timeout: 10s
|
||||||
|
|
||||||
regopts:
|
regopts:
|
||||||
someregopts:
|
someregopts:
|
||||||
timeout: 5
|
timeout: 5s
|
||||||
bintrayoptions:
|
bintrayoptions:
|
||||||
username: foo
|
username: foo
|
||||||
password: bar
|
password: bar
|
||||||
sensitive:
|
sensitive:
|
||||||
username_file: /run/secrets/username
|
usernameFile: /run/secrets/username
|
||||||
password_file: /run/secrets/password
|
passwordFile: /run/secrets/password
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
docker:
|
docker:
|
||||||
watch_by_default: true
|
watchByDefault: true
|
||||||
watch_stopped: true
|
watchStopped: true
|
||||||
swarm:
|
swarm: {}
|
||||||
watch_by_default: true
|
|
||||||
file:
|
file:
|
||||||
filename: ./test/dummy.yml
|
filename: ./fixtures/dummy.yml
|
||||||
71
internal/config/fixtures/config.validate.yml
Normal file
71
internal/config/fixtures/config.validate.yml
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
db:
|
||||||
|
path: diun.db
|
||||||
|
|
||||||
|
watch:
|
||||||
|
workers: 100
|
||||||
|
schedule: "*/30 * * * *"
|
||||||
|
firstCheckNotif: false
|
||||||
|
|
||||||
|
notif:
|
||||||
|
amqp:
|
||||||
|
host: localhost
|
||||||
|
port: 5672
|
||||||
|
username: guest
|
||||||
|
password: guest
|
||||||
|
queue: queue
|
||||||
|
gotify:
|
||||||
|
endpoint: http://gotify.foo.com
|
||||||
|
token: Token123456
|
||||||
|
priority: 1
|
||||||
|
timeout: 10s
|
||||||
|
mail:
|
||||||
|
host: localhost
|
||||||
|
port: 25
|
||||||
|
ssl: false
|
||||||
|
insecureSkipVerify: false
|
||||||
|
from: diun@example.com
|
||||||
|
to: webmaster@example.com
|
||||||
|
rocketchat:
|
||||||
|
endpoint: http://rocket.foo.com:3000
|
||||||
|
channel: "#general"
|
||||||
|
userID: abcdEFGH012345678
|
||||||
|
token: Token123456
|
||||||
|
timeout: 10s
|
||||||
|
script:
|
||||||
|
cmd: "uname"
|
||||||
|
args:
|
||||||
|
- "-a"
|
||||||
|
slack:
|
||||||
|
webhookURL: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij
|
||||||
|
teams:
|
||||||
|
webhookURL: https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij
|
||||||
|
telegram:
|
||||||
|
token: abcdef123456
|
||||||
|
chatIDs:
|
||||||
|
- 8547439
|
||||||
|
- 1234567
|
||||||
|
webhook:
|
||||||
|
endpoint: http://webhook.foo.com/sd54qad89azd5a
|
||||||
|
method: GET
|
||||||
|
headers:
|
||||||
|
content-type: application/json
|
||||||
|
authorization: Token123456
|
||||||
|
timeout: 10s
|
||||||
|
|
||||||
|
regopts:
|
||||||
|
someregopts:
|
||||||
|
timeout: 5s
|
||||||
|
bintrayoptions:
|
||||||
|
username: foo
|
||||||
|
password: bar
|
||||||
|
sensitive:
|
||||||
|
usernameFile: /run/secrets/username
|
||||||
|
passwordFile: /run/secrets/password
|
||||||
|
|
||||||
|
providers:
|
||||||
|
docker:
|
||||||
|
watchByDefault: true
|
||||||
|
watchStopped: true
|
||||||
|
swarm: {}
|
||||||
|
file:
|
||||||
|
filename: ./fixtures/dummy.yml
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/mail"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"github.com/crazy-max/diun/v3/internal/model"
|
|
||||||
"github.com/crazy-max/diun/v3/pkg/utl"
|
|
||||||
"github.com/imdario/mergo"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cfg *Config) validateNotif() error {
|
|
||||||
if cfg.Notif == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cfg.validateNotifAmqp(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := cfg.validateNotifGotify(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := cfg.validateNotifMail(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := cfg.validateNotifRocketChat(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := cfg.validateNotifScript(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := cfg.validateNotifSlack(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := cfg.validateNotifTelegram(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := cfg.validateNotifWebhook(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) validateNotifAmqp() error {
|
|
||||||
if cfg.Notif.Amqp == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mergo.Merge(cfg.Notif.Amqp, model.NotifAmqp{
|
|
||||||
Host: "localhost",
|
|
||||||
Port: 5672,
|
|
||||||
}); err != nil {
|
|
||||||
return errors.Wrap(err, "cannot set default values for amqp notif")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) validateNotifGotify() error {
|
|
||||||
if cfg.Notif.Gotify == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mergo.Merge(cfg.Notif.Gotify, model.NotifGotify{
|
|
||||||
Timeout: 10,
|
|
||||||
}); err != nil {
|
|
||||||
return errors.Wrap(err, "cannot set default values for gotify notif")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) validateNotifMail() error {
|
|
||||||
if cfg.Notif.Mail == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := mail.ParseAddress(cfg.Notif.Mail.From); err != nil {
|
|
||||||
return errors.Wrap(err, "cannot parse sender mail address")
|
|
||||||
}
|
|
||||||
if _, err := mail.ParseAddress(cfg.Notif.Mail.To); err != nil {
|
|
||||||
return errors.Wrap(err, "cannot parse recipient mail address")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mergo.Merge(cfg.Notif.Mail, model.NotifMail{
|
|
||||||
Host: "localhost",
|
|
||||||
Port: 25,
|
|
||||||
SSL: utl.NewFalse(),
|
|
||||||
InsecureSkipVerify: utl.NewFalse(),
|
|
||||||
}); err != nil {
|
|
||||||
return errors.Wrap(err, "cannot set default values for mail notif")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) validateNotifRocketChat() error {
|
|
||||||
if cfg.Notif.RocketChat == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mergo.Merge(cfg.Notif.RocketChat, model.NotifRocketChat{
|
|
||||||
Timeout: 10,
|
|
||||||
}); err != nil {
|
|
||||||
return errors.Wrap(err, "cannot set default values for rocketchat notif")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) validateNotifScript() error {
|
|
||||||
if cfg.Notif.Script == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Notif.Script.Cmd == "" {
|
|
||||||
return errors.New("command required for script provider")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := exec.LookPath(cfg.Notif.Script.Cmd); err != nil {
|
|
||||||
return errors.Wrap(err, "command not found for script provider")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) validateNotifSlack() error {
|
|
||||||
if cfg.Notif.Slack == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// noop
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) validateNotifTelegram() error {
|
|
||||||
if cfg.Notif.Telegram == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// noop
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) validateNotifWebhook() error {
|
|
||||||
if cfg.Notif.Webhook == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mergo.Merge(cfg.Notif.Webhook, model.NotifWebhook{
|
|
||||||
Method: "GET",
|
|
||||||
Timeout: 10,
|
|
||||||
}); err != nil {
|
|
||||||
return errors.Wrap(err, "cannot set default values for webhook notif")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/crazy-max/diun/v3/internal/model"
|
|
||||||
"github.com/crazy-max/diun/v3/pkg/utl"
|
|
||||||
"github.com/imdario/mergo"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cfg *Config) validateProviders() error {
|
|
||||||
if cfg.Providers == nil || (cfg.Providers.Docker == nil && cfg.Providers.Swarm == nil && cfg.Providers.File == nil) {
|
|
||||||
return errors.New("At least one provider is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cfg.validateProviderDocker(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := cfg.validateProviderSwarm(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := cfg.validateProviderFile(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) validateProviderDocker() error {
|
|
||||||
if cfg.Providers.Docker == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mergo.Merge(cfg.Providers.Docker, model.PrdDocker{
|
|
||||||
TLSVerify: utl.NewTrue(),
|
|
||||||
WatchByDefault: utl.NewFalse(),
|
|
||||||
WatchStopped: utl.NewFalse(),
|
|
||||||
}); err != nil {
|
|
||||||
return errors.Wrap(err, "cannot set default values for docker provider")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) validateProviderSwarm() error {
|
|
||||||
if cfg.Providers.Swarm == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mergo.Merge(cfg.Providers.Swarm, model.PrdSwarm{
|
|
||||||
TLSVerify: utl.NewTrue(),
|
|
||||||
WatchByDefault: utl.NewFalse(),
|
|
||||||
}); err != nil {
|
|
||||||
return errors.Wrap(err, "cannot set default values for docker provider")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) validateProviderFile() error {
|
|
||||||
if cfg.Providers.File == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case len(cfg.Providers.File.Directory) > 0:
|
|
||||||
if _, err := os.Stat(cfg.Providers.File.Directory); os.IsNotExist(err) {
|
|
||||||
return errors.Wrap(err, "directory not found for file provider")
|
|
||||||
}
|
|
||||||
case len(cfg.Providers.File.Filename) > 0:
|
|
||||||
if _, err := os.Stat(cfg.Providers.File.Filename); os.IsNotExist(err) {
|
|
||||||
return errors.Wrap(err, "filename not found for file provider")
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return errors.New("error using file provider, neither filename or directory defined")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/crazy-max/diun/v3/internal/model"
|
|
||||||
"github.com/imdario/mergo"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (cfg *Config) validateRegopts() error {
|
|
||||||
for id, regopt := range cfg.RegOpts {
|
|
||||||
if err := cfg.validateRegOpt(id, regopt); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) validateRegOpt(id string, regopts model.RegOpts) error {
|
|
||||||
defTimeout := 10
|
|
||||||
if regopts.Timeout <= 0 {
|
|
||||||
defTimeout = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mergo.Merge(®opts, model.RegOpts{
|
|
||||||
InsecureTLS: false,
|
|
||||||
Timeout: defTimeout,
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("cannot set default values for registry options %s: %v", id, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.RegOpts[id] = regopts
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
// App holds application details
|
|
||||||
type App struct {
|
|
||||||
ID string
|
|
||||||
Name string
|
|
||||||
Desc string
|
|
||||||
URL string
|
|
||||||
Author string
|
|
||||||
Version string
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ import "github.com/alecthomas/kong"
|
|||||||
// Cli holds command line args, flags and cmds
|
// Cli holds command line args, flags and cmds
|
||||||
type Cli struct {
|
type Cli struct {
|
||||||
Version kong.VersionFlag
|
Version kong.VersionFlag
|
||||||
Cfgfile string `kong:"required,name='config',env='CONFIG',help='Diun configuration file.'"`
|
Cfgfile string `kong:"name='config',env='CONFIG',help='Diun configuration file.'"`
|
||||||
Timezone string `kong:"name='timezone',env='TZ',default='UTC',help='Timezone assigned to Diun.'"`
|
Timezone string `kong:"name='timezone',env='TZ',default='UTC',help='Timezone assigned to Diun.'"`
|
||||||
LogLevel string `kong:"name='log-level',env='LOG_LEVEL',default='info',help='Set log level.'"`
|
LogLevel string `kong:"name='log-level',env='LOG_LEVEL',default='info',help='Set log level.'"`
|
||||||
LogJSON bool `kong:"name='log-json',env='LOG_JSON',default='false',help='Enable JSON logging output.'"`
|
LogJSON bool `kong:"name='log-json',env='LOG_JSON',default='false',help='Enable JSON logging output.'"`
|
||||||
|
|||||||
@@ -2,5 +2,17 @@ package model
|
|||||||
|
|
||||||
// Db holds data necessary for database configuration
|
// Db holds data necessary for database configuration
|
||||||
type Db struct {
|
type Db struct {
|
||||||
Path string `yaml:"path,omitempty"`
|
Path string `yaml:"path,omitempty" json:"path,omitempty" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *Db) GetDefaults() *Db {
|
||||||
|
n := &Db{}
|
||||||
|
n.SetDefaults()
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *Db) SetDefaults() {
|
||||||
|
s.Path = "diun.db"
|
||||||
}
|
}
|
||||||
|
|||||||
13
internal/model/meta.go
Normal file
13
internal/model/meta.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// Meta holds application details
|
||||||
|
type Meta struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Desc string
|
||||||
|
URL string
|
||||||
|
Logo string
|
||||||
|
Author string
|
||||||
|
Version string
|
||||||
|
UserAgent string
|
||||||
|
}
|
||||||
@@ -14,87 +14,23 @@ type NotifEntry struct {
|
|||||||
|
|
||||||
// Notif holds data necessary for notification configuration
|
// Notif holds data necessary for notification configuration
|
||||||
type Notif struct {
|
type Notif struct {
|
||||||
Amqp *NotifAmqp `yaml:"amqp,omitempty"`
|
Amqp *NotifAmqp `yaml:"amqp,omitempty" json:"amqp,omitempty"`
|
||||||
Gotify *NotifGotify `yaml:"gotify,omitempty"`
|
Gotify *NotifGotify `yaml:"gotify,omitempty" json:"gotify,omitempty"`
|
||||||
Mail *NotifMail `yaml:"mail,omitempty"`
|
Mail *NotifMail `yaml:"mail,omitempty" json:"mail,omitempty"`
|
||||||
RocketChat *NotifRocketChat `yaml:"rocketchat,omitempty"`
|
RocketChat *NotifRocketChat `yaml:"rocketchat,omitempty" json:"rocketchat,omitempty"`
|
||||||
Script *NotifScript `yaml:"script,omitempty"`
|
Script *NotifScript `yaml:"script,omitempty" json:"script,omitempty"`
|
||||||
Slack *NotifSlack `yaml:"slack,omitempty"`
|
Slack *NotifSlack `yaml:"slack,omitempty" json:"slack,omitempty"`
|
||||||
Teams *NotifTeams `yaml:"teams,omitempty"`
|
Teams *NotifTeams `yaml:"teams,omitempty" json:"teams,omitempty"`
|
||||||
Telegram *NotifTelegram `yaml:"telegram,omitempty"`
|
Telegram *NotifTelegram `yaml:"telegram,omitempty" json:"telegram,omitempty"`
|
||||||
Webhook *NotifWebhook `yaml:"webhook,omitempty"`
|
Webhook *NotifWebhook `yaml:"webhook,omitempty" json:"webhook,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifAmqp holds amqp notification configuration details
|
// GetDefaults gets the default values
|
||||||
type NotifAmqp struct {
|
func (s *Notif) GetDefaults() *Notif {
|
||||||
Username string `yaml:"username,omitempty"`
|
return nil
|
||||||
UsernameFile string `yaml:"username_file,omitempty"`
|
|
||||||
Password string `yaml:"password,omitempty"`
|
|
||||||
PasswordFile string `yaml:"password_file,omitempty"`
|
|
||||||
Host string `yaml:"host,omitempty"`
|
|
||||||
Port int `yaml:"port,omitempty"`
|
|
||||||
Queue string `yaml:"queue,omitempty"`
|
|
||||||
Exchange string `yaml:"exchange,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifGotify holds gotify notification configuration details
|
// SetDefaults sets the default values
|
||||||
type NotifGotify struct {
|
func (s *Notif) SetDefaults() {
|
||||||
Endpoint string `yaml:"endpoint,omitempty"`
|
// noop
|
||||||
Token string `yaml:"token,omitempty"`
|
|
||||||
Priority int `yaml:"priority,omitempty"`
|
|
||||||
Timeout int `yaml:"timeout,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotifMail holds mail notification configuration details
|
|
||||||
type NotifMail struct {
|
|
||||||
Host string `yaml:"host,omitempty"`
|
|
||||||
Port int `yaml:"port,omitempty"`
|
|
||||||
SSL *bool `yaml:"ssl,omitempty"`
|
|
||||||
InsecureSkipVerify *bool `yaml:"insecure_skip_verify,omitempty"`
|
|
||||||
Username string `yaml:"username,omitempty"`
|
|
||||||
UsernameFile string `yaml:"username_file,omitempty"`
|
|
||||||
Password string `yaml:"password,omitempty"`
|
|
||||||
PasswordFile string `yaml:"password_file,omitempty"`
|
|
||||||
From string `yaml:"from,omitempty"`
|
|
||||||
To string `yaml:"to,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotifRocketChat holds Rocket.Chat notification configuration details
|
|
||||||
type NotifRocketChat struct {
|
|
||||||
Endpoint string `yaml:"endpoint,omitempty"`
|
|
||||||
Channel string `yaml:"channel,omitempty"`
|
|
||||||
UserID string `yaml:"user_id,omitempty"`
|
|
||||||
Token string `yaml:"token,omitempty"`
|
|
||||||
Timeout int `yaml:"timeout,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotifScript holds script notification configuration details
|
|
||||||
type NotifScript struct {
|
|
||||||
Cmd string `yaml:"cmd,omitempty"`
|
|
||||||
Args []string `yaml:"args,omitempty"`
|
|
||||||
Dir string `yaml:"dir,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotifSlack holds slack notification configuration details
|
|
||||||
type NotifSlack struct {
|
|
||||||
WebhookURL string `yaml:"webhook_url,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotifTeams holds Teams notification configuration details
|
|
||||||
type NotifTeams struct {
|
|
||||||
WebhookURL string `yaml:"webhook_url,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotifTelegram holds Telegram notification configuration details
|
|
||||||
type NotifTelegram struct {
|
|
||||||
BotToken string `yaml:"token,omitempty"`
|
|
||||||
ChatIDs []int64 `yaml:"chat_ids,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NotifWebhook holds webhook notification configuration details
|
|
||||||
type NotifWebhook struct {
|
|
||||||
Endpoint string `yaml:"endpoint,omitempty"`
|
|
||||||
Method string `yaml:"method,omitempty"`
|
|
||||||
Headers map[string]string `yaml:"headers,omitempty"`
|
|
||||||
Timeout int `yaml:"timeout,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
26
internal/model/notif_amqp.go
Normal file
26
internal/model/notif_amqp.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// NotifAmqp holds amqp notification configuration details
|
||||||
|
type NotifAmqp struct {
|
||||||
|
Username string `yaml:"username,omitempty" json:"username,omitempty" validate:"omitempty"`
|
||||||
|
UsernameFile string `yaml:"usernameFile,omitempty" json:"usernameFile,omitempty" validate:"omitempty,file"`
|
||||||
|
Password string `yaml:"password,omitempty" json:"password,omitempty" validate:"omitempty"`
|
||||||
|
PasswordFile string `yaml:"passwordFile,omitempty" json:"passwordFile,omitempty" validate:"omitempty,file"`
|
||||||
|
Host string `yaml:"host,omitempty" json:"host,omitempty" validate:"required"`
|
||||||
|
Port int `yaml:"port,omitempty" json:"port,omitempty" validate:"required"`
|
||||||
|
Queue string `yaml:"queue,omitempty" json:"queue,omitempty" validate:"required"`
|
||||||
|
Exchange string `yaml:"exchange,omitempty" json:"exchange,omitempty" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *NotifAmqp) GetDefaults() *NotifAmqp {
|
||||||
|
n := &NotifAmqp{}
|
||||||
|
n.SetDefaults()
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *NotifAmqp) SetDefaults() {
|
||||||
|
s.Host = "localhost"
|
||||||
|
s.Port = 5672
|
||||||
|
}
|
||||||
28
internal/model/notif_gotify.go
Normal file
28
internal/model/notif_gotify.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/pkg/utl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotifGotify holds gotify notification configuration details
|
||||||
|
type NotifGotify struct {
|
||||||
|
Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"required"`
|
||||||
|
Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"required"`
|
||||||
|
Priority int `yaml:"priority,omitempty" json:"priority,omitempty" validate:"omitempty,min=0"`
|
||||||
|
Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *NotifGotify) GetDefaults() *NotifGotify {
|
||||||
|
n := &NotifGotify{}
|
||||||
|
n.SetDefaults()
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *NotifGotify) SetDefaults() {
|
||||||
|
s.Priority = 1
|
||||||
|
s.Timeout = utl.NewDuration(10 * time.Second)
|
||||||
|
}
|
||||||
64
internal/model/notif_mail.go
Normal file
64
internal/model/notif_mail.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/mail"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/pkg/utl"
|
||||||
|
"github.com/imdario/mergo"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotifMail holds mail notification configuration details
|
||||||
|
type NotifMail struct {
|
||||||
|
Host string `yaml:"host,omitempty" json:"host,omitempty" validate:"required"`
|
||||||
|
Port int `yaml:"port,omitempty" json:"port,omitempty" validate:"required,min=1"`
|
||||||
|
SSL *bool `yaml:"ssl,omitempty" json:"ssl,omitempty" validate:"required"`
|
||||||
|
InsecureSkipVerify *bool `yaml:"insecureSkipVerify,omitempty" json:"insecureSkipVerify,omitempty" validate:"required"`
|
||||||
|
Username string `yaml:"username,omitempty" json:"username,omitempty" validate:"omitempty"`
|
||||||
|
UsernameFile string `yaml:"usernameFile,omitempty" json:"usernameFile,omitempty" validate:"omitempty,file"`
|
||||||
|
Password string `yaml:"password,omitempty" json:"password,omitempty" validate:"omitempty"`
|
||||||
|
PasswordFile string `yaml:"passwordFile,omitempty" json:"passwordFile,omitempty" validate:"omitempty,file"`
|
||||||
|
From string `yaml:"from,omitempty" json:"from,omitempty" validate:"required,email"`
|
||||||
|
To string `yaml:"to,omitempty" json:"to,omitempty" validate:"required,email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *NotifMail) GetDefaults() *NotifMail {
|
||||||
|
n := &NotifMail{}
|
||||||
|
n.SetDefaults()
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *NotifMail) SetDefaults() {
|
||||||
|
s.Host = "localhost"
|
||||||
|
s.Port = 25
|
||||||
|
s.SSL = utl.NewFalse()
|
||||||
|
s.InsecureSkipVerify = utl.NewFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements the yaml.Unmarshaler interface
|
||||||
|
func (s *NotifMail) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type plain NotifMail
|
||||||
|
if err := unmarshal((*plain)(s)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := mail.ParseAddress(s.From); err != nil {
|
||||||
|
return errors.Wrap(err, "cannot parse sender mail address")
|
||||||
|
}
|
||||||
|
if _, err := mail.ParseAddress(s.To); err != nil {
|
||||||
|
return errors.Wrap(err, "cannot parse recipient mail address")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mergo.Merge(s, NotifMail{
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 25,
|
||||||
|
SSL: utl.NewFalse(),
|
||||||
|
InsecureSkipVerify: utl.NewFalse(),
|
||||||
|
}); err != nil {
|
||||||
|
return errors.Wrap(err, "cannot set default values for mail notif")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
28
internal/model/notif_rocketchat.go
Normal file
28
internal/model/notif_rocketchat.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/pkg/utl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotifRocketChat holds Rocket.Chat notification configuration details
|
||||||
|
type NotifRocketChat struct {
|
||||||
|
Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"required"`
|
||||||
|
Channel string `yaml:"channel,omitempty" json:"channel,omitempty" validate:"required"`
|
||||||
|
UserID string `yaml:"userID,omitempty" json:"userID,omitempty" validate:"required"`
|
||||||
|
Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"required"`
|
||||||
|
Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *NotifRocketChat) GetDefaults() *NotifRocketChat {
|
||||||
|
n := &NotifRocketChat{}
|
||||||
|
n.SetDefaults()
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *NotifRocketChat) SetDefaults() {
|
||||||
|
s.Timeout = utl.NewDuration(10 * time.Second)
|
||||||
|
}
|
||||||
18
internal/model/notif_script.go
Normal file
18
internal/model/notif_script.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// NotifScript holds script notification configuration details
|
||||||
|
type NotifScript struct {
|
||||||
|
Cmd string `yaml:"cmd,omitempty" json:"cmd,omitempty" validate:"required"`
|
||||||
|
Args []string `yaml:"args,omitempty" json:"args,omitempty" validate:"omitempty"`
|
||||||
|
Dir string `yaml:"dir,omitempty" json:"dir,omitempty" validate:"omitempty,dir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *NotifScript) GetDefaults() *NotifScript {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *NotifScript) SetDefaults() {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
16
internal/model/notif_slack.go
Normal file
16
internal/model/notif_slack.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// NotifSlack holds slack notification configuration details
|
||||||
|
type NotifSlack struct {
|
||||||
|
WebhookURL string `yaml:"webhookURL,omitempty" json:"webhookURL,omitempty" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *NotifSlack) GetDefaults() *NotifSlack {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *NotifSlack) SetDefaults() {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
16
internal/model/notif_teams.go
Normal file
16
internal/model/notif_teams.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// NotifTeams holds Teams notification configuration details
|
||||||
|
type NotifTeams struct {
|
||||||
|
WebhookURL string `yaml:"webhookURL,omitempty" json:"webhookURL,omitempty" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *NotifTeams) GetDefaults() *NotifTeams {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *NotifTeams) SetDefaults() {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
17
internal/model/notif_telegram.go
Normal file
17
internal/model/notif_telegram.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// NotifTelegram holds Telegram notification configuration details
|
||||||
|
type NotifTelegram struct {
|
||||||
|
Token string `yaml:"token,omitempty" json:"token,omitempty" validate:"required"`
|
||||||
|
ChatIDs []int64 `yaml:"chatIDs,omitempty" json:"chatIDs,omitempty" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *NotifTelegram) GetDefaults() *NotifTelegram {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *NotifTelegram) SetDefaults() {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
28
internal/model/notif_webhook.go
Normal file
28
internal/model/notif_webhook.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/pkg/utl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotifWebhook holds webhook notification configuration details
|
||||||
|
type NotifWebhook struct {
|
||||||
|
Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"required"`
|
||||||
|
Method string `yaml:"method,omitempty" json:"method,omitempty" validate:"required"`
|
||||||
|
Headers map[string]string `yaml:"headers,omitempty" json:"headers,omitempty" validate:"omitempty"`
|
||||||
|
Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *NotifWebhook) GetDefaults() *NotifWebhook {
|
||||||
|
n := &NotifWebhook{}
|
||||||
|
n.SetDefaults()
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *NotifWebhook) SetDefaults() {
|
||||||
|
s.Method = "GET"
|
||||||
|
s.Timeout = utl.NewDuration(10 * time.Second)
|
||||||
|
}
|
||||||
29
internal/model/provider_docker.go
Normal file
29
internal/model/provider_docker.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/crazy-max/diun/v3/pkg/utl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrdDocker holds docker provider configuration
|
||||||
|
type PrdDocker struct {
|
||||||
|
Endpoint string `yaml:"endpoint" json:"endpoint,omitempty" validate:"omitempty"`
|
||||||
|
APIVersion string `yaml:"apiVersion" json:"apiVersion,omitempty" validate:"omitempty"`
|
||||||
|
TLSCertsPath string `yaml:"tlsCertsPath" json:"tlsCertsPath,omitempty" validate:"omitempty"`
|
||||||
|
TLSVerify *bool `yaml:"tlsVerify" json:"tlsVerify,omitempty" validate:"required"`
|
||||||
|
WatchByDefault *bool `yaml:"watchByDefault" json:"watchByDefault,omitempty" validate:"required"`
|
||||||
|
WatchStopped *bool `yaml:"watchStopped" json:"watchStopped,omitempty" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *PrdDocker) GetDefaults() *PrdDocker {
|
||||||
|
n := &PrdDocker{}
|
||||||
|
n.SetDefaults()
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *PrdDocker) SetDefaults() {
|
||||||
|
s.TLSVerify = utl.NewTrue()
|
||||||
|
s.WatchByDefault = utl.NewFalse()
|
||||||
|
s.WatchStopped = utl.NewFalse()
|
||||||
|
}
|
||||||
17
internal/model/provider_file.go
Normal file
17
internal/model/provider_file.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// PrdFile holds file provider configuration
|
||||||
|
type PrdFile struct {
|
||||||
|
Filename string `yaml:"filename,omitempty" json:"filename,omitempty" validate:"omitempty,file"`
|
||||||
|
Directory string `yaml:"directory,omitempty" json:"directory,omitempty" validate:"omitempty,dir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *PrdFile) GetDefaults() *PrdFile {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *PrdFile) SetDefaults() {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
27
internal/model/provider_swarm.go
Normal file
27
internal/model/provider_swarm.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/crazy-max/diun/v3/pkg/utl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrdSwarm holds swarm provider configuration
|
||||||
|
type PrdSwarm struct {
|
||||||
|
Endpoint string `yaml:"endpoint,omitempty" json:"endpoint,omitempty" validate:"omitempty"`
|
||||||
|
APIVersion string `yaml:"apiVersion,omitempty" json:"apiVersion,omitempty" validate:"omitempty"`
|
||||||
|
TLSCertsPath string `yaml:"tlsCertsPath,omitempty" json:"tlsCertsPath,omitempty" validate:"omitempty"`
|
||||||
|
TLSVerify *bool `yaml:"tlsVerify,omitempty" json:"tlsVerify,omitempty" validate:"required"`
|
||||||
|
WatchByDefault *bool `yaml:"watchByDefault,omitempty" json:"watchByDefault,omitempty" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *PrdSwarm) GetDefaults() *PrdSwarm {
|
||||||
|
n := &PrdSwarm{}
|
||||||
|
n.SetDefaults()
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *PrdSwarm) SetDefaults() {
|
||||||
|
s.TLSVerify = utl.NewTrue()
|
||||||
|
s.WatchByDefault = utl.NewFalse()
|
||||||
|
}
|
||||||
@@ -2,32 +2,17 @@ package model
|
|||||||
|
|
||||||
// Providers represents a provider configuration
|
// Providers represents a provider configuration
|
||||||
type Providers struct {
|
type Providers struct {
|
||||||
Docker *PrdDocker `yaml:"docker,omitempty" json:",omitempty"`
|
Docker *PrdDocker `yaml:"docker,omitempty" json:"docker,omitempty" label:"allowEmpty"`
|
||||||
Swarm *PrdSwarm `yaml:"swarm,omitempty" json:",omitempty"`
|
Swarm *PrdSwarm `yaml:"swarm,omitempty" json:"swarm,omitempty" label:"allowEmpty"`
|
||||||
File *PrdFile `yaml:"file,omitempty" json:",omitempty"`
|
File *PrdFile `yaml:"file,omitempty" json:"file,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrdDocker holds docker provider configuration
|
// GetDefaults gets the default values
|
||||||
type PrdDocker struct {
|
func (s *Providers) GetDefaults() *Providers {
|
||||||
Endpoint string `yaml:"endpoint,omitempty" json:",omitempty"`
|
return nil
|
||||||
APIVersion string `yaml:"api_version,omitempty" json:",omitempty"`
|
|
||||||
TLSCertsPath string `yaml:"tls_certs_path,omitempty" json:",omitempty"`
|
|
||||||
TLSVerify *bool `yaml:"tls_verify,omitempty" json:",omitempty"`
|
|
||||||
WatchByDefault *bool `yaml:"watch_by_default,omitempty" json:",omitempty"`
|
|
||||||
WatchStopped *bool `yaml:"watch_stopped,omitempty" json:",omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrdSwarm holds swarm provider configuration
|
// SetDefaults sets the default values
|
||||||
type PrdSwarm struct {
|
func (s *Providers) SetDefaults() {
|
||||||
Endpoint string `yaml:"endpoint,omitempty" json:",omitempty"`
|
// noop
|
||||||
APIVersion string `yaml:"api_version,omitempty" json:",omitempty"`
|
|
||||||
TLSCertsPath string `yaml:"tls_certs_path,omitempty" json:",omitempty"`
|
|
||||||
TLSVerify *bool `yaml:"tls_verify,omitempty" json:",omitempty"`
|
|
||||||
WatchByDefault *bool `yaml:"watch_by_default,omitempty" json:",omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrdFile holds file provider configuration
|
|
||||||
type PrdFile struct {
|
|
||||||
Filename string `yaml:"filename,omitempty" json:",omitempty"`
|
|
||||||
Directory string `yaml:"directory,omitempty" json:",omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,30 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/pkg/utl"
|
||||||
|
)
|
||||||
|
|
||||||
// RegOpts holds registry options configuration
|
// RegOpts holds registry options configuration
|
||||||
type RegOpts struct {
|
type RegOpts struct {
|
||||||
Username string `yaml:"username,omitempty" json:",omitempty"`
|
Username string `yaml:"username,omitempty" json:"username,omitempty" validate:"omitempty"`
|
||||||
UsernameFile string `yaml:"username_file,omitempty" json:",omitempty"`
|
UsernameFile string `yaml:"usernameFile,omitempty" json:"usernameFile,omitempty" validate:"omitempty,file"`
|
||||||
Password string `yaml:"password,omitempty" json:",omitempty"`
|
Password string `yaml:"password,omitempty" json:"password,omitempty" validate:"omitempty"`
|
||||||
PasswordFile string `yaml:"password_file,omitempty" json:",omitempty"`
|
PasswordFile string `yaml:"passwordFile,omitempty" json:"passwordFile,omitempty" validate:"omitempty,file"`
|
||||||
InsecureTLS bool `yaml:"insecure_tls,omitempty" json:",omitempty"`
|
InsecureTLS *bool `yaml:"insecureTls,omitempty" json:"insecureTls,omitempty" validate:"required"`
|
||||||
Timeout int `yaml:"timeout,omitempty" json:",omitempty"`
|
Timeout *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *RegOpts) GetDefaults() *RegOpts {
|
||||||
|
n := &RegOpts{}
|
||||||
|
n.SetDefaults()
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *RegOpts) SetDefaults() {
|
||||||
|
s.InsecureTLS = utl.NewFalse()
|
||||||
|
s.Timeout = utl.NewDuration(10 * time.Second)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/crazy-max/diun/v3/pkg/utl"
|
||||||
|
)
|
||||||
|
|
||||||
// Watch holds data necessary for watch configuration
|
// Watch holds data necessary for watch configuration
|
||||||
type Watch struct {
|
type Watch struct {
|
||||||
Workers int `yaml:"workers,omitempty"`
|
Workers int `yaml:"workers,omitempty" json:"workers,omitempty" validate:"required,min=1"`
|
||||||
Schedule string `yaml:"schedule,omitempty"`
|
Schedule string `yaml:"schedule,omitempty" json:"schedule,omitempty" validate:"required"`
|
||||||
FirstCheckNotif *bool `yaml:"first_check_notif,omitempty"`
|
FirstCheckNotif *bool `yaml:"firstCheckNotif,omitempty" json:"firstCheckNotif,omitempty" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaults gets the default values
|
||||||
|
func (s *Watch) GetDefaults() *Watch {
|
||||||
|
n := &Watch{}
|
||||||
|
n.SetDefaults()
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets the default values
|
||||||
|
func (s *Watch) SetDefaults() {
|
||||||
|
s.Workers = 10
|
||||||
|
s.Schedule = "0 * * * *"
|
||||||
|
s.FirstCheckNotif = utl.NewFalse()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,16 +15,16 @@ import (
|
|||||||
// Client represents an active amqp notification object
|
// Client represents an active amqp notification object
|
||||||
type Client struct {
|
type Client struct {
|
||||||
*notifier.Notifier
|
*notifier.Notifier
|
||||||
cfg *model.NotifAmqp
|
cfg *model.NotifAmqp
|
||||||
app model.App
|
meta model.Meta
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new amqp notification instance
|
// New creates a new amqp notification instance
|
||||||
func New(config *model.NotifAmqp, app model.App) notifier.Notifier {
|
func New(config *model.NotifAmqp, meta model.Meta) notifier.Notifier {
|
||||||
return notifier.Notifier{
|
return notifier.Notifier{
|
||||||
Handler: &Client{
|
Handler: &Client{
|
||||||
cfg: config,
|
cfg: config,
|
||||||
app: app,
|
meta: meta,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,9 +36,7 @@ func (c *Client) Name() string {
|
|||||||
|
|
||||||
// Send creates and sends a amqp notification with an entry
|
// Send creates and sends a amqp notification with an entry
|
||||||
func (c *Client) Send(entry model.NotifEntry) error {
|
func (c *Client) Send(entry model.NotifEntry) error {
|
||||||
|
|
||||||
username, err := utl.GetSecret(c.cfg.Username, c.cfg.UsernameFile)
|
username, err := utl.GetSecret(c.cfg.Username, c.cfg.UsernameFile)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -48,52 +46,31 @@ func (c *Client) Send(entry model.NotifEntry) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
connString := fmt.Sprintf("amqp://%s:%s@%s:%d/", username, password, c.cfg.Host, c.cfg.Port)
|
conn, err := amqp.Dial(fmt.Sprintf("amqp://%s:%s@%s:%d/", username, password, c.cfg.Host, c.cfg.Port))
|
||||||
|
|
||||||
conn, err := amqp.Dial(connString)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
ch, err := conn.Channel()
|
ch, err := conn.Channel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer ch.Close()
|
defer ch.Close()
|
||||||
|
|
||||||
q, err := ch.QueueDeclare(
|
q, err := ch.QueueDeclare(
|
||||||
c.cfg.Queue, // name
|
c.cfg.Queue,
|
||||||
false, // durable
|
false,
|
||||||
false, // delete when unused
|
false,
|
||||||
false, // exclusive
|
false,
|
||||||
false, // no-wait
|
false,
|
||||||
nil, // arguments
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := buildBody(entry, c.app)
|
body, err := json.Marshal(struct {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ch.Publish(
|
|
||||||
c.cfg.Exchange, // exchange
|
|
||||||
q.Name, // routing key
|
|
||||||
false, // mandatory
|
|
||||||
false, // immediate
|
|
||||||
amqp.Publishing{
|
|
||||||
ContentType: "application/json",
|
|
||||||
Body: body,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildBody(entry model.NotifEntry, app model.App) ([]byte, error) {
|
|
||||||
return json.Marshal(struct {
|
|
||||||
Version string `json:"diun_version"`
|
Version string `json:"diun_version"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
@@ -103,7 +80,7 @@ func buildBody(entry model.NotifEntry, app model.App) ([]byte, error) {
|
|||||||
Created *time.Time `json:"created"`
|
Created *time.Time `json:"created"`
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
}{
|
}{
|
||||||
Version: app.Version,
|
Version: c.meta.Version,
|
||||||
Status: string(entry.Status),
|
Status: string(entry.Status),
|
||||||
Provider: entry.Provider,
|
Provider: entry.Provider,
|
||||||
Image: entry.Image.String(),
|
Image: entry.Image.String(),
|
||||||
@@ -112,4 +89,17 @@ func buildBody(entry model.NotifEntry, app model.App) ([]byte, error) {
|
|||||||
Created: entry.Manifest.Created,
|
Created: entry.Manifest.Created,
|
||||||
Platform: entry.Manifest.Platform,
|
Platform: entry.Manifest.Platform,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ch.Publish(
|
||||||
|
c.cfg.Exchange,
|
||||||
|
q.Name,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
amqp.Publishing{
|
||||||
|
ContentType: "application/json",
|
||||||
|
Body: body,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package notif
|
package notif
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/crazy-max/diun/v3/internal/model"
|
"github.com/crazy-max/diun/v3/internal/model"
|
||||||
"github.com/crazy-max/diun/v3/internal/notif/amqp"
|
"github.com/crazy-max/diun/v3/internal/notif/amqp"
|
||||||
"github.com/crazy-max/diun/v3/internal/notif/gotify"
|
"github.com/crazy-max/diun/v3/internal/notif/gotify"
|
||||||
@@ -18,15 +20,15 @@ import (
|
|||||||
// Client represents an active webhook notification object
|
// Client represents an active webhook notification object
|
||||||
type Client struct {
|
type Client struct {
|
||||||
cfg *model.Notif
|
cfg *model.Notif
|
||||||
app model.App
|
meta model.Meta
|
||||||
notifiers []notifier.Notifier
|
notifiers []notifier.Notifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new notification instance
|
// New creates a new notification instance
|
||||||
func New(config *model.Notif, app model.App, userAgent string) (*Client, error) {
|
func New(config *model.Notif, meta model.Meta) (*Client, error) {
|
||||||
var c = &Client{
|
var c = &Client{
|
||||||
cfg: config,
|
cfg: config,
|
||||||
app: app,
|
meta: meta,
|
||||||
notifiers: []notifier.Notifier{},
|
notifiers: []notifier.Notifier{},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,31 +39,31 @@ func New(config *model.Notif, app model.App, userAgent string) (*Client, error)
|
|||||||
|
|
||||||
// Add notifiers
|
// Add notifiers
|
||||||
if config.Amqp != nil {
|
if config.Amqp != nil {
|
||||||
c.notifiers = append(c.notifiers, amqp.New(config.Amqp, app))
|
c.notifiers = append(c.notifiers, amqp.New(config.Amqp, meta))
|
||||||
}
|
}
|
||||||
if config.Gotify != nil {
|
if config.Gotify != nil {
|
||||||
c.notifiers = append(c.notifiers, gotify.New(config.Gotify, app, userAgent))
|
c.notifiers = append(c.notifiers, gotify.New(config.Gotify, meta))
|
||||||
}
|
}
|
||||||
if config.Mail != nil {
|
if config.Mail != nil {
|
||||||
c.notifiers = append(c.notifiers, mail.New(config.Mail, app))
|
c.notifiers = append(c.notifiers, mail.New(config.Mail, meta))
|
||||||
}
|
}
|
||||||
if config.RocketChat != nil {
|
if config.RocketChat != nil {
|
||||||
c.notifiers = append(c.notifiers, rocketchat.New(config.RocketChat, app, userAgent))
|
c.notifiers = append(c.notifiers, rocketchat.New(config.RocketChat, meta))
|
||||||
}
|
}
|
||||||
if config.Script != nil {
|
if config.Script != nil {
|
||||||
c.notifiers = append(c.notifiers, script.New(config.Script, app))
|
c.notifiers = append(c.notifiers, script.New(config.Script, meta))
|
||||||
}
|
}
|
||||||
if config.Slack != nil {
|
if config.Slack != nil {
|
||||||
c.notifiers = append(c.notifiers, slack.New(config.Slack, app))
|
c.notifiers = append(c.notifiers, slack.New(config.Slack, meta))
|
||||||
}
|
}
|
||||||
if config.Teams != nil {
|
if config.Teams != nil {
|
||||||
c.notifiers = append(c.notifiers, teams.New(config.Teams, app, userAgent))
|
c.notifiers = append(c.notifiers, teams.New(config.Teams, meta))
|
||||||
}
|
}
|
||||||
if config.Telegram != nil {
|
if config.Telegram != nil {
|
||||||
c.notifiers = append(c.notifiers, telegram.New(config.Telegram, app))
|
c.notifiers = append(c.notifiers, telegram.New(config.Telegram, meta))
|
||||||
}
|
}
|
||||||
if config.Webhook != nil {
|
if config.Webhook != nil {
|
||||||
c.notifiers = append(c.notifiers, webhook.New(config.Webhook, app, userAgent))
|
c.notifiers = append(c.notifiers, webhook.New(config.Webhook, meta))
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug().Msgf("%d notifier(s) created", len(c.notifiers))
|
log.Debug().Msgf("%d notifier(s) created", len(c.notifiers))
|
||||||
@@ -73,7 +75,7 @@ func (c *Client) Send(entry model.NotifEntry) {
|
|||||||
for _, n := range c.notifiers {
|
for _, n := range c.notifiers {
|
||||||
log.Debug().Str("image", entry.Image.String()).Msgf("Sending %s notification...", n.Name())
|
log.Debug().Str("image", entry.Image.String()).Msgf("Sending %s notification...", n.Name())
|
||||||
if err := n.Send(entry); err != nil {
|
if err := n.Send(entry); err != nil {
|
||||||
log.Error().Err(err).Str("image", entry.Image.String()).Msgf("%s notification failed", n.Name())
|
log.Error().Err(err).Str("image", entry.Image.String()).Msgf("%s notification failed", strings.Title(n.Name()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/crazy-max/diun/v3/internal/model"
|
"github.com/crazy-max/diun/v3/internal/model"
|
||||||
"github.com/crazy-max/diun/v3/internal/notif/notifier"
|
"github.com/crazy-max/diun/v3/internal/notif/notifier"
|
||||||
@@ -19,18 +18,16 @@ import (
|
|||||||
// Client represents an active gotify notification object
|
// Client represents an active gotify notification object
|
||||||
type Client struct {
|
type Client struct {
|
||||||
*notifier.Notifier
|
*notifier.Notifier
|
||||||
cfg *model.NotifGotify
|
cfg *model.NotifGotify
|
||||||
app model.App
|
meta model.Meta
|
||||||
userAgent string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new gotify notification instance
|
// New creates a new gotify notification instance
|
||||||
func New(config *model.NotifGotify, app model.App, userAgent string) notifier.Notifier {
|
func New(config *model.NotifGotify, meta model.Meta) notifier.Notifier {
|
||||||
return notifier.Notifier{
|
return notifier.Notifier{
|
||||||
Handler: &Client{
|
Handler: &Client{
|
||||||
cfg: config,
|
cfg: config,
|
||||||
app: app,
|
meta: meta,
|
||||||
userAgent: userAgent,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +40,7 @@ func (c *Client) Name() string {
|
|||||||
// Send creates and sends a gotify notification with an entry
|
// Send creates and sends a gotify notification with an entry
|
||||||
func (c *Client) Send(entry model.NotifEntry) error {
|
func (c *Client) Send(entry model.NotifEntry) error {
|
||||||
hc := http.Client{
|
hc := http.Client{
|
||||||
Timeout: time.Duration(c.cfg.Timeout) * time.Second,
|
Timeout: *c.cfg.Timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
title := fmt.Sprintf("Image update for %s", entry.Image.String())
|
title := fmt.Sprintf("Image update for %s", entry.Image.String())
|
||||||
@@ -79,7 +76,7 @@ func (c *Client) Send(entry model.NotifEntry) error {
|
|||||||
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
|
req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
|
||||||
req.Header.Set("User-Agent", c.userAgent)
|
req.Header.Set("User-Agent", c.meta.UserAgent)
|
||||||
|
|
||||||
resp, err := hc.Do(req)
|
resp, err := hc.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -18,16 +18,16 @@ import (
|
|||||||
// Client represents an active mail notification object
|
// Client represents an active mail notification object
|
||||||
type Client struct {
|
type Client struct {
|
||||||
*notifier.Notifier
|
*notifier.Notifier
|
||||||
cfg *model.NotifMail
|
cfg *model.NotifMail
|
||||||
app model.App
|
meta model.Meta
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new mail notification instance
|
// New creates a new mail notification instance
|
||||||
func New(config *model.NotifMail, app model.App) notifier.Notifier {
|
func New(config *model.NotifMail, meta model.Meta) notifier.Notifier {
|
||||||
return notifier.Notifier{
|
return notifier.Notifier{
|
||||||
Handler: &Client{
|
Handler: &Client{
|
||||||
cfg: config,
|
cfg: config,
|
||||||
app: app,
|
meta: meta,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -42,14 +42,14 @@ func (c *Client) Send(entry model.NotifEntry) error {
|
|||||||
h := hermes.Hermes{
|
h := hermes.Hermes{
|
||||||
Theme: new(Theme),
|
Theme: new(Theme),
|
||||||
Product: hermes.Product{
|
Product: hermes.Product{
|
||||||
Name: c.app.Name,
|
Name: c.meta.Name,
|
||||||
Link: "https://github.com/crazy-max/diun",
|
Link: c.meta.URL,
|
||||||
Logo: "https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png",
|
Logo: c.meta.Logo,
|
||||||
Copyright: fmt.Sprintf("%s © %d %s %s",
|
Copyright: fmt.Sprintf("%s © %d %s %s",
|
||||||
c.app.Author,
|
c.meta.Author,
|
||||||
time.Now().Year(),
|
time.Now().Year(),
|
||||||
c.app.Name,
|
c.meta.Name,
|
||||||
c.app.Version),
|
c.meta.Version),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ Need help, or have questions? Go to https://github.com/crazy-max/diun and leave
|
|||||||
}
|
}
|
||||||
email := hermes.Email{
|
email := hermes.Email{
|
||||||
Body: hermes.Body{
|
Body: hermes.Body{
|
||||||
Title: fmt.Sprintf("%s 🔔 notification", c.app.Name),
|
Title: fmt.Sprintf("%s 🔔 notification", c.meta.Name),
|
||||||
FreeMarkdown: hermes.Markdown(emailBuf.String()),
|
FreeMarkdown: hermes.Markdown(emailBuf.String()),
|
||||||
Signature: "Thanks for your support",
|
Signature: "Thanks for your support",
|
||||||
},
|
},
|
||||||
@@ -94,7 +94,7 @@ Need help, or have questions? Go to https://github.com/crazy-max/diun and leave
|
|||||||
}
|
}
|
||||||
|
|
||||||
msg := gomail.NewMessage()
|
msg := gomail.NewMessage()
|
||||||
msg.SetHeader("From", fmt.Sprintf("%s <%s>", c.app.Name, c.cfg.From))
|
msg.SetHeader("From", fmt.Sprintf("%s <%s>", c.meta.Name, c.cfg.From))
|
||||||
msg.SetHeader("To", c.cfg.To)
|
msg.SetHeader("To", c.cfg.To)
|
||||||
msg.SetHeader("Subject", subject)
|
msg.SetHeader("Subject", subject)
|
||||||
msg.SetBody("text/plain", textpart)
|
msg.SetBody("text/plain", textpart)
|
||||||
|
|||||||
@@ -18,18 +18,16 @@ import (
|
|||||||
// Client represents an active rocketchat notification object
|
// Client represents an active rocketchat notification object
|
||||||
type Client struct {
|
type Client struct {
|
||||||
*notifier.Notifier
|
*notifier.Notifier
|
||||||
cfg *model.NotifRocketChat
|
cfg *model.NotifRocketChat
|
||||||
app model.App
|
meta model.Meta
|
||||||
userAgent string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new rocketchat notification instance
|
// New creates a new rocketchat notification instance
|
||||||
func New(config *model.NotifRocketChat, app model.App, userAgent string) notifier.Notifier {
|
func New(config *model.NotifRocketChat, meta model.Meta) notifier.Notifier {
|
||||||
return notifier.Notifier{
|
return notifier.Notifier{
|
||||||
Handler: &Client{
|
Handler: &Client{
|
||||||
cfg: config,
|
cfg: config,
|
||||||
app: app,
|
meta: meta,
|
||||||
userAgent: userAgent,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +41,7 @@ func (c *Client) Name() string {
|
|||||||
// https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/
|
// https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/
|
||||||
func (c *Client) Send(entry model.NotifEntry) error {
|
func (c *Client) Send(entry model.NotifEntry) error {
|
||||||
hc := http.Client{
|
hc := http.Client{
|
||||||
Timeout: time.Duration(c.cfg.Timeout) * time.Second,
|
Timeout: *c.cfg.Timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
title := fmt.Sprintf("Image update for %s", entry.Image.String())
|
title := fmt.Sprintf("Image update for %s", entry.Image.String())
|
||||||
@@ -58,8 +56,8 @@ func (c *Client) Send(entry model.NotifEntry) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data := Message{
|
data := Message{
|
||||||
Alias: c.app.Name,
|
Alias: c.meta.Name,
|
||||||
Avatar: "https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png",
|
Avatar: c.meta.Logo,
|
||||||
Channel: c.cfg.Channel,
|
Channel: c.cfg.Channel,
|
||||||
Text: title,
|
Text: title,
|
||||||
Attachments: []Attachment{
|
Attachments: []Attachment{
|
||||||
@@ -109,7 +107,7 @@ func (c *Client) Send(entry model.NotifEntry) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("User-Agent", c.userAgent)
|
req.Header.Set("User-Agent", c.meta.UserAgent)
|
||||||
req.Header.Add("X-User-Id", c.cfg.UserID)
|
req.Header.Add("X-User-Id", c.cfg.UserID)
|
||||||
req.Header.Add("X-Auth-Token", c.cfg.Token)
|
req.Header.Add("X-Auth-Token", c.cfg.Token)
|
||||||
|
|
||||||
|
|||||||
@@ -16,17 +16,16 @@ import (
|
|||||||
// Client represents an active script notification object
|
// Client represents an active script notification object
|
||||||
type Client struct {
|
type Client struct {
|
||||||
*notifier.Notifier
|
*notifier.Notifier
|
||||||
cfg *model.NotifScript
|
cfg *model.NotifScript
|
||||||
app model.App
|
meta model.Meta
|
||||||
userAgent string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new script notification instance
|
// New creates a new script notification instance
|
||||||
func New(config *model.NotifScript, app model.App) notifier.Notifier {
|
func New(config *model.NotifScript, meta model.Meta) notifier.Notifier {
|
||||||
return notifier.Notifier{
|
return notifier.Notifier{
|
||||||
Handler: &Client{
|
Handler: &Client{
|
||||||
cfg: config,
|
cfg: config,
|
||||||
app: app,
|
meta: meta,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,7 +52,7 @@ func (c *Client) Send(entry model.NotifEntry) error {
|
|||||||
|
|
||||||
// Set env vars
|
// Set env vars
|
||||||
cmd.Env = append(os.Environ(), []string{
|
cmd.Env = append(os.Environ(), []string{
|
||||||
fmt.Sprintf("DIUN_VERSION=%s", c.app.Version),
|
fmt.Sprintf("DIUN_VERSION=%s", c.meta.Version),
|
||||||
fmt.Sprintf("DIUN_ENTRY_STATUS=%s", string(entry.Status)),
|
fmt.Sprintf("DIUN_ENTRY_STATUS=%s", string(entry.Status)),
|
||||||
fmt.Sprintf("DIUN_ENTRY_PROVIDER=%s", entry.Provider),
|
fmt.Sprintf("DIUN_ENTRY_PROVIDER=%s", entry.Provider),
|
||||||
fmt.Sprintf("DIUN_ENTRY_IMAGE=%s", entry.Image.String()),
|
fmt.Sprintf("DIUN_ENTRY_IMAGE=%s", entry.Image.String()),
|
||||||
|
|||||||
@@ -16,16 +16,16 @@ import (
|
|||||||
// Client represents an active slack notification object
|
// Client represents an active slack notification object
|
||||||
type Client struct {
|
type Client struct {
|
||||||
*notifier.Notifier
|
*notifier.Notifier
|
||||||
cfg *model.NotifSlack
|
cfg *model.NotifSlack
|
||||||
app model.App
|
meta model.Meta
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new slack notification instance
|
// New creates a new slack notification instance
|
||||||
func New(config *model.NotifSlack, app model.App) notifier.Notifier {
|
func New(config *model.NotifSlack, meta model.Meta) notifier.Notifier {
|
||||||
return notifier.Notifier{
|
return notifier.Notifier{
|
||||||
Handler: &Client{
|
Handler: &Client{
|
||||||
cfg: config,
|
cfg: config,
|
||||||
app: app,
|
meta: meta,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,12 +52,12 @@ func (c *Client) Send(entry model.NotifEntry) error {
|
|||||||
Attachments: []slack.Attachment{
|
Attachments: []slack.Attachment{
|
||||||
{
|
{
|
||||||
Color: color,
|
Color: color,
|
||||||
AuthorName: "Diun",
|
AuthorName: c.meta.Name,
|
||||||
AuthorSubname: "github.com/crazy-max/diun",
|
AuthorSubname: "github.com/crazy-max/diun",
|
||||||
AuthorLink: "https://github.com/crazy-max/diun",
|
AuthorLink: c.meta.URL,
|
||||||
AuthorIcon: "https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png",
|
AuthorIcon: c.meta.Logo,
|
||||||
Text: textBuf.String(),
|
Text: textBuf.String(),
|
||||||
Footer: fmt.Sprintf("%s © %d %s %s", c.app.Author, time.Now().Year(), c.app.Name, c.app.Version),
|
Footer: fmt.Sprintf("%s © %d %s %s", c.meta.Author, time.Now().Year(), c.meta.Name, c.meta.Version),
|
||||||
Fields: []slack.AttachmentField{
|
Fields: []slack.AttachmentField{
|
||||||
{
|
{
|
||||||
Title: "Provider",
|
Title: "Provider",
|
||||||
|
|||||||
@@ -14,18 +14,16 @@ import (
|
|||||||
// Client represents an active webhook notification object
|
// Client represents an active webhook notification object
|
||||||
type Client struct {
|
type Client struct {
|
||||||
*notifier.Notifier
|
*notifier.Notifier
|
||||||
cfg *model.NotifTeams
|
cfg *model.NotifTeams
|
||||||
app model.App
|
meta model.Meta
|
||||||
userAgent string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new webhook notification instance
|
// New creates a new webhook notification instance
|
||||||
func New(config *model.NotifTeams, app model.App, userAgent string) notifier.Notifier {
|
func New(config *model.NotifTeams, meta model.Meta) notifier.Notifier {
|
||||||
return notifier.Notifier{
|
return notifier.Notifier{
|
||||||
Handler: &Client{
|
Handler: &Client{
|
||||||
cfg: config,
|
cfg: config,
|
||||||
app: app,
|
meta: meta,
|
||||||
userAgent: userAgent,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,8 +93,7 @@ func (c *Client) Send(entry model.NotifEntry) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
req.Header.Set("User-Agent", c.meta.UserAgent)
|
||||||
req.Header.Set("User-Agent", c.userAgent)
|
|
||||||
|
|
||||||
_, err = hc.Do(req)
|
_, err = hc.Do(req)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -2,34 +2,26 @@ package telegram
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/crazy-max/diun/v3/internal/model"
|
"github.com/crazy-max/diun/v3/internal/model"
|
||||||
"github.com/crazy-max/diun/v3/internal/notif/notifier"
|
"github.com/crazy-max/diun/v3/internal/notif/notifier"
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client represents an active Telegram notification object
|
// Client represents an active Telegram notification object
|
||||||
type Client struct {
|
type Client struct {
|
||||||
*notifier.Notifier
|
*notifier.Notifier
|
||||||
cfg *model.NotifTelegram
|
cfg *model.NotifTelegram
|
||||||
app model.App
|
meta model.Meta
|
||||||
bot *tgbotapi.BotAPI
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Telegram notification instance
|
// New creates a new Telegram notification instance
|
||||||
func New(config *model.NotifTelegram, app model.App) notifier.Notifier {
|
func New(config *model.NotifTelegram, meta model.Meta) notifier.Notifier {
|
||||||
bot, err := tgbotapi.NewBotAPI(config.BotToken)
|
|
||||||
if err != nil {
|
|
||||||
log.Err(err).Msgf("Failed to initialize Telegram notifications")
|
|
||||||
}
|
|
||||||
return notifier.Notifier{
|
return notifier.Notifier{
|
||||||
Handler: &Client{
|
Handler: &Client{
|
||||||
cfg: config,
|
cfg: config,
|
||||||
app: app,
|
meta: meta,
|
||||||
bot: bot,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,8 +33,9 @@ func (c *Client) Name() string {
|
|||||||
|
|
||||||
// Send creates and sends a Telegram notification with an entry
|
// Send creates and sends a Telegram notification with an entry
|
||||||
func (c *Client) Send(entry model.NotifEntry) error {
|
func (c *Client) Send(entry model.NotifEntry) error {
|
||||||
if c.bot == nil {
|
bot, err := tgbotapi.NewBotAPI(c.cfg.Token)
|
||||||
return errors.New("telegram not initialized")
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var msgBuf bytes.Buffer
|
var msgBuf bytes.Buffer
|
||||||
@@ -52,9 +45,9 @@ func (c *Client) Send(entry model.NotifEntry) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, chatID := range c.cfg.ChatIDs {
|
for _, chatID := range c.cfg.ChatIDs {
|
||||||
msg := tgbotapi.NewMessage(chatID, c.bot.Self.UserName)
|
msg := tgbotapi.NewMessage(chatID, bot.Self.UserName)
|
||||||
msg.Text = msgBuf.String()
|
msg.Text = msgBuf.String()
|
||||||
if _, err := c.bot.Send(msg); err != nil {
|
if _, err := bot.Send(msg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,18 +14,16 @@ import (
|
|||||||
// Client represents an active webhook notification object
|
// Client represents an active webhook notification object
|
||||||
type Client struct {
|
type Client struct {
|
||||||
*notifier.Notifier
|
*notifier.Notifier
|
||||||
cfg *model.NotifWebhook
|
cfg *model.NotifWebhook
|
||||||
app model.App
|
meta model.Meta
|
||||||
userAgent string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new webhook notification instance
|
// New creates a new webhook notification instance
|
||||||
func New(config *model.NotifWebhook, app model.App, userAgent string) notifier.Notifier {
|
func New(config *model.NotifWebhook, meta model.Meta) notifier.Notifier {
|
||||||
return notifier.Notifier{
|
return notifier.Notifier{
|
||||||
Handler: &Client{
|
Handler: &Client{
|
||||||
cfg: config,
|
cfg: config,
|
||||||
app: app,
|
meta: meta,
|
||||||
userAgent: userAgent,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +36,7 @@ func (c *Client) Name() string {
|
|||||||
// Send creates and sends a webhook notification with an entry
|
// Send creates and sends a webhook notification with an entry
|
||||||
func (c *Client) Send(entry model.NotifEntry) error {
|
func (c *Client) Send(entry model.NotifEntry) error {
|
||||||
hc := http.Client{
|
hc := http.Client{
|
||||||
Timeout: time.Duration(c.cfg.Timeout) * time.Second,
|
Timeout: *c.cfg.Timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := json.Marshal(struct {
|
body, err := json.Marshal(struct {
|
||||||
@@ -51,7 +49,7 @@ func (c *Client) Send(entry model.NotifEntry) error {
|
|||||||
Created *time.Time `json:"created"`
|
Created *time.Time `json:"created"`
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
}{
|
}{
|
||||||
Version: c.app.Version,
|
Version: c.meta.Version,
|
||||||
Status: string(entry.Status),
|
Status: string(entry.Status),
|
||||||
Provider: entry.Provider,
|
Provider: entry.Provider,
|
||||||
Image: entry.Image.String(),
|
Image: entry.Image.String(),
|
||||||
@@ -75,7 +73,7 @@ func (c *Client) Send(entry model.NotifEntry) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("User-Agent", c.userAgent)
|
req.Header.Set("User-Agent", c.meta.UserAgent)
|
||||||
|
|
||||||
_, err = hc.Do(req)
|
_, err = hc.Do(req)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -114,14 +114,14 @@ var (
|
|||||||
|
|
||||||
func TestListJobFilename(t *testing.T) {
|
func TestListJobFilename(t *testing.T) {
|
||||||
fc := file.New(&model.PrdFile{
|
fc := file.New(&model.PrdFile{
|
||||||
Filename: "./test/dockerhub.yml",
|
Filename: "./fixtures/dockerhub.yml",
|
||||||
})
|
})
|
||||||
assert.Equal(t, dockerhubFile, fc.ListJob())
|
assert.Equal(t, dockerhubFile, fc.ListJob())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestListJobDirectory(t *testing.T) {
|
func TestListJobDirectory(t *testing.T) {
|
||||||
fc := file.New(&model.PrdFile{
|
fc := file.New(&model.PrdFile{
|
||||||
Directory: "./test",
|
Directory: "./fixtures",
|
||||||
})
|
})
|
||||||
assert.Equal(t, append(append(bintrayFile, dockerhubFile...), quayFile...), fc.ListJob())
|
assert.Equal(t, append(append(bintrayFile, dockerhubFile...), quayFile...), fc.ListJob())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func New(opts Options) (*Client, error) {
|
|||||||
}
|
}
|
||||||
tlsc, err := tlsconfig.Client(options)
|
tlsc, err := tlsconfig.Client(options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to create tls config")
|
return nil, errors.Wrap(err, "failed to create TLS config")
|
||||||
}
|
}
|
||||||
httpCli := &http.Client{
|
httpCli := &http.Client{
|
||||||
Transport: &http.Transport{TLSClientConfig: tlsc},
|
Transport: &http.Transport{TLSClientConfig: tlsc},
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func (c *Client) Manifest(image Image) (Manifest, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
imgTag := imgInspect.Tag
|
imgTag := imgInspect.Tag
|
||||||
if imgTag == "" {
|
if len(imgTag) == 0 {
|
||||||
imgTag = image.Tag
|
imgTag = image.Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MatchString reports whether a string s
|
// MatchString reports whether a string s
|
||||||
@@ -78,3 +79,8 @@ func NewTrue() *bool {
|
|||||||
b := true
|
b := true
|
||||||
return &b
|
return &b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewDuration returns a duration pointer
|
||||||
|
func NewDuration(duration time.Duration) *time.Duration {
|
||||||
|
return &duration
|
||||||
|
}
|
||||||
|
|||||||
1
third_party/traefik/README.md
vendored
Normal file
1
third_party/traefik/README.md
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Fork of github.com/containous/traefik@v2.2.1
|
||||||
77
third_party/traefik/config/env/env.go
vendored
Normal file
77
third_party/traefik/config/env/env.go
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Package env implements encoding and decoding between environment variable and a typed Configuration.
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/config/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultNamePrefix is the default prefix for environment variable names.
|
||||||
|
const DefaultNamePrefix = "TRAEFIK_"
|
||||||
|
|
||||||
|
// Decode decodes the given environment variables into the given element.
|
||||||
|
// The operation goes through four stages roughly summarized as:
|
||||||
|
// env vars -> map
|
||||||
|
// map -> tree of untyped nodes
|
||||||
|
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
|
||||||
|
// "typed" nodes -> typed element
|
||||||
|
func Decode(environ []string, prefix string, element interface{}) error {
|
||||||
|
if err := checkPrefix(prefix); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := make(map[string]string)
|
||||||
|
for _, evr := range environ {
|
||||||
|
n := strings.SplitN(evr, "=", 2)
|
||||||
|
if strings.HasPrefix(strings.ToUpper(n[0]), prefix) {
|
||||||
|
key := strings.ReplaceAll(strings.ToLower(n[0]), "_", ".")
|
||||||
|
vars[key] = n[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootName := strings.ToLower(prefix[:len(prefix)-1])
|
||||||
|
return parser.Decode(vars, element, rootName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode encodes the configuration in element into the environment variables represented in the returned Flats.
|
||||||
|
// The operation goes through three stages roughly summarized as:
|
||||||
|
// typed configuration in element -> tree of untyped nodes
|
||||||
|
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
|
||||||
|
// "typed" nodes -> environment variables with default values (determined by type/kind)
|
||||||
|
func Encode(element interface{}) ([]parser.Flat, error) {
|
||||||
|
if element == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
etnOpts := parser.EncoderToNodeOpts{OmitEmpty: false, TagName: parser.TagLabel, AllowSliceAsStruct: true}
|
||||||
|
node, err := parser.EncodeToNode(element, parser.DefaultRootName, etnOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
metaOpts := parser.MetadataOpts{TagName: parser.TagLabel, AllowSliceAsStruct: true}
|
||||||
|
err = parser.AddMetadata(element, node, metaOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
flatOpts := parser.FlatOpts{Case: "upper", Separator: "_", TagName: parser.TagLabel}
|
||||||
|
return parser.EncodeToFlat(element, node, flatOpts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPrefix(prefix string) error {
|
||||||
|
prefixPattern := `[a-zA-Z0-9]+_`
|
||||||
|
matched, err := regexp.MatchString(prefixPattern, prefix)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matched {
|
||||||
|
return fmt.Errorf("invalid prefix %q, the prefix pattern must match the following pattern: %s", prefix, prefixPattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
462
third_party/traefik/config/env/env_test.go
vendored
Normal file
462
third_party/traefik/config/env/env_test.go
vendored
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/config/generator"
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/config/parser"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecode(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
environ []string
|
||||||
|
element interface{}
|
||||||
|
expected interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "no env vars",
|
||||||
|
environ: nil,
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "bool value",
|
||||||
|
environ: []string{"TRAEFIK_FOO=true"},
|
||||||
|
element: &struct {
|
||||||
|
Foo bool
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo bool
|
||||||
|
}{
|
||||||
|
Foo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "equal",
|
||||||
|
environ: []string{"TRAEFIK_FOO=bar"},
|
||||||
|
element: &struct {
|
||||||
|
Foo string
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo string
|
||||||
|
}{
|
||||||
|
Foo: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "multiple bool flags without value",
|
||||||
|
environ: []string{"TRAEFIK_FOO=true", "TRAEFIK_BAR=true"},
|
||||||
|
element: &struct {
|
||||||
|
Foo bool
|
||||||
|
Bar bool
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo bool
|
||||||
|
Bar bool
|
||||||
|
}{
|
||||||
|
Foo: true,
|
||||||
|
Bar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map string",
|
||||||
|
environ: []string{"TRAEFIK_FOO_NAME=bar"},
|
||||||
|
element: &struct {
|
||||||
|
Foo map[string]string
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo map[string]string
|
||||||
|
}{
|
||||||
|
Foo: map[string]string{
|
||||||
|
"name": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map struct",
|
||||||
|
environ: []string{"TRAEFIK_FOO_NAME_VALUE=bar"},
|
||||||
|
element: &struct {
|
||||||
|
Foo map[string]struct{ Value string }
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo map[string]struct{ Value string }
|
||||||
|
}{
|
||||||
|
Foo: map[string]struct{ Value string }{
|
||||||
|
"name": {
|
||||||
|
Value: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map struct with sub-struct",
|
||||||
|
environ: []string{"TRAEFIK_FOO_NAME_BAR_VALUE=bar"},
|
||||||
|
element: &struct {
|
||||||
|
Foo map[string]struct {
|
||||||
|
Bar *struct{ Value string }
|
||||||
|
}
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo map[string]struct {
|
||||||
|
Bar *struct{ Value string }
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: map[string]struct {
|
||||||
|
Bar *struct{ Value string }
|
||||||
|
}{
|
||||||
|
"name": {
|
||||||
|
Bar: &struct {
|
||||||
|
Value string
|
||||||
|
}{
|
||||||
|
Value: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map struct with sub-map",
|
||||||
|
environ: []string{"TRAEFIK_FOO_NAME1_BAR_NAME2_VALUE=bar"},
|
||||||
|
element: &struct {
|
||||||
|
Foo map[string]struct {
|
||||||
|
Bar map[string]struct{ Value string }
|
||||||
|
}
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo map[string]struct {
|
||||||
|
Bar map[string]struct{ Value string }
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: map[string]struct {
|
||||||
|
Bar map[string]struct{ Value string }
|
||||||
|
}{
|
||||||
|
"name1": {
|
||||||
|
Bar: map[string]struct{ Value string }{
|
||||||
|
"name2": {
|
||||||
|
Value: "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice",
|
||||||
|
environ: []string{"TRAEFIK_FOO=bar,baz"},
|
||||||
|
element: &struct {
|
||||||
|
Foo []string
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo []string
|
||||||
|
}{
|
||||||
|
Foo: []string{"bar", "baz"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "struct pointer value",
|
||||||
|
environ: []string{"TRAEFIK_FOO=true"},
|
||||||
|
element: &struct {
|
||||||
|
Foo *struct{ Field string } `label:"allowEmpty"`
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo *struct{ Field string } `label:"allowEmpty"`
|
||||||
|
}{
|
||||||
|
Foo: &struct{ Field string }{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := Decode(test.environ, DefaultNamePrefix, test.element)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, test.element)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncode(t *testing.T) {
|
||||||
|
element := &Ya{
|
||||||
|
Foo: &Yaa{
|
||||||
|
FieldIn1: "bar",
|
||||||
|
FieldIn2: false,
|
||||||
|
FieldIn3: 1,
|
||||||
|
FieldIn4: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
FieldIn5: map[string]int{
|
||||||
|
parser.MapNamePlaceholder: 0,
|
||||||
|
},
|
||||||
|
FieldIn6: map[string]struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {},
|
||||||
|
},
|
||||||
|
FieldIn7: map[string]struct{ Field map[string]string }{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FieldIn8: map[string]*struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {},
|
||||||
|
},
|
||||||
|
FieldIn9: map[string]*struct{ Field map[string]string }{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FieldIn10: struct{ Field string }{},
|
||||||
|
FieldIn11: &struct{ Field string }{},
|
||||||
|
FieldIn12: func(v string) *string { return &v }(""),
|
||||||
|
FieldIn13: func(v bool) *bool { return &v }(false),
|
||||||
|
FieldIn14: func(v int) *int { return &v }(0),
|
||||||
|
},
|
||||||
|
Field1: "bir",
|
||||||
|
Field2: true,
|
||||||
|
Field3: 0,
|
||||||
|
Field4: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
Field5: map[string]int{
|
||||||
|
parser.MapNamePlaceholder: 0,
|
||||||
|
},
|
||||||
|
Field6: map[string]struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {},
|
||||||
|
},
|
||||||
|
Field7: map[string]struct{ Field map[string]string }{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Field8: map[string]*struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {},
|
||||||
|
},
|
||||||
|
Field9: map[string]*struct{ Field map[string]string }{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Field10: struct{ Field string }{},
|
||||||
|
Field11: &struct{ Field string }{},
|
||||||
|
Field12: func(v string) *string { return &v }(""),
|
||||||
|
Field13: func(v bool) *bool { return &v }(false),
|
||||||
|
Field14: func(v int) *int { return &v }(0),
|
||||||
|
Field15: []int{7},
|
||||||
|
}
|
||||||
|
generator.Generate(element)
|
||||||
|
|
||||||
|
flats, err := Encode(element)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := []parser.Flat{
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD1",
|
||||||
|
Description: "",
|
||||||
|
Default: "bir",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD10",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD10_FIELD",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD11_FIELD",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD12",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD13",
|
||||||
|
Description: "",
|
||||||
|
Default: "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD14",
|
||||||
|
Description: "",
|
||||||
|
Default: "0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD15",
|
||||||
|
Description: "",
|
||||||
|
Default: "7",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD2",
|
||||||
|
Description: "",
|
||||||
|
Default: "true",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD3",
|
||||||
|
Description: "",
|
||||||
|
Default: "0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD4_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD5_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD6_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD6_\u003cNAME\u003e_FIELD",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD7_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD7_\u003cNAME\u003e_FIELD_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD8_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD8_\u003cNAME\u003e_FIELD",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD9_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FIELD9_\u003cNAME\u003e_FIELD_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN1",
|
||||||
|
Description: "",
|
||||||
|
Default: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN10",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN10_FIELD",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN11_FIELD",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN12",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN13",
|
||||||
|
Description: "",
|
||||||
|
Default: "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN14",
|
||||||
|
Description: "",
|
||||||
|
Default: "0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN2",
|
||||||
|
Description: "",
|
||||||
|
Default: "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN3",
|
||||||
|
Description: "",
|
||||||
|
Default: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN4_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN5_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN6_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN6_\u003cNAME\u003e_FIELD",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN7_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN7_\u003cNAME\u003e_FIELD_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN8_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN8_\u003cNAME\u003e_FIELD",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN9_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "false",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "TRAEFIK_FOO_FIELDIN9_\u003cNAME\u003e_FIELD_\u003cNAME\u003e",
|
||||||
|
Description: "",
|
||||||
|
Default: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, flats)
|
||||||
|
}
|
||||||
64
third_party/traefik/config/env/filter.go
vendored
Normal file
64
third_party/traefik/config/env/filter.go
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/config/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FindPrefixedEnvVars finds prefixed environment variables.
|
||||||
|
func FindPrefixedEnvVars(environ []string, prefix string, element interface{}) []string {
|
||||||
|
prefixes := getRootPrefixes(element, prefix)
|
||||||
|
|
||||||
|
var values []string
|
||||||
|
for _, px := range prefixes {
|
||||||
|
for _, value := range environ {
|
||||||
|
if strings.HasPrefix(value, px) {
|
||||||
|
values = append(values, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRootPrefixes(element interface{}, prefix string) []string {
|
||||||
|
if element == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rootType := reflect.TypeOf(element)
|
||||||
|
|
||||||
|
return getPrefixes(prefix, rootType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPrefixes(prefix string, rootType reflect.Type) []string {
|
||||||
|
var names []string
|
||||||
|
|
||||||
|
if rootType.Kind() == reflect.Ptr {
|
||||||
|
rootType = rootType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rootType.Kind() != reflect.Struct {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < rootType.NumField(); i++ {
|
||||||
|
field := rootType.Field(i)
|
||||||
|
|
||||||
|
if !parser.IsExported(field) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Anonymous &&
|
||||||
|
(field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct || field.Type.Kind() == reflect.Struct) {
|
||||||
|
names = append(names, getPrefixes(prefix, field.Type)...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
names = append(names, prefix+strings.ToUpper(field.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
87
third_party/traefik/config/env/filter_test.go
vendored
Normal file
87
third_party/traefik/config/env/filter_test.go
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFindPrefixedEnvVars(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
environ []string
|
||||||
|
element interface{}
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "exact name",
|
||||||
|
environ: []string{"TRAEFIK_FOO"},
|
||||||
|
element: &Yo{},
|
||||||
|
expected: []string{"TRAEFIK_FOO"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "prefixed name",
|
||||||
|
environ: []string{"TRAEFIK_FII01"},
|
||||||
|
element: &Yo{},
|
||||||
|
expected: []string{"TRAEFIK_FII01"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "excluded env vars",
|
||||||
|
environ: []string{"TRAEFIK_NOPE", "TRAEFIK_NO"},
|
||||||
|
element: &Yo{},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "filter",
|
||||||
|
environ: []string{"TRAEFIK_NOPE", "TRAEFIK_NO", "TRAEFIK_FOO", "TRAEFIK_FII01"},
|
||||||
|
element: &Yo{},
|
||||||
|
expected: []string{"TRAEFIK_FOO", "TRAEFIK_FII01"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
vars := FindPrefixedEnvVars(test.environ, DefaultNamePrefix, test.element)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, vars)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getRootFieldNames(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
element interface{}
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "simple fields",
|
||||||
|
element: &Yo{},
|
||||||
|
expected: []string{"TRAEFIK_FOO", "TRAEFIK_FII", "TRAEFIK_FUU", "TRAEFIK_YI", "TRAEFIK_YU"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "embedded struct",
|
||||||
|
element: &Yu{},
|
||||||
|
expected: []string{"TRAEFIK_FOO", "TRAEFIK_FII", "TRAEFIK_FUU"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "embedded struct pointer",
|
||||||
|
element: &Ye{},
|
||||||
|
expected: []string{"TRAEFIK_FOO", "TRAEFIK_FII", "TRAEFIK_FUU"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
names := getRootPrefixes(test.element, DefaultNamePrefix)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, names)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
69
third_party/traefik/config/env/fixtures_test.go
vendored
Normal file
69
third_party/traefik/config/env/fixtures_test.go
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
type Ya struct {
|
||||||
|
Foo *Yaa
|
||||||
|
Field1 string
|
||||||
|
Field2 bool
|
||||||
|
Field3 int
|
||||||
|
Field4 map[string]string
|
||||||
|
Field5 map[string]int
|
||||||
|
Field6 map[string]struct{ Field string }
|
||||||
|
Field7 map[string]struct{ Field map[string]string }
|
||||||
|
Field8 map[string]*struct{ Field string }
|
||||||
|
Field9 map[string]*struct{ Field map[string]string }
|
||||||
|
Field10 struct{ Field string }
|
||||||
|
Field11 *struct{ Field string }
|
||||||
|
Field12 *string
|
||||||
|
Field13 *bool
|
||||||
|
Field14 *int
|
||||||
|
Field15 []int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Yaa struct {
|
||||||
|
FieldIn1 string
|
||||||
|
FieldIn2 bool
|
||||||
|
FieldIn3 int
|
||||||
|
FieldIn4 map[string]string
|
||||||
|
FieldIn5 map[string]int
|
||||||
|
FieldIn6 map[string]struct{ Field string }
|
||||||
|
FieldIn7 map[string]struct{ Field map[string]string }
|
||||||
|
FieldIn8 map[string]*struct{ Field string }
|
||||||
|
FieldIn9 map[string]*struct{ Field map[string]string }
|
||||||
|
FieldIn10 struct{ Field string }
|
||||||
|
FieldIn11 *struct{ Field string }
|
||||||
|
FieldIn12 *string
|
||||||
|
FieldIn13 *bool
|
||||||
|
FieldIn14 *int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Yo struct {
|
||||||
|
Foo string `description:"Foo description"`
|
||||||
|
Fii string `description:"Fii description"`
|
||||||
|
Fuu string `description:"Fuu description"`
|
||||||
|
Yi *Yi `label:"allowEmpty"`
|
||||||
|
Yu *Yi
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Yo) SetDefaults() {
|
||||||
|
y.Foo = "foo"
|
||||||
|
y.Fii = "fii"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Yi struct {
|
||||||
|
Foo string
|
||||||
|
Fii string
|
||||||
|
Fuu string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Yi) SetDefaults() {
|
||||||
|
y.Foo = "foo"
|
||||||
|
y.Fii = "fii"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Yu struct {
|
||||||
|
Yi
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ye struct {
|
||||||
|
*Yi
|
||||||
|
}
|
||||||
32
third_party/traefik/config/file/file.go
vendored
Normal file
32
third_party/traefik/config/file/file.go
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Package file implements decoding between configuration in a file and a typed Configuration.
|
||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/config/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decode decodes the given configuration file into the given element.
|
||||||
|
// The operation goes through three stages roughly summarized as:
|
||||||
|
// file contents -> tree of untyped nodes
|
||||||
|
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
|
||||||
|
// "typed" nodes -> typed element
|
||||||
|
func Decode(filePath string, element interface{}) error {
|
||||||
|
if element == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filters := getRootFieldNames(element)
|
||||||
|
|
||||||
|
root, err := decodeFileToNode(filePath, filters...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
metaOpts := parser.MetadataOpts{TagName: parser.TagLabel, AllowSliceAsStruct: true}
|
||||||
|
err = parser.AddMetadata(element, root, metaOpts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return parser.Fill(element, root, parser.FillerOpts{AllowSliceAsStruct: true})
|
||||||
|
}
|
||||||
90
third_party/traefik/config/file/file_node.go
vendored
Normal file
90
third_party/traefik/config/file/file_node.go
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/config/parser"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// decodeFileToNode decodes the configuration in filePath in a tree of untyped nodes.
|
||||||
|
// If filters is not empty, it skips any configuration element whose name is not among filters.
|
||||||
|
func decodeFileToNode(filePath string, filters ...string) (*parser.Node, error) {
|
||||||
|
content, err := ioutil.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
|
||||||
|
switch strings.ToLower(filepath.Ext(filePath)) {
|
||||||
|
|
||||||
|
case ".yml", ".yaml":
|
||||||
|
err = yaml.Unmarshal(content, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported file extension: %s", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil, fmt.Errorf("no configuration found in file: %s", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
node, err := decodeRawToNode(data, parser.DefaultRootName, filters...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(node.Children) == 0 {
|
||||||
|
return nil, fmt.Errorf("no valid configuration found in file: %s", filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRootFieldNames(element interface{}) []string {
|
||||||
|
if element == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rootType := reflect.TypeOf(element)
|
||||||
|
|
||||||
|
return getFieldNames(rootType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFieldNames(rootType reflect.Type) []string {
|
||||||
|
var names []string
|
||||||
|
|
||||||
|
if rootType.Kind() == reflect.Ptr {
|
||||||
|
rootType = rootType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if rootType.Kind() != reflect.Struct {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < rootType.NumField(); i++ {
|
||||||
|
field := rootType.Field(i)
|
||||||
|
|
||||||
|
if !parser.IsExported(field) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Anonymous &&
|
||||||
|
(field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct || field.Type.Kind() == reflect.Struct) {
|
||||||
|
names = append(names, getFieldNames(field.Type)...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
names = append(names, field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
276
third_party/traefik/config/file/file_node_test.go
vendored
Normal file
276
third_party/traefik/config/file/file_node_test.go
vendored
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/config/parser"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_getRootFieldNames(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
element interface{}
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "simple fields",
|
||||||
|
element: &Yo{},
|
||||||
|
expected: []string{"Foo", "Fii", "Fuu", "Yi"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "embedded struct",
|
||||||
|
element: &Yu{},
|
||||||
|
expected: []string{"Foo", "Fii", "Fuu"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "embedded struct pointer",
|
||||||
|
element: &Ye{},
|
||||||
|
expected: []string{"Foo", "Fii", "Fuu"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
names := getRootFieldNames(test.element)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, names)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_decodeFileToNode_Yaml(t *testing.T) {
|
||||||
|
node, err := decodeFileToNode("./fixtures/sample.yml")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "accessLog", Children: []*parser.Node{
|
||||||
|
{Name: "bufferingSize", Value: "42"},
|
||||||
|
{Name: "fields", Children: []*parser.Node{
|
||||||
|
{Name: "defaultMode", Value: "foobar"},
|
||||||
|
{Name: "headers", Children: []*parser.Node{
|
||||||
|
{Name: "defaultMode", Value: "foobar"},
|
||||||
|
{Name: "names", Children: []*parser.Node{
|
||||||
|
{Name: "name0", Value: "foobar"},
|
||||||
|
{Name: "name1", Value: "foobar"}}}}},
|
||||||
|
{Name: "names", Children: []*parser.Node{
|
||||||
|
{Name: "name0", Value: "foobar"},
|
||||||
|
{Name: "name1", Value: "foobar"}}}}},
|
||||||
|
{Name: "filePath", Value: "foobar"},
|
||||||
|
{Name: "filters", Children: []*parser.Node{
|
||||||
|
{Name: "minDuration", Value: "42"},
|
||||||
|
{Name: "retryAttempts", Value: "true"},
|
||||||
|
{Name: "statusCodes", Value: "foobar,foobar"}}},
|
||||||
|
{Name: "format", Value: "foobar"}}},
|
||||||
|
{Name: "api", Children: []*parser.Node{
|
||||||
|
{Name: "dashboard", Value: "true"},
|
||||||
|
{Name: "entryPoint", Value: "foobar"},
|
||||||
|
{Name: "middlewares", Value: "foobar,foobar"},
|
||||||
|
{Name: "statistics", Children: []*parser.Node{
|
||||||
|
{Name: "recentErrors", Value: "42"}}}}},
|
||||||
|
{Name: "certificatesResolvers", Children: []*parser.Node{
|
||||||
|
{Name: "default", Children: []*parser.Node{
|
||||||
|
{Name: "acme",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "acmeLogging", Value: "true"},
|
||||||
|
{Name: "caServer", Value: "foobar"},
|
||||||
|
{Name: "dnsChallenge", Children: []*parser.Node{
|
||||||
|
{Name: "delayBeforeCheck", Value: "42"},
|
||||||
|
{Name: "disablePropagationCheck", Value: "true"},
|
||||||
|
{Name: "provider", Value: "foobar"},
|
||||||
|
{Name: "resolvers", Value: "foobar,foobar"},
|
||||||
|
}},
|
||||||
|
{Name: "email", Value: "foobar"},
|
||||||
|
{Name: "entryPoint", Value: "foobar"},
|
||||||
|
{Name: "httpChallenge", Children: []*parser.Node{
|
||||||
|
{Name: "entryPoint", Value: "foobar"}}},
|
||||||
|
{Name: "keyType", Value: "foobar"},
|
||||||
|
{Name: "storage", Value: "foobar"},
|
||||||
|
{Name: "tlsChallenge"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
{Name: "entryPoints", Children: []*parser.Node{
|
||||||
|
{Name: "EntryPoint0", Children: []*parser.Node{
|
||||||
|
{Name: "address", Value: "foobar"},
|
||||||
|
{Name: "forwardedHeaders", Children: []*parser.Node{
|
||||||
|
{Name: "insecure", Value: "true"},
|
||||||
|
{Name: "trustedIPs", Value: "foobar,foobar"}}},
|
||||||
|
{Name: "proxyProtocol", Children: []*parser.Node{
|
||||||
|
{Name: "insecure", Value: "true"},
|
||||||
|
{Name: "trustedIPs", Value: "foobar,foobar"}}},
|
||||||
|
{Name: "transport", Children: []*parser.Node{
|
||||||
|
{Name: "lifeCycle", Children: []*parser.Node{
|
||||||
|
{Name: "graceTimeOut", Value: "42"},
|
||||||
|
{Name: "requestAcceptGraceTimeout", Value: "42"}}},
|
||||||
|
{Name: "respondingTimeouts", Children: []*parser.Node{
|
||||||
|
{Name: "idleTimeout", Value: "42"},
|
||||||
|
{Name: "readTimeout", Value: "42"},
|
||||||
|
{Name: "writeTimeout", Value: "42"}}}}}}}}},
|
||||||
|
{Name: "global", Children: []*parser.Node{
|
||||||
|
{Name: "checkNewVersion", Value: "true"},
|
||||||
|
{Name: "sendAnonymousUsage", Value: "true"}}},
|
||||||
|
{Name: "hostResolver", Children: []*parser.Node{
|
||||||
|
{Name: "cnameFlattening", Value: "true"},
|
||||||
|
{Name: "resolvConfig", Value: "foobar"},
|
||||||
|
{Name: "resolvDepth", Value: "42"}}},
|
||||||
|
{Name: "log", Children: []*parser.Node{
|
||||||
|
{Name: "filePath", Value: "foobar"},
|
||||||
|
{Name: "format", Value: "foobar"},
|
||||||
|
{Name: "level", Value: "foobar"}}},
|
||||||
|
{Name: "metrics", Children: []*parser.Node{
|
||||||
|
{Name: "datadog", Children: []*parser.Node{
|
||||||
|
{Name: "address", Value: "foobar"},
|
||||||
|
{Name: "pushInterval", Value: "10s"}}},
|
||||||
|
{Name: "influxDB", Children: []*parser.Node{
|
||||||
|
{Name: "address", Value: "foobar"},
|
||||||
|
{Name: "database", Value: "foobar"},
|
||||||
|
{Name: "password", Value: "foobar"},
|
||||||
|
{Name: "protocol", Value: "foobar"},
|
||||||
|
{Name: "pushInterval", Value: "10s"},
|
||||||
|
{Name: "retentionPolicy", Value: "foobar"},
|
||||||
|
{Name: "username", Value: "foobar"}}},
|
||||||
|
{Name: "prometheus", Children: []*parser.Node{
|
||||||
|
{Name: "buckets", Value: "42,42"},
|
||||||
|
{Name: "entryPoint", Value: "foobar"},
|
||||||
|
{Name: "middlewares", Value: "foobar,foobar"}}},
|
||||||
|
{Name: "statsD", Children: []*parser.Node{
|
||||||
|
{Name: "address", Value: "foobar"},
|
||||||
|
{Name: "pushInterval", Value: "10s"}}}}},
|
||||||
|
{Name: "ping", Children: []*parser.Node{
|
||||||
|
{Name: "entryPoint", Value: "foobar"},
|
||||||
|
{Name: "middlewares", Value: "foobar,foobar"}}},
|
||||||
|
{Name: "providers", Children: []*parser.Node{
|
||||||
|
{Name: "docker", Children: []*parser.Node{
|
||||||
|
{Name: "constraints", Value: "foobar"},
|
||||||
|
{Name: "defaultRule", Value: "foobar"},
|
||||||
|
{Name: "endpoint", Value: "foobar"},
|
||||||
|
{Name: "exposedByDefault", Value: "true"},
|
||||||
|
{Name: "network", Value: "foobar"},
|
||||||
|
{Name: "swarmMode", Value: "true"},
|
||||||
|
{Name: "swarmModeRefreshSeconds", Value: "42"},
|
||||||
|
{Name: "tls", Children: []*parser.Node{
|
||||||
|
{Name: "ca", Value: "foobar"},
|
||||||
|
{Name: "caOptional", Value: "true"},
|
||||||
|
{Name: "cert", Value: "foobar"},
|
||||||
|
{Name: "insecureSkipVerify", Value: "true"},
|
||||||
|
{Name: "key", Value: "foobar"}}},
|
||||||
|
{Name: "useBindPortIP", Value: "true"},
|
||||||
|
{Name: "watch", Value: "true"}}},
|
||||||
|
{Name: "file", Children: []*parser.Node{
|
||||||
|
{Name: "debugLogGeneratedTemplate", Value: "true"},
|
||||||
|
{Name: "directory", Value: "foobar"},
|
||||||
|
{Name: "filename", Value: "foobar"},
|
||||||
|
{Name: "watch", Value: "true"}}},
|
||||||
|
{Name: "kubernetesCRD",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "certAuthFilePath", Value: "foobar"},
|
||||||
|
{Name: "disablePassHostHeaders", Value: "true"},
|
||||||
|
{Name: "endpoint", Value: "foobar"},
|
||||||
|
{Name: "ingressClass", Value: "foobar"},
|
||||||
|
{Name: "labelSelector", Value: "foobar"},
|
||||||
|
{Name: "namespaces", Value: "foobar,foobar"},
|
||||||
|
{Name: "token", Value: "foobar"}}},
|
||||||
|
{Name: "kubernetesIngress", Children: []*parser.Node{
|
||||||
|
{Name: "certAuthFilePath", Value: "foobar"},
|
||||||
|
{Name: "disablePassHostHeaders", Value: "true"},
|
||||||
|
{Name: "endpoint", Value: "foobar"},
|
||||||
|
{Name: "ingressClass", Value: "foobar"},
|
||||||
|
{Name: "ingressEndpoint", Children: []*parser.Node{
|
||||||
|
{Name: "hostname", Value: "foobar"},
|
||||||
|
{Name: "ip", Value: "foobar"},
|
||||||
|
{Name: "publishedService", Value: "foobar"}}},
|
||||||
|
{Name: "labelSelector", Value: "foobar"},
|
||||||
|
{Name: "namespaces", Value: "foobar,foobar"},
|
||||||
|
{Name: "token", Value: "foobar"}}},
|
||||||
|
{Name: "marathon", Children: []*parser.Node{
|
||||||
|
{Name: "basic", Children: []*parser.Node{
|
||||||
|
{Name: "httpBasicAuthUser", Value: "foobar"},
|
||||||
|
{Name: "httpBasicPassword", Value: "foobar"}}},
|
||||||
|
{Name: "constraints", Value: "foobar"},
|
||||||
|
{Name: "dcosToken", Value: "foobar"},
|
||||||
|
{Name: "defaultRule", Value: "foobar"},
|
||||||
|
{Name: "dialerTimeout", Value: "42"},
|
||||||
|
{Name: "endpoint", Value: "foobar"},
|
||||||
|
{Name: "exposedByDefault", Value: "true"},
|
||||||
|
{Name: "forceTaskHostname", Value: "true"},
|
||||||
|
{Name: "keepAlive", Value: "42"},
|
||||||
|
{Name: "respectReadinessChecks", Value: "true"},
|
||||||
|
{Name: "responseHeaderTimeout", Value: "42"},
|
||||||
|
{Name: "tls", Children: []*parser.Node{
|
||||||
|
{Name: "ca", Value: "foobar"},
|
||||||
|
{Name: "caOptional", Value: "true"},
|
||||||
|
{Name: "cert", Value: "foobar"},
|
||||||
|
{Name: "insecureSkipVerify", Value: "true"},
|
||||||
|
{Name: "key", Value: "foobar"}}},
|
||||||
|
{Name: "tlsHandshakeTimeout", Value: "42"},
|
||||||
|
{Name: "trace", Value: "true"},
|
||||||
|
{Name: "watch", Value: "true"}}},
|
||||||
|
{Name: "providersThrottleDuration", Value: "42"},
|
||||||
|
{Name: "rancher", Children: []*parser.Node{
|
||||||
|
{Name: "constraints", Value: "foobar"},
|
||||||
|
{Name: "defaultRule", Value: "foobar"},
|
||||||
|
{Name: "enableServiceHealthFilter", Value: "true"},
|
||||||
|
{Name: "exposedByDefault", Value: "true"},
|
||||||
|
{Name: "intervalPoll", Value: "true"},
|
||||||
|
{Name: "prefix", Value: "foobar"},
|
||||||
|
{Name: "refreshSeconds", Value: "42"},
|
||||||
|
{Name: "watch", Value: "true"}}},
|
||||||
|
{Name: "rest", Children: []*parser.Node{
|
||||||
|
{Name: "entryPoint", Value: "foobar"}}}}},
|
||||||
|
{Name: "serversTransport", Children: []*parser.Node{
|
||||||
|
{Name: "forwardingTimeouts", Children: []*parser.Node{
|
||||||
|
{Name: "dialTimeout", Value: "42"},
|
||||||
|
{Name: "idleConnTimeout", Value: "42"},
|
||||||
|
{Name: "responseHeaderTimeout", Value: "42"}}},
|
||||||
|
{Name: "insecureSkipVerify", Value: "true"},
|
||||||
|
{Name: "maxIdleConnsPerHost", Value: "42"},
|
||||||
|
{Name: "rootCAs", Value: "foobar,foobar"}}},
|
||||||
|
{Name: "tracing", Children: []*parser.Node{
|
||||||
|
{Name: "datadog", Children: []*parser.Node{
|
||||||
|
{Name: "bagagePrefixHeaderName", Value: "foobar"},
|
||||||
|
{Name: "debug", Value: "true"},
|
||||||
|
{Name: "globalTag", Value: "foobar"},
|
||||||
|
{Name: "localAgentHostPort", Value: "foobar"},
|
||||||
|
{Name: "parentIDHeaderName", Value: "foobar"},
|
||||||
|
{Name: "prioritySampling", Value: "true"},
|
||||||
|
{Name: "samplingPriorityHeaderName", Value: "foobar"},
|
||||||
|
{Name: "traceIDHeaderName", Value: "foobar"}}},
|
||||||
|
{Name: "haystack", Children: []*parser.Node{
|
||||||
|
{Name: "globalTag", Value: "foobar"},
|
||||||
|
{Name: "localAgentHost", Value: "foobar"},
|
||||||
|
{Name: "localAgentPort", Value: "42"},
|
||||||
|
{Name: "parentIDHeaderName", Value: "foobar"},
|
||||||
|
{Name: "spanIDHeaderName", Value: "foobar"},
|
||||||
|
{Name: "traceIDHeaderName", Value: "foobar"}}},
|
||||||
|
{Name: "instana", Children: []*parser.Node{
|
||||||
|
{Name: "localAgentHost", Value: "foobar"},
|
||||||
|
{Name: "localAgentPort", Value: "42"},
|
||||||
|
{Name: "logLevel", Value: "foobar"}}},
|
||||||
|
{Name: "jaeger", Children: []*parser.Node{
|
||||||
|
{Name: "gen128Bit", Value: "true"},
|
||||||
|
{Name: "localAgentHostPort", Value: "foobar"},
|
||||||
|
{Name: "propagation", Value: "foobar"},
|
||||||
|
{Name: "samplingParam", Value: "42"},
|
||||||
|
{Name: "samplingServerURL", Value: "foobar"},
|
||||||
|
{Name: "samplingType", Value: "foobar"},
|
||||||
|
{Name: "traceContextHeaderName", Value: "foobar"}}},
|
||||||
|
{Name: "serviceName", Value: "foobar"},
|
||||||
|
{Name: "spanNameLimit", Value: "42"},
|
||||||
|
{Name: "zipkin", Children: []*parser.Node{
|
||||||
|
{Name: "httpEndpoint", Value: "foobar"},
|
||||||
|
{Name: "id128Bit", Value: "true"},
|
||||||
|
{Name: "sameSpan", Value: "true"},
|
||||||
|
{Name: "sampleRate", Value: "42"}}}}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, node)
|
||||||
|
}
|
||||||
43
third_party/traefik/config/file/file_test.go
vendored
Normal file
43
third_party/traefik/config/file/file_test.go
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecode_YAML(t *testing.T) {
|
||||||
|
f, err := ioutil.TempFile("", "traefik-config-*.yaml")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
_ = os.Remove(f.Name())
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = f.Write([]byte(`
|
||||||
|
foo: bar
|
||||||
|
fii: bir
|
||||||
|
yi: {}
|
||||||
|
`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
element := &Yo{
|
||||||
|
Fuu: "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = Decode(f.Name(), element)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expected := &Yo{
|
||||||
|
Foo: "bar",
|
||||||
|
Fii: "bir",
|
||||||
|
Fuu: "test",
|
||||||
|
Yi: &Yi{
|
||||||
|
Foo: "foo",
|
||||||
|
Fii: "fii",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, element)
|
||||||
|
}
|
||||||
235
third_party/traefik/config/file/fixtures/sample.yml
vendored
Normal file
235
third_party/traefik/config/file/fixtures/sample.yml
vendored
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
global:
|
||||||
|
checkNewVersion: true
|
||||||
|
sendAnonymousUsage: true
|
||||||
|
serversTransport:
|
||||||
|
insecureSkipVerify: true
|
||||||
|
rootCAs:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
maxIdleConnsPerHost: 42
|
||||||
|
forwardingTimeouts:
|
||||||
|
dialTimeout: 42
|
||||||
|
responseHeaderTimeout: 42
|
||||||
|
idleConnTimeout: 42
|
||||||
|
entryPoints:
|
||||||
|
EntryPoint0:
|
||||||
|
address: foobar
|
||||||
|
transport:
|
||||||
|
lifeCycle:
|
||||||
|
requestAcceptGraceTimeout: 42
|
||||||
|
graceTimeOut: 42
|
||||||
|
respondingTimeouts:
|
||||||
|
readTimeout: 42
|
||||||
|
writeTimeout: 42
|
||||||
|
idleTimeout: 42
|
||||||
|
proxyProtocol:
|
||||||
|
insecure: true
|
||||||
|
trustedIPs:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
forwardedHeaders:
|
||||||
|
insecure: true
|
||||||
|
trustedIPs:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
providers:
|
||||||
|
providersThrottleDuration: 42
|
||||||
|
docker:
|
||||||
|
constraints: foobar
|
||||||
|
watch: true
|
||||||
|
endpoint: foobar
|
||||||
|
defaultRule: foobar
|
||||||
|
tls:
|
||||||
|
ca: foobar
|
||||||
|
caOptional: true
|
||||||
|
cert: foobar
|
||||||
|
key: foobar
|
||||||
|
insecureSkipVerify: true
|
||||||
|
exposedByDefault: true
|
||||||
|
useBindPortIP: true
|
||||||
|
swarmMode: true
|
||||||
|
network: foobar
|
||||||
|
swarmModeRefreshSeconds: 42
|
||||||
|
file:
|
||||||
|
directory: foobar
|
||||||
|
watch: true
|
||||||
|
filename: foobar
|
||||||
|
debugLogGeneratedTemplate: true
|
||||||
|
marathon:
|
||||||
|
constraints: foobar
|
||||||
|
trace: true
|
||||||
|
watch: true
|
||||||
|
endpoint: foobar
|
||||||
|
defaultRule: foobar
|
||||||
|
exposedByDefault: true
|
||||||
|
dcosToken: foobar
|
||||||
|
tls:
|
||||||
|
ca: foobar
|
||||||
|
caOptional: true
|
||||||
|
cert: foobar
|
||||||
|
key: foobar
|
||||||
|
insecureSkipVerify: true
|
||||||
|
dialerTimeout: 42
|
||||||
|
responseHeaderTimeout: 42
|
||||||
|
tlsHandshakeTimeout: 42
|
||||||
|
keepAlive: 42
|
||||||
|
forceTaskHostname: true
|
||||||
|
basic:
|
||||||
|
httpBasicAuthUser: foobar
|
||||||
|
httpBasicPassword: foobar
|
||||||
|
respectReadinessChecks: true
|
||||||
|
kubernetesIngress:
|
||||||
|
endpoint: foobar
|
||||||
|
token: foobar
|
||||||
|
certAuthFilePath: foobar
|
||||||
|
disablePassHostHeaders: true
|
||||||
|
namespaces:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
labelSelector: foobar
|
||||||
|
ingressClass: foobar
|
||||||
|
ingressEndpoint:
|
||||||
|
ip: foobar
|
||||||
|
hostname: foobar
|
||||||
|
publishedService: foobar
|
||||||
|
kubernetesCRD:
|
||||||
|
endpoint: foobar
|
||||||
|
token: foobar
|
||||||
|
certAuthFilePath: foobar
|
||||||
|
disablePassHostHeaders: true
|
||||||
|
namespaces:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
labelSelector: foobar
|
||||||
|
ingressClass: foobar
|
||||||
|
rest:
|
||||||
|
entryPoint: foobar
|
||||||
|
rancher:
|
||||||
|
constraints: foobar
|
||||||
|
watch: true
|
||||||
|
defaultRule: foobar
|
||||||
|
exposedByDefault: true
|
||||||
|
enableServiceHealthFilter: true
|
||||||
|
refreshSeconds: 42
|
||||||
|
intervalPoll: true
|
||||||
|
prefix: foobar
|
||||||
|
api:
|
||||||
|
entryPoint: foobar
|
||||||
|
dashboard: true
|
||||||
|
statistics:
|
||||||
|
recentErrors: 42
|
||||||
|
middlewares:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
metrics:
|
||||||
|
prometheus:
|
||||||
|
buckets:
|
||||||
|
- 42
|
||||||
|
- 42
|
||||||
|
entryPoint: foobar
|
||||||
|
middlewares:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
datadog:
|
||||||
|
address: foobar
|
||||||
|
pushInterval: 10s
|
||||||
|
statsD:
|
||||||
|
address: foobar
|
||||||
|
pushInterval: 10s
|
||||||
|
influxDB:
|
||||||
|
address: foobar
|
||||||
|
protocol: foobar
|
||||||
|
pushInterval: 10s
|
||||||
|
database: foobar
|
||||||
|
retentionPolicy: foobar
|
||||||
|
username: foobar
|
||||||
|
password: foobar
|
||||||
|
ping:
|
||||||
|
entryPoint: foobar
|
||||||
|
middlewares:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
log:
|
||||||
|
level: foobar
|
||||||
|
filePath: foobar
|
||||||
|
format: foobar
|
||||||
|
accessLog:
|
||||||
|
filePath: foobar
|
||||||
|
format: foobar
|
||||||
|
filters:
|
||||||
|
statusCodes:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
retryAttempts: true
|
||||||
|
minDuration: 42
|
||||||
|
fields:
|
||||||
|
defaultMode: foobar
|
||||||
|
names:
|
||||||
|
name0: foobar
|
||||||
|
name1: foobar
|
||||||
|
headers:
|
||||||
|
defaultMode: foobar
|
||||||
|
names:
|
||||||
|
name0: foobar
|
||||||
|
name1: foobar
|
||||||
|
bufferingSize: 42
|
||||||
|
tracing:
|
||||||
|
serviceName: foobar
|
||||||
|
spanNameLimit: 42
|
||||||
|
jaeger:
|
||||||
|
samplingServerURL: foobar
|
||||||
|
samplingType: foobar
|
||||||
|
samplingParam: 42
|
||||||
|
localAgentHostPort: foobar
|
||||||
|
gen128Bit: true
|
||||||
|
propagation: foobar
|
||||||
|
traceContextHeaderName: foobar
|
||||||
|
zipkin:
|
||||||
|
httpEndpoint: foobar
|
||||||
|
sameSpan: true
|
||||||
|
id128Bit: true
|
||||||
|
sampleRate: 42
|
||||||
|
datadog:
|
||||||
|
localAgentHostPort: foobar
|
||||||
|
globalTag: foobar
|
||||||
|
debug: true
|
||||||
|
prioritySampling: true
|
||||||
|
traceIDHeaderName: foobar
|
||||||
|
parentIDHeaderName: foobar
|
||||||
|
samplingPriorityHeaderName: foobar
|
||||||
|
bagagePrefixHeaderName: foobar
|
||||||
|
instana:
|
||||||
|
localAgentHost: foobar
|
||||||
|
localAgentPort: 42
|
||||||
|
logLevel: foobar
|
||||||
|
haystack:
|
||||||
|
localAgentHost: foobar
|
||||||
|
localAgentPort: 42
|
||||||
|
globalTag: foobar
|
||||||
|
traceIDHeaderName: foobar
|
||||||
|
parentIDHeaderName: foobar
|
||||||
|
spanIDHeaderName: foobar
|
||||||
|
hostResolver:
|
||||||
|
cnameFlattening: true
|
||||||
|
resolvConfig: foobar
|
||||||
|
resolvDepth: 42
|
||||||
|
|
||||||
|
certificatesResolvers:
|
||||||
|
default:
|
||||||
|
acme:
|
||||||
|
email: foobar
|
||||||
|
acmeLogging: true
|
||||||
|
caServer: foobar
|
||||||
|
storage: foobar
|
||||||
|
entryPoint: foobar
|
||||||
|
keyType: foobar
|
||||||
|
dnsChallenge:
|
||||||
|
provider: foobar
|
||||||
|
delayBeforeCheck: 42
|
||||||
|
resolvers:
|
||||||
|
- foobar
|
||||||
|
- foobar
|
||||||
|
disablePropagationCheck: true
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: foobar
|
||||||
|
tlsChallenge: {}
|
||||||
34
third_party/traefik/config/file/fixtures_test.go
vendored
Normal file
34
third_party/traefik/config/file/fixtures_test.go
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
type bar string
|
||||||
|
|
||||||
|
type Yo struct {
|
||||||
|
Foo string
|
||||||
|
Fii string
|
||||||
|
Fuu string
|
||||||
|
Yi *Yi `label:"allowEmpty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Yo) SetDefaults() {
|
||||||
|
y.Foo = "foo"
|
||||||
|
y.Fii = "fii"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Yi struct {
|
||||||
|
Foo string
|
||||||
|
Fii string
|
||||||
|
Fuu string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Yi) SetDefaults() {
|
||||||
|
y.Foo = "foo"
|
||||||
|
y.Fii = "fii"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Yu struct {
|
||||||
|
Yi
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ye struct {
|
||||||
|
*Yi
|
||||||
|
}
|
||||||
148
third_party/traefik/config/file/raw_node.go
vendored
Normal file
148
third_party/traefik/config/file/raw_node.go
vendored
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/config/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
func decodeRawToNode(data map[string]interface{}, rootName string, filters ...string) (*parser.Node, error) {
|
||||||
|
root := &parser.Node{
|
||||||
|
Name: rootName,
|
||||||
|
}
|
||||||
|
|
||||||
|
vData := reflect.ValueOf(data)
|
||||||
|
err := decodeRaw(root, vData, filters...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeRaw(node *parser.Node, vData reflect.Value, filters ...string) error {
|
||||||
|
sortedKeys := sortKeys(vData, filters)
|
||||||
|
|
||||||
|
for _, key := range sortedKeys {
|
||||||
|
value := reflect.ValueOf(vData.MapIndex(key).Interface())
|
||||||
|
|
||||||
|
child := &parser.Node{Name: key.String()}
|
||||||
|
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Bool:
|
||||||
|
fallthrough
|
||||||
|
case reflect.String:
|
||||||
|
value, err := getSimpleValue(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
child.Value = value
|
||||||
|
case reflect.Slice:
|
||||||
|
var values []string
|
||||||
|
|
||||||
|
for i := 0; i < value.Len(); i++ {
|
||||||
|
item := value.Index(i)
|
||||||
|
switch item.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Bool:
|
||||||
|
fallthrough
|
||||||
|
case reflect.String:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Map:
|
||||||
|
fallthrough
|
||||||
|
case reflect.Interface:
|
||||||
|
sValue := reflect.ValueOf(item.Interface())
|
||||||
|
if sValue.Kind() == reflect.Map {
|
||||||
|
ch := &parser.Node{
|
||||||
|
Name: "[" + strconv.Itoa(i) + "]",
|
||||||
|
}
|
||||||
|
|
||||||
|
child.Children = append(child.Children, ch)
|
||||||
|
err := decodeRaw(ch, sValue)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val, err := getSimpleValue(sValue)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values = append(values, val)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("field %s uses unsupported slice type: %s", child.Name, item.Kind().String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
child.Value = strings.Join(values, ",")
|
||||||
|
case reflect.Map:
|
||||||
|
err := decodeRaw(child, value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("field %s uses unsupported type: %s", child.Name, value.Kind().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Children = append(node.Children, child)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSimpleValue(item reflect.Value) (string, error) {
|
||||||
|
switch item.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
return item.String(), nil
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return strconv.FormatInt(item.Int(), 10), nil
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return strconv.FormatUint(item.Uint(), 10), nil
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return strings.TrimSuffix(strconv.FormatFloat(item.Float(), 'f', 6, 64), ".000000"), nil
|
||||||
|
case reflect.Bool:
|
||||||
|
return strconv.FormatBool(item.Bool()), nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unsupported simple value type: %s", item.Kind().String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortKeys(vData reflect.Value, filters []string) []reflect.Value {
|
||||||
|
var sortedKeys []reflect.Value
|
||||||
|
|
||||||
|
for _, v := range vData.MapKeys() {
|
||||||
|
rValue := reflect.ValueOf(v.Interface())
|
||||||
|
key := rValue.String()
|
||||||
|
|
||||||
|
if len(filters) == 0 {
|
||||||
|
sortedKeys = append(sortedKeys, rValue)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filter := range filters {
|
||||||
|
if strings.EqualFold(key, filter) {
|
||||||
|
sortedKeys = append(sortedKeys, rValue)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(sortedKeys, func(i, j int) bool {
|
||||||
|
return sortedKeys[i].String() < sortedKeys[j].String()
|
||||||
|
})
|
||||||
|
|
||||||
|
return sortedKeys
|
||||||
|
}
|
||||||
564
third_party/traefik/config/file/raw_node_test.go
vendored
Normal file
564
third_party/traefik/config/file/raw_node_test.go
vendored
Normal file
@@ -0,0 +1,564 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/config/parser"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_decodeRawToNode(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
data map[string]interface{}
|
||||||
|
expected *parser.Node
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "empty",
|
||||||
|
data: map[string]interface{}{},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "string",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "string named type",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": bar("bar"),
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "bool",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": true,
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "true"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": 1,
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int8",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": int8(1),
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int16",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": int16(1),
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int32",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": int32(1),
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int64",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": int64(1),
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "uint",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": uint(1),
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "uint8",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": uint8(1),
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "uint16",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": uint16(1),
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "uint32",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": uint32(1),
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "uint64",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": uint64(1),
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "float32",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": float32(1),
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "float64",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": float64(1),
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "string slice",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": []string{"A", "B"},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "A,B"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int slice",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": []int{1, 2},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1,2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int8 slice",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": []int8{1, 2},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1,2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int16 slice",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": []int16{1, 2},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1,2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int32 slice",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": []int32{1, 2},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1,2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int64 slice",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": []int64{1, 2},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1,2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "bool slice",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": []bool{true, false},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "true,false"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "interface (string) slice",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": []interface{}{"A", "B"},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "A,B"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "interface (int) slice",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": []interface{}{1, 2},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Value: "1,2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "2 strings",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
"fii": "bir",
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "fii", Value: "bir"},
|
||||||
|
{Name: "foo", Value: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "string, level 2",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"fii": map[interface{}]interface{}{
|
||||||
|
"fuu": "bur",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "bur"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int, level 2",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"fii": map[interface{}]interface{}{
|
||||||
|
"fuu": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "1"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "uint, level 2",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"fii": map[interface{}]interface{}{
|
||||||
|
"fuu": uint(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "1"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "bool, level 2",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"fii": map[interface{}]interface{}{
|
||||||
|
"fuu": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "true"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "string, level 3",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": map[interface{}]interface{}{
|
||||||
|
"fii": map[interface{}]interface{}{
|
||||||
|
"fuu": "bur",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Children: []*parser.Node{
|
||||||
|
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "bur"}}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int, level 3",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"fii": map[interface{}]interface{}{
|
||||||
|
"fuu": 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "1"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "uint, level 3",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"fii": map[interface{}]interface{}{
|
||||||
|
"fuu": uint(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "1"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "bool, level 3",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"fii": map[interface{}]interface{}{
|
||||||
|
"fuu": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "fii", Children: []*parser.Node{{Name: "fuu", Value: "true"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "struct",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": map[interface{}]interface{}{
|
||||||
|
"field1": "C",
|
||||||
|
"field2": "C",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Children: []*parser.Node{
|
||||||
|
{Name: "field1", Value: "C"},
|
||||||
|
{Name: "field2", Value: "C"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice struct 1",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": []map[string]interface{}{
|
||||||
|
{"field1": "A", "field2": "A"},
|
||||||
|
{"field1": "B", "field2": "B"},
|
||||||
|
{"field2": "C", "field1": "C"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Children: []*parser.Node{
|
||||||
|
{Name: "[0]", Children: []*parser.Node{
|
||||||
|
{Name: "field1", Value: "A"},
|
||||||
|
{Name: "field2", Value: "A"},
|
||||||
|
}},
|
||||||
|
{Name: "[1]", Children: []*parser.Node{
|
||||||
|
{Name: "field1", Value: "B"},
|
||||||
|
{Name: "field2", Value: "B"},
|
||||||
|
}},
|
||||||
|
{Name: "[2]", Children: []*parser.Node{
|
||||||
|
{Name: "field1", Value: "C"},
|
||||||
|
{Name: "field2", Value: "C"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice struct 2",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": []interface{}{
|
||||||
|
map[interface{}]interface{}{
|
||||||
|
"field2": "A",
|
||||||
|
"field1": "A",
|
||||||
|
},
|
||||||
|
map[interface{}]interface{}{
|
||||||
|
"field1": "B",
|
||||||
|
"field2": "B",
|
||||||
|
},
|
||||||
|
map[interface{}]interface{}{
|
||||||
|
"field1": "C",
|
||||||
|
"field2": "C",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: &parser.Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*parser.Node{
|
||||||
|
{Name: "foo", Children: []*parser.Node{
|
||||||
|
{Name: "[0]", Children: []*parser.Node{
|
||||||
|
{Name: "field1", Value: "A"},
|
||||||
|
{Name: "field2", Value: "A"},
|
||||||
|
}},
|
||||||
|
{Name: "[1]", Children: []*parser.Node{
|
||||||
|
{Name: "field1", Value: "B"},
|
||||||
|
{Name: "field2", Value: "B"},
|
||||||
|
}},
|
||||||
|
{Name: "[2]", Children: []*parser.Node{
|
||||||
|
{Name: "field1", Value: "C"},
|
||||||
|
{Name: "field2", Value: "C"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
node, err := decodeRawToNode(test.data, parser.DefaultRootName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, node)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_decodeRawToNode_errors(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
data map[string]interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "invalid type",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"foo": struct{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := decodeRawToNode(test.data, parser.DefaultRootName)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
97
third_party/traefik/config/generator/generator.go
vendored
Normal file
97
third_party/traefik/config/generator/generator.go
vendored
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
// Package generator implements the custom initialization of all the fields of an empty interface.
|
||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/config/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
type initializer interface {
|
||||||
|
SetDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate recursively initializes an empty structure, calling SetDefaults on each field, when it applies.
|
||||||
|
func Generate(element interface{}) {
|
||||||
|
if element == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
generate(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generate(element interface{}) {
|
||||||
|
field := reflect.ValueOf(element)
|
||||||
|
|
||||||
|
fill(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fill(field reflect.Value) {
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
setPtr(field)
|
||||||
|
case reflect.Struct:
|
||||||
|
setStruct(field)
|
||||||
|
case reflect.Map:
|
||||||
|
setMap(field)
|
||||||
|
case reflect.Slice:
|
||||||
|
if field.Type().Elem().Kind() == reflect.Struct ||
|
||||||
|
field.Type().Elem().Kind() == reflect.Ptr && field.Type().Elem().Elem().Kind() == reflect.Struct {
|
||||||
|
slice := reflect.MakeSlice(field.Type(), 1, 1)
|
||||||
|
field.Set(slice)
|
||||||
|
|
||||||
|
// use Ptr to allow "SetDefaults"
|
||||||
|
value := reflect.New(reflect.PtrTo(field.Type().Elem()))
|
||||||
|
setPtr(value)
|
||||||
|
|
||||||
|
elem := value.Elem().Elem()
|
||||||
|
field.Index(0).Set(elem)
|
||||||
|
} else if field.Len() == 0 {
|
||||||
|
slice := reflect.MakeSlice(field.Type(), 0, 0)
|
||||||
|
field.Set(slice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setPtr(field reflect.Value) {
|
||||||
|
if field.IsNil() {
|
||||||
|
field.Set(reflect.New(field.Type().Elem()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Type().Implements(reflect.TypeOf((*initializer)(nil)).Elem()) {
|
||||||
|
method := field.MethodByName("SetDefaults")
|
||||||
|
if method.IsValid() {
|
||||||
|
method.Call([]reflect.Value{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fill(field.Elem())
|
||||||
|
}
|
||||||
|
|
||||||
|
func setStruct(field reflect.Value) {
|
||||||
|
for i := 0; i < field.NumField(); i++ {
|
||||||
|
fd := field.Field(i)
|
||||||
|
structField := field.Type().Field(i)
|
||||||
|
|
||||||
|
if structField.Tag.Get(parser.TagLabel) == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if parser.IsExported(structField) {
|
||||||
|
fill(fd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMap(field reflect.Value) {
|
||||||
|
if field.IsNil() {
|
||||||
|
field.Set(reflect.MakeMap(field.Type()))
|
||||||
|
}
|
||||||
|
|
||||||
|
ptrValue := reflect.New(reflect.PtrTo(field.Type().Elem()))
|
||||||
|
fill(ptrValue)
|
||||||
|
|
||||||
|
value := ptrValue.Elem().Elem()
|
||||||
|
key := reflect.ValueOf(parser.MapNamePlaceholder)
|
||||||
|
field.SetMapIndex(key, value)
|
||||||
|
}
|
||||||
439
third_party/traefik/config/generator/generator_test.go
vendored
Normal file
439
third_party/traefik/config/generator/generator_test.go
vendored
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
package generator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/config/parser"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
element interface{}
|
||||||
|
expected interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "nil",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "simple",
|
||||||
|
element: &Ya{},
|
||||||
|
expected: &Ya{
|
||||||
|
Foo: &Yaa{
|
||||||
|
FieldIn1: "",
|
||||||
|
FieldIn2: false,
|
||||||
|
FieldIn3: 0,
|
||||||
|
FieldIn4: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
FieldIn5: map[string]int{
|
||||||
|
parser.MapNamePlaceholder: 0,
|
||||||
|
},
|
||||||
|
FieldIn6: map[string]struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {},
|
||||||
|
},
|
||||||
|
FieldIn7: map[string]struct{ Field map[string]string }{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FieldIn8: map[string]*struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {},
|
||||||
|
},
|
||||||
|
FieldIn9: map[string]*struct{ Field map[string]string }{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FieldIn10: struct{ Field string }{},
|
||||||
|
FieldIn11: &struct{ Field string }{},
|
||||||
|
FieldIn12: func(v string) *string { return &v }(""),
|
||||||
|
FieldIn13: func(v bool) *bool { return &v }(false),
|
||||||
|
FieldIn14: func(v int) *int { return &v }(0),
|
||||||
|
},
|
||||||
|
Field1: "",
|
||||||
|
Field2: false,
|
||||||
|
Field3: 0,
|
||||||
|
Field4: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
Field5: map[string]int{
|
||||||
|
parser.MapNamePlaceholder: 0,
|
||||||
|
},
|
||||||
|
Field6: map[string]struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {},
|
||||||
|
},
|
||||||
|
Field7: map[string]struct{ Field map[string]string }{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Field8: map[string]*struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {},
|
||||||
|
},
|
||||||
|
Field9: map[string]*struct{ Field map[string]string }{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Field10: struct{ Field string }{},
|
||||||
|
Field11: &struct{ Field string }{},
|
||||||
|
Field12: func(v string) *string { return &v }(""),
|
||||||
|
Field13: func(v bool) *bool { return &v }(false),
|
||||||
|
Field14: func(v int) *int { return &v }(0),
|
||||||
|
Field15: []int{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "with initial state",
|
||||||
|
element: &Ya{
|
||||||
|
Foo: &Yaa{
|
||||||
|
FieldIn1: "bar",
|
||||||
|
FieldIn2: false,
|
||||||
|
FieldIn3: 1,
|
||||||
|
FieldIn4: nil,
|
||||||
|
FieldIn5: nil,
|
||||||
|
FieldIn6: nil,
|
||||||
|
FieldIn7: nil,
|
||||||
|
FieldIn8: nil,
|
||||||
|
FieldIn9: nil,
|
||||||
|
FieldIn10: struct{ Field string }{},
|
||||||
|
FieldIn11: nil,
|
||||||
|
FieldIn12: nil,
|
||||||
|
FieldIn13: nil,
|
||||||
|
FieldIn14: nil,
|
||||||
|
},
|
||||||
|
Field1: "bir",
|
||||||
|
Field2: true,
|
||||||
|
Field3: 0,
|
||||||
|
Field4: nil,
|
||||||
|
Field5: nil,
|
||||||
|
Field6: nil,
|
||||||
|
Field7: nil,
|
||||||
|
Field8: nil,
|
||||||
|
Field9: nil,
|
||||||
|
Field10: struct{ Field string }{},
|
||||||
|
Field11: nil,
|
||||||
|
Field12: nil,
|
||||||
|
Field13: nil,
|
||||||
|
Field14: nil,
|
||||||
|
Field15: []int{7},
|
||||||
|
},
|
||||||
|
expected: &Ya{
|
||||||
|
Foo: &Yaa{
|
||||||
|
FieldIn1: "bar",
|
||||||
|
FieldIn2: false,
|
||||||
|
FieldIn3: 1,
|
||||||
|
FieldIn4: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
FieldIn5: map[string]int{
|
||||||
|
parser.MapNamePlaceholder: 0,
|
||||||
|
},
|
||||||
|
FieldIn6: map[string]struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {},
|
||||||
|
},
|
||||||
|
FieldIn7: map[string]struct{ Field map[string]string }{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FieldIn8: map[string]*struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {},
|
||||||
|
},
|
||||||
|
FieldIn9: map[string]*struct{ Field map[string]string }{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
FieldIn10: struct{ Field string }{},
|
||||||
|
FieldIn11: &struct{ Field string }{},
|
||||||
|
FieldIn12: func(v string) *string { return &v }(""),
|
||||||
|
FieldIn13: func(v bool) *bool { return &v }(false),
|
||||||
|
FieldIn14: func(v int) *int { return &v }(0),
|
||||||
|
},
|
||||||
|
Field1: "bir",
|
||||||
|
Field2: true,
|
||||||
|
Field3: 0,
|
||||||
|
Field4: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
Field5: map[string]int{
|
||||||
|
parser.MapNamePlaceholder: 0,
|
||||||
|
},
|
||||||
|
Field6: map[string]struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {},
|
||||||
|
},
|
||||||
|
Field7: map[string]struct{ Field map[string]string }{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Field8: map[string]*struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {},
|
||||||
|
},
|
||||||
|
Field9: map[string]*struct{ Field map[string]string }{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Field10: struct{ Field string }{},
|
||||||
|
Field11: &struct{ Field string }{},
|
||||||
|
Field12: func(v string) *string { return &v }(""),
|
||||||
|
Field13: func(v bool) *bool { return &v }(false),
|
||||||
|
Field14: func(v int) *int { return &v }(0),
|
||||||
|
Field15: []int{7},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "setDefault",
|
||||||
|
element: &Hu{},
|
||||||
|
expected: &Hu{
|
||||||
|
Foo: "hu",
|
||||||
|
Fii: &Hi{
|
||||||
|
Field: "hi",
|
||||||
|
},
|
||||||
|
Fuu: map[string]string{"<name>": ""},
|
||||||
|
Fee: map[string]Hi{"<name>": {Field: "hi"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
Generate(test.element)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, test.element)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_generate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
element interface{}
|
||||||
|
expected interface{}
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "struct pointer",
|
||||||
|
element: &struct {
|
||||||
|
Foo string
|
||||||
|
Fii *struct{ Field string }
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo string
|
||||||
|
Fii *struct{ Field string }
|
||||||
|
}{
|
||||||
|
Foo: "",
|
||||||
|
Fii: &struct{ Field string }{
|
||||||
|
Field: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "string slice",
|
||||||
|
element: &struct {
|
||||||
|
Foo []string
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo []string
|
||||||
|
}{
|
||||||
|
Foo: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int slice",
|
||||||
|
element: &struct {
|
||||||
|
Foo []int
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo []int
|
||||||
|
}{
|
||||||
|
Foo: []int{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "struct slice",
|
||||||
|
element: &struct {
|
||||||
|
Foo []struct {
|
||||||
|
Field string
|
||||||
|
}
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo []struct {
|
||||||
|
Field string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: []struct {
|
||||||
|
Field string
|
||||||
|
}{
|
||||||
|
{Field: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map string",
|
||||||
|
element: &struct {
|
||||||
|
Foo string
|
||||||
|
Fii map[string]string
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo string
|
||||||
|
Fii map[string]string
|
||||||
|
}{
|
||||||
|
Foo: "",
|
||||||
|
Fii: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map struct",
|
||||||
|
element: &struct {
|
||||||
|
Foo string
|
||||||
|
Fii map[string]struct{ Field string }
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo string
|
||||||
|
Fii map[string]struct{ Field string }
|
||||||
|
}{
|
||||||
|
Foo: "",
|
||||||
|
Fii: map[string]struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map struct pointer level 2",
|
||||||
|
element: &struct {
|
||||||
|
Foo string
|
||||||
|
Fuu *struct {
|
||||||
|
Fii map[string]*struct{ Field string }
|
||||||
|
}
|
||||||
|
}{},
|
||||||
|
expected: &struct {
|
||||||
|
Foo string
|
||||||
|
Fuu *struct {
|
||||||
|
Fii map[string]*struct{ Field string }
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: "",
|
||||||
|
Fuu: &struct {
|
||||||
|
Fii map[string]*struct {
|
||||||
|
Field string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Fii: map[string]*struct{ Field string }{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "SetDefaults",
|
||||||
|
element: &Hu{},
|
||||||
|
expected: &Hu{
|
||||||
|
Foo: "hu",
|
||||||
|
Fii: &Hi{
|
||||||
|
Field: "hi",
|
||||||
|
},
|
||||||
|
Fuu: map[string]string{
|
||||||
|
parser.MapNamePlaceholder: "",
|
||||||
|
},
|
||||||
|
Fee: map[string]Hi{
|
||||||
|
parser.MapNamePlaceholder: {
|
||||||
|
Field: "hi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
generate(test.element)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, test.element)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hu struct {
|
||||||
|
Foo string
|
||||||
|
Fii *Hi
|
||||||
|
Fuu map[string]string
|
||||||
|
Fee map[string]Hi
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hu) SetDefaults() {
|
||||||
|
h.Foo = "hu"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hi struct {
|
||||||
|
Field string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hi) SetDefaults() {
|
||||||
|
h.Field = "hi"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ya struct {
|
||||||
|
Foo *Yaa
|
||||||
|
Field1 string
|
||||||
|
Field2 bool
|
||||||
|
Field3 int
|
||||||
|
Field4 map[string]string
|
||||||
|
Field5 map[string]int
|
||||||
|
Field6 map[string]struct{ Field string }
|
||||||
|
Field7 map[string]struct{ Field map[string]string }
|
||||||
|
Field8 map[string]*struct{ Field string }
|
||||||
|
Field9 map[string]*struct{ Field map[string]string }
|
||||||
|
Field10 struct{ Field string }
|
||||||
|
Field11 *struct{ Field string }
|
||||||
|
Field12 *string
|
||||||
|
Field13 *bool
|
||||||
|
Field14 *int
|
||||||
|
Field15 []int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Yaa struct {
|
||||||
|
FieldIn1 string
|
||||||
|
FieldIn2 bool
|
||||||
|
FieldIn3 int
|
||||||
|
FieldIn4 map[string]string
|
||||||
|
FieldIn5 map[string]int
|
||||||
|
FieldIn6 map[string]struct{ Field string }
|
||||||
|
FieldIn7 map[string]struct{ Field map[string]string }
|
||||||
|
FieldIn8 map[string]*struct{ Field string }
|
||||||
|
FieldIn9 map[string]*struct{ Field map[string]string }
|
||||||
|
FieldIn10 struct{ Field string }
|
||||||
|
FieldIn11 *struct{ Field string }
|
||||||
|
FieldIn12 *string
|
||||||
|
FieldIn13 *bool
|
||||||
|
FieldIn14 *int
|
||||||
|
}
|
||||||
341
third_party/traefik/config/parser/element_fill.go
vendored
Normal file
341
third_party/traefik/config/parser/element_fill.go
vendored
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type initializer interface {
|
||||||
|
SetDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillerOpts Options for the filler.
|
||||||
|
type FillerOpts struct {
|
||||||
|
AllowSliceAsStruct bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill populates the fields of the element using the information in node.
|
||||||
|
func Fill(element interface{}, node *Node, opts FillerOpts) error {
|
||||||
|
return filler{FillerOpts: opts}.Fill(element, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
type filler struct {
|
||||||
|
FillerOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill populates the fields of the element using the information in node.
|
||||||
|
func (f filler) Fill(element interface{}, node *Node) error {
|
||||||
|
if element == nil || node == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Kind == 0 {
|
||||||
|
return fmt.Errorf("missing node type: %s", node.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
root := reflect.ValueOf(element)
|
||||||
|
if root.Kind() == reflect.Struct {
|
||||||
|
return fmt.Errorf("struct are not supported, use pointer instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.fill(root.Elem(), node)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f filler) fill(field reflect.Value, node *Node) error {
|
||||||
|
// related to allow-empty tag
|
||||||
|
if node.Disabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
field.SetString(node.Value)
|
||||||
|
return nil
|
||||||
|
case reflect.Bool:
|
||||||
|
val, err := strconv.ParseBool(node.Value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.SetBool(val)
|
||||||
|
return nil
|
||||||
|
case reflect.Int8:
|
||||||
|
return setInt(field, node.Value, 8)
|
||||||
|
case reflect.Int16:
|
||||||
|
return setInt(field, node.Value, 16)
|
||||||
|
case reflect.Int32:
|
||||||
|
return setInt(field, node.Value, 32)
|
||||||
|
case reflect.Int64, reflect.Int:
|
||||||
|
return setInt(field, node.Value, 64)
|
||||||
|
case reflect.Uint8:
|
||||||
|
return setUint(field, node.Value, 8)
|
||||||
|
case reflect.Uint16:
|
||||||
|
return setUint(field, node.Value, 16)
|
||||||
|
case reflect.Uint32:
|
||||||
|
return setUint(field, node.Value, 32)
|
||||||
|
case reflect.Uint64, reflect.Uint:
|
||||||
|
return setUint(field, node.Value, 64)
|
||||||
|
case reflect.Float32:
|
||||||
|
return setFloat(field, node.Value, 32)
|
||||||
|
case reflect.Float64:
|
||||||
|
return setFloat(field, node.Value, 64)
|
||||||
|
case reflect.Struct:
|
||||||
|
return f.setStruct(field, node)
|
||||||
|
case reflect.Ptr:
|
||||||
|
return f.setPtr(field, node)
|
||||||
|
case reflect.Map:
|
||||||
|
return f.setMap(field, node)
|
||||||
|
case reflect.Slice:
|
||||||
|
return f.setSlice(field, node)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f filler) setPtr(field reflect.Value, node *Node) error {
|
||||||
|
if field.IsNil() {
|
||||||
|
field.Set(reflect.New(field.Type().Elem()))
|
||||||
|
|
||||||
|
if field.Type().Implements(reflect.TypeOf((*initializer)(nil)).Elem()) {
|
||||||
|
method := field.MethodByName("SetDefaults")
|
||||||
|
if method.IsValid() {
|
||||||
|
method.Call([]reflect.Value{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.fill(field.Elem(), node)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f filler) setStruct(field reflect.Value, node *Node) error {
|
||||||
|
for _, child := range node.Children {
|
||||||
|
fd := field.FieldByName(child.FieldName)
|
||||||
|
|
||||||
|
zeroValue := reflect.Value{}
|
||||||
|
if fd == zeroValue {
|
||||||
|
return fmt.Errorf("field not found, node: %s (%s)", child.Name, child.FieldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := f.fill(fd, child)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f filler) setSlice(field reflect.Value, node *Node) error {
|
||||||
|
if field.Type().Elem().Kind() == reflect.Struct ||
|
||||||
|
field.Type().Elem().Kind() == reflect.Ptr && field.Type().Elem().Elem().Kind() == reflect.Struct {
|
||||||
|
return f.setSliceStruct(field, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(node.Value) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
values := strings.Split(node.Value, ",")
|
||||||
|
|
||||||
|
slice := reflect.MakeSlice(field.Type(), len(values), len(values))
|
||||||
|
field.Set(slice)
|
||||||
|
|
||||||
|
for i := 0; i < len(values); i++ {
|
||||||
|
value := strings.TrimSpace(values[i])
|
||||||
|
|
||||||
|
switch field.Type().Elem().Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
field.Index(i).SetString(value)
|
||||||
|
case reflect.Int:
|
||||||
|
val, err := strconv.ParseInt(value, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.Index(i).SetInt(val)
|
||||||
|
case reflect.Int8:
|
||||||
|
err := setInt(field.Index(i), value, 8)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case reflect.Int16:
|
||||||
|
err := setInt(field.Index(i), value, 16)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case reflect.Int32:
|
||||||
|
err := setInt(field.Index(i), value, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case reflect.Int64:
|
||||||
|
err := setInt(field.Index(i), value, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case reflect.Uint:
|
||||||
|
val, err := strconv.ParseUint(value, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.Index(i).SetUint(val)
|
||||||
|
case reflect.Uint8:
|
||||||
|
err := setUint(field.Index(i), value, 8)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case reflect.Uint16:
|
||||||
|
err := setUint(field.Index(i), value, 16)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case reflect.Uint32:
|
||||||
|
err := setUint(field.Index(i), value, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case reflect.Uint64:
|
||||||
|
err := setUint(field.Index(i), value, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case reflect.Float32:
|
||||||
|
err := setFloat(field.Index(i), value, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case reflect.Float64:
|
||||||
|
err := setFloat(field.Index(i), value, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
val, err := strconv.ParseBool(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
field.Index(i).SetBool(val)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported type: %s", field.Type().Elem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f filler) setSliceStruct(field reflect.Value, node *Node) error {
|
||||||
|
if f.AllowSliceAsStruct && node.Tag.Get(TagLabelSliceAsStruct) != "" {
|
||||||
|
return f.setSliceAsStruct(field, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
field.Set(reflect.MakeSlice(field.Type(), len(node.Children), len(node.Children)))
|
||||||
|
|
||||||
|
for i, child := range node.Children {
|
||||||
|
// use Ptr to allow "SetDefaults"
|
||||||
|
value := reflect.New(reflect.PtrTo(field.Type().Elem()))
|
||||||
|
err := f.setPtr(value, child)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
field.Index(i).Set(value.Elem().Elem())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f filler) setSliceAsStruct(field reflect.Value, node *Node) error {
|
||||||
|
if len(node.Children) == 0 {
|
||||||
|
return fmt.Errorf("invalid slice: node %s", node.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use Ptr to allow "SetDefaults"
|
||||||
|
value := reflect.New(reflect.PtrTo(field.Type().Elem()))
|
||||||
|
err := f.setPtr(value, node)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
elem := value.Elem().Elem()
|
||||||
|
|
||||||
|
field.Set(reflect.MakeSlice(field.Type(), 1, 1))
|
||||||
|
field.Index(0).Set(elem)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f filler) setMap(field reflect.Value, node *Node) error {
|
||||||
|
if field.IsNil() {
|
||||||
|
field.Set(reflect.MakeMap(field.Type()))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range node.Children {
|
||||||
|
ptrValue := reflect.New(reflect.PtrTo(field.Type().Elem()))
|
||||||
|
|
||||||
|
err := f.fill(ptrValue, child)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
value := ptrValue.Elem().Elem()
|
||||||
|
|
||||||
|
key := reflect.ValueOf(child.Name)
|
||||||
|
field.SetMapIndex(key, value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setInt(field reflect.Value, value string, bitSize int) error {
|
||||||
|
switch field.Type() {
|
||||||
|
case reflect.TypeOf(types.Duration(0)):
|
||||||
|
return setDuration(field, value, bitSize, time.Second)
|
||||||
|
case reflect.TypeOf(time.Duration(0)):
|
||||||
|
return setDuration(field, value, bitSize, time.Nanosecond)
|
||||||
|
default:
|
||||||
|
val, err := strconv.ParseInt(value, 10, bitSize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
field.Set(reflect.ValueOf(val).Convert(field.Type()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDuration(field reflect.Value, value string, bitSize int, defaultUnit time.Duration) error {
|
||||||
|
val, err := strconv.ParseInt(value, 10, bitSize)
|
||||||
|
if err == nil {
|
||||||
|
field.Set(reflect.ValueOf(time.Duration(val) * defaultUnit).Convert(field.Type()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
duration, err := time.ParseDuration(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
field.Set(reflect.ValueOf(duration).Convert(field.Type()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUint(field reflect.Value, value string, bitSize int) error {
|
||||||
|
val, err := strconv.ParseUint(value, 10, bitSize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
field.Set(reflect.ValueOf(val).Convert(field.Type()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setFloat(field reflect.Value, value string, bitSize int) error {
|
||||||
|
val, err := strconv.ParseFloat(value, bitSize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
field.Set(reflect.ValueOf(val).Convert(field.Type()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
1431
third_party/traefik/config/parser/element_fill_test.go
vendored
Normal file
1431
third_party/traefik/config/parser/element_fill_test.go
vendored
Normal file
File diff suppressed because it is too large
Load Diff
210
third_party/traefik/config/parser/element_nodes.go
vendored
Normal file
210
third_party/traefik/config/parser/element_nodes.go
vendored
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncoderToNodeOpts Options for the encoderToNode.
|
||||||
|
type EncoderToNodeOpts struct {
|
||||||
|
OmitEmpty bool
|
||||||
|
TagName string
|
||||||
|
AllowSliceAsStruct bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeToNode converts an element to a node.
|
||||||
|
// element -> nodes
|
||||||
|
func EncodeToNode(element interface{}, rootName string, opts EncoderToNodeOpts) (*Node, error) {
|
||||||
|
rValue := reflect.ValueOf(element)
|
||||||
|
node := &Node{Name: rootName}
|
||||||
|
|
||||||
|
encoder := encoderToNode{EncoderToNodeOpts: opts}
|
||||||
|
|
||||||
|
err := encoder.setNodeValue(node, rValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoderToNode struct {
|
||||||
|
EncoderToNodeOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoderToNode) setNodeValue(node *Node, rValue reflect.Value) error {
|
||||||
|
switch rValue.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
node.Value = rValue.String()
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
node.Value = strconv.FormatInt(rValue.Int(), 10)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
node.Value = strconv.FormatUint(rValue.Uint(), 10)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
node.Value = strconv.FormatFloat(rValue.Float(), 'f', 6, 64)
|
||||||
|
case reflect.Bool:
|
||||||
|
node.Value = strconv.FormatBool(rValue.Bool())
|
||||||
|
case reflect.Struct:
|
||||||
|
return e.setStructValue(node, rValue)
|
||||||
|
case reflect.Ptr:
|
||||||
|
return e.setNodeValue(node, rValue.Elem())
|
||||||
|
case reflect.Map:
|
||||||
|
return e.setMapValue(node, rValue)
|
||||||
|
case reflect.Slice:
|
||||||
|
return e.setSliceValue(node, rValue)
|
||||||
|
default:
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoderToNode) setStructValue(node *Node, rValue reflect.Value) error {
|
||||||
|
rType := rValue.Type()
|
||||||
|
|
||||||
|
for i := 0; i < rValue.NumField(); i++ {
|
||||||
|
field := rType.Field(i)
|
||||||
|
fieldValue := rValue.Field(i)
|
||||||
|
|
||||||
|
if !IsExported(field) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Tag.Get(e.TagName) == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := isSupportedType(field); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.isSkippedField(field, fieldValue) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeName := field.Name
|
||||||
|
if e.AllowSliceAsStruct && field.Type.Kind() == reflect.Slice && len(field.Tag.Get(TagLabelSliceAsStruct)) != 0 {
|
||||||
|
nodeName = field.Tag.Get(TagLabelSliceAsStruct)
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Anonymous {
|
||||||
|
if err := e.setNodeValue(node, fieldValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
child := &Node{Name: nodeName, FieldName: field.Name, Description: field.Tag.Get(TagDescription)}
|
||||||
|
|
||||||
|
if err := e.setNodeValue(child, fieldValue); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Type.Kind() == reflect.Ptr {
|
||||||
|
if field.Type.Elem().Kind() != reflect.Struct && fieldValue.IsNil() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Type.Elem().Kind() == reflect.Struct && len(child.Children) == 0 {
|
||||||
|
if field.Tag.Get(e.TagName) != TagLabelAllowEmpty {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
child.Value = "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Children = append(node.Children, child)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoderToNode) setMapValue(node *Node, rValue reflect.Value) error {
|
||||||
|
for _, key := range rValue.MapKeys() {
|
||||||
|
child := &Node{Name: key.String(), FieldName: key.String()}
|
||||||
|
node.Children = append(node.Children, child)
|
||||||
|
|
||||||
|
if err := e.setNodeValue(child, rValue.MapIndex(key)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoderToNode) setSliceValue(node *Node, rValue reflect.Value) error {
|
||||||
|
// label-slice-as-struct
|
||||||
|
if rValue.Type().Elem().Kind() == reflect.Struct && !strings.EqualFold(node.Name, node.FieldName) {
|
||||||
|
if rValue.Len() > 1 {
|
||||||
|
return fmt.Errorf("node %s has too many slice entries: %d", node.Name, rValue.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.setNodeValue(node, rValue.Index(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
if rValue.Type().Elem().Kind() == reflect.Struct ||
|
||||||
|
rValue.Type().Elem().Kind() == reflect.Ptr && rValue.Type().Elem().Elem().Kind() == reflect.Struct {
|
||||||
|
for i := 0; i < rValue.Len(); i++ {
|
||||||
|
child := &Node{Name: "[" + strconv.Itoa(i) + "]"}
|
||||||
|
|
||||||
|
eValue := rValue.Index(i)
|
||||||
|
|
||||||
|
err := e.setNodeValue(child, eValue)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Children = append(node.Children, child)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var values []string
|
||||||
|
|
||||||
|
for i := 0; i < rValue.Len(); i++ {
|
||||||
|
eValue := rValue.Index(i)
|
||||||
|
|
||||||
|
switch eValue.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
values = append(values, eValue.String())
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
values = append(values, strconv.FormatInt(eValue.Int(), 10))
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
values = append(values, strconv.FormatUint(eValue.Uint(), 10))
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
values = append(values, strconv.FormatFloat(eValue.Float(), 'f', 6, 64))
|
||||||
|
case reflect.Bool:
|
||||||
|
values = append(values, strconv.FormatBool(eValue.Bool()))
|
||||||
|
default:
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Value = strings.Join(values, ", ")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoderToNode) isSkippedField(field reflect.StructField, fieldValue reflect.Value) bool {
|
||||||
|
if e.OmitEmpty && field.Type.Kind() == reflect.String && fieldValue.Len() == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct && fieldValue.IsNil() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.OmitEmpty && (field.Type.Kind() == reflect.Slice) &&
|
||||||
|
(fieldValue.IsNil() || fieldValue.Len() == 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.Type.Kind() == reflect.Map) &&
|
||||||
|
(fieldValue.IsNil() || fieldValue.Len() == 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
738
third_party/traefik/config/parser/element_nodes_test.go
vendored
Normal file
738
third_party/traefik/config/parser/element_nodes_test.go
vendored
Normal file
@@ -0,0 +1,738 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodeToNode(t *testing.T) {
|
||||||
|
type expected struct {
|
||||||
|
node *Node
|
||||||
|
error bool
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
element interface{}
|
||||||
|
expected expected
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Description",
|
||||||
|
element: struct {
|
||||||
|
Foo string `description:"text"`
|
||||||
|
}{Foo: "bar"},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "bar", Description: "text"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "string",
|
||||||
|
element: struct {
|
||||||
|
Foo string
|
||||||
|
}{Foo: "bar"},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "bar"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "2 string fields",
|
||||||
|
element: struct {
|
||||||
|
Foo string
|
||||||
|
Fii string
|
||||||
|
}{Foo: "bar", Fii: "hii"},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "bar"},
|
||||||
|
{Name: "Fii", FieldName: "Fii", Value: "hii"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int",
|
||||||
|
element: struct {
|
||||||
|
Foo int
|
||||||
|
}{Foo: 1},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "1"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int8",
|
||||||
|
element: struct {
|
||||||
|
Foo int8
|
||||||
|
}{Foo: 2},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "2"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int16",
|
||||||
|
element: struct {
|
||||||
|
Foo int16
|
||||||
|
}{Foo: 2},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "2"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int32",
|
||||||
|
element: struct {
|
||||||
|
Foo int32
|
||||||
|
}{Foo: 2},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "2"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int64",
|
||||||
|
element: struct {
|
||||||
|
Foo int64
|
||||||
|
}{Foo: 2},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "2"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "uint",
|
||||||
|
element: struct {
|
||||||
|
Foo uint
|
||||||
|
}{Foo: 1},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "1"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "uint8",
|
||||||
|
element: struct {
|
||||||
|
Foo uint8
|
||||||
|
}{Foo: 2},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "2"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "uint16",
|
||||||
|
element: struct {
|
||||||
|
Foo uint16
|
||||||
|
}{Foo: 2},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "2"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "uint32",
|
||||||
|
element: struct {
|
||||||
|
Foo uint32
|
||||||
|
}{Foo: 2},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "2"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "uint64",
|
||||||
|
element: struct {
|
||||||
|
Foo uint64
|
||||||
|
}{Foo: 2},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "2"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "float32",
|
||||||
|
element: struct {
|
||||||
|
Foo float32
|
||||||
|
}{Foo: 1.12},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "1.120000"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "float64",
|
||||||
|
element: struct {
|
||||||
|
Foo float64
|
||||||
|
}{Foo: 1.12},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "1.120000"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "bool",
|
||||||
|
element: struct {
|
||||||
|
Foo bool
|
||||||
|
}{Foo: true},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "true"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "struct",
|
||||||
|
element: struct {
|
||||||
|
Foo struct {
|
||||||
|
Fii string
|
||||||
|
Fuu string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: struct {
|
||||||
|
Fii string
|
||||||
|
Fuu string
|
||||||
|
}{
|
||||||
|
Fii: "hii",
|
||||||
|
Fuu: "huu",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Children: []*Node{
|
||||||
|
{Name: "Fii", FieldName: "Fii", Value: "hii"},
|
||||||
|
{Name: "Fuu", FieldName: "Fuu", Value: "huu"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "struct unexported field",
|
||||||
|
element: struct {
|
||||||
|
Foo struct {
|
||||||
|
Fii string
|
||||||
|
fuu string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: struct {
|
||||||
|
Fii string
|
||||||
|
fuu string
|
||||||
|
}{
|
||||||
|
Fii: "hii",
|
||||||
|
fuu: "huu",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Children: []*Node{
|
||||||
|
{Name: "Fii", FieldName: "Fii", Value: "hii"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "struct pointer",
|
||||||
|
element: struct {
|
||||||
|
Foo *struct {
|
||||||
|
Fii string
|
||||||
|
Fuu string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: &struct {
|
||||||
|
Fii string
|
||||||
|
Fuu string
|
||||||
|
}{
|
||||||
|
Fii: "hii",
|
||||||
|
Fuu: "huu",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Children: []*Node{
|
||||||
|
{Name: "Fii", FieldName: "Fii", Value: "hii"},
|
||||||
|
{Name: "Fuu", FieldName: "Fuu", Value: "huu"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "string pointer",
|
||||||
|
element: struct {
|
||||||
|
Foo *struct {
|
||||||
|
Fii *string
|
||||||
|
Fuu string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: &struct {
|
||||||
|
Fii *string
|
||||||
|
Fuu string
|
||||||
|
}{
|
||||||
|
Fii: func(v string) *string { return &v }("hii"),
|
||||||
|
Fuu: "huu",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Children: []*Node{
|
||||||
|
{Name: "Fii", FieldName: "Fii", Value: "hii"},
|
||||||
|
{Name: "Fuu", FieldName: "Fuu", Value: "huu"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "string nil pointer",
|
||||||
|
element: struct {
|
||||||
|
Foo *struct {
|
||||||
|
Fii *string
|
||||||
|
Fuu string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: &struct {
|
||||||
|
Fii *string
|
||||||
|
Fuu string
|
||||||
|
}{
|
||||||
|
Fii: nil,
|
||||||
|
Fuu: "huu",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Children: []*Node{
|
||||||
|
{Name: "Fuu", FieldName: "Fuu", Value: "huu"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "int pointer",
|
||||||
|
element: struct {
|
||||||
|
Foo *struct {
|
||||||
|
Fii *int
|
||||||
|
Fuu int
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: &struct {
|
||||||
|
Fii *int
|
||||||
|
Fuu int
|
||||||
|
}{
|
||||||
|
Fii: func(v int) *int { return &v }(6),
|
||||||
|
Fuu: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Children: []*Node{
|
||||||
|
{Name: "Fii", FieldName: "Fii", Value: "6"},
|
||||||
|
{Name: "Fuu", FieldName: "Fuu", Value: "4"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "bool pointer",
|
||||||
|
element: struct {
|
||||||
|
Foo *struct {
|
||||||
|
Fii *bool
|
||||||
|
Fuu bool
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: &struct {
|
||||||
|
Fii *bool
|
||||||
|
Fuu bool
|
||||||
|
}{
|
||||||
|
Fii: func(v bool) *bool { return &v }(true),
|
||||||
|
Fuu: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Children: []*Node{
|
||||||
|
{Name: "Fii", FieldName: "Fii", Value: "true"},
|
||||||
|
{Name: "Fuu", FieldName: "Fuu", Value: "true"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "struct nil struct pointer",
|
||||||
|
element: struct {
|
||||||
|
Foo *struct {
|
||||||
|
Fii *string
|
||||||
|
Fuu string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: nil,
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "struct pointer, not allowEmpty",
|
||||||
|
element: struct {
|
||||||
|
Foo *struct {
|
||||||
|
Fii string
|
||||||
|
Fuu string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: &struct {
|
||||||
|
Fii string
|
||||||
|
Fuu string
|
||||||
|
}{},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "struct pointer, allowEmpty",
|
||||||
|
element: struct {
|
||||||
|
Foo *struct {
|
||||||
|
Fii string
|
||||||
|
Fuu string
|
||||||
|
} `label:"allowEmpty"`
|
||||||
|
}{
|
||||||
|
Foo: &struct {
|
||||||
|
Fii string
|
||||||
|
Fuu string
|
||||||
|
}{},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Value: "true"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map",
|
||||||
|
element: struct {
|
||||||
|
Foo struct {
|
||||||
|
Bar map[string]string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: struct {
|
||||||
|
Bar map[string]string
|
||||||
|
}{
|
||||||
|
Bar: map[string]string{
|
||||||
|
"name1": "huu",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Children: []*Node{
|
||||||
|
{Name: "name1", FieldName: "name1", Value: "huu"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty map",
|
||||||
|
element: struct {
|
||||||
|
Bar map[string]string
|
||||||
|
}{
|
||||||
|
Bar: map[string]string{},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map nil",
|
||||||
|
element: struct {
|
||||||
|
Bar map[string]string
|
||||||
|
}{
|
||||||
|
Bar: nil,
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "map with non string key",
|
||||||
|
element: struct {
|
||||||
|
Foo struct {
|
||||||
|
Bar map[int]string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: struct {
|
||||||
|
Bar map[int]string
|
||||||
|
}{
|
||||||
|
Bar: map[int]string{
|
||||||
|
1: "huu",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{error: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of string",
|
||||||
|
element: struct{ Bar []string }{Bar: []string{"huu", "hii"}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "huu, hii"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of int",
|
||||||
|
element: struct{ Bar []int }{Bar: []int{4, 2, 3}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of int8",
|
||||||
|
element: struct{ Bar []int8 }{Bar: []int8{4, 2, 3}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of int16",
|
||||||
|
element: struct{ Bar []int16 }{Bar: []int16{4, 2, 3}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of int32",
|
||||||
|
element: struct{ Bar []int32 }{Bar: []int32{4, 2, 3}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of int64",
|
||||||
|
element: struct{ Bar []int64 }{Bar: []int64{4, 2, 3}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of uint",
|
||||||
|
element: struct{ Bar []uint }{Bar: []uint{4, 2, 3}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of uint8",
|
||||||
|
element: struct{ Bar []uint8 }{Bar: []uint8{4, 2, 3}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of uint16",
|
||||||
|
element: struct{ Bar []uint16 }{Bar: []uint16{4, 2, 3}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of uint32",
|
||||||
|
element: struct{ Bar []uint32 }{Bar: []uint32{4, 2, 3}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of uint64",
|
||||||
|
element: struct{ Bar []uint64 }{Bar: []uint64{4, 2, 3}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of float32",
|
||||||
|
element: struct{ Bar []float32 }{Bar: []float32{4.1, 2, 3.2}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "4.100000, 2.000000, 3.200000"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of float64",
|
||||||
|
element: struct{ Bar []float64 }{Bar: []float64{4.1, 2, 3.2}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "4.100000, 2.000000, 3.200000"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of bool",
|
||||||
|
element: struct{ Bar []bool }{Bar: []bool{true, false, true}},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "true, false, true"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice label-slice-as-struct",
|
||||||
|
element: &struct {
|
||||||
|
Foo []struct {
|
||||||
|
Bar string
|
||||||
|
Bir string
|
||||||
|
} `label-slice-as-struct:"Fii"`
|
||||||
|
}{
|
||||||
|
Foo: []struct {
|
||||||
|
Bar string
|
||||||
|
Bir string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Bar: "haa",
|
||||||
|
Bir: "hii",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{{
|
||||||
|
Name: "Fii",
|
||||||
|
FieldName: "Foo",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "Bar", FieldName: "Bar", Value: "haa"},
|
||||||
|
{Name: "Bir", FieldName: "Bir", Value: "hii"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice label-slice-as-struct several slice entries",
|
||||||
|
element: &struct {
|
||||||
|
Foo []struct {
|
||||||
|
Bar string
|
||||||
|
Bir string
|
||||||
|
} `label-slice-as-struct:"Fii"`
|
||||||
|
}{
|
||||||
|
Foo: []struct {
|
||||||
|
Bar string
|
||||||
|
Bir string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Bar: "haa",
|
||||||
|
Bir: "hii",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Bar: "haa",
|
||||||
|
Bir: "hii",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{error: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of struct",
|
||||||
|
element: struct {
|
||||||
|
Foo []struct {
|
||||||
|
Field string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: []struct {
|
||||||
|
Field string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Field: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "bir",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Children: []*Node{
|
||||||
|
{Name: "[0]", Children: []*Node{
|
||||||
|
{Name: "Field", FieldName: "Field", Value: "bar"},
|
||||||
|
}},
|
||||||
|
{Name: "[1]", Children: []*Node{
|
||||||
|
{Name: "Field", FieldName: "Field", Value: "bir"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of pointer of struct",
|
||||||
|
element: struct {
|
||||||
|
Foo []*struct {
|
||||||
|
Field string
|
||||||
|
}
|
||||||
|
}{
|
||||||
|
Foo: []*struct {
|
||||||
|
Field string
|
||||||
|
}{
|
||||||
|
{Field: "bar"},
|
||||||
|
{Field: "bir"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Children: []*Node{
|
||||||
|
{Name: "[0]", Children: []*Node{
|
||||||
|
{Name: "Field", FieldName: "Field", Value: "bar"},
|
||||||
|
}},
|
||||||
|
{Name: "[1]", Children: []*Node{
|
||||||
|
{Name: "Field", FieldName: "Field", Value: "bir"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "empty slice",
|
||||||
|
element: struct {
|
||||||
|
Bar []string
|
||||||
|
}{
|
||||||
|
Bar: []string{},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "nil slice",
|
||||||
|
element: struct {
|
||||||
|
Bar []string
|
||||||
|
}{
|
||||||
|
Bar: nil,
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "ignore slice",
|
||||||
|
element: struct {
|
||||||
|
Bar []string `label:"-"`
|
||||||
|
}{
|
||||||
|
Bar: []string{"huu", "hii"},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "embedded",
|
||||||
|
element: struct {
|
||||||
|
Foo struct{ FiiFoo }
|
||||||
|
}{
|
||||||
|
Foo: struct{ FiiFoo }{
|
||||||
|
FiiFoo: FiiFoo{
|
||||||
|
Fii: "hii",
|
||||||
|
Fuu: "huu",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{Name: "traefik", Children: []*Node{
|
||||||
|
{Name: "Foo", FieldName: "Foo", Children: []*Node{
|
||||||
|
{Name: "Fii", FieldName: "Fii", Value: "hii"},
|
||||||
|
{Name: "Fuu", FieldName: "Fuu", Value: "huu"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
etnOpts := EncoderToNodeOpts{OmitEmpty: true, TagName: TagLabel, AllowSliceAsStruct: true}
|
||||||
|
node, err := EncodeToNode(test.element, DefaultRootName, etnOpts)
|
||||||
|
|
||||||
|
if test.expected.error {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected.node, node)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
167
third_party/traefik/config/parser/flat_encode.go
vendored
Normal file
167
third_party/traefik/config/parser/flat_encode.go
vendored
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/crazy-max/diun/v3/third_party/traefik/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultPtrValue = "false"
|
||||||
|
|
||||||
|
// FlatOpts holds options used when encoding to Flat.
|
||||||
|
type FlatOpts struct {
|
||||||
|
Case string // "lower" or "upper", defaults to "lower".
|
||||||
|
Separator string
|
||||||
|
SkipRoot bool
|
||||||
|
TagName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flat is a configuration item representation.
|
||||||
|
type Flat struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Default string
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeToFlat encodes a node to a Flat representation.
|
||||||
|
// Even though the given node argument should have already been augmented with metadata such as kind,
|
||||||
|
// the element (and its type information) is still needed to treat remaining edge cases.
|
||||||
|
func EncodeToFlat(element interface{}, node *Node, opts FlatOpts) ([]Flat, error) {
|
||||||
|
if element == nil || node == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Kind == 0 {
|
||||||
|
return nil, fmt.Errorf("missing node type: %s", node.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
elem := reflect.ValueOf(element)
|
||||||
|
if elem.Kind() == reflect.Struct {
|
||||||
|
return nil, fmt.Errorf("structs are not supported, use pointer instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
encoder := encoderToFlat{FlatOpts: opts}
|
||||||
|
|
||||||
|
var entries []Flat
|
||||||
|
if encoder.SkipRoot {
|
||||||
|
for _, child := range node.Children {
|
||||||
|
field := encoder.getField(elem.Elem(), child)
|
||||||
|
entries = append(entries, encoder.createFlat(field, child.Name, child)...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entries = encoder.createFlat(elem, strings.ToLower(node.Name), node)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
|
||||||
|
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoderToFlat struct {
|
||||||
|
FlatOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoderToFlat) createFlat(field reflect.Value, name string, node *Node) []Flat {
|
||||||
|
var entries []Flat
|
||||||
|
if node.Kind != reflect.Map && node.Description != "-" {
|
||||||
|
if !(node.Kind == reflect.Ptr && len(node.Children) > 0) ||
|
||||||
|
(node.Kind == reflect.Ptr && node.Tag.Get(e.TagName) == TagLabelAllowEmpty) {
|
||||||
|
if node.Name[0] != '[' {
|
||||||
|
entries = append(entries, Flat{
|
||||||
|
Name: e.getName(name),
|
||||||
|
Description: node.Description,
|
||||||
|
Default: e.getNodeValue(e.getField(field, node), node),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range node.Children {
|
||||||
|
if node.Kind == reflect.Map {
|
||||||
|
fChild := e.getField(field, child)
|
||||||
|
|
||||||
|
var v string
|
||||||
|
if child.Kind == reflect.Struct {
|
||||||
|
v = defaultPtrValue
|
||||||
|
} else {
|
||||||
|
v = e.getNodeValue(fChild, child)
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Description != "-" {
|
||||||
|
entries = append(entries, Flat{
|
||||||
|
Name: e.getName(name, child.Name),
|
||||||
|
Description: node.Description,
|
||||||
|
Default: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if child.Kind == reflect.Struct || child.Kind == reflect.Ptr {
|
||||||
|
for _, ch := range child.Children {
|
||||||
|
f := e.getField(fChild, ch)
|
||||||
|
n := e.getName(name, child.Name, ch.Name)
|
||||||
|
entries = append(entries, e.createFlat(f, n, ch)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f := e.getField(field, child)
|
||||||
|
n := e.getName(name, child.Name)
|
||||||
|
entries = append(entries, e.createFlat(f, n, child)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoderToFlat) getField(field reflect.Value, node *Node) reflect.Value {
|
||||||
|
switch field.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
return field.FieldByName(node.FieldName)
|
||||||
|
case reflect.Ptr:
|
||||||
|
if field.Elem().Kind() == reflect.Struct {
|
||||||
|
return field.Elem().FieldByName(node.FieldName)
|
||||||
|
}
|
||||||
|
return field.Elem()
|
||||||
|
case reflect.Map:
|
||||||
|
return field.MapIndex(reflect.ValueOf(node.FieldName))
|
||||||
|
default:
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoderToFlat) getNodeValue(field reflect.Value, node *Node) string {
|
||||||
|
if node.Kind == reflect.Ptr && len(node.Children) > 0 {
|
||||||
|
return defaultPtrValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Kind() == reflect.Int64 {
|
||||||
|
i, _ := strconv.ParseInt(node.Value, 10, 64)
|
||||||
|
|
||||||
|
switch field.Type() {
|
||||||
|
case reflect.TypeOf(types.Duration(time.Second)):
|
||||||
|
return strconv.Itoa(int(i) / int(time.Second))
|
||||||
|
case reflect.TypeOf(time.Second):
|
||||||
|
return time.Duration(i).String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoderToFlat) getName(names ...string) string {
|
||||||
|
var name string
|
||||||
|
if names[len(names)-1][0] == '[' {
|
||||||
|
name = strings.Join(names, "")
|
||||||
|
} else {
|
||||||
|
name = strings.Join(names, e.Separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(e.Case, "upper") {
|
||||||
|
return strings.ToUpper(name)
|
||||||
|
}
|
||||||
|
return strings.ToLower(name)
|
||||||
|
}
|
||||||
1251
third_party/traefik/config/parser/flat_encode_test.go
vendored
Normal file
1251
third_party/traefik/config/parser/flat_encode_test.go
vendored
Normal file
File diff suppressed because it is too large
Load Diff
97
third_party/traefik/config/parser/labels_decode.go
vendored
Normal file
97
third_party/traefik/config/parser/labels_decode.go
vendored
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DecodeToNode converts the labels to a tree of nodes.
|
||||||
|
// If any filters are present, labels which do not match the filters are skipped.
|
||||||
|
func DecodeToNode(labels map[string]string, rootName string, filters ...string) (*Node, error) {
|
||||||
|
sortedKeys := sortKeys(labels, filters)
|
||||||
|
|
||||||
|
var node *Node
|
||||||
|
for i, key := range sortedKeys {
|
||||||
|
split := strings.Split(key, ".")
|
||||||
|
|
||||||
|
if split[0] != rootName {
|
||||||
|
return nil, fmt.Errorf("invalid label root %s", split[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts []string
|
||||||
|
for _, v := range split {
|
||||||
|
if v == "" {
|
||||||
|
return nil, fmt.Errorf("invalid element: %s", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v[0] == '[' {
|
||||||
|
return nil, fmt.Errorf("invalid leading character '[' in field name (bracket is a slice delimiter): %s", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(v, "]") && v[0] != '[' {
|
||||||
|
indexLeft := strings.Index(v, "[")
|
||||||
|
parts = append(parts, v[:indexLeft], v[indexLeft:])
|
||||||
|
} else {
|
||||||
|
parts = append(parts, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
node = &Node{}
|
||||||
|
}
|
||||||
|
decodeToNode(node, parts, labels[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
return node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeToNode(root *Node, path []string, value string) {
|
||||||
|
if len(root.Name) == 0 {
|
||||||
|
root.Name = path[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's a leaf or not -> children
|
||||||
|
if len(path) > 1 {
|
||||||
|
if n := containsNode(root.Children, path[1]); n != nil {
|
||||||
|
// the child already exists
|
||||||
|
decodeToNode(n, path[1:], value)
|
||||||
|
} else {
|
||||||
|
// new child
|
||||||
|
child := &Node{Name: path[1]}
|
||||||
|
decodeToNode(child, path[1:], value)
|
||||||
|
root.Children = append(root.Children, child)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
root.Value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsNode(nodes []*Node, name string) *Node {
|
||||||
|
for _, n := range nodes {
|
||||||
|
if strings.EqualFold(name, n.Name) {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortKeys(labels map[string]string, filters []string) []string {
|
||||||
|
var sortedKeys []string
|
||||||
|
for key := range labels {
|
||||||
|
if len(filters) == 0 {
|
||||||
|
sortedKeys = append(sortedKeys, key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filter := range filters {
|
||||||
|
if len(key) >= len(filter) && strings.EqualFold(key[:len(filter)], filter) {
|
||||||
|
sortedKeys = append(sortedKeys, key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(sortedKeys)
|
||||||
|
|
||||||
|
return sortedKeys
|
||||||
|
}
|
||||||
261
third_party/traefik/config/parser/labels_decode_test.go
vendored
Normal file
261
third_party/traefik/config/parser/labels_decode_test.go
vendored
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecodeToNode(t *testing.T) {
|
||||||
|
type expected struct {
|
||||||
|
error bool
|
||||||
|
node *Node
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
in map[string]string
|
||||||
|
filters []string
|
||||||
|
expected expected
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "no label",
|
||||||
|
in: map[string]string{},
|
||||||
|
expected: expected{node: nil},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "invalid label, ending by a dot",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.http.": "bar",
|
||||||
|
},
|
||||||
|
expected: expected{
|
||||||
|
error: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "level 1",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.foo": "bar",
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "foo", Value: "bar"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "level 1 empty value",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.foo": "",
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "foo", Value: ""},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "level 2",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.foo.bar": "bar",
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{{
|
||||||
|
Name: "foo",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "bar", Value: "bar"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "several entries, level 0",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik": "bar",
|
||||||
|
"traefic": "bur",
|
||||||
|
},
|
||||||
|
expected: expected{error: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "several entries, prefix filter",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.foo": "bar",
|
||||||
|
"traefik.fii": "bir",
|
||||||
|
},
|
||||||
|
filters: []string{"traefik.Foo"},
|
||||||
|
expected: expected{node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "foo", Value: "bar"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "several entries, level 1",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.foo": "bar",
|
||||||
|
"traefik.fii": "bur",
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "fii", Value: "bur"},
|
||||||
|
{Name: "foo", Value: "bar"},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "several entries, level 2",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.foo.aaa": "bar",
|
||||||
|
"traefik.foo.bbb": "bur",
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "foo", Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar"},
|
||||||
|
{Name: "bbb", Value: "bur"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "several entries, level 2, case insensitive",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.foo.aaa": "bar",
|
||||||
|
"traefik.Foo.bbb": "bur",
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "Foo", Children: []*Node{
|
||||||
|
{Name: "bbb", Value: "bur"},
|
||||||
|
{Name: "aaa", Value: "bar"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "several entries, level 2, 3 children",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.foo.aaa": "bar",
|
||||||
|
"traefik.foo.bbb": "bur",
|
||||||
|
"traefik.foo.ccc": "bir",
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "foo", Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar"},
|
||||||
|
{Name: "bbb", Value: "bur"},
|
||||||
|
{Name: "ccc", Value: "bir"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "several entries, level 3",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.foo.bar.aaa": "bar",
|
||||||
|
"traefik.foo.bar.bbb": "bur",
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "foo", Children: []*Node{
|
||||||
|
{Name: "bar", Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar"},
|
||||||
|
{Name: "bbb", Value: "bur"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "several entries, level 3, 2 children level 1",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.foo.bar.aaa": "bar",
|
||||||
|
"traefik.foo.bar.bbb": "bur",
|
||||||
|
"traefik.bar.foo.bbb": "bir",
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "bar", Children: []*Node{
|
||||||
|
{Name: "foo", Children: []*Node{
|
||||||
|
{Name: "bbb", Value: "bir"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
{Name: "foo", Children: []*Node{
|
||||||
|
{Name: "bar", Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar"},
|
||||||
|
{Name: "bbb", Value: "bur"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "several entries, slice syntax",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.foo[0].aaa": "bar0",
|
||||||
|
"traefik.foo[0].bbb": "bur0",
|
||||||
|
"traefik.foo[1].aaa": "bar1",
|
||||||
|
"traefik.foo[1].bbb": "bur1",
|
||||||
|
},
|
||||||
|
expected: expected{node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "foo", Children: []*Node{
|
||||||
|
{Name: "[0]", Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar0"},
|
||||||
|
{Name: "bbb", Value: "bur0"},
|
||||||
|
}},
|
||||||
|
{Name: "[1]", Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar1"},
|
||||||
|
{Name: "bbb", Value: "bur1"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "several entries, invalid slice syntax",
|
||||||
|
in: map[string]string{
|
||||||
|
"traefik.foo.[0].aaa": "bar0",
|
||||||
|
"traefik.foo.[0].bbb": "bur0",
|
||||||
|
"traefik.foo.[1].aaa": "bar1",
|
||||||
|
"traefik.foo.[1].bbb": "bur1",
|
||||||
|
},
|
||||||
|
expected: expected{error: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
out, err := DecodeToNode(test.in, DefaultRootName, test.filters...)
|
||||||
|
|
||||||
|
if test.expected.error {
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if !assert.Equal(t, test.expected.node, out) {
|
||||||
|
bytes, err := json.MarshalIndent(out, "", " ")
|
||||||
|
require.NoError(t, err)
|
||||||
|
fmt.Println(string(bytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
30
third_party/traefik/config/parser/labels_encode.go
vendored
Normal file
30
third_party/traefik/config/parser/labels_encode.go
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
// EncodeNode Converts a node to labels.
|
||||||
|
// nodes -> labels
|
||||||
|
func EncodeNode(node *Node) map[string]string {
|
||||||
|
labels := make(map[string]string)
|
||||||
|
encodeNode(labels, node.Name, node)
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeNode(labels map[string]string, root string, node *Node) {
|
||||||
|
for _, child := range node.Children {
|
||||||
|
if child.Disabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var sep string
|
||||||
|
if child.Name[0] != '[' {
|
||||||
|
sep = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
childName := root + sep + child.Name
|
||||||
|
|
||||||
|
if len(child.Children) > 0 {
|
||||||
|
encodeNode(labels, childName, child)
|
||||||
|
} else if len(child.Name) > 0 {
|
||||||
|
labels[childName] = child.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
third_party/traefik/config/parser/labels_encode_test.go
vendored
Normal file
180
third_party/traefik/config/parser/labels_encode_test.go
vendored
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodeNode(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
node *Node
|
||||||
|
expected map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "1 label",
|
||||||
|
node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: map[string]string{
|
||||||
|
"traefik.aaa": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "2 labels",
|
||||||
|
node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar"},
|
||||||
|
{Name: "bbb", Value: "bur"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: map[string]string{
|
||||||
|
"traefik.aaa": "bar",
|
||||||
|
"traefik.bbb": "bur",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "2 labels, 1 disabled",
|
||||||
|
node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar"},
|
||||||
|
{Name: "bbb", Value: "bur", Disabled: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: map[string]string{
|
||||||
|
"traefik.aaa": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "2 levels",
|
||||||
|
node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "foo", Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: map[string]string{
|
||||||
|
"traefik.foo.aaa": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "3 levels",
|
||||||
|
node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "foo", Children: []*Node{
|
||||||
|
{Name: "bar", Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: map[string]string{
|
||||||
|
"traefik.foo.bar.aaa": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "2 levels, same root",
|
||||||
|
node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "foo", Children: []*Node{
|
||||||
|
{Name: "bar", Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar"},
|
||||||
|
{Name: "bbb", Value: "bur"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: map[string]string{
|
||||||
|
"traefik.foo.bar.aaa": "bar",
|
||||||
|
"traefik.foo.bar.bbb": "bur",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "several levels, different root",
|
||||||
|
node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "bar", Children: []*Node{
|
||||||
|
{Name: "ccc", Value: "bir"},
|
||||||
|
}},
|
||||||
|
{Name: "foo", Children: []*Node{
|
||||||
|
{Name: "bar", Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: map[string]string{
|
||||||
|
"traefik.foo.bar.aaa": "bar",
|
||||||
|
"traefik.bar.ccc": "bir",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "multiple labels, multiple levels",
|
||||||
|
node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "bar", Children: []*Node{
|
||||||
|
{Name: "ccc", Value: "bir"},
|
||||||
|
}},
|
||||||
|
{Name: "foo", Children: []*Node{
|
||||||
|
{Name: "bar", Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar"},
|
||||||
|
{Name: "bbb", Value: "bur"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: map[string]string{
|
||||||
|
"traefik.foo.bar.aaa": "bar",
|
||||||
|
"traefik.foo.bar.bbb": "bur",
|
||||||
|
"traefik.bar.ccc": "bir",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "slice of struct syntax",
|
||||||
|
node: &Node{
|
||||||
|
Name: "traefik",
|
||||||
|
Children: []*Node{
|
||||||
|
{Name: "foo", Children: []*Node{
|
||||||
|
{Name: "[0]", Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar0"},
|
||||||
|
{Name: "bbb", Value: "bur0"},
|
||||||
|
}},
|
||||||
|
{Name: "[1]", Children: []*Node{
|
||||||
|
{Name: "aaa", Value: "bar1"},
|
||||||
|
{Name: "bbb", Value: "bur1"},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: map[string]string{
|
||||||
|
"traefik.foo[0].aaa": "bar0",
|
||||||
|
"traefik.foo[0].bbb": "bur0",
|
||||||
|
"traefik.foo[1].aaa": "bar1",
|
||||||
|
"traefik.foo[1].bbb": "bur1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
labels := EncodeNode(test.node)
|
||||||
|
|
||||||
|
assert.Equal(t, test.expected, labels)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
21
third_party/traefik/config/parser/node.go
vendored
Normal file
21
third_party/traefik/config/parser/node.go
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
// DefaultRootName is the default name of the root node and the prefix of element name from the resources.
|
||||||
|
const DefaultRootName = "traefik"
|
||||||
|
|
||||||
|
// MapNamePlaceholder is the placeholder for the map name.
|
||||||
|
const MapNamePlaceholder = "<name>"
|
||||||
|
|
||||||
|
// Node is a label node.
|
||||||
|
type Node struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
FieldName string `json:"fieldName"`
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
Disabled bool `json:"disabled,omitempty"`
|
||||||
|
Kind reflect.Kind `json:"kind,omitempty"`
|
||||||
|
Tag reflect.StructTag `json:"tag,omitempty"`
|
||||||
|
Children []*Node `json:"children,omitempty"`
|
||||||
|
}
|
||||||
196
third_party/traefik/config/parser/nodes_metadata.go
vendored
Normal file
196
third_party/traefik/config/parser/nodes_metadata.go
vendored
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MetadataOpts Options for the metadata.
|
||||||
|
type MetadataOpts struct {
|
||||||
|
TagName string
|
||||||
|
AllowSliceAsStruct bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddMetadata adds metadata such as type, inferred from element, to a node.
|
||||||
|
func AddMetadata(element interface{}, node *Node, opts MetadataOpts) error {
|
||||||
|
return metadata{MetadataOpts: opts}.Add(element, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
type metadata struct {
|
||||||
|
MetadataOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds metadata such as type, inferred from element, to a node.
|
||||||
|
func (m metadata) Add(element interface{}, node *Node) error {
|
||||||
|
if node == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(node.Children) == 0 {
|
||||||
|
return fmt.Errorf("invalid node %s: no child", node.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if element == nil {
|
||||||
|
return errors.New("nil structure")
|
||||||
|
}
|
||||||
|
|
||||||
|
rootType := reflect.TypeOf(element)
|
||||||
|
node.Kind = rootType.Kind()
|
||||||
|
|
||||||
|
return m.browseChildren(rootType, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m metadata) browseChildren(fType reflect.Type, node *Node) error {
|
||||||
|
for _, child := range node.Children {
|
||||||
|
if err := m.add(fType, child); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m metadata) add(rootType reflect.Type, node *Node) error {
|
||||||
|
rType := rootType
|
||||||
|
if rootType.Kind() == reflect.Ptr {
|
||||||
|
rType = rootType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
field, err := m.findTypedField(rType, node)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = isSupportedType(field); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fType := field.Type
|
||||||
|
node.Kind = fType.Kind()
|
||||||
|
node.Tag = field.Tag
|
||||||
|
|
||||||
|
if fType.Kind() == reflect.Struct || fType.Kind() == reflect.Ptr && fType.Elem().Kind() == reflect.Struct ||
|
||||||
|
fType.Kind() == reflect.Map {
|
||||||
|
if len(node.Children) == 0 && field.Tag.Get(m.TagName) != TagLabelAllowEmpty {
|
||||||
|
return fmt.Errorf("%s cannot be a standalone element (type %s)", node.Name, fType)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.Disabled = len(node.Value) > 0 && !strings.EqualFold(node.Value, "true") && field.Tag.Get(m.TagName) == TagLabelAllowEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(node.Children) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if fType.Kind() == reflect.Struct || fType.Kind() == reflect.Ptr && fType.Elem().Kind() == reflect.Struct {
|
||||||
|
return m.browseChildren(fType, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fType.Kind() == reflect.Map {
|
||||||
|
for _, child := range node.Children {
|
||||||
|
// elem is a map entry value type
|
||||||
|
elem := fType.Elem()
|
||||||
|
child.Kind = elem.Kind()
|
||||||
|
|
||||||
|
if elem.Kind() == reflect.Map || elem.Kind() == reflect.Struct ||
|
||||||
|
(elem.Kind() == reflect.Ptr && elem.Elem().Kind() == reflect.Struct) {
|
||||||
|
if err = m.browseChildren(elem, child); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if fType.Kind() == reflect.Slice {
|
||||||
|
if m.AllowSliceAsStruct && field.Tag.Get(TagLabelSliceAsStruct) != "" {
|
||||||
|
return m.browseChildren(fType.Elem(), node)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ch := range node.Children {
|
||||||
|
ch.Kind = fType.Elem().Kind()
|
||||||
|
if err = m.browseChildren(fType.Elem(), ch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("invalid node %s: %v", node.Name, fType.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m metadata) findTypedField(rType reflect.Type, node *Node) (reflect.StructField, error) {
|
||||||
|
for i := 0; i < rType.NumField(); i++ {
|
||||||
|
cField := rType.Field(i)
|
||||||
|
|
||||||
|
fieldName := cField.Tag.Get(TagLabelSliceAsStruct)
|
||||||
|
if !m.AllowSliceAsStruct || len(fieldName) == 0 {
|
||||||
|
fieldName = cField.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsExported(cField) {
|
||||||
|
if cField.Anonymous {
|
||||||
|
if cField.Type.Kind() == reflect.Struct {
|
||||||
|
structField, err := m.findTypedField(cField.Type, node)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return structField, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.EqualFold(fieldName, node.Name) {
|
||||||
|
node.FieldName = cField.Name
|
||||||
|
return cField, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflect.StructField{}, fmt.Errorf("field not found, node: %s", node.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExported reports whether f is exported.
|
||||||
|
// https://golang.org/pkg/reflect/#StructField
|
||||||
|
func IsExported(f reflect.StructField) bool {
|
||||||
|
return f.PkgPath == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSupportedType(field reflect.StructField) error {
|
||||||
|
fType := field.Type
|
||||||
|
|
||||||
|
if fType.Kind() == reflect.Slice {
|
||||||
|
switch fType.Elem().Kind() {
|
||||||
|
case reflect.String,
|
||||||
|
reflect.Bool,
|
||||||
|
reflect.Int,
|
||||||
|
reflect.Int8,
|
||||||
|
reflect.Int16,
|
||||||
|
reflect.Int32,
|
||||||
|
reflect.Int64,
|
||||||
|
reflect.Uint,
|
||||||
|
reflect.Uint8,
|
||||||
|
reflect.Uint16,
|
||||||
|
reflect.Uint32,
|
||||||
|
reflect.Uint64,
|
||||||
|
reflect.Uintptr,
|
||||||
|
reflect.Float32,
|
||||||
|
reflect.Float64,
|
||||||
|
reflect.Struct,
|
||||||
|
reflect.Ptr:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported slice type: %v", fType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fType.Kind() == reflect.Map && fType.Key().Kind() != reflect.String {
|
||||||
|
return fmt.Errorf("unsupported map key type: %v", fType.Key())
|
||||||
|
}
|
||||||
|
|
||||||
|
if fType.Kind() == reflect.Func {
|
||||||
|
return fmt.Errorf("unsupported type: %v", fType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
1011
third_party/traefik/config/parser/nodes_metadata_test.go
vendored
Normal file
1011
third_party/traefik/config/parser/nodes_metadata_test.go
vendored
Normal file
File diff suppressed because it is too large
Load Diff
40
third_party/traefik/config/parser/parser.go
vendored
Normal file
40
third_party/traefik/config/parser/parser.go
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// Package parser implements decoding and encoding between a flat map of labels and a typed Configuration.
|
||||||
|
package parser
|
||||||
|
|
||||||
|
// Decode decodes the given map of labels into the given element.
|
||||||
|
// If any filters are present, labels which do not match the filters are skipped.
|
||||||
|
// The operation goes through three stages roughly summarized as:
|
||||||
|
// labels -> tree of untyped nodes
|
||||||
|
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
|
||||||
|
// "typed" nodes -> typed element
|
||||||
|
func Decode(labels map[string]string, element interface{}, rootName string, filters ...string) error {
|
||||||
|
node, err := DecodeToNode(labels, rootName, filters...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
metaOpts := MetadataOpts{TagName: TagLabel, AllowSliceAsStruct: true}
|
||||||
|
err = AddMetadata(element, node, metaOpts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = Fill(element, node, FillerOpts{AllowSliceAsStruct: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode converts an element to labels.
|
||||||
|
// element -> node (value) -> label (node)
|
||||||
|
func Encode(element interface{}, rootName string) (map[string]string, error) {
|
||||||
|
etnOpts := EncoderToNodeOpts{OmitEmpty: true, TagName: TagLabel, AllowSliceAsStruct: true}
|
||||||
|
node, err := EncodeToNode(element, rootName, etnOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return EncodeNode(node), nil
|
||||||
|
}
|
||||||
19
third_party/traefik/config/parser/tags.go
vendored
Normal file
19
third_party/traefik/config/parser/tags.go
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TagLabel allows to apply a custom behavior.
|
||||||
|
// - "allowEmpty": allows to create an empty struct.
|
||||||
|
// - "-": ignore the field.
|
||||||
|
TagLabel = "label"
|
||||||
|
|
||||||
|
// TagLabelSliceAsStruct allows to use a slice of struct by creating one entry into the slice.
|
||||||
|
// The value is the substitution name used in the label to access the slice.
|
||||||
|
TagLabelSliceAsStruct = "label-slice-as-struct"
|
||||||
|
|
||||||
|
// TagDescription is the documentation for the field.
|
||||||
|
// - "-": ignore the field.
|
||||||
|
TagDescription = "description"
|
||||||
|
|
||||||
|
// TagLabelAllowEmpty is related to TagLabel.
|
||||||
|
TagLabelAllowEmpty = "allowEmpty"
|
||||||
|
)
|
||||||
61
third_party/traefik/types/duration.go
vendored
Normal file
61
third_party/traefik/types/duration.go
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Duration is a custom type suitable for parsing duration values.
|
||||||
|
// It supports `time.ParseDuration`-compatible values and suffix-less digits; in
|
||||||
|
// the latter case, seconds are assumed.
|
||||||
|
type Duration time.Duration
|
||||||
|
|
||||||
|
// Set sets the duration from the given string value.
|
||||||
|
func (d *Duration) Set(s string) error {
|
||||||
|
if v, err := strconv.ParseInt(s, 10, 64); err == nil {
|
||||||
|
*d = Duration(time.Duration(v) * time.Second)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := time.ParseDuration(s)
|
||||||
|
*d = Duration(v)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the duration value.
|
||||||
|
func (d Duration) String() string { return (time.Duration)(d).String() }
|
||||||
|
|
||||||
|
// MarshalText serialize the given duration value into a text.
|
||||||
|
func (d Duration) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(d.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText deserializes the given text into a duration value.
|
||||||
|
// It is meant to support TOML decoding of durations.
|
||||||
|
func (d *Duration) UnmarshalText(text []byte) error {
|
||||||
|
return d.Set(string(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON serializes the given duration value.
|
||||||
|
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(time.Duration(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON deserializes the given text into a duration value.
|
||||||
|
func (d *Duration) UnmarshalJSON(text []byte) error {
|
||||||
|
if v, err := strconv.ParseInt(string(text), 10, 64); err == nil {
|
||||||
|
*d = Duration(time.Duration(v))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use json unmarshal on value because we have the quoted version
|
||||||
|
var value string
|
||||||
|
err := json.Unmarshal(text, &value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v, err := time.ParseDuration(value)
|
||||||
|
*d = Duration(v)
|
||||||
|
return err
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user