diff --git a/.res/compose/docker-compose.yml b/.res/compose/docker-compose.yml index 74e9f570..59a33dc1 100644 --- a/.res/compose/docker-compose.yml +++ b/.res/compose/docker-compose.yml @@ -6,9 +6,15 @@ services: container_name: diun volumes: - "./data:/data" - - "./diun.yml:/diun.yml:ro" + - "/var/run/docker.sock:/var/run/docker.sock" environment: - "TZ=Europe/Paris" - "LOG_LEVEL=info" - "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 diff --git a/.res/systemd/diun.service b/.res/systemd/diun.service index ba2ce978..f2b3a877 100644 --- a/.res/systemd/diun.service +++ b/.res/systemd/diun.service @@ -11,7 +11,7 @@ User=diun Group=diun ExecStart=/usr/local/bin/diun --config /etc/diun/diun.yml --log-level info Restart=always -Environment=DIUN_DB=/var/lib/diun/diun.db +Environment=DIUN_DB_PATH=/var/lib/diun/diun.db [Install] WantedBy=multi-user.target diff --git a/Dockerfile b/Dockerfile index 05b599b3..9904ea88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 RUN diun --version -ENV DIUN_DB="/data/diun.db" +ENV DIUN_DB_PATH="/data/diun.db" VOLUME [ "/data" ] - ENTRYPOINT [ "diun" ] -CMD [ "--config", "/diun.yml" ] diff --git a/README.md b/README.md index 706cdb29..e4cb6744 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ * [With Docker](doc/install/docker.md) * [From binary](doc/install/binary.md) * [Linux service](doc/install/linux-service.md) -* [Usage](doc/usage.md) +* [Getting started](doc/getting-started.md) * [Configuration](doc/configuration.md) * Providers * [Docker](doc/providers/docker.md) diff --git a/cmd/main.go b/cmd/main.go index c8ef9591..d389a670 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -5,6 +5,7 @@ import ( "os" "os/signal" "runtime" + "strings" "syscall" "time" @@ -20,15 +21,25 @@ var ( diun *app.Diun cli model.Cli 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() { 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 _ = kong.Parse(&cli, - kong.Name("diun"), - kong.Description(`Docker image update notifier. More info: https://github.com/crazy-max/diun`), + kong.Name(meta.ID), + kong.Description(fmt.Sprintf("%s. More info: %s", meta.Desc, meta.URL)), kong.UsageOnError(), kong.Vars{ "version": fmt.Sprintf("%s", version), @@ -46,7 +57,7 @@ func main() { // Init logging.Configure(&cli, location) - log.Info().Msgf("Starting Diun %s", version) + log.Info().Str("version", version).Msgf("Starting %s", meta.Name) // Handle os signals channel := make(chan os.Signal) @@ -59,15 +70,15 @@ func main() { }() // Load configuration - cfg, err := config.Load(cli, version) + cfg, err := config.Load(cli.Cfgfile) if err != nil { log.Fatal().Err(err).Msg("Cannot load configuration") } - log.Debug().Msg(cfg.Display()) + log.Debug().Msg(cfg.String()) // Init - if diun, err = app.New(cfg, location); err != nil { - log.Fatal().Err(err).Msg("Cannot initialize Diun") + if diun, err = app.New(meta, cfg, location); err != nil { + log.Fatal().Err(err).Msgf("Cannot initialize %s", meta.Name) } // Test notif @@ -78,6 +89,6 @@ func main() { // Start if err = diun.Start(); err != nil { - log.Fatal().Err(err).Msg("Cannot start Diun") + log.Fatal().Err(err).Msgf("Cannot start %s", meta.Name) } } diff --git a/doc/configuration.md b/doc/configuration.md index 52b7ea1d..5e0745f4 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -1,6 +1,7 @@ # Configuration * [Overview](#overview) +* [Configuration file](#configuration-file) * [Reference](#reference) * [db](#db) * [watch](#watch) @@ -10,6 +11,21 @@ ## 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 db: path: diun.db @@ -17,7 +33,7 @@ db: watch: workers: 10 schedule: "0 * * * *" - first_check_notif: false + firstCheckNotif: false notif: amqp: @@ -25,65 +41,62 @@ notif: port: 5672 username: guest password: guest - exchange: queue: queue gotify: endpoint: http://gotify.foo.com token: Token123456 priority: 1 - timeout: 10 + timeout: 10s mail: host: localhost port: 25 ssl: false - insecure_skip_verify: false - username: - password: - from: - to: + insecureSkipVerify: false + from: diun@example.com + to: webmaster@example.com rocketchat: endpoint: http://rocket.foo.com:3000 channel: "#general" - user_id: abcdEFGH012345678 + userID: abcdEFGH012345678 token: Token123456 - timeout: 10 + timeout: 10s script: cmd: "myprogram" args: - "--anarg" - "another" slack: - webhook_url: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij + webhookURL: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij teams: - webhook_url: https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij + webhookURL: https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij telegram: token: aabbccdd:11223344 - chat_ids: + chatIDs: - 123456789 - 987654321 webhook: endpoint: http://webhook.foo.com/sd54qad89azd5a method: GET headers: - Content-Type: application/json - Authorization: Token123456 - timeout: 10 + content-type: application/json + authorization: Token123456 + timeout: 10s regopts: someregistryoptions: username: foo password: bar - timeout: 20 + timeout: 20s onemore: username: foo2 password: bar2 - insecure_tls: true + insecureTls: true providers: docker: - watch_stopped: true + watchStopped: true swarm: - watch_by_default: true + watchByDefault: true file: directory: ./imagesdir ``` @@ -92,13 +105,23 @@ providers: ### 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 -* `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 * * * *`). -* `first_check_notif`: Send notification at the very first analysis of an image. (default: `false`). +* `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 * * * *`) +* `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 @@ -107,7 +130,7 @@ providers: * [mail](notifications.md#mail) * [rocketchat](notifications.md#rocketchat) * [script](notifications.md#script) -* [slack](notifications.md#slack) +* [slack](notifications.md#slack--mattermost) * [teams](notifications.md#teams) * [telegram](notifications.md#telegram) * [webhook](notifications.md#webhook) @@ -115,11 +138,20 @@ providers: ### regopts * `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_file`: 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`). -* `insecure_tls`: Allow contacting docker registry over HTTP, or HTTPS with failed TLS verification (default: `false`). +* `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. (default `10s`) +* `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__USERNAME` +* `DIUN_REGOPTS__USERNAMEFILE` +* `DIUN_REGOPTS__PASSWORD` +* `DIUN_REGOPTS__PASSWORDFILE` +* `DIUN_REGOPTS__TIMEOUT` +* `DIUN_REGOPTS__INSECURETLS` ### providers diff --git a/doc/faq.md b/doc/faq.md index 1fa0d557..797331bb 100644 --- a/doc/faq.md +++ b/doc/faq.md @@ -1,7 +1,74 @@ # 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-) +## 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 [] 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. diff --git a/doc/getting-started.md b/doc/getting-started.md new file mode 100644 index 00000000..b4839495 --- /dev/null +++ b/doc/getting-started.md @@ -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: {} +``` diff --git a/doc/install/binary.md b/doc/install/binary.md index 530ac6aa..d2c50cb8 100644 --- a/doc/install/binary.md +++ b/doc/install/binary.md @@ -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: ``` -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` 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. -``` +After getting the binary, it can be tested with [`./diun --help`](../getting-started.md#diun-cli) command and moved to a permanent location. ## Server configuration @@ -57,13 +38,15 @@ chmod 770 /etc/diun ### 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 chmod 644 /etc/diun/diun.yml ``` +> 💡 Not required if you want to only rely on environment variables + ### 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 ``` -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 diff --git a/doc/install/docker.md b/doc/install/docker.md index ff8ac583..cdec993b 100644 --- a/doc/install/docker.md +++ b/doc/install/docker.md @@ -18,37 +18,32 @@ Image: crazymax/diun:latest - 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 -* `/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. +* `/data`: Contains bbolt database which retains Docker images manifests ## 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 logs -f ``` -Or use the following command : +Or use the following command: ``` $ docker run -d --name diun \ -e "TZ=Europe/Paris" \ -e "LOG_LEVEL=info" \ -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)/diun.yml:/diun.yml:ro" \ + -v "/var/run/docker.sock:/var/run/docker.sock" \ crazymax/diun:latest ``` diff --git a/doc/notifications.md b/doc/notifications.md index b5200034..fd34f7eb 100644 --- a/doc/notifications.md +++ b/doc/notifications.md @@ -12,18 +12,33 @@ ## 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` - * `host`: AMQP server host (default: `localhost`). **required** - * `port`: AMQP server port (default: `5672`). **required** - * `username`: AMQP username. **required** - * `username_file`: Use content of secret file as AMQP username if `username` not defined. - * `password`: AMQP password. **required** - * `password_file`: 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`) + * `host`: AMQP server host (default `localhost`). **required** + * `port`: AMQP server port (default `5672`). **required** + * `username`: AMQP username. + * `usernameFile`: Use content of secret file as AMQP username if `username` not defined. + * `password`: AMQP password. + * `passwordFile`: Use content of secret file as AMQP password if `password` not defined. + * `exchange`: Name of the exchange the message will be sent to. * `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: ```json @@ -41,59 +56,92 @@ The JSON response will look like this: ## 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` * `endpoint`: Gotify base URL (e.g. `http://gotify.foo.com`). **required** * `token`: Application token. **required** - * `priority`: The priority of the message. - * `timeout`: Timeout specifies a time limit for the request to be made. (default: `10`). + * `priority`: The priority of the message (default `1`). + * `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 ![](../.res/notif-gotify.png) ## Mail -Notifications can be sent through SMTP: +Notifications can be sent through SMTP. + +### Configuration file * `mail` - * `host`: SMTP server host (default: `localhost`). **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`). - * `insecure_skip_verify`: Controls whether a client verifies the server's certificate chain and hostname (default: `false`). + * `host`: SMTP server host. (default `localhost`) **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`) + * `insecureSkipVerify`: Controls whether a client verifies the server's certificate chain and hostname. (default `false`) * `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_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** * `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 + ![](../.res/notif-mail.png) ## 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. +### Configuration file + * `rocketchat` * `endpoint`: Rocket.Chat base URL (e.g. `http://rocket.foo.com:3000`). **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** - * `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 ![](../.res/notif-rocketchat.png) ## Script -You can send script notifications with the following settings: - -* `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: +You can call a script when a notification occured. Following environment variables will be passed: ``` 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 ``` +### 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 -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` - * `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 ![](../.res/notif-slack.png) @@ -119,10 +188,18 @@ Mattermost webhooks are compatible with Slack notification without any special c ## 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` - * `webhook_url`: Teams [incoming webhook URL](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors). **required** +### Configuration file + +* `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 ![](../.res/notif-teams.png) @@ -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. Multiple chat IDs can be provided in order to deliver notifications to multiple recipients. +### Configuration file + * `telegram` * `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 ![](../.res/notif-telegram.png) ## Webhook -You can send webhook notifications with the following settings: +You can send webhook notifications with the following settings. + +### Configuration file * `webhook` * `endpoint`: URL of the HTTP request. **required** - * `method`: HTTP method (default: `GET`). **required** - * `headers`: Map of additional headers to be sent. - * `timeout`: Timeout specifies a time limit for the request to be made. (default: `10`). + * `method`: HTTP method (default `GET`). **required** + * `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 `10s`) + +### Environment variables + +* `DIUN_NOTIF_WEBHOOK_ENDPOINT` +* `DIUN_NOTIF_WEBHOOK_METHOD` +* `DIUN_NOTIF_WEBHOOK_HEADERS_` +* `DIUN_NOTIF_WEBHOOK_TIMEOUT` + +### Sample The JSON response will look like this: diff --git a/doc/providers/docker.md b/doc/providers/docker.md index ac1ff1a6..1ccf7abd 100644 --- a/doc/providers/docker.md +++ b/doc/providers/docker.md @@ -3,6 +3,8 @@ * [About](#about) * [Quick start](#quick-start) * [Provider configuration](#provider-configuration) + * [Configuration file](#configuration-file) + * [Environment variables](#environment-variables) * [Docker labels](#docker-labels) ## 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. -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. Now let's create a simple docker-compose file with Diun and some simple services: @@ -37,12 +27,15 @@ services: image: crazymax/diun:latest volumes: - "./data:/data" - - "./diun.yml:/diun.yml:ro" - "/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_WATCHSTOPPED=true" restart: always cloudflared: @@ -90,20 +83,32 @@ diun_1 | Sat, 14 Dec 2019 15:30:13 CET INF Next run in 29 minutes (2019- ## Provider configuration +### Configuration file + * `endpoint`: Server address to connect to. Local if empty. -* `api_version`: Overrides the client version with the specified one. -* `tls_certs_path`: Path to load the TLS certificates from. -* `tls_verify`: 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`). -* `watch_stopped`: Include created and exited containers too (default: `false`). +* `apiVersion`: Overrides the client version with the specified one. +* `tlsCertsPath`: Path to load the TLS certificates from. +* `tlsVerify`: Controls whether client verifies the server's certificate chain and hostname (default `true`). +* `watchByDefault`: Enable watch by default. If false, containers that don't have `diun.enable=true` label will be ignored (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 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.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.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.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`. diff --git a/doc/providers/file.md b/doc/providers/file.md index 0fb5018c..7c120a8f 100644 --- a/doc/providers/file.md +++ b/doc/providers/file.md @@ -4,8 +4,8 @@ * [Example](#example) * [Quick start](#quick-start) * [Provider configuration](#provider-configuration) - * [filename](#filename) - * [directory](#directory) + * [Configuration file](#configuration-file) + * [Environment variables](#environment-variables) * [YAML configuration file](#yaml-configuration-file) ## About @@ -32,7 +32,7 @@ regopts: onemore: username: foo2 password: bar2 - insecure_tls: true + insecureTls: true providers: 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 -### filename +### Configuration file + +#### filename Defines the path to the [configuration file](#yaml-configuration-file). @@ -143,7 +145,7 @@ providers: 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`). @@ -155,14 +157,19 @@ providers: directory: /path/to/config ``` +### Environment variables + +* `DIUN_PROVIDERS_FILE_DIRECTORY` +* `DIUN_PROVIDERS_FILE_FILENAME` + ## YAML configuration file 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** * `regopts_id`: Registry options ID from [`regopts`](../configuration.md#regopts) to use. -* `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`). +* `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`). * `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`. * `platform`: Check a custom platform. (default will retrieve platform dynamically based on your operating system). diff --git a/doc/providers/swarm.md b/doc/providers/swarm.md index 1eca526c..e1678b2e 100644 --- a/doc/providers/swarm.md +++ b/doc/providers/swarm.md @@ -3,6 +3,8 @@ * [About](#about) * [Quick start](#quick-start) * [Provider configuration](#provider-configuration) + * [Configuration file](#configuration-file) + * [Environment variables](#environment-variables) * [Docker labels](#docker-labels) ## 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. -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). Now let's create a simple stack for Diun: @@ -36,12 +27,14 @@ services: image: crazymax/diun:latest volumes: - "./data:/data" - - "./diun.yml:/diun.yml:ro" - "/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_SWARM=true" deploy: placement: constraints: @@ -75,7 +68,7 @@ docker stack deploy -c diun.yml diun 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 @@ -105,19 +98,30 @@ diun_diun.1.i1l4yuiafq6y@docker-desktop | Sat, 14 Dec 2019 16:20:02 CET INF N ## Provider configuration +### Configuration file + * `endpoint`: Server address to connect to. Local if empty. -* `api_version`: Overrides the client version with the specified one. -* `tls_certs_path`: Path to load the TLS certificates from. -* `tls_verify`: 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`). +* `apiVersion`: Overrides the client version with the specified one. +* `TLSCertsPath`: Path to load the TLS certificates from. +* `TLSVerify`: Controls whether client verifies the server's certificate chain and hostname (default `true`). +* `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 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.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.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.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`. diff --git a/doc/usage.md b/doc/usage.md deleted file mode 100644 index a1a6f91f..00000000 --- a/doc/usage.md +++ /dev/null @@ -1,14 +0,0 @@ -# Usage - -## Command line - -`diun --config=STRING` - -* `--help`: Show help text and exit. -* `--version`: Show version and exit. -* `--config `: Diun YAML configuration file. **Required**. (e.g. `diun.yml`). -* `--timezone `: Timezone assigned to Diun. (default `UTC`). -* `--log-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. diff --git a/go.mod b/go.mod index 3d6110c0..d4b735bf 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/docker/docker v1.4.2-0.20191219165747-a9416c67da9f github.com/docker/go-connections v0.4.0 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/hako/durafmt v0.0.0-20190612201238-650ed9f29a84 github.com/imdario/mergo v0.3.9 diff --git a/go.sum b/go.sum index f206bbeb..7f80f28d 100644 --- a/go.sum +++ b/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-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-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-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= @@ -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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 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/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI= github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= diff --git a/internal/app/diun.go b/internal/app/diun.go index a0c2ae3c..fdfa6bc7 100644 --- a/internal/app/diun.go +++ b/internal/app/diun.go @@ -1,9 +1,6 @@ package app import ( - "fmt" - "runtime" - "strings" "sync" "sync/atomic" "time" @@ -25,42 +22,39 @@ import ( // Diun represents an active diun object type Diun struct { - cfg *config.Config - cron *cron.Cron - db *db.Client - notif *notif.Client - userAgent string - jobID cron.EntryID - locker uint32 - pool *ants.PoolWithFunc - wg *sync.WaitGroup + meta model.Meta + cfg *config.Config + cron *cron.Cron + db *db.Client + notif *notif.Client + jobID cron.EntryID + locker uint32 + pool *ants.PoolWithFunc + wg *sync.WaitGroup } // 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 - dbcli, err := db.New(cfg.Db) + dbcli, err := db.New(*cfg.Db) if err != nil { 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 - notifcli, err := notif.New(cfg.Notif, cfg.App, userAgent) + notifcli, err := notif.New(cfg.Notif, meta) if err != nil { return nil, err } return &Diun{ - cfg: cfg, + meta: meta, + cfg: cfg, cron: cron.New(cron.WithLocation(location), cron.WithParser(cron.NewParser( cron.SecondOptional|cron.Minute|cron.Hour|cron.Dom|cron.Month|cron.Dow|cron.Descriptor), )), - db: dbcli, - notif: notifcli, - userAgent: userAgent, + db: dbcli, + notif: notifcli, }, nil } diff --git a/internal/app/job.go b/internal/app/job.go index 5b6cb8eb..190c08be 100644 --- a/internal/app/job.go +++ b/internal/app/job.go @@ -3,7 +3,6 @@ package app import ( "fmt" "regexp" - "time" "github.com/crazy-max/diun/v3/internal/model" "github.com/crazy-max/diun/v3/pkg/registry" @@ -35,7 +34,7 @@ func (di *Diun) createJob(job model.Job) { } // Registry options - regOpts, err := di.getRegOpts(job.Image.RegOptsID) + regOpts, err := di.cfg.GetRegOpts(job.Image.RegOptsID) if err != nil { 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{ Username: regUser, Password: regPassword, - Timeout: time.Duration(regOpts.Timeout) * time.Second, - InsecureTLS: regOpts.InsecureTLS, - UserAgent: di.userAgent, + Timeout: *regOpts.Timeout, + InsecureTLS: *regOpts.InsecureTLS, + UserAgent: di.meta.UserAgent, ImageOs: job.Image.Platform.Os, ImageArch: job.Image.Platform.Arch, ImageVariant: job.Image.Platform.Variant, @@ -94,7 +93,7 @@ func (di *Diun) createJob(job model.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 } @@ -157,7 +156,7 @@ func (di *Diun) runJob(job model.Job) error { } status := model.ImageStatusUnchange - if dbManifest.Name == "" { + if len(dbManifest.Name) == 0 { status = model.ImageStatusNew sublog.Info().Msg("New image found") } else if !liveManifest.Created.Equal(*dbManifest.Created) { @@ -187,13 +186,3 @@ func (di *Diun) runJob(job model.Job) error { 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) -} diff --git a/internal/config/config.go b/internal/config/config.go index 1e8f69e5..200b07f7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,92 +3,91 @@ package config import ( "encoding/json" "fmt" - "io/ioutil" "os" - "path" "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" - "gopkg.in/yaml.v2" ) // Config holds configuration details type Config struct { - Cli model.Cli - App model.App - Db model.Db `yaml:"db,omitempty"` - Watch model.Watch `yaml:"watch,omitempty"` - Notif *model.Notif `yaml:"notif,omitempty"` - RegOpts map[string]model.RegOpts `yaml:"regopts,omitempty"` - Providers *model.Providers `yaml:"providers,omitempty"` + Db *model.Db `yaml:"db,omitempty" json:"db,omitempty"` + Watch *model.Watch `yaml:"watch,omitempty" json:"watch,omitempty"` + Notif *model.Notif `yaml:"notif,omitempty" json:"notif,omitempty"` + RegOpts map[string]*model.RegOpts `yaml:"regopts,omitempty" json:"regopts,omitempty" validate:"unique"` + Providers *model.Providers `yaml:"providers,omitempty" json:"providers,omitempty" validate:"required"` } // Load returns Configuration struct -func Load(cli model.Cli, version string) (*Config, error) { - var err error - var cfg = Config{ - Cli: cli, - 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(), - }, +func Load(cfgfile string) (*Config, error) { + cfg := Config{ + Db: (&model.Db{}).GetDefaults(), + Watch: (&model.Watch{}).GetDefaults(), } - if _, err = os.Lstat(cli.Cfgfile); err != nil { - return nil, fmt.Errorf("unable to open config file, %s", err) + if err := cfg.loadFile(cfgfile, &cfg); err != nil { + return nil, err } - bytes, err := ioutil.ReadFile(cli.Cfgfile) - if err != nil { - return nil, fmt.Errorf("unable to read config file, %s", err) + if err := cfg.loadEnv(&cfg); err != nil { + return nil, err } - if err := yaml.UnmarshalStrict(bytes, &cfg); err != nil { - return nil, fmt.Errorf("unable to decode into struct, %v", err) - } - - if err := cfg.validate(); err != nil { + validate := validator.New() + if err := validate.Struct(&cfg); err != nil { return nil, err } return &cfg, nil } -func (cfg *Config) validate() error { - cfg.Db.Path = utl.GetEnv("DIUN_DB", cfg.Db.Path) - if cfg.Db.Path == "" { - return errors.New("database path is required") +func (cfg *Config) loadFile(cfgfile string, out interface{}) error { + if len(cfgfile) == 0 { + return nil } - cfg.Db.Path = path.Clean(cfg.Db.Path) - if err := cfg.validateNotif(); err != nil { - return err + if _, err := os.Lstat(cfgfile); os.IsNotExist(err) { + return fmt.Errorf("config file %s not found", cfgfile) } - if err := cfg.validateRegopts(); err != nil { - return err - } - if err := cfg.validateProviders(); err != nil { - return err + + if err := file.Decode(cfgfile, out); err != nil { + return errors.Wrap(err, "failed to decode configuration from file") } return nil } -// Display configuration in a pretty JSON format -func (cfg *Config) Display() string { +func (cfg *Config) loadEnv(out interface{}) error { + 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, "", " ") return string(b) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 83b8fb0a..2964a97a 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,57 +1,46 @@ package config_test import ( + "os" + "strings" "testing" + "time" "github.com/crazy-max/diun/v3/internal/config" "github.com/crazy-max/diun/v3/internal/model" "github.com/crazy-max/diun/v3/pkg/utl" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestLoad(t *testing.T) { +func TestLoadFile(t *testing.T) { cases := []struct { name string - cli model.Cli + cfgfile string wantData *config.Config wantErr bool }{ { - name: "Fail on non-existing file", - cli: model.Cli{}, + name: "Failed on non-existing file", + cfgfile: "", wantErr: true, }, { - name: "Fail on wrong file format", - cli: model.Cli{ - Cfgfile: "./test/config.invalid.yml", - }, + name: "Fail on wrong file format", + cfgfile: "./fixtures/config.invalid.yml", wantErr: true, }, { - name: "Success", - cli: model.Cli{ - Cfgfile: "./test/config.test.yml", - }, + name: "Success", + cfgfile: "./fixtures/config.test.yml", wantData: &config.Config{ - Cli: model.Cli{ - 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{ + Db: &model.Db{ Path: "diun.db", }, - Watch: model.Watch{ + Watch: &model.Watch{ Workers: 100, Schedule: "*/30 * * * *", - FirstCheckNotif: utl.NewFalse(), + FirstCheckNotif: utl.NewTrue(), }, Notif: &model.Notif{ Amqp: &model.NotifAmqp{ @@ -65,7 +54,7 @@ func TestLoad(t *testing.T) { Endpoint: "http://gotify.foo.com", Token: "Token123456", Priority: 1, - Timeout: 10, + Timeout: utl.NewDuration(10 * time.Second), }, Mail: &model.NotifMail{ Host: "localhost", @@ -80,12 +69,12 @@ func TestLoad(t *testing.T) { Channel: "#general", UserID: "abcdEFGH012345678", Token: "Token123456", - Timeout: 10, + Timeout: utl.NewDuration(10 * time.Second), }, Script: &model.NotifScript{ - Cmd: "go", + Cmd: "uname", Args: []string{ - "version", + "-a", }, }, Slack: &model.NotifSlack{ @@ -95,30 +84,35 @@ func TestLoad(t *testing.T) { WebhookURL: "https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij", }, Telegram: &model.NotifTelegram{ - BotToken: "abcdef123456", - ChatIDs: []int64{8547439, 1234567}, + Token: "abcdef123456", + ChatIDs: []int64{8547439, 1234567}, }, Webhook: &model.NotifWebhook{ Endpoint: "http://webhook.foo.com/sd54qad89azd5a", Method: "GET", Headers: map[string]string{ - "Content-Type": "application/json", - "Authorization": "Token123456", + "content-type": "application/json", + "authorization": "Token123456", }, - Timeout: 10, + Timeout: utl.NewDuration(10 * time.Second), }, }, - RegOpts: map[string]model.RegOpts{ + RegOpts: map[string]*model.RegOpts{ "someregopts": { - Timeout: 5, + InsecureTLS: utl.NewFalse(), + Timeout: utl.NewDuration(5 * time.Second), }, "bintrayoptions": { - Username: "foo", - Password: "bar", + Username: "foo", + Password: "bar", + InsecureTLS: utl.NewFalse(), + Timeout: utl.NewDuration(10 * time.Second), }, "sensitive": { UsernameFile: "/run/secrets/username", PasswordFile: "/run/secrets/password", + InsecureTLS: utl.NewFalse(), + Timeout: utl.NewDuration(10 * time.Second), }, }, Providers: &model.Providers{ @@ -129,10 +123,10 @@ func TestLoad(t *testing.T) { }, Swarm: &model.PrdSwarm{ TLSVerify: utl.NewTrue(), - WatchByDefault: utl.NewTrue(), + WatchByDefault: utl.NewFalse(), }, File: &model.PrdFile{ - Filename: "./test/dummy.yml", + Filename: "./fixtures/dummy.yml", }, }, }, @@ -140,14 +134,341 @@ func TestLoad(t *testing.T) { } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - cfg, err := config.Load(tt.cli, "test") - if !tt.wantErr && err != nil { - t.Error(err) + cfg, err := config.Load(tt.cfgfile) + if tt.wantErr { + require.Error(t, err) + return } + require.NoError(t, err) assert.Equal(t, tt.wantData, cfg) - if !tt.wantErr && cfg != nil { - assert.NotEmpty(t, cfg.Display()) + if cfg != nil { + 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) + } + } + } +} diff --git a/internal/config/fixtures/config.docker.yml b/internal/config/fixtures/config.docker.yml new file mode 100644 index 00000000..6922649a --- /dev/null +++ b/internal/config/fixtures/config.docker.yml @@ -0,0 +1,11 @@ +notif: + mail: + host: localhost + port: 25 + ssl: false + insecureSkipVerify: false + from: diun@example.com + to: webmaster@example.com + +providers: + docker: {} diff --git a/internal/config/fixtures/config.file-regopts.yml b/internal/config/fixtures/config.file-regopts.yml new file mode 100644 index 00000000..3bd6d090 --- /dev/null +++ b/internal/config/fixtures/config.file-regopts.yml @@ -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" diff --git a/internal/config/test/config.invalid.yml b/internal/config/fixtures/config.invalid.yml similarity index 100% rename from internal/config/test/config.invalid.yml rename to internal/config/fixtures/config.invalid.yml diff --git a/internal/config/test/config.test.yml b/internal/config/fixtures/config.test.yml similarity index 52% rename from internal/config/test/config.test.yml rename to internal/config/fixtures/config.test.yml index 7b39320f..79d51008 100644 --- a/internal/config/test/config.test.yml +++ b/internal/config/fixtures/config.test.yml @@ -4,7 +4,7 @@ db: watch: workers: 100 schedule: "*/30 * * * *" - first_check_notif: false + firstCheckNotif: true notif: amqp: @@ -17,60 +17,55 @@ notif: endpoint: http://gotify.foo.com token: Token123456 priority: 1 - timeout: 10 + timeout: 10s mail: host: localhost port: 25 ssl: false - insecure_skip_verify: false - username: - username_file: - password: - password_file: + insecureSkipVerify: false from: diun@example.com to: webmaster@example.com rocketchat: endpoint: http://rocket.foo.com:3000 channel: "#general" - user_id: abcdEFGH012345678 + userID: abcdEFGH012345678 token: Token123456 - timeout: 10 + timeout: 10s script: - cmd: "go" + cmd: "uname" args: - - "version" + - "-a" slack: - webhook_url: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij + webhookURL: https://hooks.slack.com/services/ABCD12EFG/HIJK34LMN/01234567890abcdefghij teams: - webhook_url: https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij + webhookURL: https://outlook.office.com/webhook/ABCD12EFG/HIJK34LMN/01234567890abcdefghij telegram: token: abcdef123456 - chat_ids: + chatIDs: - 8547439 - 1234567 webhook: endpoint: http://webhook.foo.com/sd54qad89azd5a method: GET headers: - Content-Type: application/json - Authorization: Token123456 - timeout: 10 + content-type: application/json + authorization: Token123456 + timeout: 10s regopts: someregopts: - timeout: 5 + timeout: 5s bintrayoptions: username: foo password: bar sensitive: - username_file: /run/secrets/username - password_file: /run/secrets/password + usernameFile: /run/secrets/username + passwordFile: /run/secrets/password providers: docker: - watch_by_default: true - watch_stopped: true - swarm: - watch_by_default: true + watchByDefault: true + watchStopped: true + swarm: {} file: - filename: ./test/dummy.yml + filename: ./fixtures/dummy.yml diff --git a/internal/config/fixtures/config.validate.yml b/internal/config/fixtures/config.validate.yml new file mode 100644 index 00000000..ab54e5ad --- /dev/null +++ b/internal/config/fixtures/config.validate.yml @@ -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 diff --git a/internal/config/test/dummy.yml b/internal/config/fixtures/dummy.yml similarity index 100% rename from internal/config/test/dummy.yml rename to internal/config/fixtures/dummy.yml diff --git a/internal/config/test/myscript.sh b/internal/config/fixtures/myscript.sh similarity index 100% rename from internal/config/test/myscript.sh rename to internal/config/fixtures/myscript.sh diff --git a/internal/config/notif.go b/internal/config/notif.go deleted file mode 100644 index fcf368cf..00000000 --- a/internal/config/notif.go +++ /dev/null @@ -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 -} diff --git a/internal/config/providers.go b/internal/config/providers.go deleted file mode 100644 index 924ce23f..00000000 --- a/internal/config/providers.go +++ /dev/null @@ -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 -} diff --git a/internal/config/regopts.go b/internal/config/regopts.go deleted file mode 100644 index 8a0c17e9..00000000 --- a/internal/config/regopts.go +++ /dev/null @@ -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 -} diff --git a/internal/model/app.go b/internal/model/app.go deleted file mode 100644 index 567fd433..00000000 --- a/internal/model/app.go +++ /dev/null @@ -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 -} diff --git a/internal/model/cli.go b/internal/model/cli.go index 9e3489d1..122b4dc4 100644 --- a/internal/model/cli.go +++ b/internal/model/cli.go @@ -5,7 +5,7 @@ import "github.com/alecthomas/kong" // Cli holds command line args, flags and cmds type Cli struct { 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.'"` 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.'"` diff --git a/internal/model/db.go b/internal/model/db.go index a05c4b64..b2a2450b 100644 --- a/internal/model/db.go +++ b/internal/model/db.go @@ -2,5 +2,17 @@ package model // Db holds data necessary for database configuration 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" } diff --git a/internal/model/meta.go b/internal/model/meta.go new file mode 100644 index 00000000..c73f36ae --- /dev/null +++ b/internal/model/meta.go @@ -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 +} diff --git a/internal/model/notif.go b/internal/model/notif.go index f8a17405..3e7fa781 100644 --- a/internal/model/notif.go +++ b/internal/model/notif.go @@ -14,87 +14,23 @@ type NotifEntry struct { // Notif holds data necessary for notification configuration type Notif struct { - Amqp *NotifAmqp `yaml:"amqp,omitempty"` - Gotify *NotifGotify `yaml:"gotify,omitempty"` - Mail *NotifMail `yaml:"mail,omitempty"` - RocketChat *NotifRocketChat `yaml:"rocketchat,omitempty"` - Script *NotifScript `yaml:"script,omitempty"` - Slack *NotifSlack `yaml:"slack,omitempty"` - Teams *NotifTeams `yaml:"teams,omitempty"` - Telegram *NotifTelegram `yaml:"telegram,omitempty"` - Webhook *NotifWebhook `yaml:"webhook,omitempty"` + Amqp *NotifAmqp `yaml:"amqp,omitempty" json:"amqp,omitempty"` + Gotify *NotifGotify `yaml:"gotify,omitempty" json:"gotify,omitempty"` + Mail *NotifMail `yaml:"mail,omitempty" json:"mail,omitempty"` + RocketChat *NotifRocketChat `yaml:"rocketchat,omitempty" json:"rocketchat,omitempty"` + Script *NotifScript `yaml:"script,omitempty" json:"script,omitempty"` + Slack *NotifSlack `yaml:"slack,omitempty" json:"slack,omitempty"` + Teams *NotifTeams `yaml:"teams,omitempty" json:"teams,omitempty"` + Telegram *NotifTelegram `yaml:"telegram,omitempty" json:"telegram,omitempty"` + Webhook *NotifWebhook `yaml:"webhook,omitempty" json:"webhook,omitempty"` } -// NotifAmqp holds amqp notification configuration details -type NotifAmqp struct { - Username string `yaml:"username,omitempty"` - 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"` +// GetDefaults gets the default values +func (s *Notif) GetDefaults() *Notif { + return nil } -// NotifGotify holds gotify notification configuration details -type NotifGotify struct { - Endpoint string `yaml:"endpoint,omitempty"` - 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"` +// SetDefaults sets the default values +func (s *Notif) SetDefaults() { + // noop } diff --git a/internal/model/notif_amqp.go b/internal/model/notif_amqp.go new file mode 100644 index 00000000..c3bf3638 --- /dev/null +++ b/internal/model/notif_amqp.go @@ -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 +} diff --git a/internal/model/notif_gotify.go b/internal/model/notif_gotify.go new file mode 100644 index 00000000..87770312 --- /dev/null +++ b/internal/model/notif_gotify.go @@ -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) +} diff --git a/internal/model/notif_mail.go b/internal/model/notif_mail.go new file mode 100644 index 00000000..6563bfa8 --- /dev/null +++ b/internal/model/notif_mail.go @@ -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 +} diff --git a/internal/model/notif_rocketchat.go b/internal/model/notif_rocketchat.go new file mode 100644 index 00000000..ea9d5ae2 --- /dev/null +++ b/internal/model/notif_rocketchat.go @@ -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) +} diff --git a/internal/model/notif_script.go b/internal/model/notif_script.go new file mode 100644 index 00000000..6ee13f96 --- /dev/null +++ b/internal/model/notif_script.go @@ -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 +} diff --git a/internal/model/notif_slack.go b/internal/model/notif_slack.go new file mode 100644 index 00000000..4d2dd68a --- /dev/null +++ b/internal/model/notif_slack.go @@ -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 +} diff --git a/internal/model/notif_teams.go b/internal/model/notif_teams.go new file mode 100644 index 00000000..e2245308 --- /dev/null +++ b/internal/model/notif_teams.go @@ -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 +} diff --git a/internal/model/notif_telegram.go b/internal/model/notif_telegram.go new file mode 100644 index 00000000..992bbaed --- /dev/null +++ b/internal/model/notif_telegram.go @@ -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 +} diff --git a/internal/model/notif_webhook.go b/internal/model/notif_webhook.go new file mode 100644 index 00000000..08a39ae7 --- /dev/null +++ b/internal/model/notif_webhook.go @@ -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) +} diff --git a/internal/model/provider_docker.go b/internal/model/provider_docker.go new file mode 100644 index 00000000..640de832 --- /dev/null +++ b/internal/model/provider_docker.go @@ -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() +} diff --git a/internal/model/provider_file.go b/internal/model/provider_file.go new file mode 100644 index 00000000..50a2f169 --- /dev/null +++ b/internal/model/provider_file.go @@ -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 +} diff --git a/internal/model/provider_swarm.go b/internal/model/provider_swarm.go new file mode 100644 index 00000000..f3c04a48 --- /dev/null +++ b/internal/model/provider_swarm.go @@ -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() +} diff --git a/internal/model/providers.go b/internal/model/providers.go index 48a19353..4fb3fd96 100644 --- a/internal/model/providers.go +++ b/internal/model/providers.go @@ -2,32 +2,17 @@ package model // Providers represents a provider configuration type Providers struct { - Docker *PrdDocker `yaml:"docker,omitempty" json:",omitempty"` - Swarm *PrdSwarm `yaml:"swarm,omitempty" json:",omitempty"` - File *PrdFile `yaml:"file,omitempty" json:",omitempty"` + Docker *PrdDocker `yaml:"docker,omitempty" json:"docker,omitempty" label:"allowEmpty"` + Swarm *PrdSwarm `yaml:"swarm,omitempty" json:"swarm,omitempty" label:"allowEmpty"` + File *PrdFile `yaml:"file,omitempty" json:"file,omitempty"` } -// PrdDocker holds docker provider configuration -type PrdDocker struct { - Endpoint string `yaml:"endpoint,omitempty" json:",omitempty"` - 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"` +// GetDefaults gets the default values +func (s *Providers) GetDefaults() *Providers { + return nil } -// PrdSwarm holds swarm provider configuration -type PrdSwarm struct { - Endpoint string `yaml:"endpoint,omitempty" json:",omitempty"` - 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"` +// SetDefaults sets the default values +func (s *Providers) SetDefaults() { + // noop } diff --git a/internal/model/regopts.go b/internal/model/regopts.go index 7b4dc112..1faf2c73 100644 --- a/internal/model/regopts.go +++ b/internal/model/regopts.go @@ -1,11 +1,30 @@ package model +import ( + "time" + + "github.com/crazy-max/diun/v3/pkg/utl" +) + // RegOpts holds registry options configuration type RegOpts struct { - Username string `yaml:"username,omitempty" json:",omitempty"` - UsernameFile string `yaml:"username_file,omitempty" json:",omitempty"` - Password string `yaml:"password,omitempty" json:",omitempty"` - PasswordFile string `yaml:"password_file,omitempty" json:",omitempty"` - InsecureTLS bool `yaml:"insecure_tls,omitempty" json:",omitempty"` - Timeout int `yaml:"timeout,omitempty" json:",omitempty"` + 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"` + InsecureTLS *bool `yaml:"insecureTls,omitempty" json:"insecureTls,omitempty" validate:"required"` + 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) } diff --git a/internal/model/watch.go b/internal/model/watch.go index cdd76445..8e3c7060 100644 --- a/internal/model/watch.go +++ b/internal/model/watch.go @@ -1,8 +1,26 @@ package model +import ( + "github.com/crazy-max/diun/v3/pkg/utl" +) + // Watch holds data necessary for watch configuration type Watch struct { - Workers int `yaml:"workers,omitempty"` - Schedule string `yaml:"schedule,omitempty"` - FirstCheckNotif *bool `yaml:"first_check_notif,omitempty"` + Workers int `yaml:"workers,omitempty" json:"workers,omitempty" validate:"required,min=1"` + Schedule string `yaml:"schedule,omitempty" json:"schedule,omitempty" validate:"required"` + 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() } diff --git a/internal/notif/amqp/client.go b/internal/notif/amqp/client.go index c9961258..7c633c8b 100644 --- a/internal/notif/amqp/client.go +++ b/internal/notif/amqp/client.go @@ -15,16 +15,16 @@ import ( // Client represents an active amqp notification object type Client struct { *notifier.Notifier - cfg *model.NotifAmqp - app model.App + cfg *model.NotifAmqp + meta model.Meta } // 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{ Handler: &Client{ - cfg: config, - app: app, + cfg: config, + meta: meta, }, } } @@ -36,9 +36,7 @@ func (c *Client) Name() string { // Send creates and sends a amqp notification with an entry func (c *Client) Send(entry model.NotifEntry) error { - username, err := utl.GetSecret(c.cfg.Username, c.cfg.UsernameFile) - if err != nil { return err } @@ -48,52 +46,31 @@ func (c *Client) Send(entry model.NotifEntry) error { return err } - connString := fmt.Sprintf("amqp://%s:%s@%s:%d/", username, password, c.cfg.Host, c.cfg.Port) - - conn, err := amqp.Dial(connString) + conn, err := amqp.Dial(fmt.Sprintf("amqp://%s:%s@%s:%d/", username, password, c.cfg.Host, c.cfg.Port)) if err != nil { return err } - defer conn.Close() ch, err := conn.Channel() if err != nil { return err } - defer ch.Close() q, err := ch.QueueDeclare( - c.cfg.Queue, // name - false, // durable - false, // delete when unused - false, // exclusive - false, // no-wait - nil, // arguments + c.cfg.Queue, + false, + false, + false, + false, + nil, ) if err != nil { return err } - body, err := buildBody(entry, c.app) - 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 { + body, err := json.Marshal(struct { Version string `json:"diun_version"` Status string `json:"status"` Provider string `json:"provider"` @@ -103,7 +80,7 @@ func buildBody(entry model.NotifEntry, app model.App) ([]byte, error) { Created *time.Time `json:"created"` Platform string `json:"platform"` }{ - Version: app.Version, + Version: c.meta.Version, Status: string(entry.Status), Provider: entry.Provider, Image: entry.Image.String(), @@ -112,4 +89,17 @@ func buildBody(entry model.NotifEntry, app model.App) ([]byte, error) { Created: entry.Manifest.Created, 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, + }) } diff --git a/internal/notif/client.go b/internal/notif/client.go index a3b04577..1420bbbb 100644 --- a/internal/notif/client.go +++ b/internal/notif/client.go @@ -1,6 +1,8 @@ package notif import ( + "strings" + "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/gotify" @@ -18,15 +20,15 @@ import ( // Client represents an active webhook notification object type Client struct { cfg *model.Notif - app model.App + meta model.Meta notifiers []notifier.Notifier } // 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{ cfg: config, - app: app, + meta: meta, notifiers: []notifier.Notifier{}, } @@ -37,31 +39,31 @@ func New(config *model.Notif, app model.App, userAgent string) (*Client, error) // Add notifiers 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 { - 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 { - c.notifiers = append(c.notifiers, mail.New(config.Mail, app)) + c.notifiers = append(c.notifiers, mail.New(config.Mail, meta)) } 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 { - c.notifiers = append(c.notifiers, script.New(config.Script, app)) + c.notifiers = append(c.notifiers, script.New(config.Script, meta)) } 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 { - 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 { - c.notifiers = append(c.notifiers, telegram.New(config.Telegram, app)) + c.notifiers = append(c.notifiers, telegram.New(config.Telegram, meta)) } 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)) @@ -73,7 +75,7 @@ func (c *Client) Send(entry model.NotifEntry) { for _, n := range c.notifiers { log.Debug().Str("image", entry.Image.String()).Msgf("Sending %s notification...", n.Name()) 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())) } } } diff --git a/internal/notif/gotify/client.go b/internal/notif/gotify/client.go index 12f8b895..5b38a438 100644 --- a/internal/notif/gotify/client.go +++ b/internal/notif/gotify/client.go @@ -10,7 +10,6 @@ import ( "strconv" "strings" "text/template" - "time" "github.com/crazy-max/diun/v3/internal/model" "github.com/crazy-max/diun/v3/internal/notif/notifier" @@ -19,18 +18,16 @@ import ( // Client represents an active gotify notification object type Client struct { *notifier.Notifier - cfg *model.NotifGotify - app model.App - userAgent string + cfg *model.NotifGotify + meta model.Meta } // 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{ Handler: &Client{ - cfg: config, - app: app, - userAgent: userAgent, + cfg: config, + meta: meta, }, } } @@ -43,7 +40,7 @@ func (c *Client) Name() string { // Send creates and sends a gotify notification with an entry func (c *Client) Send(entry model.NotifEntry) error { 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()) @@ -79,7 +76,7 @@ func (c *Client) Send(entry model.NotifEntry) error { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 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) if err != nil { diff --git a/internal/notif/mail/client.go b/internal/notif/mail/client.go index e4151711..198de2d9 100644 --- a/internal/notif/mail/client.go +++ b/internal/notif/mail/client.go @@ -18,16 +18,16 @@ import ( // Client represents an active mail notification object type Client struct { *notifier.Notifier - cfg *model.NotifMail - app model.App + cfg *model.NotifMail + meta model.Meta } // 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{ Handler: &Client{ - cfg: config, - app: app, + cfg: config, + meta: meta, }, } } @@ -42,14 +42,14 @@ func (c *Client) Send(entry model.NotifEntry) error { h := hermes.Hermes{ Theme: new(Theme), Product: hermes.Product{ - Name: c.app.Name, - Link: "https://github.com/crazy-max/diun", - Logo: "https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png", + Name: c.meta.Name, + Link: c.meta.URL, + Logo: c.meta.Logo, Copyright: fmt.Sprintf("%s © %d %s %s", - c.app.Author, + c.meta.Author, time.Now().Year(), - c.app.Name, - c.app.Version), + c.meta.Name, + 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{ Body: hermes.Body{ - Title: fmt.Sprintf("%s 🔔 notification", c.app.Name), + Title: fmt.Sprintf("%s 🔔 notification", c.meta.Name), FreeMarkdown: hermes.Markdown(emailBuf.String()), 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.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("Subject", subject) msg.SetBody("text/plain", textpart) diff --git a/internal/notif/rocketchat/client.go b/internal/notif/rocketchat/client.go index 9903a86b..63e9b49c 100644 --- a/internal/notif/rocketchat/client.go +++ b/internal/notif/rocketchat/client.go @@ -18,18 +18,16 @@ import ( // Client represents an active rocketchat notification object type Client struct { *notifier.Notifier - cfg *model.NotifRocketChat - app model.App - userAgent string + cfg *model.NotifRocketChat + meta model.Meta } // 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{ Handler: &Client{ - cfg: config, - app: app, - userAgent: userAgent, + cfg: config, + meta: meta, }, } } @@ -43,7 +41,7 @@ func (c *Client) Name() string { // https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/ func (c *Client) Send(entry model.NotifEntry) error { 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()) @@ -58,8 +56,8 @@ func (c *Client) Send(entry model.NotifEntry) error { } data := Message{ - Alias: c.app.Name, - Avatar: "https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png", + Alias: c.meta.Name, + Avatar: c.meta.Logo, Channel: c.cfg.Channel, Text: title, Attachments: []Attachment{ @@ -109,7 +107,7 @@ func (c *Client) Send(entry model.NotifEntry) error { } 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-Auth-Token", c.cfg.Token) diff --git a/internal/notif/script/client.go b/internal/notif/script/client.go index b751ee29..6c86071e 100644 --- a/internal/notif/script/client.go +++ b/internal/notif/script/client.go @@ -16,17 +16,16 @@ import ( // Client represents an active script notification object type Client struct { *notifier.Notifier - cfg *model.NotifScript - app model.App - userAgent string + cfg *model.NotifScript + meta model.Meta } // 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{ Handler: &Client{ - cfg: config, - app: app, + cfg: config, + meta: meta, }, } } @@ -53,7 +52,7 @@ func (c *Client) Send(entry model.NotifEntry) error { // Set env vars 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_PROVIDER=%s", entry.Provider), fmt.Sprintf("DIUN_ENTRY_IMAGE=%s", entry.Image.String()), diff --git a/internal/notif/slack/slack.go b/internal/notif/slack/slack.go index 43f629a6..cbc239ba 100644 --- a/internal/notif/slack/slack.go +++ b/internal/notif/slack/slack.go @@ -16,16 +16,16 @@ import ( // Client represents an active slack notification object type Client struct { *notifier.Notifier - cfg *model.NotifSlack - app model.App + cfg *model.NotifSlack + meta model.Meta } // 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{ Handler: &Client{ - cfg: config, - app: app, + cfg: config, + meta: meta, }, } } @@ -52,12 +52,12 @@ func (c *Client) Send(entry model.NotifEntry) error { Attachments: []slack.Attachment{ { Color: color, - AuthorName: "Diun", + AuthorName: c.meta.Name, AuthorSubname: "github.com/crazy-max/diun", - AuthorLink: "https://github.com/crazy-max/diun", - AuthorIcon: "https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png", + AuthorLink: c.meta.URL, + AuthorIcon: c.meta.Logo, 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{ { Title: "Provider", diff --git a/internal/notif/teams/client.go b/internal/notif/teams/client.go index 29828490..d3e24208 100644 --- a/internal/notif/teams/client.go +++ b/internal/notif/teams/client.go @@ -14,18 +14,16 @@ import ( // Client represents an active webhook notification object type Client struct { *notifier.Notifier - cfg *model.NotifTeams - app model.App - userAgent string + cfg *model.NotifTeams + meta model.Meta } // 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{ Handler: &Client{ - cfg: config, - app: app, - userAgent: userAgent, + cfg: config, + meta: meta, }, } } @@ -95,8 +93,7 @@ func (c *Client) Send(entry model.NotifEntry) error { } req.Header.Add("Content-Type", "application/json") - - req.Header.Set("User-Agent", c.userAgent) + req.Header.Set("User-Agent", c.meta.UserAgent) _, err = hc.Do(req) return err diff --git a/internal/notif/telegram/telegram.go b/internal/notif/telegram/telegram.go index f4887cdb..80ed3a08 100644 --- a/internal/notif/telegram/telegram.go +++ b/internal/notif/telegram/telegram.go @@ -2,34 +2,26 @@ package telegram import ( "bytes" - "errors" "text/template" "github.com/crazy-max/diun/v3/internal/model" "github.com/crazy-max/diun/v3/internal/notif/notifier" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" - "github.com/rs/zerolog/log" ) // Client represents an active Telegram notification object type Client struct { *notifier.Notifier - cfg *model.NotifTelegram - app model.App - bot *tgbotapi.BotAPI + cfg *model.NotifTelegram + meta model.Meta } // New creates a new Telegram notification instance -func New(config *model.NotifTelegram, app model.App) notifier.Notifier { - bot, err := tgbotapi.NewBotAPI(config.BotToken) - if err != nil { - log.Err(err).Msgf("Failed to initialize Telegram notifications") - } +func New(config *model.NotifTelegram, meta model.Meta) notifier.Notifier { return notifier.Notifier{ Handler: &Client{ - cfg: config, - app: app, - bot: bot, + cfg: config, + meta: meta, }, } } @@ -41,8 +33,9 @@ func (c *Client) Name() string { // Send creates and sends a Telegram notification with an entry func (c *Client) Send(entry model.NotifEntry) error { - if c.bot == nil { - return errors.New("telegram not initialized") + bot, err := tgbotapi.NewBotAPI(c.cfg.Token) + if err != nil { + return err } var msgBuf bytes.Buffer @@ -52,9 +45,9 @@ func (c *Client) Send(entry model.NotifEntry) error { } 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() - if _, err := c.bot.Send(msg); err != nil { + if _, err := bot.Send(msg); err != nil { return err } } diff --git a/internal/notif/webhook/client.go b/internal/notif/webhook/client.go index 019fc190..b46c8577 100644 --- a/internal/notif/webhook/client.go +++ b/internal/notif/webhook/client.go @@ -14,18 +14,16 @@ import ( // Client represents an active webhook notification object type Client struct { *notifier.Notifier - cfg *model.NotifWebhook - app model.App - userAgent string + cfg *model.NotifWebhook + meta model.Meta } // 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{ Handler: &Client{ - cfg: config, - app: app, - userAgent: userAgent, + cfg: config, + meta: meta, }, } } @@ -38,7 +36,7 @@ func (c *Client) Name() string { // Send creates and sends a webhook notification with an entry func (c *Client) Send(entry model.NotifEntry) error { hc := http.Client{ - Timeout: time.Duration(c.cfg.Timeout) * time.Second, + Timeout: *c.cfg.Timeout, } body, err := json.Marshal(struct { @@ -51,7 +49,7 @@ func (c *Client) Send(entry model.NotifEntry) error { Created *time.Time `json:"created"` Platform string `json:"platform"` }{ - Version: c.app.Version, + Version: c.meta.Version, Status: string(entry.Status), Provider: entry.Provider, 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) return err diff --git a/internal/provider/file/file_test.go b/internal/provider/file/file_test.go index 1e8a49b2..d93baf20 100644 --- a/internal/provider/file/file_test.go +++ b/internal/provider/file/file_test.go @@ -114,14 +114,14 @@ var ( func TestListJobFilename(t *testing.T) { fc := file.New(&model.PrdFile{ - Filename: "./test/dockerhub.yml", + Filename: "./fixtures/dockerhub.yml", }) assert.Equal(t, dockerhubFile, fc.ListJob()) } func TestListJobDirectory(t *testing.T) { fc := file.New(&model.PrdFile{ - Directory: "./test", + Directory: "./fixtures", }) assert.Equal(t, append(append(bintrayFile, dockerhubFile...), quayFile...), fc.ListJob()) } diff --git a/internal/provider/file/test/bintray.yml b/internal/provider/file/fixtures/bintray.yml similarity index 100% rename from internal/provider/file/test/bintray.yml rename to internal/provider/file/fixtures/bintray.yml diff --git a/internal/provider/file/test/dockerhub.yml b/internal/provider/file/fixtures/dockerhub.yml similarity index 100% rename from internal/provider/file/test/dockerhub.yml rename to internal/provider/file/fixtures/dockerhub.yml diff --git a/internal/provider/file/test/quay.yml b/internal/provider/file/fixtures/quay.yml similarity index 100% rename from internal/provider/file/test/quay.yml rename to internal/provider/file/fixtures/quay.yml diff --git a/pkg/docker/client.go b/pkg/docker/client.go index 66b71314..27e8ed5a 100644 --- a/pkg/docker/client.go +++ b/pkg/docker/client.go @@ -44,7 +44,7 @@ func New(opts Options) (*Client, error) { } tlsc, err := tlsconfig.Client(options) 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{ Transport: &http.Transport{TLSClientConfig: tlsc}, diff --git a/pkg/registry/manifest.go b/pkg/registry/manifest.go index 15ba732c..fe90ed4f 100644 --- a/pkg/registry/manifest.go +++ b/pkg/registry/manifest.go @@ -48,7 +48,7 @@ func (c *Client) Manifest(image Image) (Manifest, error) { } imgTag := imgInspect.Tag - if imgTag == "" { + if len(imgTag) == 0 { imgTag = image.Tag } diff --git a/pkg/utl/utl.go b/pkg/utl/utl.go index 06967243..61a58f0c 100644 --- a/pkg/utl/utl.go +++ b/pkg/utl/utl.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "regexp" + "time" ) // MatchString reports whether a string s @@ -78,3 +79,8 @@ func NewTrue() *bool { b := true return &b } + +// NewDuration returns a duration pointer +func NewDuration(duration time.Duration) *time.Duration { + return &duration +} diff --git a/third_party/traefik/README.md b/third_party/traefik/README.md new file mode 100644 index 00000000..379a2713 --- /dev/null +++ b/third_party/traefik/README.md @@ -0,0 +1 @@ +Fork of github.com/containous/traefik@v2.2.1 diff --git a/third_party/traefik/config/env/env.go b/third_party/traefik/config/env/env.go new file mode 100644 index 00000000..776a9cd0 --- /dev/null +++ b/third_party/traefik/config/env/env.go @@ -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 +} diff --git a/third_party/traefik/config/env/env_test.go b/third_party/traefik/config/env/env_test.go new file mode 100644 index 00000000..bb9c7221 --- /dev/null +++ b/third_party/traefik/config/env/env_test.go @@ -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) +} diff --git a/third_party/traefik/config/env/filter.go b/third_party/traefik/config/env/filter.go new file mode 100644 index 00000000..83d651ce --- /dev/null +++ b/third_party/traefik/config/env/filter.go @@ -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 +} diff --git a/third_party/traefik/config/env/filter_test.go b/third_party/traefik/config/env/filter_test.go new file mode 100644 index 00000000..ecabc898 --- /dev/null +++ b/third_party/traefik/config/env/filter_test.go @@ -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) + }) + } +} diff --git a/third_party/traefik/config/env/fixtures_test.go b/third_party/traefik/config/env/fixtures_test.go new file mode 100644 index 00000000..bbcb4c46 --- /dev/null +++ b/third_party/traefik/config/env/fixtures_test.go @@ -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 +} diff --git a/third_party/traefik/config/file/file.go b/third_party/traefik/config/file/file.go new file mode 100644 index 00000000..58db0fbf --- /dev/null +++ b/third_party/traefik/config/file/file.go @@ -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}) +} diff --git a/third_party/traefik/config/file/file_node.go b/third_party/traefik/config/file/file_node.go new file mode 100644 index 00000000..d841d546 --- /dev/null +++ b/third_party/traefik/config/file/file_node.go @@ -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 +} diff --git a/third_party/traefik/config/file/file_node_test.go b/third_party/traefik/config/file/file_node_test.go new file mode 100644 index 00000000..22e6b8e1 --- /dev/null +++ b/third_party/traefik/config/file/file_node_test.go @@ -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) +} diff --git a/third_party/traefik/config/file/file_test.go b/third_party/traefik/config/file/file_test.go new file mode 100644 index 00000000..832ebb12 --- /dev/null +++ b/third_party/traefik/config/file/file_test.go @@ -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) +} diff --git a/third_party/traefik/config/file/fixtures/sample.yml b/third_party/traefik/config/file/fixtures/sample.yml new file mode 100644 index 00000000..b1bf1ac4 --- /dev/null +++ b/third_party/traefik/config/file/fixtures/sample.yml @@ -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: {} diff --git a/third_party/traefik/config/file/fixtures_test.go b/third_party/traefik/config/file/fixtures_test.go new file mode 100644 index 00000000..25521e8a --- /dev/null +++ b/third_party/traefik/config/file/fixtures_test.go @@ -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 +} diff --git a/third_party/traefik/config/file/raw_node.go b/third_party/traefik/config/file/raw_node.go new file mode 100644 index 00000000..2609cb12 --- /dev/null +++ b/third_party/traefik/config/file/raw_node.go @@ -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 +} diff --git a/third_party/traefik/config/file/raw_node_test.go b/third_party/traefik/config/file/raw_node_test.go new file mode 100644 index 00000000..3656d526 --- /dev/null +++ b/third_party/traefik/config/file/raw_node_test.go @@ -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) + }) + } +} diff --git a/third_party/traefik/config/generator/generator.go b/third_party/traefik/config/generator/generator.go new file mode 100644 index 00000000..ac483029 --- /dev/null +++ b/third_party/traefik/config/generator/generator.go @@ -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) +} diff --git a/third_party/traefik/config/generator/generator_test.go b/third_party/traefik/config/generator/generator_test.go new file mode 100644 index 00000000..a09d76ef --- /dev/null +++ b/third_party/traefik/config/generator/generator_test.go @@ -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{"": ""}, + Fee: map[string]Hi{"": {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 +} diff --git a/third_party/traefik/config/parser/element_fill.go b/third_party/traefik/config/parser/element_fill.go new file mode 100644 index 00000000..601664b3 --- /dev/null +++ b/third_party/traefik/config/parser/element_fill.go @@ -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 +} diff --git a/third_party/traefik/config/parser/element_fill_test.go b/third_party/traefik/config/parser/element_fill_test.go new file mode 100644 index 00000000..d98f1204 --- /dev/null +++ b/third_party/traefik/config/parser/element_fill_test.go @@ -0,0 +1,1431 @@ +package parser + +import ( + "reflect" + "testing" + "time" + + "github.com/crazy-max/diun/v3/third_party/traefik/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFill(t *testing.T) { + type expected struct { + element interface{} + error bool + } + + testCases := []struct { + desc string + node *Node + element interface{} + expected expected + }{ + { + desc: "empty node", + node: &Node{}, + element: &struct{ Foo string }{}, + expected: expected{error: true}, + }, + { + desc: "empty element", + node: &Node{Name: "traefik", Kind: reflect.Struct}, + element: &struct{}{}, + expected: expected{element: &struct{}{}}, + }, + { + desc: "type struct as root", + node: &Node{Name: "traefik", Kind: reflect.Struct}, + element: struct{}{}, + expected: expected{error: true}, + }, + { + desc: "nil node", + node: nil, + element: &struct{ Foo string }{}, + expected: expected{element: &struct{ Foo string }{}}, + }, + { + desc: "nil element", + node: &Node{Name: "traefik", Kind: reflect.Struct}, + element: nil, + expected: expected{element: nil}, + }, + { + desc: "string", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, + }, + }, + element: &struct{ Foo string }{}, + expected: expected{element: &struct{ Foo string }{Foo: "bar"}}, + }, + { + desc: "field not found", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Fii", Value: "bar", Kind: reflect.String}, + }, + }, + element: &struct{ Foo string }{}, + expected: expected{error: true}, + }, + { + desc: "2 children", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "bir", Kind: reflect.String}, + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int}, + }, + }, + element: &struct { + Fii string + Foo int + }{}, + expected: expected{element: &struct { + Fii string + Foo int + }{Fii: "bir", Foo: 4}}, + }, + { + desc: "case insensitive", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "foo", FieldName: "Foo", Value: "bir", Kind: reflect.String}, + }, + }, + element: &struct { + Foo string + foo int + }{}, + expected: expected{element: &struct { + Foo string + foo int + }{Foo: "bir"}}, + }, + { + desc: "func", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Func}, + }, + }, + element: &struct{ Foo func() }{}, + expected: expected{element: &struct{ Foo func() }{}}, + }, + { + desc: "int", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int}, + }, + }, + element: &struct{ Foo int }{}, + expected: expected{element: &struct{ Foo int }{Foo: 4}}, + }, + { + desc: "invalid int", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int}, + }, + }, + element: &struct{ Foo int }{}, + expected: expected{error: true}, + }, + { + desc: "int8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int8}, + }, + }, + element: &struct{ Foo int8 }{}, + expected: expected{element: &struct{ Foo int8 }{Foo: 4}}, + }, + { + desc: "invalid int8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int8}, + }, + }, + element: &struct{ Foo int8 }{}, + expected: expected{error: true}, + }, + { + desc: "int16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int16}, + }, + }, + element: &struct{ Foo int16 }{}, + expected: expected{element: &struct{ Foo int16 }{Foo: 4}}, + }, + { + desc: "invalid int16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int16}, + }, + }, + element: &struct{ Foo int16 }{}, + expected: expected{error: true}, + }, + { + desc: "int32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int32}, + }, + }, + element: &struct{ Foo int32 }{}, + expected: expected{element: &struct{ Foo int32 }{Foo: 4}}, + }, + { + desc: "invalid int32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int32}, + }, + }, + element: &struct{ Foo int32 }{}, + expected: expected{error: true}, + }, + { + desc: "int64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int64}, + }, + }, + element: &struct{ Foo int64 }{}, + expected: expected{element: &struct{ Foo int64 }{Foo: 4}}, + }, + { + desc: "invalid int64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int64}, + }, + }, + element: &struct{ Foo int64 }{}, + expected: expected{error: true}, + }, + { + desc: "uint", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint}, + }, + }, + element: &struct{ Foo uint }{}, + expected: expected{element: &struct{ Foo uint }{Foo: 4}}, + }, + { + desc: "invalid uint", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint}, + }, + }, + element: &struct{ Foo uint }{}, + expected: expected{error: true}, + }, + { + desc: "uint8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint8}, + }, + }, + element: &struct{ Foo uint8 }{}, + expected: expected{element: &struct{ Foo uint8 }{Foo: 4}}, + }, + { + desc: "invalid uint8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint8}, + }, + }, + element: &struct{ Foo uint8 }{}, + expected: expected{error: true}, + }, + { + desc: "uint16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint16}, + }, + }, + element: &struct{ Foo uint16 }{}, + expected: expected{element: &struct{ Foo uint16 }{Foo: 4}}, + }, + { + desc: "invalid uint16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint16}, + }, + }, + element: &struct{ Foo uint16 }{}, + expected: expected{error: true}, + }, + { + desc: "uint32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint32}, + }, + }, + element: &struct{ Foo uint32 }{}, + expected: expected{element: &struct{ Foo uint32 }{Foo: 4}}, + }, + { + desc: "invalid uint32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint32}, + }, + }, + element: &struct{ Foo uint32 }{}, + expected: expected{error: true}, + }, + { + desc: "uint64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint64}, + }, + }, + element: &struct{ Foo uint64 }{}, + expected: expected{element: &struct{ Foo uint64 }{Foo: 4}}, + }, + { + desc: "invalid uint64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint64}, + }, + }, + element: &struct{ Foo uint64 }{}, + expected: expected{error: true}, + }, + { + desc: "time.Duration with unit", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4s", Kind: reflect.Int64}, + }, + }, + element: &struct{ Foo time.Duration }{}, + expected: expected{element: &struct{ Foo time.Duration }{Foo: 4 * time.Second}}, + }, + { + desc: "time.Duration without unit", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int64}, + }, + }, + element: &struct{ Foo time.Duration }{}, + expected: expected{element: &struct{ Foo time.Duration }{Foo: 4 * time.Nanosecond}}, + }, + { + desc: "types.Duration with unit", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4s", Kind: reflect.Int64}, + }, + }, + element: &struct{ Foo types.Duration }{}, + expected: expected{element: &struct{ Foo types.Duration }{Foo: types.Duration(4 * time.Second)}}, + }, + { + desc: "types.Duration without unit", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int64}, + }, + }, + element: &struct{ Foo types.Duration }{}, + expected: expected{element: &struct{ Foo types.Duration }{Foo: types.Duration(4 * time.Second)}}, + }, + { + desc: "bool", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "true", Kind: reflect.Bool}, + }, + }, + element: &struct{ Foo bool }{}, + expected: expected{element: &struct{ Foo bool }{Foo: true}}, + }, + { + desc: "invalid bool", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bool", Kind: reflect.Bool}, + }, + }, + element: &struct{ Foo bool }{}, + expected: expected{error: true}, + }, + { + desc: "float32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "2.1", Kind: reflect.Float32}, + }, + }, + element: &struct{ Foo float32 }{}, + expected: expected{element: &struct{ Foo float32 }{Foo: 2.1}}, + }, + { + desc: "invalid float32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "two dot one", Kind: reflect.Float32}, + }, + }, + element: &struct{ Foo float32 }{}, + expected: expected{error: true}, + }, + { + desc: "float64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "2.1", Kind: reflect.Float64}, + }, + }, + element: &struct{ Foo float64 }{}, + expected: expected{element: &struct{ Foo float64 }{Foo: 2.1}}, + }, + { + desc: "invalid float64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "two dot one", Kind: reflect.Float64}, + }, + }, + element: &struct{ Foo float64 }{}, + expected: expected{error: true}, + }, + { + desc: "struct", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "huu", Kind: reflect.String}, + {Name: "Fuu", FieldName: "Fuu", Value: "6", Kind: reflect.Int}, + }}, + }}, + element: &struct { + Foo struct { + Fii string + Fuu int + } + }{}, + expected: expected{element: &struct { + Foo struct { + Fii string + Fuu int + } + }{ + Foo: struct { + Fii string + Fuu int + }{ + Fii: "huu", + Fuu: 6, + }}, + }, + }, + { + desc: "pointer", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "huu", Kind: reflect.String}, + {Name: "Fuu", FieldName: "Fuu", Value: "6", Kind: reflect.Int}, + }}, + }}, + element: &struct { + Foo *struct { + Fii string + Fuu int + } + }{}, + expected: expected{element: &struct { + Foo *struct { + Fii string + Fuu int + } + }{ + Foo: &struct { + Fii string + Fuu int + }{ + Fii: "huu", + Fuu: 6, + }}, + }, + }, + { + desc: "pointer disabled false without children", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Ptr, + }, + }}, + element: &struct { + Foo *struct { + Fii string + Fuu int + } `label:"allowEmpty"` + }{}, + expected: expected{element: &struct { + Foo *struct { + Fii string + Fuu int + } `label:"allowEmpty"` + }{ + Foo: &struct { + Fii string + Fuu int + }{}}, + }, + }, + { + desc: "pointer disabled true without children", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Ptr, + Disabled: true, + }, + }}, + element: &struct { + Foo *struct { + Fii string + Fuu int + } `label:"allowEmpty"` + }{}, + expected: expected{element: &struct { + Foo *struct { + Fii string + Fuu int + } `label:"allowEmpty"` + }{}, + }, + }, + { + desc: "pointer disabled true with children", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Disabled: true, + Kind: reflect.Ptr, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "huu", Kind: reflect.String}, + {Name: "Fuu", FieldName: "Fuu", Value: "6", Kind: reflect.Int}, + }}, + }}, + element: &struct { + Foo *struct { + Fii string + Fuu int + } `label:"allowEmpty"` + }{}, + expected: expected{element: &struct { + Foo *struct { + Fii string + Fuu int + } `label:"allowEmpty"` + }{}, + }, + }, + { + desc: "map string", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Map, + Children: []*Node{ + {Name: "name1", Value: "hii", Kind: reflect.String}, + {Name: "name2", Value: "huu", Kind: reflect.String}, + }}, + }}, + element: &struct { + Foo map[string]string + }{}, + expected: expected{element: &struct { + Foo map[string]string + }{ + Foo: map[string]string{ + "name1": "hii", + "name2": "huu", + }}, + }, + }, + { + desc: "map struct", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Map, + Children: []*Node{ + { + Name: "name1", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Kind: reflect.String, Value: "hii"}, + }, + }, + { + Name: "name2", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Kind: reflect.String, Value: "huu"}, + }, + }, + }}, + }}, + element: &struct { + Foo map[string]struct{ Fii string } + }{}, + expected: expected{element: &struct { + Foo map[string]struct{ Fii string } + }{ + Foo: map[string]struct{ Fii string }{ + "name1": {Fii: "hii"}, + "name2": {Fii: "huu"}, + }}, + }, + }, + { + desc: "slice string", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "huu,hii,hoo", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []string }{}, + expected: expected{element: &struct{ Foo []string }{Foo: []string{"huu", "hii", "hoo"}}}, + }, + { + desc: "slice named type", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "huu,hii,hoo", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []NamedType }{}, + expected: expected{element: &struct{ Foo []NamedType }{Foo: []NamedType{"huu", "hii", "hoo"}}}, + }, + { + desc: "slice named type int", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "1,2,3", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []NamedTypeInt }{}, + expected: expected{element: &struct{ Foo []NamedTypeInt }{Foo: []NamedTypeInt{1, 2, 3}}}, + }, + { + desc: "empty slice", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []string }{}, + expected: expected{element: &struct{ Foo []string }{Foo: nil}}, + }, + { + desc: "slice int", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int }{}, + expected: expected{element: &struct{ Foo []int }{Foo: []int{4, 3, 6}}}, + }, + { + desc: "slice invalid int", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int }{}, + expected: expected{error: true}, + }, + { + desc: "slice int8", + node: &Node{ + Name: "traefik", + Kind: reflect.Slice, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int8 }{}, + expected: expected{element: &struct{ Foo []int8 }{Foo: []int8{4, 3, 6}}}, + }, + { + desc: "slice invalid int8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int8 }{}, + expected: expected{error: true}, + }, + { + desc: "slice int16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int16 }{}, + expected: expected{element: &struct{ Foo []int16 }{Foo: []int16{4, 3, 6}}}, + }, + { + desc: "slice invalid int16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int16 }{}, + expected: expected{error: true}, + }, + { + desc: "slice int32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int32 }{}, + expected: expected{element: &struct{ Foo []int32 }{Foo: []int32{4, 3, 6}}}, + }, + { + desc: "slice invalid int32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int32 }{}, + expected: expected{error: true}, + }, + { + desc: "slice int64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int64 }{}, + expected: expected{element: &struct{ Foo []int64 }{Foo: []int64{4, 3, 6}}}, + }, + { + desc: "slice invalid int64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int64 }{}, + expected: expected{error: true}, + }, + { + desc: "slice uint", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint }{}, + expected: expected{element: &struct{ Foo []uint }{Foo: []uint{4, 3, 6}}}, + }, + { + desc: "slice invalid uint", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint }{}, + expected: expected{error: true}, + }, + { + desc: "slice uint8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint8 }{}, + expected: expected{element: &struct{ Foo []uint8 }{Foo: []uint8{4, 3, 6}}}, + }, + { + desc: "slice invalid uint8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint8 }{}, + expected: expected{error: true}, + }, + { + desc: "slice uint16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint16 }{}, + expected: expected{element: &struct{ Foo []uint16 }{Foo: []uint16{4, 3, 6}}}, + }, + { + desc: "slice invalid uint16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint16 }{}, + expected: expected{error: true}, + }, + { + desc: "slice uint32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint32 }{}, + expected: expected{element: &struct{ Foo []uint32 }{Foo: []uint32{4, 3, 6}}}, + }, + { + desc: "slice invalid uint32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint32 }{}, + expected: expected{error: true}, + }, + { + desc: "slice uint64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint64 }{}, + expected: expected{element: &struct{ Foo []uint64 }{Foo: []uint64{4, 3, 6}}}, + }, + { + desc: "slice invalid uint64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint64 }{}, + expected: expected{error: true}, + }, + { + desc: "slice float32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []float32 }{}, + expected: expected{element: &struct{ Foo []float32 }{Foo: []float32{4, 3, 6}}}, + }, + { + desc: "slice invalid float32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []float32 }{}, + expected: expected{error: true}, + }, + { + desc: "slice float64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []float64 }{}, + expected: expected{element: &struct{ Foo []float64 }{Foo: []float64{4, 3, 6}}}, + }, + { + desc: "slice invalid float64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []float64 }{}, + expected: expected{error: true}, + }, + { + desc: "slice bool", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "true, false, true", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []bool }{}, + expected: expected{element: &struct{ Foo []bool }{Foo: []bool{true, false, true}}}, + }, + { + desc: "slice invalid bool", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bool, false, true", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []bool }{}, + expected: expected{error: true}, + }, + { + desc: "slice slice-as-struct", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Fii", + FieldName: "Foo", + Kind: reflect.Slice, + Tag: `label-slice-as-struct:"Fii"`, + Children: []*Node{ + {Name: "bar", FieldName: "Bar", Kind: reflect.String, Value: "haa"}, + {Name: "bir", FieldName: "Bir", Kind: reflect.String, Value: "hii"}, + }, + }, + }, + }, + element: &struct { + Foo []struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{}, + expected: expected{element: &struct { + Foo []struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{ + Foo: []struct { + Bar string + Bir string + }{ + { + Bar: "haa", + Bir: "hii", + }, + }, + }}, + }, + { + desc: "slice slice-as-struct pointer", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Fii", + FieldName: "Foo", + Kind: reflect.Slice, + Tag: `label-slice-as-struct:"Fii"`, + Children: []*Node{ + {Name: "bar", FieldName: "Bar", Kind: reflect.String, Value: "haa"}, + {Name: "bir", FieldName: "Bir", Kind: reflect.String, Value: "hii"}, + }, + }, + }, + }, + element: &struct { + Foo []*struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{}, + expected: expected{element: &struct { + Foo []*struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{ + Foo: []*struct { + Bar string + Bir string + }{ + { + Bar: "haa", + Bir: "hii", + }, + }, + }}, + }, + { + desc: "slice slice-as-struct without children", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Fii", + FieldName: "Foo", + Tag: `label-slice-as-struct:"Fii"`, + Kind: reflect.Slice, + }, + }, + }, + element: &struct { + Foo []struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{}, + expected: expected{error: true}, + }, + { + desc: "pointer SetDefaults method", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fuu", FieldName: "Fuu", Value: "huu", Kind: reflect.String}, + }}, + }}, + element: &struct { + Foo *InitializedFoo + }{}, + expected: expected{element: &struct { + Foo *InitializedFoo + }{ + Foo: &InitializedFoo{ + Fii: "default", + Fuu: "huu", + }, + }}, + }, + { + desc: "pointer wrong SetDefaults method", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fuu", FieldName: "Fuu", Value: "huu", Kind: reflect.String}, + }}, + }}, + element: &struct { + Foo *wrongInitialledFoo + }{}, + expected: expected{element: &struct { + Foo *wrongInitialledFoo + }{ + Foo: &wrongInitialledFoo{ + Fuu: "huu", + }, + }}, + }, + { + desc: "int pointer", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Ptr}, + }, + }, + element: &struct{ Foo *int }{}, + expected: expected{element: &struct{ Foo *int }{Foo: func(v int) *int { return &v }(4)}}, + }, + { + desc: "bool pointer", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "true", Kind: reflect.Ptr}, + }, + }, + element: &struct{ Foo *bool }{}, + expected: expected{element: &struct{ Foo *bool }{Foo: func(v bool) *bool { return &v }(true)}}, + }, + { + desc: "string pointer", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.Ptr}, + }, + }, + element: &struct{ Foo *string }{}, + expected: expected{element: &struct{ Foo *string }{Foo: func(v string) *string { return &v }("bar")}}, + }, + { + desc: "embedded", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fuu", FieldName: "Fuu", Value: "huu", Kind: reflect.String}, + }}, + }}, + element: &struct { + Foo struct { + FiiFoo + } + }{}, + expected: expected{element: &struct { + Foo struct { + FiiFoo + } + }{ + Foo: struct { + FiiFoo + }{ + FiiFoo: FiiFoo{ + Fii: "", + Fuu: "huu", + }, + }, + }}, + }, + { + desc: "slice struct", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Slice, Children: []*Node{ + {Name: "[0]", Kind: reflect.Struct, Children: []*Node{ + {Name: "Field1", FieldName: "Field1", Value: "A", Kind: reflect.String}, + {Name: "Field2", FieldName: "Field2", Value: "A", Kind: reflect.String}, + }}, + {Name: "[1]", Kind: reflect.Struct, Children: []*Node{ + {Name: "Field1", FieldName: "Field1", Value: "B", Kind: reflect.String}, + {Name: "Field2", FieldName: "Field2", Value: "B", Kind: reflect.String}, + }}, + {Name: "[2]", Kind: reflect.Struct, Children: []*Node{ + {Name: "Field1", FieldName: "Field1", Value: "C", Kind: reflect.String}, + {Name: "Field2", FieldName: "Field2", Value: "C", Kind: reflect.String}, + }}, + }}, + }, + }, + element: &struct { + Foo []struct { + Field1 string + Field2 string + } + }{}, + expected: expected{element: &struct { + Foo []struct { + Field1 string + Field2 string + } + }{ + Foo: []struct { + Field1 string + Field2 string + }{ + {Field1: "A", Field2: "A"}, + {Field1: "B", Field2: "B"}, + {Field1: "C", Field2: "C"}, + }, + }}, + }, + { + desc: "slice pointer struct", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Slice, Children: []*Node{ + {Name: "[0]", Kind: reflect.Ptr, Children: []*Node{ + {Name: "Field1", FieldName: "Field1", Value: "A", Kind: reflect.String}, + {Name: "Field2", FieldName: "Field2", Value: "A", Kind: reflect.String}, + }}, + {Name: "[1]", Kind: reflect.Ptr, Children: []*Node{ + {Name: "Field1", FieldName: "Field1", Value: "B", Kind: reflect.String}, + {Name: "Field2", FieldName: "Field2", Value: "B", Kind: reflect.String}, + }}, + {Name: "[2]", Kind: reflect.Ptr, Children: []*Node{ + {Name: "Field1", FieldName: "Field1", Value: "C", Kind: reflect.String}, + {Name: "Field2", FieldName: "Field2", Value: "C", Kind: reflect.String}, + }}, + }}, + }, + }, + element: &struct { + Foo []*struct { + Field1 string + Field2 string + } + }{}, + expected: expected{element: &struct { + Foo []*struct { + Field1 string + Field2 string + } + }{ + Foo: []*struct { + Field1 string + Field2 string + }{ + {Field1: "A", Field2: "A"}, + {Field1: "B", Field2: "B"}, + {Field1: "C", Field2: "C"}, + }, + }}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + err := filler{FillerOpts: FillerOpts{AllowSliceAsStruct: true}}.Fill(test.element, test.node) + if test.expected.error { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, test.expected.element, test.element) + } + }) + } +} + +type NamedType string +type NamedTypeInt int + +type InitializedFoo struct { + Fii string + Fuu string +} + +func (t *InitializedFoo) SetDefaults() { + t.Fii = "default" +} + +type wrongInitialledFoo struct { + Fii string + Fuu string +} + +func (t *wrongInitialledFoo) SetDefaults() error { + t.Fii = "default" + return nil +} + +type Bouya string + +type FiiFoo struct { + Fii string + Fuu Bouya +} diff --git a/third_party/traefik/config/parser/element_nodes.go b/third_party/traefik/config/parser/element_nodes.go new file mode 100644 index 00000000..0f61d924 --- /dev/null +++ b/third_party/traefik/config/parser/element_nodes.go @@ -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 +} diff --git a/third_party/traefik/config/parser/element_nodes_test.go b/third_party/traefik/config/parser/element_nodes_test.go new file mode 100644 index 00000000..c90063f3 --- /dev/null +++ b/third_party/traefik/config/parser/element_nodes_test.go @@ -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) + } + }) + } +} diff --git a/third_party/traefik/config/parser/flat_encode.go b/third_party/traefik/config/parser/flat_encode.go new file mode 100644 index 00000000..d13733f8 --- /dev/null +++ b/third_party/traefik/config/parser/flat_encode.go @@ -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) +} diff --git a/third_party/traefik/config/parser/flat_encode_test.go b/third_party/traefik/config/parser/flat_encode_test.go new file mode 100644 index 00000000..bea3bd0e --- /dev/null +++ b/third_party/traefik/config/parser/flat_encode_test.go @@ -0,0 +1,1251 @@ +package parser + +import ( + "reflect" + "testing" + "time" + + "github.com/crazy-max/diun/v3/third_party/traefik/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEncodeToFlat(t *testing.T) { + testCases := []struct { + desc string + element interface{} + node *Node + opts *FlatOpts + expected []Flat + }{ + { + desc: "string field", + element: &struct { + Field string `description:"field description"` + }{ + Field: "test", + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + FieldName: "Field", + Description: "field description", + Value: "test", + Kind: reflect.String, + Tag: `description:"field description"`, + }, + }, + }, + expected: []Flat{{ + Name: "field", + Description: "field description", + Default: "test", + }}, + }, + { + desc: "int field", + element: &struct { + Field int `description:"field description"` + }{ + Field: 6, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "6", + Kind: reflect.Int, + Tag: `description:"field description"`, + }, + }, + }, + expected: []Flat{{ + Name: "field", + Description: "field description", + Default: "6", + }}, + }, + { + desc: "bool field", + element: &struct { + Field bool `description:"field description"` + }{ + Field: true, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "true", + Kind: reflect.Bool, + Tag: `description:"field description"`, + }, + }, + }, + expected: []Flat{{ + Name: "field", + Description: "field description", + Default: "true", + }}, + }, + { + desc: "string pointer field", + element: &struct { + Field *string `description:"field description"` + }{ + Field: func(v string) *string { return &v }("test"), + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "test", + Kind: reflect.Ptr, + Tag: `description:"field description"`, + }, + }, + }, + expected: []Flat{{ + Name: "field", + Description: "field description", + Default: "test", + }}, + }, + { + desc: "string pointer field, custom option", + element: &struct { + Field *string `description:"field description"` + }{ + Field: func(v string) *string { return &v }("test"), + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "test", + Kind: reflect.Ptr, + Tag: `description:"field description"`, + }, + }, + }, + opts: &FlatOpts{ + Case: "upper", + Separator: "_", + SkipRoot: false, + TagName: TagLabel, + }, + expected: []Flat{{ + Name: "TRAEFIK_FIELD", + Description: "field description", + Default: "test", + }}, + }, + { + desc: "int pointer field", + element: &struct { + Field *int `description:"field description"` + }{ + Field: func(v int) *int { return &v }(6), + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "6", + Kind: reflect.Ptr, + Tag: `description:"field description"`, + }, + }, + }, + expected: []Flat{{ + Name: "field", + Description: "field description", + Default: "6", + }}, + }, + { + desc: "bool pointer field", + element: &struct { + Field *bool `description:"field description"` + }{ + Field: func(v bool) *bool { return &v }(true), + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "true", + Kind: reflect.Ptr, + Tag: `description:"field description"`, + }, + }, + }, + expected: []Flat{{ + Name: "field", + Description: "field description", + Default: "true", + }}, + }, + { + desc: "slice of string field, no initial value", + element: &struct { + Field []string `description:"field description"` + }{}, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Kind: reflect.Slice, + Tag: `description:"field description"`, + }, + }, + }, + expected: []Flat{{ + Name: "field", + Description: "field description", + Default: "", + }}, + }, + { + desc: "slice of string field, with initial value", + element: &struct { + Field []string `description:"field description"` + }{ + Field: []string{"foo", "bar"}, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "foo, bar", + Kind: reflect.Slice, + Tag: `description:"field description"`, + }, + }, + }, + expected: []Flat{{ + Name: "field", + Description: "field description", + Default: "foo, bar", + }}, + }, + { + desc: "slice of int field, no initial value", + element: &struct { + Field []int `description:"field description"` + }{}, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Kind: reflect.Slice, + Tag: `description:"field description"`, + }, + }, + }, + expected: []Flat{{ + Name: "field", + Description: "field description", + Default: "", + }}, + }, + { + desc: "slice of int field, with initial value", + element: &struct { + Field []int `description:"field description"` + }{ + Field: []int{6, 3}, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "6, 3", + Kind: reflect.Slice, + Tag: `description:"field description"`, + }, + }, + }, + expected: []Flat{{ + Name: "field", + Description: "field description", + Default: "6, 3", + }}, + }, + { + desc: "map string field", + element: &struct { + Field map[string]string `description:"field description"` + }{ + Field: map[string]string{ + MapNamePlaceholder: "", + }, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Kind: reflect.Map, + Tag: `description:"field description"`, + Children: []*Node{ + { + Name: "\u003cname\u003e", + FieldName: "\u003cname\u003e", + Kind: reflect.String, + }, + }, + }, + }, + }, + expected: []Flat{{ + Name: "field.", + Description: "field description", + Default: "", + }}, + }, + { + desc: "struct pointer field", + element: &struct { + Foo *struct { + Field string `description:"field description"` + } `description:"foo description"` + }{ + Foo: &struct { + Field string `description:"field description"` + }{ + Field: "test", + }, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Foo", + Description: "foo description", + FieldName: "Foo", + Kind: reflect.Ptr, + Tag: `description:"foo description"`, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "test", + Kind: reflect.String, + Tag: `description:"field description"`, + }, + }, + }, + }, + }, + expected: []Flat{ + { + Name: "foo.field", + Description: "field description", + Default: "test", + }, + }, + }, + { + desc: "struct pointer field, hide field", + element: &struct { + Foo *struct { + Field string `description:"-"` + } `description:"foo description"` + }{ + Foo: &struct { + Field string `description:"-"` + }{ + Field: "test", + }, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Foo", + Description: "foo description", + FieldName: "Foo", + Kind: reflect.Ptr, + Tag: `description:"foo description"`, + Children: []*Node{ + { + Name: "Field", + Description: "-", + FieldName: "Field", + Value: "test", + Kind: reflect.String, + Tag: `description:"-"`, + }, + }, + }, + }, + }, + expected: nil, + }, + { + desc: "struct pointer field, allow empty", + element: &struct { + Foo *struct { + Field string `description:"field description"` + } `description:"foo description" label:"allowEmpty"` + }{ + Foo: &struct { + Field string `description:"field description"` + }{ + Field: "test", + }, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Foo", + Description: "foo description", + FieldName: "Foo", + Kind: reflect.Ptr, + Tag: `description:"foo description" label:"allowEmpty"`, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "test", + Kind: reflect.String, + Tag: `description:"field description"`, + }, + }, + }, + }, + }, + expected: []Flat{ + { + Name: "foo", + Description: "foo description", + Default: "false", + }, + { + Name: "foo.field", + Description: "field description", + Default: "test", + }, + }, + }, + { + desc: "struct pointer field level 2", + element: &struct { + Foo *struct { + Fii *struct { + Field string `description:"field description"` + } `description:"fii description"` + } `description:"foo description"` + }{ + Foo: &struct { + Fii *struct { + Field string `description:"field description"` + } `description:"fii description"` + }{ + Fii: &struct { + Field string `description:"field description"` + }{ + Field: "test", + }, + }, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Foo", + Description: "foo description", + FieldName: "Foo", + Kind: reflect.Ptr, + Tag: `description:"foo description"`, + Children: []*Node{ + { + Name: "Fii", + Description: "fii description", + FieldName: "Fii", + Kind: reflect.Ptr, + Tag: `description:"fii description"`, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "test", + Kind: reflect.String, + Tag: `description:"field description"`, + }, + }, + }, + }, + }, + }, + }, + expected: []Flat{ + { + Name: "foo.fii.field", + Description: "field description", + Default: "test", + }, + }, + }, + { + desc: "struct pointer field level 2, allow empty", + element: &struct { + Foo *struct { + Fii *struct { + Field string `description:"field description"` + } `description:"fii description" label:"allowEmpty"` + } `description:"foo description" label:"allowEmpty"` + }{ + Foo: &struct { + Fii *struct { + Field string `description:"field description"` + } `description:"fii description" label:"allowEmpty"` + }{ + Fii: &struct { + Field string `description:"field description"` + }{ + Field: "test", + }, + }, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Foo", + Description: "foo description", + FieldName: "Foo", + Kind: reflect.Ptr, + Tag: `description:"foo description" label:"allowEmpty"`, + Children: []*Node{ + { + Name: "Fii", + Description: "fii description", + FieldName: "Fii", + Kind: reflect.Ptr, + Tag: `description:"fii description" label:"allowEmpty"`, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "test", + Kind: reflect.String, + Tag: `description:"field description"`, + }, + }, + }, + }, + }, + }, + }, + expected: []Flat{ + { + Name: "foo", + Description: "foo description", + Default: "false", + }, + { + Name: "foo.fii", + Description: "fii description", + Default: "false", + }, + { + Name: "foo.fii.field", + Description: "field description", + Default: "test", + }, + }, + }, + { + desc: "map string field level 2", + element: &struct { + Foo *struct { + Fii map[string]string `description:"fii description"` + } `description:"foo description"` + }{ + Foo: &struct { + Fii map[string]string `description:"fii description"` + }{ + Fii: map[string]string{ + MapNamePlaceholder: "", + }, + }, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Foo", + Description: "foo description", + FieldName: "Foo", + Kind: reflect.Ptr, + Tag: `description:"foo description"`, + Children: []*Node{ + { + Name: "Fii", + Description: "fii description", + FieldName: "Fii", + Kind: reflect.Map, + Tag: `description:"fii description"`, + Children: []*Node{ + { + Name: "\u003cname\u003e", + FieldName: "\u003cname\u003e", + Kind: reflect.String, + }, + }, + }, + }, + }, + }, + }, + expected: []Flat{ + { + Name: "foo.fii.", + Description: "fii description", + Default: "", + }, + }, + }, + { + desc: "map string pointer field level 2", + element: &struct { + Foo *struct { + Fii map[string]*string `description:"fii description"` + } `description:"foo description"` + }{ + Foo: &struct { + Fii map[string]*string `description:"fii description"` + }{ + Fii: map[string]*string{ + MapNamePlaceholder: func(v string) *string { return &v }(""), + }, + }, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Foo", + Description: "foo description", + FieldName: "Foo", + Kind: reflect.Ptr, + Tag: `description:"foo description"`, + Children: []*Node{ + { + Name: "Fii", + Description: "fii description", + FieldName: "Fii", + Kind: reflect.Map, + Tag: `description:"fii description"`, + Children: []*Node{ + { + Name: "\u003cname\u003e", + FieldName: "\u003cname\u003e", + Kind: reflect.Ptr, + }, + }, + }, + }, + }, + }, + }, + expected: []Flat{ + { + Name: "foo.fii.", + Description: "fii description", + Default: "", + }, + }, + }, + { + desc: "map struct level 1", + element: &struct { + Foo map[string]struct { + Field string `description:"field description"` + Yo int `description:"yo description"` + } `description:"foo description"` + }{}, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Foo", + Description: "foo description", + FieldName: "Foo", + Kind: reflect.Map, + Tag: `description:"foo description"`, + Children: []*Node{ + { + Name: "\u003cname\u003e", + FieldName: "\u003cname\u003e", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Kind: reflect.String, + Tag: `description:"field description"`, + }, + { + Name: "Yo", + Description: "yo description", + FieldName: "Yo", + Value: "0", + Kind: reflect.Int, + Tag: `description:"yo description"`, + }, + }, + }, + }, + }, + }, + }, + expected: []Flat{ + { + Name: "foo.", + Description: "foo description", + Default: "false", + }, + { + Name: "foo..field", + Description: "field description", + Default: "", + }, + { + Name: "foo..yo", + Description: "yo description", + Default: "0", + }, + }, + }, + { + desc: "map struct pointer level 1", + element: &struct { + Foo map[string]*struct { + Field string `description:"field description"` + Yo string `description:"yo description"` + } `description:"foo description"` + }{}, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Foo", + Description: "foo description", + FieldName: "Foo", + Kind: reflect.Map, + Tag: `description:"foo description"`, + Children: []*Node{ + { + Name: "\u003cname\u003e", + FieldName: "\u003cname\u003e", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Kind: reflect.String, + Tag: `description:"field description"`, + }, + { + Name: "Yo", + Description: "yo description", + FieldName: "Yo", + Kind: reflect.String, + Tag: `description:"yo description"`, + }, + }, + }, + }, + }, + }, + }, + expected: []Flat{ + { + Name: "foo.", + Description: "foo description", + Default: "false", + }, + { + Name: "foo..field", + Description: "field description", + Default: "", + }, + { + Name: "foo..yo", + Description: "yo description", + Default: "", + }, + }, + }, + { + desc: "time duration field", + element: &struct { + Field time.Duration `description:"field description"` + }{ + Field: 1 * time.Second, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "1000000000", + Kind: reflect.Int64, + Tag: `description:"field description"`, + }, + }, + }, + expected: []Flat{{ + Name: "field", + Description: "field description", + Default: "1s", + }}, + }, + { + desc: "time duration field map", + element: &struct { + Foo map[string]*struct { + Field time.Duration `description:"field description"` + } `description:"foo description"` + }{ + Foo: map[string]*struct { + Field time.Duration `description:"field description"` + }{ + "": { + Field: 0, + }, + }, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Foo", + Description: "foo description", + FieldName: "Foo", + Kind: reflect.Map, + Tag: `description:"foo description"`, + Children: []*Node{ + { + Name: "\u003cname\u003e", + FieldName: "\u003cname\u003e", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "0", + Kind: reflect.Int64, + Tag: `description:"field description"`, + }, + }, + }, + }, + }, + }, + }, + expected: []Flat{ + { + Name: "foo.", + Description: "foo description", + Default: "false", + }, + { + Name: "foo..field", + Description: "field description", + Default: "0s", + }, + }, + }, + { + desc: "time duration field map 2", + element: &struct { + Foo map[string]*struct { + Fii *struct { + Field time.Duration `description:"field description"` + } + } `description:"foo description"` + }{ + Foo: map[string]*struct { + Fii *struct { + Field time.Duration `description:"field description"` + } + }{ + "": { + Fii: &struct { + Field time.Duration `description:"field description"` + }{ + Field: 0, + }, + }, + }, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Foo", + Description: "foo description", + FieldName: "Foo", + Kind: reflect.Map, + Tag: `description:"foo description"`, + Children: []*Node{ + { + Name: "\u003cname\u003e", + FieldName: "\u003cname\u003e", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Fii", + FieldName: "Fii", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "0", + Kind: reflect.Int64, + Tag: `description:"field description"`, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expected: []Flat{ + { + Name: "foo.", + Description: "foo description", + Default: "false", + }, + { + Name: "foo..fii.field", + Description: "field description", + Default: "0s", + }, + }, + }, + { + desc: "time duration field 2", + element: &struct { + Foo *struct { + Field time.Duration `description:"field description"` + } + }{ + Foo: &struct { + Field time.Duration `description:"field description"` + }{ + Field: 1 * time.Second, + }, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "1000000000", + Kind: reflect.Int64, + Tag: `description:"field description"`, + }, + }, + }, + }, + }, + expected: []Flat{{ + Name: "foo.field", + Description: "field description", + Default: "1s", + }}, + }, + { + desc: "time duration field 3", + element: &struct { + Foo *struct { + Fii *struct { + Field time.Duration `description:"field description"` + } + } + }{ + Foo: &struct { + Fii *struct { + Field time.Duration `description:"field description"` + } + }{ + Fii: &struct { + Field time.Duration `description:"field description"` + }{ + Field: 1 * time.Second, + }, + }, + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Fii", + FieldName: "Fii", + Kind: reflect.Ptr, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "1000000000", + Kind: reflect.Int64, + Tag: `description:"field description"`, + }, + }, + }, + }, + }, + }, + }, + expected: []Flat{{ + Name: "foo.fii.field", + Description: "field description", + Default: "1s", + }}, + }, + { + desc: "time duration field", + element: &struct { + Field types.Duration `description:"field description"` + }{ + Field: types.Duration(180 * time.Second), + }, + node: &Node{ + Name: "traefik", + FieldName: "", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Field", + Description: "field description", + FieldName: "Field", + Value: "180000000000", + Kind: reflect.Int64, + Tag: `description:"field description"`, + }, + }, + }, + expected: []Flat{{ + Name: "field", + Description: "field description", + Default: "180", + }}, + }, + { + desc: "slice of struct", + element: &struct { + Foo *struct { + Fii []struct { + Field1 string `description:"field1 description"` + Field2 int `description:"field2 description"` + } `description:"fii description"` + } `description:"foo description"` + }{ + Foo: &struct { + Fii []struct { + Field1 string `description:"field1 description"` + Field2 int `description:"field2 description"` + } `description:"fii description"` + }{ + Fii: []struct { + Field1 string `description:"field1 description"` + Field2 int `description:"field2 description"` + }{ + { + Field1: "", + Field2: 0, + }, + }, + }, + }, + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", Kind: reflect.Ptr, Description: "foo description", Children: []*Node{ + {Name: "Fii", Kind: reflect.Slice, Description: "fii description", Children: []*Node{ + {Name: "[0]", Kind: reflect.Struct, Children: []*Node{ + {Name: "Field1", Value: "", Kind: reflect.String, Description: "field1 description"}, + {Name: "Field2", Value: "0", Kind: reflect.Int, Description: "field2 description"}, + }}, + }}, + }}, + }, + }, + expected: []Flat{ + { + Name: "foo.fii", + Description: "fii description", + Default: "", + }, + { + Name: "foo.fii[0].field1", + Description: "field1 description", + Default: "", + }, + { + Name: "foo.fii[0].field2", + Description: "field2 description", + Default: "0", + }, + }, + }, + // Skipped: because realistically not needed in Traefik for now. + // { + // desc: "map of map field level 2", + // element: &struct { + // Foo *struct { + // Fii map[string]map[string]string `description:"fii description"` + // } `description:"foo description"` + // }{ + // Foo: &struct { + // Fii map[string]map[string]string `description:"fii description"` + // }{ + // Fii: map[string]map[string]string{ + // MapNamePlaceholder: { + // MapNamePlaceholder: "test", + // }, + // }, + // }, + // }, + // expected: `XXX`, + // }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var opts FlatOpts + if test.opts == nil { + opts = FlatOpts{Separator: ".", SkipRoot: true, TagName: TagLabel} + } else { + opts = *test.opts + } + + entries, err := EncodeToFlat(test.element, test.node, opts) + require.NoError(t, err) + + assert.Equal(t, test.expected, entries) + }) + } +} diff --git a/third_party/traefik/config/parser/labels_decode.go b/third_party/traefik/config/parser/labels_decode.go new file mode 100644 index 00000000..4ecc5be4 --- /dev/null +++ b/third_party/traefik/config/parser/labels_decode.go @@ -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 +} diff --git a/third_party/traefik/config/parser/labels_decode_test.go b/third_party/traefik/config/parser/labels_decode_test.go new file mode 100644 index 00000000..b742a615 --- /dev/null +++ b/third_party/traefik/config/parser/labels_decode_test.go @@ -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)) + } + } + }) + } +} diff --git a/third_party/traefik/config/parser/labels_encode.go b/third_party/traefik/config/parser/labels_encode.go new file mode 100644 index 00000000..e2b35311 --- /dev/null +++ b/third_party/traefik/config/parser/labels_encode.go @@ -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 + } + } +} diff --git a/third_party/traefik/config/parser/labels_encode_test.go b/third_party/traefik/config/parser/labels_encode_test.go new file mode 100644 index 00000000..cc8fa693 --- /dev/null +++ b/third_party/traefik/config/parser/labels_encode_test.go @@ -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) + }) + } +} diff --git a/third_party/traefik/config/parser/node.go b/third_party/traefik/config/parser/node.go new file mode 100644 index 00000000..88f0c799 --- /dev/null +++ b/third_party/traefik/config/parser/node.go @@ -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 = "" + +// 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"` +} diff --git a/third_party/traefik/config/parser/nodes_metadata.go b/third_party/traefik/config/parser/nodes_metadata.go new file mode 100644 index 00000000..3357a0bd --- /dev/null +++ b/third_party/traefik/config/parser/nodes_metadata.go @@ -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 +} diff --git a/third_party/traefik/config/parser/nodes_metadata_test.go b/third_party/traefik/config/parser/nodes_metadata_test.go new file mode 100644 index 00000000..c3ab1aa5 --- /dev/null +++ b/third_party/traefik/config/parser/nodes_metadata_test.go @@ -0,0 +1,1011 @@ +package parser + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAddMetadata(t *testing.T) { + type expected struct { + node *Node + error bool + } + + type interf interface{} + + testCases := []struct { + desc string + tree *Node + structure interface{} + expected expected + }{ + { + desc: "Node Nil", + tree: nil, + structure: nil, + expected: expected{node: nil}, + }, + { + desc: "Empty Node", + tree: &Node{}, + structure: nil, + expected: expected{error: true}, + }, + { + desc: "Nil structure", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "bar"}, + }, + }, + structure: nil, + expected: expected{error: true}, + }, + { + desc: "level 0", + tree: &Node{Name: "traefik", Value: "bar"}, + expected: expected{error: true}, + }, + { + desc: "level 1", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar"}, + }, + }, + structure: struct{ Foo string }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, + }, + }, + }, + }, + { + desc: "level 1, pointer", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "bar"}, + }, + }, + structure: &struct{ Foo string }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Ptr, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, + }, + }, + }, + }, + { + desc: "level 1, slice", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "bar,bur"}, + }, + }, + structure: struct{ Foo []string }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar,bur", Kind: reflect.Slice}, + }, + }, + }, + }, + { + desc: "level 1, interface", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "", Children: []*Node{ + {Name: "Fii", Value: "hii"}, + }}, + }, + }, + structure: struct{ Foo interf }{}, + expected: expected{error: true}, + }, + { + desc: "level 1, map string", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "name1", Value: "bar"}, + {Name: "name2", Value: "bur"}, + }}, + }, + }, + structure: struct{ Foo map[string]string }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Map, Children: []*Node{ + {Name: "name1", Value: "bar", Kind: reflect.String}, + {Name: "name2", Value: "bur", Kind: reflect.String}, + }}, + }, + }, + }, + }, + { + desc: "level 1, map struct", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "name1", Children: []*Node{ + {Name: "Fii", Value: "bar"}, + }}, + }}, + }, + }, + structure: struct { + Foo map[string]struct{ Fii string } + }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Map, Children: []*Node{ + {Name: "name1", Kind: reflect.Struct, Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "bar", Kind: reflect.String}, + }}, + }}, + }, + }, + }, + }, + { + desc: "level 1, map int as key", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "name1", Children: []*Node{ + {Name: "Fii", Value: "bar"}, + }}, + }}, + }, + }, + structure: struct { + Foo map[int]struct{ Fii string } + }{}, + expected: expected{error: true}, + }, + { + desc: "level 1, int pointer", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "0"}, + }, + }, + structure: struct { + Foo *int + }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "0", Kind: reflect.Ptr}, + }, + }, + }, + }, + { + desc: "level 1, bool pointer", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "0"}, + }, + }, + structure: struct { + Foo *bool + }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "0", Kind: reflect.Ptr}, + }, + }, + }, + }, + { + desc: "level 1, string pointer", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "0"}, + }, + }, + structure: struct { + Foo *string + }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "0", Kind: reflect.Ptr}, + }, + }, + }, + }, + { + desc: "level 1, 2 children with different types", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "bar"}, + {Name: "Fii", Value: "1"}, + }, + }, + structure: struct { + Foo string + Fii int + }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, + {Name: "Fii", FieldName: "Fii", Value: "1", Kind: reflect.Int}, + }, + }, + }, + }, + { + desc: "level 1, use exported instead of unexported", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "foo", Value: "bar"}, + }, + }, + structure: struct { + foo int + Foo string + }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "foo", Value: "bar", FieldName: "Foo", Kind: reflect.String}, + }, + }, + }, + }, + { + desc: "level 1, unexported", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "foo", Value: "bar"}, + }, + }, + structure: struct { + foo string + }{}, + expected: expected{error: true}, + }, + { + desc: "level 1, 3 children with different types", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "bar"}, + {Name: "Fii", Value: "1"}, + {Name: "Fuu", Value: "true"}, + }, + }, + structure: struct { + Foo string + Fii int + Fuu bool + }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, + {Name: "Fii", FieldName: "Fii", Value: "1", Kind: reflect.Int}, + {Name: "Fuu", FieldName: "Fuu", Value: "true", Kind: reflect.Bool}, + }, + }, + }, + }, + { + desc: "level 2", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "Bar", Value: "bir"}, + }}, + }, + }, + structure: struct { + Foo struct { + Bar string + } + }{ + Foo: struct { + Bar string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "bir", Kind: reflect.String}, + }}, + }, + }, + }, + }, + { + desc: "level 2, struct without children", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo"}, + }, + }, + structure: struct { + Foo struct { + Bar string + } + }{ + Foo: struct { + Bar string + }{}, + }, + expected: expected{error: true}, + }, + { + desc: "level 2, slice-as-struct", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Fii", Children: []*Node{ + {Name: "bar", Value: "haa"}, + {Name: "bir", Value: "hii"}, + }}, + }, + }, + structure: struct { + Foo []struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{ + Foo: []struct { + Bar string + Bir string + }{}, + }, + expected: expected{node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Fii", + FieldName: "Foo", + Kind: reflect.Slice, + Tag: reflect.StructTag(`label-slice-as-struct:"Fii"`), + Children: []*Node{ + {Name: "bar", FieldName: "Bar", Kind: reflect.String, Value: "haa"}, + {Name: "bir", FieldName: "Bir", Kind: reflect.String, Value: "hii"}, + }, + }, + }, + }}, + }, + { + desc: "level 2, slice-as-struct without children", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Fii"}, + }, + }, + structure: struct { + Foo []struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{ + Foo: []struct { + Bar string + Bir string + }{}, + }, + expected: expected{node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Fii", + FieldName: "Foo", + Kind: reflect.Slice, + Tag: reflect.StructTag(`label-slice-as-struct:"Fii"`), + }, + }, + }}, + }, + { + desc: "level 2, struct with allowEmpty, value true", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "true"}, + }, + }, + structure: struct { + Foo struct { + Bar string + } `label:"allowEmpty"` + }{ + Foo: struct { + Bar string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "true", Kind: reflect.Struct, Tag: reflect.StructTag(`label:"allowEmpty"`)}, + }, + }, + }, + }, + { + desc: "level 2, struct with allowEmpty, value true with case variation", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "TruE"}, + }, + }, + structure: struct { + Foo struct { + Bar string + } `label:"allowEmpty"` + }{ + Foo: struct { + Bar string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "TruE", Kind: reflect.Struct, Tag: reflect.StructTag(`label:"allowEmpty"`)}, + }, + }, + }, + }, + { + desc: "level 2, struct with allowEmpty, value false", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "false"}, + }, + }, + structure: struct { + Foo struct { + Bar string + } `label:"allowEmpty"` + }{ + Foo: struct { + Bar string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "false", Disabled: true, Kind: reflect.Struct, Tag: reflect.StructTag(`label:"allowEmpty"`)}, + }, + }, + }, + }, + { + desc: "level 2, struct with allowEmpty with children, value false", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "false", Children: []*Node{ + {Name: "Bar", Value: "hii"}, + }}, + }, + }, + structure: struct { + Foo struct { + Bar string + } `label:"allowEmpty"` + }{ + Foo: struct { + Bar string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Value: "false", + Disabled: true, + Kind: reflect.Struct, + Tag: reflect.StructTag(`label:"allowEmpty"`), + Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "hii", Kind: reflect.String}, + }, + }, + }, + }, + }, + }, + { + desc: "level 2, struct pointer without children", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo"}, + }, + }, + structure: struct { + Foo *struct { + Bar string + } + }{ + Foo: &struct { + Bar string + }{}, + }, + expected: expected{error: true}, + }, + { + desc: "level 2, map without children", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo"}, + }, + }, + structure: struct { + Foo map[string]string + }{ + Foo: map[string]string{}, + }, + expected: expected{error: true}, + }, + { + desc: "level 2, pointer", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "Bar", Value: "bir"}, + }}, + }, + }, + structure: struct { + Foo *struct { + Bar string + } + }{ + Foo: &struct { + Bar string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Ptr, Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "bir", Kind: reflect.String}, + }}, + }, + }, + }, + }, + { + desc: "level 2, 2 children", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "Bar", Value: "bir"}, + {Name: "Bur", Value: "fuu"}, + }}, + }, + }, + structure: struct { + Foo struct { + Bar string + Bur string + } + }{ + Foo: struct { + Bar string + Bur string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "bir", Kind: reflect.String}, + {Name: "Bur", FieldName: "Bur", Value: "fuu", Kind: reflect.String}, + }}, + }, + }, + }, + }, + { + desc: "level 3", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "Bar", Children: []*Node{ + {Name: "Bur", Value: "fuu"}, + }}, + }}, + }, + }, + structure: struct { + Foo struct { + Bar struct { + Bur string + } + } + }{ + Foo: struct { + Bar struct { + Bur string + } + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Bar", + FieldName: "Bar", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Bur", FieldName: "Bur", Value: "fuu", Kind: reflect.String}, + }}, + }}, + }, + }, + }, + }, + { + desc: "level 3, 2 children level 1, 2 children level 2, 2 children level 3", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "Bar", Children: []*Node{ + {Name: "Fii", Value: "fii"}, + {Name: "Fee", Value: "1"}, + }}, + {Name: "Bur", Children: []*Node{ + {Name: "Faa", Value: "faa"}, + }}, + }}, + {Name: "Fii", Children: []*Node{ + {Name: "FiiBar", Value: "fiiBar"}, + }}, + }, + }, + structure: struct { + Foo struct { + Bar struct { + Fii string + Fee int + } + Bur struct { + Faa string + } + } + Fii struct { + FiiBar string + } + }{ + Foo: struct { + Bar struct { + Fii string + Fee int + } + Bur struct { + Faa string + } + }{}, + Fii: struct { + FiiBar string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Bar", + FieldName: "Bar", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Kind: reflect.String, Value: "fii"}, + {Name: "Fee", FieldName: "Fee", Kind: reflect.Int, Value: "1"}, + }}, + { + Name: "Bur", + FieldName: "Bur", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Faa", FieldName: "Faa", Kind: reflect.String, Value: "faa"}, + }}, + }}, + { + Name: "Fii", + FieldName: "Fii", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "FiiBar", FieldName: "FiiBar", Kind: reflect.String, Value: "fiiBar"}, + }}, + }, + }, + }, + }, + { + desc: "Slice struct", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "[0]", Children: []*Node{ + {Name: "Field1", Value: "A"}, + {Name: "Field2", Value: "A"}, + }}, + {Name: "[1]", Children: []*Node{ + {Name: "Field1", Value: "B"}, + {Name: "Field2", Value: "B"}, + }}, + {Name: "[2]", Children: []*Node{ + {Name: "Field1", Value: "C"}, + {Name: "Field2", Value: "C"}, + }}, + }}, + }, + }, + structure: struct { + Foo []struct { + Field1 string + Field2 string + } + }{}, + expected: expected{node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Slice, Children: []*Node{ + {Name: "[0]", Kind: reflect.Struct, Children: []*Node{ + {Name: "Field1", FieldName: "Field1", Value: "A", Kind: reflect.String}, + {Name: "Field2", FieldName: "Field2", Value: "A", Kind: reflect.String}, + }}, + {Name: "[1]", Kind: reflect.Struct, Children: []*Node{ + {Name: "Field1", FieldName: "Field1", Value: "B", Kind: reflect.String}, + {Name: "Field2", FieldName: "Field2", Value: "B", Kind: reflect.String}, + }}, + {Name: "[2]", Kind: reflect.Struct, Children: []*Node{ + {Name: "Field1", FieldName: "Field1", Value: "C", Kind: reflect.String}, + {Name: "Field2", FieldName: "Field2", Value: "C", Kind: reflect.String}, + }}, + }}, + }, + }}, + }, + { + desc: "Slice pointer struct", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "[0]", Children: []*Node{ + {Name: "Field1", Value: "A"}, + {Name: "Field2", Value: "A"}, + }}, + {Name: "[1]", Children: []*Node{ + {Name: "Field1", Value: "B"}, + {Name: "Field2", Value: "B"}, + }}, + {Name: "[2]", Children: []*Node{ + {Name: "Field1", Value: "C"}, + {Name: "Field2", Value: "C"}, + }}, + }}, + }, + }, + structure: struct { + Foo []*struct { + Field1 string + Field2 string + } + }{}, + expected: expected{node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Slice, Children: []*Node{ + {Name: "[0]", Kind: reflect.Ptr, Children: []*Node{ + {Name: "Field1", FieldName: "Field1", Value: "A", Kind: reflect.String}, + {Name: "Field2", FieldName: "Field2", Value: "A", Kind: reflect.String}, + }}, + {Name: "[1]", Kind: reflect.Ptr, Children: []*Node{ + {Name: "Field1", FieldName: "Field1", Value: "B", Kind: reflect.String}, + {Name: "Field2", FieldName: "Field2", Value: "B", Kind: reflect.String}, + }}, + {Name: "[2]", Kind: reflect.Ptr, Children: []*Node{ + {Name: "Field1", FieldName: "Field1", Value: "C", Kind: reflect.String}, + {Name: "Field2", FieldName: "Field2", Value: "C", Kind: reflect.String}, + }}, + }}, + }, + }}, + }, + { + desc: "embedded", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "Fii", Value: "bir"}, + {Name: "Fuu", Value: "bur"}, + }}, + }, + }, + structure: struct { + Foo struct { + FiiFoo + } + }{}, + expected: expected{node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "bir", Kind: reflect.String}, + {Name: "Fuu", FieldName: "Fuu", Value: "bur", Kind: reflect.String}, + }}, + }, + }}, + }, + { + desc: "embedded slice", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "MySliceType", Value: "foo,fii"}, + }, + }, + structure: struct { + MySliceType + }{}, + expected: expected{node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "MySliceType", FieldName: "MySliceType", Value: "foo,fii", Kind: reflect.Slice}, + }, + }}, + }, + { + desc: "embedded slice 2", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "MySliceType", Value: "foo,fii"}, + }}, + }, + }, + structure: struct { + Foo struct { + MySliceType + } + }{}, + expected: expected{node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ + {Name: "MySliceType", FieldName: "MySliceType", Value: "foo,fii", Kind: reflect.Slice}, + }}, + }, + }}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + err := metadata{MetadataOpts{TagName: TagLabel, AllowSliceAsStruct: true}}.Add(test.structure, test.tree) + + if test.expected.error { + assert.Error(t, err) + } else { + require.NoError(t, err) + + if !assert.Equal(t, test.expected.node, test.tree) { + bytes, errM := json.MarshalIndent(test.tree, "", " ") + require.NoError(t, errM) + fmt.Println(string(bytes)) + } + } + }) + } +} + +type MySliceType []string diff --git a/third_party/traefik/config/parser/parser.go b/third_party/traefik/config/parser/parser.go new file mode 100644 index 00000000..957a27be --- /dev/null +++ b/third_party/traefik/config/parser/parser.go @@ -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 +} diff --git a/third_party/traefik/config/parser/tags.go b/third_party/traefik/config/parser/tags.go new file mode 100644 index 00000000..a4c479ae --- /dev/null +++ b/third_party/traefik/config/parser/tags.go @@ -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" +) diff --git a/third_party/traefik/types/duration.go b/third_party/traefik/types/duration.go new file mode 100644 index 00000000..936f1f0f --- /dev/null +++ b/third_party/traefik/types/duration.go @@ -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 +}