Repository: crazy-max/diun
Pull request #103 (2020-06-21 23:47:14)

Automatically determine registry options based on image name

codecov[bot]: # [Codecov](https://codecov.io/gh/crazy-max/diun/pull/103?src=pr&el=h1) Report
> Merging [#103](https://codecov.io/gh/crazy-max/diun/pull/103?src=pr&el=desc) into [v4](https://codecov.io/gh/crazy-max/diun/commit/f521384ee10a3c808822acbc58a830d8be3c5d99&el=desc) will **increase** coverage by `1.90%`.
> The diff coverage is `75.00%`.

[![Impacted file tree graph](https://codecov.io/gh/crazy-max/diun/pull/103/graphs/tree.svg?width=650&height=150&src=pr&token=phc5KYIjsV)](https://codecov.io/gh/crazy-max/diun/pull/103?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##               v4     #103      +/-   ##
==========================================
+ Coverage   67.20%   69.10%   +1.90%     
==========================================
  Files           7        7              
  Lines         250      246       -4     
==========================================
+ Hits          168      170       +2     
+ Misses         55       49       -6     
  Partials       27       27              
```


| [Impacted Files](https://codecov.io/gh/crazy-max/diun/pull/103?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [internal/config/config.go](https://codecov.io/gh/crazy-max/diun/pull/103/diff?src=pr&el=tree#diff-aW50ZXJuYWwvY29uZmlnL2NvbmZpZy5nbw==) | `81.25% <ø> (+12.82%)` | :arrow_up: |
| [pkg/registry/image.go](https://codecov.io/gh/crazy-max/diun/pull/103/diff?src=pr&el=tree#diff-cGtnL3JlZ2lzdHJ5L2ltYWdlLmdv) | `67.74% <0.00%> (ø)` | |
| [pkg/registry/registry.go](https://codecov.io/gh/crazy-max/diun/pull/103/diff?src=pr&el=tree#diff-cGtnL3JlZ2lzdHJ5L3JlZ2lzdHJ5Lmdv) | `71.42% <100.00%> (+1.73%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/crazy-max/diun/pull/103?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/crazy-max/diun/pull/103?src=pr&el=footer). Last update [ca61056...5f2a434](https://codecov.io/gh/crazy-max/diun/pull/103?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/config/index.md b/docs/config/index.md
index f0195b4e..57285971 100644
--- a/docs/config/index.md
+++ b/docs/config/index.md
@@ -75,14 +75,15 @@ You can define a configuration file through the option `--config` with the follo
         timeout: 10s
     
     regopts:
-      someregistryoptions:
+      - name: "myregistry"
         username: foo
         password: bar
         timeout: 20s
-      onemore:
+        insecureTLS: true
+      - name: "docker.io"
+        selector: image
         username: foo2
         password: bar2
-        insecureTLS: true
     
     providers:
       docker:
@@ -131,11 +132,16 @@ All configuration from file can be transposed into environment variables. As an
         timeout: 10s
     
     regopts:
-      someregistryoptions:
+      - name: "docker.io"
+        selector: image
         username: foo
         password: bar
+      - name: "registry.gitlab.com"
+        selector: image
+        username: fii
+        password: bor
         timeout: 20s
-    
+
     providers:
       kubernetes:
         tlsInsecure: false
@@ -168,9 +174,15 @@ Can be transposed to:
     DIUN_NOTIF_WEBHOOK_HEADERS_AUTHORIZATION=Token123456
     DIUN_NOTIF_WEBHOOK_TIMEOUT=10s
     
-    DIUN_REGOPTS_SOMEREGISTRYOPTIONS_USERNAME=foo
-    DIUN_REGOPTS_SOMEREGISTRYOPTIONS_PASSWORD=bar
-    DIUN_REGOPTS_SOMEREGISTRYOPTIONS_TIMEOUT=20s
+    DIUN_REGOPTS_0_NAME=docker.io
+    DIUN_REGOPTS_0_SELECTOR=image
+    DIUN_REGOPTS_0_USERNAME=foo
+    DIUN_REGOPTS_0_PASSWORD=bar
+    DIUN_REGOPTS_1_NAME=registry.gitlab.com
+    DIUN_REGOPTS_1_SELECTOR=image
+    DIUN_REGOPTS_1_USERNAME=fii
+    DIUN_REGOPTS_1_PASSWORD=bor
+    DIUN_REGOPTS_1_TIMEOUT=20s
     
     PROVIDERS_KUBERNETES_TLSINSECURE=false
     PROVIDERS_KUBERNETES_NAMESPACES=default,production
diff --git a/docs/config/regopts.md b/docs/config/regopts.md
index 6b4c5eb3..e842a47d 100644
--- a/docs/config/regopts.md
+++ b/docs/config/regopts.md
@@ -1,87 +1,155 @@
-# Registries options configuration
+# Registry options configuration
 
-## `username`
+## Overview
+
+Registry options is used to authenticate against a registry during the analysis of an image:
+
+```yaml
+regopts:
+  - name: "myregistry"
+    username: fii
+    password: bor
+    timeout: 5s
+  - name: "docker.io"
+    selector: image
+    username: foo
+    password: bar
+  - name: "docker.io/crazymax"
+    selector: image
+    usernameFile: /run/secrets/username
+    passwordFile: /run/secrets/password
+```
+
+`myregistry` will be used as a `name` selector (default) if referenced by its [name](#name).
+
+`docker.io` will be used as an `image` selector. If an image is on DockerHub (`docker.io` domain), this registry options will
+be selected if not referenced as a `regopt` name.
+
+`docker.io/crazymax` will be used as an `image` selector. If an image is on DockerHub and in `crazymax` namespace, this registry options will
+be selected if not referenced as a `regopt` name.
+
+## Configuration
+
+### `name`
+
+Unique name for registry options. This name can be used through `diun.regopt`
+[Docker](../providers/docker.md#docker-labels) / [Swarm](../providers/swarm.md#docker-labels) label
+or [Kubernetes annotation](../providers/kubernetes.md#kubernetes-annotations) and also as `regopt` for the [file provider](../providers/file.md).
+
+!!! warning
+    * **Required**
+    * Must be **unique**
+
+!!! example "Config file"
+    ```yaml
+    regopts:
+      - name: "myregistry"
+    ```
+
+!!! abstract "Environment variables"
+    * `DIUN_REGOPTS_<KEY>_NAME`
+
+### `selector`
+
+What kind of selector to use to retrieve registry options. (default `name`)
+
+!!! warning
+    * Accepted values are `name` or `image`
+
+* `name` selector is the default value and will retrieve this registry options only if it's referenced by its [name](#name).
+* `image` selector will retrieve this registry options if the given image matches the registry domain or repository path.
+
+!!! example "Config file"
+    ```yaml
+    regopts:
+      - name: "myregistry"
+        selector: name
+    ```
+
+!!! abstract "Environment variables"
+    * `DIUN_REGOPTS_<KEY>_SELECTOR`
+
+### `username`
 
 Registry username.
 
 !!! example "Config file"
     ```yaml
     regopts:
-      <name>:
+      - name: "myregistry"
         username: foo
     ```
 
 !!! abstract "Environment variables"
-    * `DIUN_REGOPTS_<NAME>_USERNAME`
+    * `DIUN_REGOPTS_<KEY>_USERNAME`
 
-## `usernameFile`
+### `usernameFile`
 
 Use content of secret file as registry username if `username` not defined.
 
 !!! example "Config file"
     ```yaml
     regopts:
-      <name>:
+      - name: "myregistry"
         usernameFile: /run/secrets/username
     ```
 
 !!! abstract "Environment variables"
-    * `DIUN_REGOPTS_<NAME>_USERNAMEFILE`
+    * `DIUN_REGOPTS_<KEY>_USERNAMEFILE`
 
-## `password`
+### `password`
 
 Registry password.
 
 !!! example "Config file"
     ```yaml
     regopts:
-      <name>:
+      - name: "myregistry"
         username: foo
         password: bar
     ```
 
 !!! abstract "Environment variables"
-    * `DIUN_REGOPTS_<NAME>_PASSWORD`
+    * `DIUN_REGOPTS_<KEY>_PASSWORD`
 
-## `passwordFile`
+### `passwordFile`
 
 Use content of secret file as registry password if `password` not defined.
 
 !!! example "Config file"
     ```yaml
     regopts:
-      <name>:
-        usernameFile: /run/secrets/username
-        usernameFile: /run/secrets/password
+      - name: "myregistry"
+        passwordFile: /run/secrets/password
     ```
 
 !!! abstract "Environment variables"
-    * `DIUN_REGOPTS_<NAME>_PASSWORDFILE`
+    * `DIUN_REGOPTS_<KEY>_PASSWORDFILE`
 
-## `timeout`
+### `timeout`
 
 Timeout is the maximum amount of time for the TCP connection to establish. (default `10s`)
 
 !!! example "Config file"
     ```yaml
     regopts:
-      <name>:
+      - name: "myregistry"
         timeout: 10s
     ```
 
 !!! abstract "Environment variables"
-    * `DIUN_REGOPTS_<NAME>_TIMEOUT`
+    * `DIUN_REGOPTS_<KEY>_TIMEOUT`
 
-## `insecureTLS`
+### `insecureTLS`
 
 Allow contacting docker registry over HTTP, or HTTPS with failed TLS verification. (default `false`)
 
 !!! example "Config file"
     ```yaml
     regopts:
-      <name>:
+      - name: "myregistry"
         insecureTLS: false
     ```
 
 !!! abstract "Environment variables"
-    * `DIUN_REGOPTS_<NAME>_INSECURETLS`
+    * `DIUN_REGOPTS_<KEY>_INSECURETLS`
diff --git a/docs/providers/docker.md b/docs/providers/docker.md
index 3ac94c21..6e53e1a7 100644
--- a/docs/providers/docker.md
+++ b/docs/providers/docker.md
@@ -170,7 +170,7 @@ You can configure more finely the way to analyze the image of your container thr
 | Name                          | Default       | Description   |
 |-------------------------------|---------------|---------------|
 | `diun.enable`                 |               | Set to true to enable image analysis of this container |
-| `diun.regopts_id`             |               | Registry options ID from [`regopts`](../config/regopts.md) to use |
+| `diun.regopt`                 |               | Registry options name from [`regopts`](../config/regopts.md) to use |
 | `diun.watch_repo`             | `false`       | Watch all tags of this container image |
 | `diun.max_tags`               | `0`           | Maximum number of tags to watch if `diun.watch_repo` enabled. `0` means all of them |
 | `diun.include_tags`           |               | Semi-colon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` |
diff --git a/docs/providers/file.md b/docs/providers/file.md
index dc578552..729b02aa 100644
--- a/docs/providers/file.md
+++ b/docs/providers/file.md
@@ -17,14 +17,18 @@ watch:
   schedule: "* * * * *"
 
 regopts:
-  someregistryoptions:
+  - name: "myregistry"
+    username: fii
+    password: bor
+    timeout: 5s
+  - name: "docker.io/crazymax"
+    selector: image
+    username: fii
+    password: bor
+  - name: "docker.io"
+    selector: image
     username: foo
     password: bar
-    timeout: 20
-  onemore:
-    username: foo2
-    password: bar2
-    insecureTLS: true
 
 providers:
   file:
@@ -34,18 +38,20 @@ providers:
 ```yaml
 ### /path/to/config.yml
 
-# Watch latest tag of crazymax/nextcloud image on docker.io (DockerHub) with registry ID 'someregistryoptions'.
+# Watch latest tag of crazymax/nextcloud image on docker.io (DockerHub)
+# with registry options named 'docker.io/crazymax' (image selector).
 - name: docker.io/crazymax/nextcloud:latest
-  regopts_id: someregistryoptions
 
-# Watch 4.0.0 tag of jfrog/artifactory-oss image on frog-docker-reg2.bintray.io (Bintray) with registry ID 'onemore'.
+# Watch 4.0.0 tag of jfrog/artifactory-oss image on frog-docker-reg2.bintray.io (Bintray)
+# with registry options named 'myregistry' (name selector).
 - name: jfrog-docker-reg2.bintray.io/jfrog/artifactory-oss:4.0.0
-  regopts_id: onemore
+  regopt: myregistry
 
 # Watch coreos/hyperkube image on quay.io (Quay) and assume latest tag.
 - name: quay.io/coreos/hyperkube
 
-# Watch crazymax/swarm-cronjob image and assume docker.io registry and latest tag.
+# Watch crazymax/swarm-cronjob image and assume docker.io registry and latest tag
+# with registry options named 'docker.io/crazymax' (image selector).
 # Only include tags matching regexp ^1\.2\..*
 - name: crazymax/swarm-cronjob
   watch_repo: true
@@ -53,6 +59,7 @@ providers:
     - ^1\.2\..*
 
 # Watch portainer/portainer image on docker.io (DockerHub) and assume latest tag
+# with registry options named 'docker.io' (image selector).
 # Only watch latest 10 tags and include tags matching regexp ^(0|[1-9]\d*)\..*
 - name: docker.io/portainer/portainer
   watch_repo: true
@@ -60,7 +67,8 @@ providers:
   include_tags:
     - ^(0|[1-9]\d*)\..*
 
-# Watch alpine image (library) and assume docker.io registry and latest tag.
+# Watch alpine image (library) and assume docker.io registry and latest tag
+# with registry options named 'docker.io' (image selector).
 # Force linux/arm64/v8 platform for this image
 - name: alpine
   watch_repo: true
@@ -83,7 +91,8 @@ watch:
   schedule: "* * * * *"
 
 regopts:
-  jfrog:
+  - name: "docker.bintray.io"
+    selector: image
     username: foo
     password: bar
 
@@ -97,7 +106,6 @@ providers:
 - name: crazymax/cloudflared
   watch_repo: true
 - name: docker.bintray.io/jfrog/xray-mongo:3.2.6
-  regopts_id: jfrog
 ```
 
 Here we want to analyze all tags of `crazymax/cloudflared` and `docker.bintray.io/jfrog/xray-mongo:3.2.6` tag. Now let's start Diun:
@@ -164,7 +172,7 @@ The configuration file(s) defines a slice of images to analyze with the followin
 | Name                          | Default                          | Description   |
 |-------------------------------|----------------------------------|---------------|
 | `name`                        | `latest`                         | 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 |
-| `regopts_id`                  |                                  | Registry options ID from [`regopts`](../config/regopts.md) to use |
+| `regopt`                      |                                  | Registry options name from [`regopts`](../config/regopts.md) to use |
 | `watch_repo`                  | `false`                          | Watch all tags of this image |
 | `max_tags`                    | `0`                              | Maximum number of tags to watch if `watch_repo` enabled. `0` means all of them |
 | `include_tags`                |                                  | List of regular expressions to include tags. Can be useful if you enable `watch_repo` |
diff --git a/docs/providers/kubernetes.md b/docs/providers/kubernetes.md
index d4188c6b..ee304a83 100644
--- a/docs/providers/kubernetes.md
+++ b/docs/providers/kubernetes.md
@@ -273,7 +273,7 @@ You can configure more finely the way to analyze the image of your pods through
 | Name                          | Default       | Description   |
 |-------------------------------|---------------|---------------|
 | `diun.enable`                 |               | Set to true to enable image analysis of this pod |
-| `diun.regopts_id`             |               | Registry options ID from [`regopts`](../config/regopts.md) to use |
+| `diun.regopt`                 |               | Registry options name from [`regopts`](../config/regopts.md) to use |
 | `diun.watch_repo`             | `false`       | Watch all tags of this pod image |
 | `diun.max_tags`               | `0`           | Maximum number of tags to watch if `diun.watch_repo` enabled. `0` means all of them |
 | `diun.include_tags`           |               | Semi-colon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` |
diff --git a/docs/providers/swarm.md b/docs/providers/swarm.md
index 1d4a2012..6ea18cd2 100644
--- a/docs/providers/swarm.md
+++ b/docs/providers/swarm.md
@@ -174,7 +174,7 @@ You can configure more finely the way to analyze the image of your service throu
 | Name                          | Default       | Description   |
 |-------------------------------|---------------|---------------|
 | `diun.enable`                 |               | Set to true to enable image analysis of this service |
-| `diun.regopts_id`             |               | Registry options ID from [`regopts`](../config/regopts.md) to use |
+| `diun.regopt`                 |               | Registry options name to use from [`regopts`](../config/regopts.md) to use |
 | `diun.watch_repo`             | `false`       | Watch all tags of this service image |
 | `diun.max_tags`               | `0`           | Maximum number of tags to watch if `diun.watch_repo` enabled. `0` means all of them |
 | `diun.include_tags`           |               | Semi-colon separated list of regular expressions to include tags. Can be useful if you enable `diun.watch_repo` |
diff --git a/internal/app/job.go b/internal/app/job.go
index 2b59924c..f8d6eca9 100644
--- a/internal/app/job.go
+++ b/internal/app/job.go
@@ -36,19 +36,23 @@ func (di *Diun) createJob(job model.Job) {
 		return
 	}
 
-	// Registry options
-	regOpts, err := di.cfg.GetRegOpts(job.Image.RegOptsID)
+	// Get registry options
+	reg, err := di.cfg.RegOpts.Select(job.Image.RegOpt, job.RegImage)
 	if err != nil {
 		sublog.Warn().Err(err).Msg("Registry options")
+	} else if reg != nil {
+		sublog.Debug().Str("regopt", reg.Name).Msg("Registry options will be used")
+	} else {
+		reg = (&model.RegOpt{}).GetDefaults()
 	}
 
-	regUser, err := utl.GetSecret(regOpts.Username, regOpts.UsernameFile)
+	regUser, err := utl.GetSecret(reg.Username, reg.UsernameFile)
 	if err != nil {
-		log.Warn().Err(err).Msgf("Cannot retrieve username secret for regopts %s", job.Image.RegOptsID)
+		log.Warn().Err(err).Msgf("Cannot retrieve username secret for regopts %s", reg.Name)
 	}
-	regPassword, err := utl.GetSecret(regOpts.Password, regOpts.PasswordFile)
+	regPassword, err := utl.GetSecret(reg.Password, reg.PasswordFile)
 	if err != nil {
-		log.Warn().Err(err).Msgf("Cannot retrieve password secret for regopts %s", job.Image.RegOptsID)
+		log.Warn().Err(err).Msgf("Cannot retrieve password secret for regopts %s", reg.Name)
 	}
 
 	// Set defaults
@@ -78,8 +82,8 @@ func (di *Diun) createJob(job model.Job) {
 	job.Registry, err = registry.New(registry.Options{
 		Username:     regUser,
 		Password:     regPassword,
-		Timeout:      *regOpts.Timeout,
-		InsecureTLS:  *regOpts.InsecureTLS,
+		Timeout:      *reg.Timeout,
+		InsecureTLS:  *reg.InsecureTLS,
 		UserAgent:    di.meta.UserAgent,
 		ImageOs:      job.Image.Platform.Os,
 		ImageArch:    job.Image.Platform.Arch,
diff --git a/internal/config/config.go b/internal/config/config.go
index b247813e..7d6d59b7 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -14,11 +14,11 @@ import (
 
 // Config holds configuration details
 type Config struct {
-	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"`
+	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   model.RegOpts    `yaml:"regopts,omitempty" json:"regopts,omitempty" validate:"unique=Name,dive"`
+	Providers *model.Providers `yaml:"providers,omitempty" json:"providers,omitempty" validate:"required"`
 }
 
 // Load returns Configuration struct
@@ -76,16 +76,6 @@ func (cfg *Config) loadEnv(out interface{}) error {
 	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, "", "  ")
diff --git a/internal/config/fixtures/config.file-regopts.yml b/internal/config/fixtures/config.file.yml
similarity index 83%
rename from internal/config/fixtures/config.file-regopts.yml
rename to internal/config/fixtures/config.file.yml
index 3bd6d090..1d607681 100644
--- a/internal/config/fixtures/config.file-regopts.yml
+++ b/internal/config/fixtures/config.file.yml
@@ -8,4 +8,4 @@ notif:
 
 providers:
   file:
-    filename: "./fixtures/dummy.yml"
+    filename: "./fixtures/file.yml"
diff --git a/internal/config/fixtures/config.validate.yml b/internal/config/fixtures/config.validate.yml
index ab54e5ad..cfe83ba6 100644
--- a/internal/config/fixtures/config.validate.yml
+++ b/internal/config/fixtures/config.validate.yml
@@ -53,14 +53,16 @@ notif:
     timeout: 10s
 
 regopts:
-  someregopts:
+  - name: "myregistry"
     timeout: 5s
-  bintrayoptions:
+  - name: "docker.io"
+    selector: image
     username: foo
     password: bar
-  sensitive:
-    usernameFile: /run/secrets/username
-    passwordFile: /run/secrets/password
+  - name: "docker.io/crazymax"
+    selector: image
+    usernameFile: ./fixtures/run_secrets_username
+    passwordFile: ./fixtures/run_secrets_username
 
 providers:
   docker:
@@ -68,4 +70,4 @@ providers:
     watchStopped: true
   swarm: {}
   file:
-    filename: ./fixtures/dummy.yml
+    filename: ./fixtures/file.yml
diff --git a/internal/config/fixtures/dummy.yml b/internal/config/fixtures/dummy.yml
deleted file mode 100644
index 740503eb..00000000
--- a/internal/config/fixtures/dummy.yml
+++ /dev/null
@@ -1,1 +0,0 @@
-# noop
diff --git a/internal/config/fixtures/file.yml b/internal/config/fixtures/file.yml
new file mode 100644
index 00000000..e415be02
--- /dev/null
+++ b/internal/config/fixtures/file.yml
@@ -0,0 +1,24 @@
+- name: docker.io/crazymax/nextcloud:latest
+  regopt: myregistry
+- name: crazymax/swarm-cronjob
+  watch_repo: true
+  include_tags:
+    - ^1\.2\..*
+- name: docker.io/portainer/portainer
+  watch_repo: true
+  max_tags: 10
+  include_tags:
+    - ^(0|[1-9]\d*)\..*
+- name: traefik
+  watch_repo: true
+- name: alpine
+  platform:
+    os: linux
+    arch: arm64
+    variant: v8
+- name: docker.io/graylog/graylog:3.2.0
+- name: jacobalberty/unifi:5.9
+- name: crazymax/ddns-route53
+  watch_repo: true
+  include_tags:
+    - ^1\..*
diff --git a/internal/config/fixtures/run_secrets_password b/internal/config/fixtures/run_secrets_password
new file mode 100644
index 00000000..ba0e162e
--- /dev/null
+++ b/internal/config/fixtures/run_secrets_password
@@ -0,0 +1,1 @@
+bar
\ No newline at end of file
diff --git a/internal/config/fixtures/run_secrets_username b/internal/config/fixtures/run_secrets_username
new file mode 100644
index 00000000..19102815
--- /dev/null
+++ b/internal/config/fixtures/run_secrets_username
@@ -0,0 +1,1 @@
+foo
\ No newline at end of file
diff --git a/internal/db/migrate.go b/internal/db/migrate.go
index ceb8057c..bfbe0c7b 100644
--- a/internal/db/migrate.go
+++ b/internal/db/migrate.go
@@ -28,7 +28,7 @@ func (c *Client) Migrate() error {
 
 		log.Info().Msgf("Database migration v%d...", version)
 		if err := migration(c); err != nil {
-			return errors.Wrap(err, fmt.Sprintf("Database migration v%d failed", version))
+			return errors.Wrapf(err, "Database migration v%d failed", version)
 		}
 	}
 
diff --git a/internal/model/image.go b/internal/model/image.go
index 2049153f..062ede1d 100644
--- a/internal/model/image.go
+++ b/internal/model/image.go
@@ -4,7 +4,7 @@ package model
 type Image struct {
 	Name        string        `yaml:"name,omitempty" json:",omitempty"`
 	Platform    ImagePlatform `yaml:"platform,omitempty" json:",omitempty"`
-	RegOptsID   string        `yaml:"regopts_id,omitempty" json:",omitempty"`
+	RegOpt      string        `yaml:"regopt,omitempty" json:",omitempty"`
 	WatchRepo   bool          `yaml:"watch_repo,omitempty" json:",omitempty"`
 	MaxTags     int           `yaml:"max_tags,omitempty" json:",omitempty"`
 	IncludeTags []string      `yaml:"include_tags,omitempty" json:",omitempty"`
diff --git a/internal/model/regopts.go b/internal/model/regopts.go
index f66b28df..190f01f1 100644
--- a/internal/model/regopts.go
+++ b/internal/model/regopts.go
@@ -1,13 +1,21 @@
 package model
 
 import (
+	"fmt"
+	"strings"
 	"time"
 
+	"github.com/crazy-max/diun/v4/pkg/registry"
 	"github.com/crazy-max/diun/v4/pkg/utl"
 )
 
-// RegOpts holds registry options configuration
-type RegOpts struct {
+// RegOpts holds slice of registry options
+type RegOpts []RegOpt
+
+// RegOpt holds registry options configuration
+type RegOpt struct {
+	Name         string         `yaml:"name,omitempty" json:"name,omitempty" validate:"required"`
+	Selector     RegOptSelector `yaml:"selector,omitempty" json:"selector,omitempty" validate:"required,oneof=name image"`
 	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"`
@@ -16,15 +24,41 @@ type RegOpts struct {
 	Timeout      *time.Duration `yaml:"timeout,omitempty" json:"timeout,omitempty" validate:"required"`
 }
 
+// RegOpt selector constants
+const (
+	RegOptSelectorName  = RegOptSelector("name")
+	RegOptSelectorImage = RegOptSelector("image")
+)
+
+// RegOptSelector holds registry options selector
+type RegOptSelector string
+
 // GetDefaults gets the default values
-func (s *RegOpts) GetDefaults() *RegOpts {
-	n := &RegOpts{}
+func (s *RegOpt) GetDefaults() *RegOpt {
+	n := &RegOpt{}
 	n.SetDefaults()
 	return n
 }
 
 // SetDefaults sets the default values
-func (s *RegOpts) SetDefaults() {
+func (s *RegOpt) SetDefaults() {
+	s.Selector = RegOptSelectorName
 	s.InsecureTLS = utl.NewFalse()
 	s.Timeout = utl.NewDuration(10 * time.Second)
 }
+
+// Select returns a registry based on its selector
+func (s *RegOpts) Select(name string, image registry.Image) (*RegOpt, error) {
+	for _, regOpt := range *s {
+		if regOpt.Selector == RegOptSelectorName && name == regOpt.Name {
+			return &regOpt, nil
+		}
+		if regOpt.Selector == RegOptSelectorImage && strings.HasPrefix(image.Name(), regOpt.Name) {
+			return &regOpt, nil
+		}
+	}
+	if len(name) == 0 {
+		return nil, nil
+	}
+	return nil, fmt.Errorf("%s not found", name)
+}
diff --git a/internal/provider/common.go b/internal/provider/common.go
index 060544ce..4b964e9b 100644
--- a/internal/provider/common.go
+++ b/internal/provider/common.go
@@ -31,8 +31,8 @@ func ValidateContainerImage(image string, labels map[string]string, watchByDef b
 
 	for key, value := range labels {
 		switch key {
-		case "diun.regopts_id":
-			img.RegOptsID = value
+		case "diun.regopt":
+			img.RegOpt = value
 		case "diun.watch_repo":
 			if img.WatchRepo, err = strconv.ParseBool(value); err != nil {
 				return img, fmt.Errorf("cannot parse %s value of label %s", value, key)
diff --git a/internal/provider/file/fixtures/bintray.yml b/internal/provider/file/fixtures/bintray.yml
index 4bd71e0c..3f9efd1a 100644
--- a/internal/provider/file/fixtures/bintray.yml
+++ b/internal/provider/file/fixtures/bintray.yml
@@ -1,5 +1,5 @@
 - name: jfrog-docker-reg2.bintray.io/jfrog/artifactory-oss:4.0.0
-  regopts_id: bintrayoptions
+  regopt: bintrayoptions
 - name: docker.bintray.io/jfrog/xray-server:2.8.6
   watch_repo: true
   max_tags: 50
diff --git a/internal/provider/file/fixtures/dockerhub.yml b/internal/provider/file/fixtures/dockerhub.yml
index 68cd3c92..e415be02 100644
--- a/internal/provider/file/fixtures/dockerhub.yml
+++ b/internal/provider/file/fixtures/dockerhub.yml
@@ -1,5 +1,5 @@
 - name: docker.io/crazymax/nextcloud:latest
-  regopts_id: someregopts
+  regopt: myregistry
 - name: crazymax/swarm-cronjob
   watch_repo: true
   include_tags:
diff --git a/pkg/registry/image.go b/pkg/registry/image.go
index 25da291f..eac77971 100644
--- a/pkg/registry/image.go
+++ b/pkg/registry/image.go
@@ -60,7 +60,7 @@ func ParseImage(parseOpts ParseImageOptions) (Image, error) {
 	// Parse the image name and tag.
 	named, err := reference.ParseNormalizedNamed(parseOpts.Name)
 	if err != nil {
-		return Image{}, errors.Wrap(err, fmt.Sprintf("parsing image %s failed", parseOpts.Name))
+		return Image{}, errors.Wrapf(err, "parsing image %s failed", parseOpts.Name)
 	}
 	// Add the latest lag if they did not provide one.
 	named = reference.TagNameOnly(named)
diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go
index 7521b071..6b658406 100644
--- a/pkg/registry/registry.go
+++ b/pkg/registry/registry.go
@@ -32,7 +32,7 @@ type Options struct {
 // New creates new docker registry client instance
 func New(opts Options) (*Client, error) {
 	// Auth
-	auth := &types.DockerAuthConfig{}
+	var auth *types.DockerAuthConfig
 	if opts.Username != "" {
 		auth = &types.DockerAuthConfig{
 			Username: opts.Username,
@@ -75,6 +75,16 @@ func (c *Client) newImage(ctx context.Context, imageStr string) (types.ImageClos
 		return nil, errors.Wrap(err, "Invalid image name")
 	}
 
+	if c.sysCtx.DockerAuthConfig == nil {
+		c.sysCtx.DockerAuthConfig = &types.DockerAuthConfig{}
+		// TODO: Seek credentials
+		//auth, err := config.GetCredentials(c.sysCtx, reference.Domain(ref.DockerReference()))
+		//if err != nil {
+		//	return nil, errors.Wrap(err, "Cannot get registry credentials")
+		//}
+		//*c.sysCtx.DockerAuthConfig = auth
+	}
+
 	img, err := ref.NewImage(ctx, c.sysCtx)
 	if err != nil {
 		return nil, err


Test patch:
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 941b5ef8..029da8ea 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -97,20 +97,28 @@ func TestLoadFile(t *testing.T) {
 						Timeout: utl.NewDuration(10 * time.Second),
 					},
 				},
-				RegOpts: map[string]*model.RegOpts{
-					"someregopts": {
+				RegOpts: model.RegOpts{
+					{
+						Name:        "myregistry",
+						Selector:    model.RegOptSelectorName,
+						Username:    "fii",
+						Password:    "bor",
 						InsecureTLS: utl.NewFalse(),
 						Timeout:     utl.NewDuration(5 * time.Second),
 					},
-					"bintrayoptions": {
+					{
+						Name:        "docker.io",
+						Selector:    model.RegOptSelectorImage,
 						Username:    "foo",
 						Password:    "bar",
 						InsecureTLS: utl.NewFalse(),
 						Timeout:     utl.NewDuration(10 * time.Second),
 					},
-					"sensitive": {
-						UsernameFile: "/run/secrets/username",
-						PasswordFile: "/run/secrets/password",
+					{
+						Name:         "docker.io/crazymax",
+						Selector:     model.RegOptSelectorImage,
+						UsernameFile: "./fixtures/run_secrets_username",
+						PasswordFile: "./fixtures/run_secrets_password",
 						InsecureTLS:  utl.NewFalse(),
 						Timeout:      utl.NewDuration(10 * time.Second),
 					},
@@ -130,7 +138,7 @@ func TestLoadFile(t *testing.T) {
 						WatchByDefault: utl.NewTrue(),
 					},
 					File: &model.PrdFile{
-						Filename: "./fixtures/dummy.yml",
+						Filename: "./fixtures/file.yml",
 					},
 				},
 			},
@@ -191,18 +199,22 @@ func TestLoadEnv(t *testing.T) {
 		{
 			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_REGOPTS_0_NAME=docker.io",
+				"DIUN_REGOPTS_0_SELECTOR=image",
+				"DIUN_REGOPTS_0_USERNAMEFILE=./fixtures/run_secrets_username",
+				"DIUN_REGOPTS_0_PASSWORDFILE=./fixtures/run_secrets_password",
+				"DIUN_REGOPTS_0_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",
+				RegOpts: model.RegOpts{
+					{
+						Name:         "docker.io",
+						Selector:     model.RegOptSelectorImage,
+						UsernameFile: "./fixtures/run_secrets_username",
+						PasswordFile: "./fixtures/run_secrets_password",
 						InsecureTLS:  utl.NewFalse(),
 						Timeout:      utl.NewDuration(30 * time.Second),
 					},
@@ -348,8 +360,8 @@ func TestLoadMixed(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			desc:    "file provider, regopts (file) and notif webhook env override",
-			cfgfile: "./fixtures/config.file-regopts.yml",
+			desc:    "file provider and notif webhook env override",
+			cfgfile: "./fixtures/config.file.yml",
 			environ: []string{
 				"DIUN_NOTIF_WEBHOOK_ENDPOINT=http://webhook.foo.com/sd54qad89azd5a",
 				"DIUN_NOTIF_WEBHOOK_HEADERS_AUTHORIZATION=Token78910",
@@ -374,7 +386,7 @@ func TestLoadMixed(t *testing.T) {
 				RegOpts: nil,
 				Providers: &model.Providers{
 					File: &model.PrdFile{
-						Filename: "./fixtures/dummy.yml",
+						Filename: "./fixtures/file.yml",
 					},
 				},
 			},
diff --git a/internal/config/fixtures/config.test.yml b/internal/config/fixtures/config.test.yml
index 88c0d0d0..208256ae 100644
--- a/internal/config/fixtures/config.test.yml
+++ b/internal/config/fixtures/config.test.yml
@@ -53,14 +53,18 @@ notif:
     timeout: 10s
 
 regopts:
-  someregopts:
+  - name: "myregistry"
+    username: fii
+    password: bor
     timeout: 5s
-  bintrayoptions:
+  - name: "docker.io"
+    selector: image
     username: foo
     password: bar
-  sensitive:
-    usernameFile: /run/secrets/username
-    passwordFile: /run/secrets/password
+  - name: "docker.io/crazymax"
+    selector: image
+    usernameFile: ./fixtures/run_secrets_username
+    passwordFile: ./fixtures/run_secrets_password
 
 providers:
   docker:
@@ -70,4 +74,4 @@ providers:
   kubernetes:
     watchByDefault: true
   file:
-    filename: ./fixtures/dummy.yml
+    filename: ./fixtures/file.yml
diff --git a/internal/provider/file/file_test.go b/internal/provider/file/file_test.go
index a462645d..1ef059eb 100644
--- a/internal/provider/file/file_test.go
+++ b/internal/provider/file/file_test.go
@@ -13,8 +13,8 @@ var (
 		{
 			Provider: "file",
 			Image: model.Image{
-				Name:      "jfrog-docker-reg2.bintray.io/jfrog/artifactory-oss:4.0.0",
-				RegOptsID: "bintrayoptions",
+				Name:   "jfrog-docker-reg2.bintray.io/jfrog/artifactory-oss:4.0.0",
+				RegOpt: "bintrayoptions",
 			},
 		},
 		{
@@ -30,8 +30,8 @@ var (
 		{
 			Provider: "file",
 			Image: model.Image{
-				Name:      "docker.io/crazymax/nextcloud:latest",
-				RegOptsID: "someregopts",
+				Name:   "docker.io/crazymax/nextcloud:latest",
+				RegOpt: "myregistry",
 			},
 		},
 		{