From 6318e4f069f756fa600a40aa55a8abb7d4a1e8c5 Mon Sep 17 00:00:00 2001 From: Ian Fijolek Date: Sun, 25 Dec 2022 22:29:08 -0800 Subject: [PATCH] Add Nomad provider I modeled it off the Kubernetes provider a bit. It supports setting task config at group and task levels using services and meta attributes. --- docs/config/index.md | 2 + docs/providers/nomad.md | 211 +++++++++++++++++++++++++++ go.mod | 10 +- go.sum | 26 +++- internal/app/diun.go | 6 + internal/model/provider_nomad.go | 28 ++++ internal/model/providers.go | 1 + internal/provider/nomad/nomad.go | 49 +++++++ internal/provider/nomad/task.go | 126 ++++++++++++++++ internal/provider/nomad/task_test.go | 53 +++++++ 10 files changed, 505 insertions(+), 7 deletions(-) create mode 100644 docs/providers/nomad.md create mode 100644 internal/model/provider_nomad.go create mode 100644 internal/provider/nomad/nomad.go create mode 100644 internal/provider/nomad/task.go create mode 100644 internal/provider/nomad/task_test.go diff --git a/docs/config/index.md b/docs/config/index.md index f95b67e4..ad3d2239 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -105,6 +105,8 @@ You can override this using the [`--config` flag or `CONFIG` env var with `serve - production file: directory: ./imagesdir + nomad: + watchByDefault: true ``` ## Environment variables diff --git a/docs/providers/nomad.md b/docs/providers/nomad.md new file mode 100644 index 00000000..43ea1d59 --- /dev/null +++ b/docs/providers/nomad.md @@ -0,0 +1,211 @@ +# Nomad provider + +## About + +The Nomad provider allows you watch tasks running using the Docker provider for registry updates. + +## Quick start + +Here we'll go over basic deployment using a local Nomad cluster. + +First, we'll deploy a Diun job: + +```hcl +job "diun" { + type = "service" + + group "diun" { + task "diun" { + driver = "docker" + + config { + image = "crazymax/diun:latest" + args = ["serve"] + } + + env = { + "NOMAD_ADDR" = "http://${attr.unique.network.ip-address}:4646/", + "DIUN_PROVIDERS_NOMAD" = true, + } + } + } +} +``` + +Task based configuration can be passed through Service tags or meta attributes. These can be defined at the task or even the group level where it will apply to all tasks within the group. + +The example below will show all methods, but you only need to use one. + +```hcl +job "whoami" { + type = "service" + + group "whoami" { + network { + mode = "bridge" + + port "web" { + to = 80 + } + } + + // This + meta { + diun.enable = true + } + + // Or this + service { + tags = [ + "diun.enable=true" + ] + } + + task "diun" { + driver = "docker" + + config { + image = "containous/whoami:latest" + } + + // Or this + meta { + diun.enable = true + } + + // Or this + service { + tags = [ + "diun.enable=true" + ] + } + } + } +} +``` + +## Configuration + +!!! hint + Environment variable `DIUN_PROVIDERS_NOMAD=true` can be used to enable this provider with default values. + +Default values are assigned by the Nomad client. If not provided in your Diun configuration, the client will default to using the same config values as the `nomad` cli client. + +!!! abstract "Environment variables" + * `NOMAD_ADDR` + * `NOMAD_REGION` + * `NOMAD_NAMESPACE` + * `NOMAD_HTTP_AUTH` + * `NOMAD_CACERT` + * `NOMAD_CAPATH` + * `NOMAD_CLIENT_CERT` + * `NOMAD_CLIENT_KEY` + * `NOMAD_TLS_SERVER_NAME` + * `NOMAD_SKIP_VERIFY` + * `NOMAD_TOKEN` + + +### `address` + +The Nomad server address as URL. + +!!! example "File" + ```yaml + providers: + nomad: + address: "http://localhost:4646" + ``` + +!!! abstract "Environment variables" + * `DIUN_PROVIDERS_NOMAD_ENDPOINT` + +Nomad server endpoint as URL, which is only used when the behavior based on environment variables described below +does not apply. + +### `region` + +Nomad region to query from + +!!! example "File" + ```yaml + providers: + nomad: + region: "region1" + ``` + +!!! abstract "Environment variables" + * `DIUN_PROVIDERS_NOMAD_REGION` + +### `namespace` + +Nomad namespace to query from + +!!! example "File" + ```yaml + providers: + nomad: + namespace: "namespace1" + ``` + +!!! abstract "Environment variables" + * `DIUN_PROVIDERS_NOMAD_NAMESPACE` + +### `secretID` + +SecretID to connect to Nomad API. This token must have permission to query and view Nomad jobs. + +!!! example "File" + ```yaml + providers: + nomad: + secretID: "secret" + ``` + +!!! abstract "Environment variables" + * `DIUN_PROVIDERS_NOMAD_SECRETID` + +### `tlsInsecure` + +Controls whether client does not verify the server's certificate chain and hostname (default `false`). + +!!! example "File" + ```yaml + providers: + nomad: + tlsInsecure: false + ``` + +!!! abstract "Environment variables" + * `DIUN_PROVIDERS_NOMAD_TLSINSECURE` + +### `watchByDefault` + +Enable watch by default. If false, tasks that don't have `diun.enable = true` in their meta or service tags will be ignored +(default `false`). + +!!! example "File" + ```yaml + providers: + nomad: + watchByDefault: false + ``` + +!!! abstract "Environment variables" + * `DIUN_PROVIDERS_NOMAD_WATCHBYDEFAULT` + +## Nomad annotations + +You can configure more finely the way to analyze the image of your tasks through Nomad meta attributes or service tags: + +| Name | Default | Description | +|---------------------|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `diun.enable` | | Set to true to enable image analysis of this task | +| `diun.regopt` | | [Registry options](../config/regopts.md) name to use | +| `diun.watch_repo` | `false` | Watch all tags of this task image ([be careful](../faq.md#docker-hub-rate-limits) with this setting) | +| `diun.notify_on` | `new;update` | Semicolon separated list of status to be notified: `new`, `update`. | +| `diun.sort_tags` | `reverse` | [Sort tags method](../faq.md#tags-sorting-when-using-watch_repo) if `diun.watch_repo` enabled. One of `default`, `reverse`, `semver`, `lexicographical` | +| `diun.max_tags` | `0` | Maximum number of tags to watch if `diun.watch_repo` enabled. `0` means all of them | +| `diun.include_tags` | | Semicolon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` | +| `diun.exclude_tags` | | Semicolon separated list of regular expressions to exclude tags. Can be useful if you enable `diun.watch_repo` | +| `diun.hub_link` | _automatic_ | Set registry hub link for this image | +| `diun.platform` | _automatic_ | Platform to use (e.g. `linux/amd64`) | diff --git a/go.mod b/go.mod index c67c4898..c2dcba8a 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible github.com/gregdel/pushover v1.1.0 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b + github.com/hashicorp/nomad/api v0.0.0-20221121184155-2372c6d20c54 github.com/imdario/mergo v0.3.13 github.com/jedib0t/go-pretty/v6 v6.4.2 github.com/matcornic/hermes/v2 v2.1.0 @@ -36,7 +37,7 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 github.com/sirupsen/logrus v1.9.0 github.com/streadway/amqp v1.0.0 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.1 github.com/tidwall/pretty v1.2.1 go.etcd.io/bbolt v1.3.6 golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 @@ -86,9 +87,12 @@ require ( github.com/google/uuid v1.3.0 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/mux v1.8.0 // indirect - github.com/gorilla/websocket v1.4.2 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/hashicorp/cronexpr v1.1.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/huandu/xstrings v1.2.0 // indirect github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -102,6 +106,8 @@ require ( github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/sys/mountinfo v0.6.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/go.sum b/go.sum index 1cecb04c..efac2198 100644 --- a/go.sum +++ b/go.sum @@ -443,7 +443,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -482,8 +482,9 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregdel/pushover v1.1.0 h1:dwHyvrcpZCOS9V1fAnKPaGRRI5OC55cVaKhMybqNsKQ= github.com/gregdel/pushover v1.1.0/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -495,17 +496,25 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4= github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= +github.com/hashicorp/cronexpr v1.1.1 h1:NJZDd87hGXjoZBdvyCF9mX4DCq5Wy7+A/w+A7q0wn6c= +github.com/hashicorp/cronexpr v1.1.1/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/nomad/api v0.0.0-20221121184155-2372c6d20c54 h1:eyCj5YuMoOAcHOP2i+NXylxvjQkzSHd5diuQqVT0vMw= +github.com/hashicorp/nomad/api v0.0.0-20221121184155-2372c6d20c54/go.mod h1:CkWF80BBiX2ha+nczdcXGjt6LeYgUVvaNEwRg2YVgE0= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= @@ -564,8 +573,8 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -612,8 +621,12 @@ github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WT github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mistifyio/go-zfs/v3 v3.0.0/go.mod h1:CzVgeB0RvF2EGzQnytKVvVSDwmKJXxkOTUGbNrTja/k= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/moby/buildkit v0.10.1-0.20220712094726-874eef9b70db h1:q7oAjNPIsQWTaT1GVvzAWeRJ70qPJHNtmQS046Thd3U= github.com/moby/buildkit v0.10.1-0.20220712094726-874eef9b70db/go.mod h1:yle9eiU1fiJ/WhC4VTLOaQ6rxFou1mc4AhwScHwysi0= @@ -755,8 +768,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= @@ -768,6 +781,7 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/shoenig/test v0.4.4 h1:juucmjQTPYUfO2+rID1wfQqsreSlk+2I8K/bQFsUZ9c= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= @@ -808,6 +822,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -817,8 +832,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= diff --git a/internal/app/diun.go b/internal/app/diun.go index 83bf4c27..68fd1da4 100644 --- a/internal/app/diun.go +++ b/internal/app/diun.go @@ -16,6 +16,7 @@ import ( dockerfilePrd "github.com/crazy-max/diun/v4/internal/provider/dockerfile" filePrd "github.com/crazy-max/diun/v4/internal/provider/file" kubernetesPrd "github.com/crazy-max/diun/v4/internal/provider/kubernetes" + nomadPrd "github.com/crazy-max/diun/v4/internal/provider/nomad" swarmPrd "github.com/crazy-max/diun/v4/internal/provider/swarm" "github.com/crazy-max/gohealthchecks" "github.com/hako/durafmt" @@ -175,6 +176,11 @@ func (di *Diun) Run() { di.createJob(job) } + // Nomad provider + for _, job := range nomadPrd.New(di.cfg.Providers.Nomad).ListJob() { + di.createJob(job) + } + di.wg.Wait() log.Info(). Int("added", entries.CountNew). diff --git a/internal/model/provider_nomad.go b/internal/model/provider_nomad.go new file mode 100644 index 00000000..633391b7 --- /dev/null +++ b/internal/model/provider_nomad.go @@ -0,0 +1,28 @@ +package model + +import ( + "github.com/crazy-max/diun/v4/pkg/utl" +) + +// PrdNomad holds nomad provider configuration +type PrdNomad struct { + Address string `yaml:"address" json:"address,omitempty" validate:"omitempty"` + Region string `yaml:"region,omitempty" json:"region,omitempty" validate:"omitempty"` + SecretID string `yaml:"secretID,omitempty" json:"secretID,omitempty" validate:"omitempty"` + Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty" validate:"omitempty"` + TLSInsecure *bool `yaml:"tlsInsecure" json:"tlsInsecure,omitempty" validate:"required"` + WatchByDefault *bool `yaml:"watchByDefault" json:"watchByDefault,omitempty" validate:"required"` +} + +// GetDefaults gets the default values +func (s *PrdNomad) GetDefaults() *PrdNomad { + n := &PrdNomad{} + n.SetDefaults() + return n +} + +// SetDefaults sets the default values +func (s *PrdNomad) SetDefaults() { + s.TLSInsecure = utl.NewFalse() + s.WatchByDefault = utl.NewFalse() +} diff --git a/internal/model/providers.go b/internal/model/providers.go index db9f2694..d7be78bc 100644 --- a/internal/model/providers.go +++ b/internal/model/providers.go @@ -7,6 +7,7 @@ type Providers struct { Kubernetes *PrdKubernetes `yaml:"kubernetes,omitempty" json:"kubernetes,omitempty" label:"allowEmpty" file:"allowEmpty"` File *PrdFile `yaml:"file,omitempty" json:"file,omitempty"` Dockerfile *PrdDockerfile `yaml:"dockerfile,omitempty" json:"dockerfile,omitempty"` + Nomad *PrdNomad `yaml:"nomad,omitempty" json:"nomad,omitempty" label:"allowEmpty" file:"allowEmpty"` } // GetDefaults gets the default values diff --git a/internal/provider/nomad/nomad.go b/internal/provider/nomad/nomad.go new file mode 100644 index 00000000..ebc6c323 --- /dev/null +++ b/internal/provider/nomad/nomad.go @@ -0,0 +1,49 @@ +package nomad + +import ( + "github.com/crazy-max/diun/v4/internal/model" + "github.com/crazy-max/diun/v4/internal/provider" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +// Client represents an active nomad provider object +type Client struct { + *provider.Client + config *model.PrdNomad + logger zerolog.Logger +} + +// New creates new kubernetes provider instance +func New(config *model.PrdNomad) *provider.Client { + return &provider.Client{ + Handler: &Client{ + config: config, + logger: log.With().Str("provider", "nomad").Logger(), + }, + } +} + +// ListJob returns job list to process +func (c *Client) ListJob() []model.Job { + if c.config == nil { + return []model.Job{} + } + + images := c.listTaskImages() + if len(images) == 0 { + log.Warn().Msg("No image found") + return []model.Job{} + } + + c.logger.Info().Msgf("Found %d image(s) to analyze", len(images)) + var list []model.Job + for _, image := range images { + list = append(list, model.Job{ + Provider: "nomad", + Image: image, + }) + } + + return list +} diff --git a/internal/provider/nomad/task.go b/internal/provider/nomad/task.go new file mode 100644 index 00000000..4f25925d --- /dev/null +++ b/internal/provider/nomad/task.go @@ -0,0 +1,126 @@ +package nomad + +import ( + "reflect" + "strings" + + "github.com/crazy-max/diun/v4/internal/model" + "github.com/crazy-max/diun/v4/internal/provider" + nomad "github.com/hashicorp/nomad/api" +) + +func parseServiceTags(tags []string) map[string]string { + labels := map[string]string{} + + for _, tag := range tags { + tagParts := strings.SplitN(tag, "=", 2) + if len(tagParts) < 2 { + continue + } + + labels[tagParts[0]] = tagParts[1] + } + + return labels +} + +func updateMap(m1, m2 map[string]string) map[string]string { + for key, value := range m2 { + m1[key] = value + } + + return m1 +} + +func (c *Client) listTaskImages() []model.Image { + config := &nomad.Config{ + Address: c.config.Address, + Region: c.config.Region, + SecretID: c.config.SecretID, + Namespace: c.config.Namespace, + } + + if *c.config.TLSInsecure { + config.TLSConfig.Insecure = true + } + + client, err := nomad.NewClient(config) + if err != nil { + c.logger.Error().Err(err).Msg("Cannot create Nomad client") + return []model.Image{} + } + + jobs, _, err := client.Jobs().List(nil) + if err != nil { + c.logger.Error().Err(err).Msg("Cannot list Nomad jobs") + } + + var list []model.Image + + for _, job := range jobs { + jobInfo, _, err := client.Jobs().Info(job.ID, nil) + if err != nil { + c.logger.Error().Err(err).Msg("Cannot get info for job") + } + + for _, taskGroup := range jobInfo.TaskGroups { + // Get task group service labels + groupLabels := map[string]string{} + for _, service := range taskGroup.Services { + groupLabels = updateMap(groupLabels, parseServiceTags(service.Tags)) + } + + for _, task := range taskGroup.Tasks { + if task.Driver != "docker" { + continue + } + + if taskImage, ok := task.Config["image"]; ok { + imageName := taskImage.(string) + if imageName == "${meta.connect.sidecar_image}" { + c.logger.Debug(). + Str("job_id", job.ID). + Str("task_group", *taskGroup.Name). + Str("task_name", task.Name). + Msg("Skipping connect sidecar") + continue + } + + // Get task service labels + labels := map[string]string{} + labels = updateMap(labels, groupLabels) + for _, service := range task.Services { + labels = updateMap(labels, parseServiceTags(service.Tags)) + } + + // Finally, merge task meta values + labels = updateMap(labels, task.Meta) + + image, err := provider.ValidateImage(imageName, labels, *c.config.WatchByDefault) + if err != nil { + c.logger.Error(). + Err(err). + Str("job_id", job.ID). + Str("task_group", *taskGroup.Name). + Str("task_name", task.Name). + Str("image_name", imageName). + Msg("Error validating image") + continue + } else if reflect.DeepEqual(image, model.Image{}) { + c.logger.Debug(). + Str("job_id", job.ID). + Str("task_group", *taskGroup.Name). + Str("task_name", task.Name). + Str("image_name", imageName). + Msg("Watch disabled") + continue + } + + list = append(list, image) + } + } + } + } + + return list +} diff --git a/internal/provider/nomad/task_test.go b/internal/provider/nomad/task_test.go new file mode 100644 index 00000000..00c16ca0 --- /dev/null +++ b/internal/provider/nomad/task_test.go @@ -0,0 +1,53 @@ +package nomad + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseServiceTags(t *testing.T) { + testCases := []struct { + input []string + expected map[string]string + }{ + { + input: []string{ + "noequal", + }, + expected: map[string]string{}, + }, + { + input: []string{ + "emptyequal=", + }, + expected: map[string]string{ + "emptyequal": "", + }, + }, + { + input: []string{ + "key=value", + }, + expected: map[string]string{ + "key": "value", + }, + }, + { + input: []string{ + "withequal=a=b", + }, + expected: map[string]string{ + "withequal": "a=b", + }, + }, + } + + for _, tt := range testCases { + tt := tt + t.Run(tt.input[0], func(t *testing.T) { + result := parseServiceTags(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +}