Repository: crazy-max/diun
Pull request #722 (2022-11-21 21:55:26)

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.

crazy-max: @IamTheFij Will review this soon, thanks for your contrib!
IamTheFij: Just wanted to check in and see if there were any suggested changes here.
crazy-max: @IamTheFij It looks mostly fine to me thanks! I will create a Nomad instance on my side to play a bit with it and finalize the review.

Patch:
diff --git a/README.md b/README.md
index 4cf70c03..5285fae4 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
   <a href="https://crazymax.dev/diun/"><img src="https://img.shields.io/badge/doc-mkdocs-02a6f2?style=flat-square&logo=read-the-docs" alt="Documentation"></a>
   <a href="https://github.com/crazy-max/diun/releases/latest"><img src="https://img.shields.io/github/release/crazy-max/diun.svg?style=flat-square" alt="GitHub release"></a>
   <a href="https://github.com/crazy-max/diun/releases/latest"><img src="https://img.shields.io/github/downloads/crazy-max/diun/total.svg?style=flat-square" alt="Total downloads"></a>
-  <a href="https://github.com/crazy-max/diun/actions?workflow=build"><img src="https://img.shields.io/github/actions/workflow/status/crazy-max/diun/build.yml?branch=master&label=build&logo=github&style=flat-square" alt="Build Status"></a>
+  <a href="https://github.com/crazy-max/diun/actions?workflow=build"><img src="https://img.shields.io/github/workflow/status/crazy-max/diun/build?label=build&logo=github&style=flat-square" alt="Build Status"></a>
   <a href="https://hub.docker.com/r/crazymax/diun/"><img src="https://img.shields.io/docker/stars/crazymax/diun.svg?style=flat-square&logo=docker" alt="Docker Stars"></a>
   <a href="https://hub.docker.com/r/crazymax/diun/"><img src="https://img.shields.io/docker/pulls/crazymax/diun.svg?style=flat-square&logo=docker" alt="Docker Pulls"></a>
   <br /><a href="https://goreportcard.com/report/github.com/crazy-max/diun"><img src="https://goreportcard.com/badge/github.com/crazy-max/diun?style=flat-square" alt="Go Report"></a>
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/index.md b/docs/index.md
index 657379b2..06fa1f32 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -3,7 +3,7 @@
 <p align="center">
   <a href="https://github.com/crazy-max/diun/releases/latest"><img src="https://img.shields.io/github/release/crazy-max/diun.svg?style=flat-square" alt="GitHub release"></a>
   <a href="https://github.com/crazy-max/diun/releases/latest"><img src="https://img.shields.io/github/downloads/crazy-max/diun/total.svg?style=flat-square" alt="Total downloads"></a>
-  <a href="https://github.com/crazy-max/diun/actions?workflow=build"><img src="https://img.shields.io/github/actions/workflow/status/crazy-max/diun/build.yml?branch=master&label=build&logo=github&style=flat-square" alt="Build Status"></a>
+  <a href="https://github.com/crazy-max/diun/actions?workflow=build"><img src="https://img.shields.io/github/workflow/status/crazy-max/diun/build?label=build&logo=github&style=flat-square" alt="Build Status"></a>
   <a href="https://hub.docker.com/r/crazymax/diun/"><img src="https://img.shields.io/docker/stars/crazymax/diun.svg?style=flat-square&logo=docker" alt="Docker Stars"></a>
   <a href="https://hub.docker.com/r/crazymax/diun/"><img src="https://img.shields.io/docker/pulls/crazymax/diun.svg?style=flat-square&logo=docker" alt="Docker Pulls"></a>
   <br /><a href="https://goreportcard.com/report/github.com/crazy-max/diun"><img src="https://goreportcard.com/badge/github.com/crazy-max/diun?style=flat-square" alt="Go Report"></a>
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
+}


Test patch:
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)
+		})
+	}
+}