From c0e3f7e85f7dab41bacadcb4a74a1a98f02095bb Mon Sep 17 00:00:00 2001 From: CrazyMax <1951866+crazy-max@users.noreply.github.com> Date: Mon, 22 Jun 2020 00:19:59 +0000 Subject: [PATCH] Automatically determine registry options based on image name (#103) Co-authored-by: CrazyMax --- docs/config/index.md | 28 +++-- docs/config/regopts.md | 110 ++++++++++++++---- docs/providers/docker.md | 2 +- docs/providers/file.md | 38 +++--- docs/providers/kubernetes.md | 2 +- docs/providers/swarm.md | 2 +- internal/app/job.go | 20 ++-- internal/config/config.go | 20 +--- internal/config/config_test.go | 46 +++++--- ...onfig.file-regopts.yml => config.file.yml} | 2 +- internal/config/fixtures/config.test.yml | 16 ++- internal/config/fixtures/config.validate.yml | 14 ++- internal/config/fixtures/dummy.yml | 1 - internal/config/fixtures/file.yml | 24 ++++ internal/config/fixtures/run_secrets_password | 1 + internal/config/fixtures/run_secrets_username | 1 + internal/db/migrate.go | 2 +- internal/model/image.go | 2 +- internal/model/regopts.go | 44 ++++++- internal/provider/common.go | 4 +- internal/provider/file/file_test.go | 8 +- internal/provider/file/fixtures/bintray.yml | 2 +- internal/provider/file/fixtures/dockerhub.yml | 2 +- pkg/registry/image.go | 2 +- pkg/registry/registry.go | 12 +- 25 files changed, 287 insertions(+), 118 deletions(-) rename internal/config/fixtures/{config.file-regopts.yml => config.file.yml} (83%) delete mode 100644 internal/config/fixtures/dummy.yml create mode 100644 internal/config/fixtures/file.yml create mode 100644 internal/config/fixtures/run_secrets_password create mode 100644 internal/config/fixtures/run_secrets_username 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__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__SELECTOR` + +### `username` Registry username. !!! example "Config file" ```yaml regopts: - : + - name: "myregistry" username: foo ``` !!! abstract "Environment variables" - * `DIUN_REGOPTS__USERNAME` + * `DIUN_REGOPTS__USERNAME` -## `usernameFile` +### `usernameFile` Use content of secret file as registry username if `username` not defined. !!! example "Config file" ```yaml regopts: - : + - name: "myregistry" usernameFile: /run/secrets/username ``` !!! abstract "Environment variables" - * `DIUN_REGOPTS__USERNAMEFILE` + * `DIUN_REGOPTS__USERNAMEFILE` -## `password` +### `password` Registry password. !!! example "Config file" ```yaml regopts: - : + - name: "myregistry" username: foo password: bar ``` !!! abstract "Environment variables" - * `DIUN_REGOPTS__PASSWORD` + * `DIUN_REGOPTS__PASSWORD` -## `passwordFile` +### `passwordFile` Use content of secret file as registry password if `password` not defined. !!! example "Config file" ```yaml regopts: - : - usernameFile: /run/secrets/username - usernameFile: /run/secrets/password + - name: "myregistry" + passwordFile: /run/secrets/password ``` !!! abstract "Environment variables" - * `DIUN_REGOPTS__PASSWORDFILE` + * `DIUN_REGOPTS__PASSWORDFILE` -## `timeout` +### `timeout` Timeout is the maximum amount of time for the TCP connection to establish. (default `10s`) !!! example "Config file" ```yaml regopts: - : + - name: "myregistry" timeout: 10s ``` !!! abstract "Environment variables" - * `DIUN_REGOPTS__TIMEOUT` + * `DIUN_REGOPTS__TIMEOUT` -## `insecureTLS` +### `insecureTLS` Allow contacting docker registry over HTTP, or HTTPS with failed TLS verification. (default `false`) !!! example "Config file" ```yaml regopts: - : + - name: "myregistry" insecureTLS: false ``` !!! abstract "Environment variables" - * `DIUN_REGOPTS__INSECURETLS` + * `DIUN_REGOPTS__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/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.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.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/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 +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 @@ +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 @@ +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 ®Opt, nil + } + if regOpt.Selector == RegOptSelectorImage && strings.HasPrefix(image.Name(), regOpt.Name) { + return ®Opt, 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/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", }, }, { 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