Repository: crazy-max/diun
Pull request #218 (2020-11-11 15:16:36)

Go 1.15

codecov[bot]: # [Codecov](https://codecov.io/gh/crazy-max/diun/pull/218?src=pr&el=h1) Report
> Merging [#218](https://codecov.io/gh/crazy-max/diun/pull/218?src=pr&el=desc) (073bb13) into [master](https://codecov.io/gh/crazy-max/diun/commit/9ac9642fd09198259a174d273406c6ac66458d47?el=desc) (9ac9642) will **not change** coverage.
> The diff coverage is `n/a`.

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

```diff
@@           Coverage Diff           @@
##           master     #218   +/-   ##
=======================================
  Coverage   70.33%   70.33%           
=======================================
  Files           7        7           
  Lines         236      236           
=======================================
  Hits          166      166           
  Misses         44       44           
  Partials       26       26           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/crazy-max/diun/pull/218?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/218?src=pr&el=footer). Last update [2fa4696...073bb13](https://codecov.io/gh/crazy-max/diun/pull/218?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b802b5dd..da3a2d89 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -38,7 +38,7 @@ jobs:
         name: Set up Go
         uses: actions/setup-go@v2
         with:
-          go-version: 1.13
+          go-version: 1.15
       -
         name: Cache Go modules
         uses: actions/cache@v2
@@ -124,7 +124,7 @@ jobs:
         with:
           context: .
           file: ./Dockerfile
-          platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le,linux/s390x
+          platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le
           push: ${{ github.event_name != 'pull_request' }}
           tags: ${{ steps.docker_meta.outputs.tags }}
           build-args: |
diff --git a/.github/workflows/gosum.yml b/.github/workflows/gosum.yml
index d11e40f6..d68cc27a 100644
--- a/.github/workflows/gosum.yml
+++ b/.github/workflows/gosum.yml
@@ -20,7 +20,7 @@ jobs:
         name: Set up Go
         uses: actions/setup-go@v2
         with:
-          go-version: 1.13
+          go-version: 1.15
       -
         name: Tidy
         run: |
diff --git a/Dockerfile b/Dockerfile
index 44669383..c3afc249 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-ARG GO_VERSION=1.13
+ARG GO_VERSION=1.15
 ARG VERSION=dev
 
 FROM --platform=${BUILDPLATFORM:-linux/amd64} tonistiigi/xx:golang AS xgo
@@ -30,7 +30,6 @@ RUN apk --update --no-cache add \
   && rm -rf /tmp/* /var/cache/apk/*
 
 COPY --from=build /opt/diun /usr/local/bin/diun
-COPY --from=build /usr/local/go/lib/time/zoneinfo.zip /usr/local/go/lib/time/zoneinfo.zip
 RUN diun --version
 
 ENV DIUN_DB_PATH="/data/diun.db"
diff --git a/cmd/main.go b/cmd/main.go
index 98c749cf..5cf6bf39 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -7,6 +7,7 @@ import (
 	"runtime"
 	"strings"
 	"syscall"
+	_ "time/tzdata"
 
 	"github.com/alecthomas/kong"
 	"github.com/crazy-max/diun/v4/internal/app"
diff --git a/docs/config/watch.md b/docs/config/watch.md
index 87ee42f2..2175b5a7 100644
--- a/docs/config/watch.md
+++ b/docs/config/watch.md
@@ -7,6 +7,7 @@ watch:
   workers: 10
   schedule: "0 */6 * * *"
   firstCheckNotif: false
+  compareDigest: true
   healthchecks:
     baseURL: https://hc-ping.com/
     uuid: 5bf66975-d4c7-4bf5-bcc8-b8d8a82ea278
@@ -53,6 +54,21 @@ Send notification at the very first analysis of an image. (default `false`)
 !!! abstract "Environment variables"
     * `DIUN_WATCH_FIRSTCHECKNOTIF`
 
+### `compareDigest`
+
+Compare the digest of an image with the registry before downloading the image manifest. It is strongly
+recommended to leave this value at `true`, especially with [Docker Hub which imposes a rate-limit](../faq.md#docker-hub-rate-limits)
+on image pull. (default `true`)
+
+!!! example "Config file"
+    ```yaml
+    watch:
+      compareDigest: false
+    ```
+
+!!! abstract "Environment variables"
+    * `DIUN_WATCH_COMPAREDIGEST`
+
 ### `healthchecks`
 
 Healthchecks allows to monitor Diun watcher by sending start and success notification
diff --git a/docs/faq.md b/docs/faq.md
index 5c97cd6d..eb236ae7 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -96,3 +96,18 @@ regopts:
 
 If this is not enough, tweak the [`schedule` setting](config/watch.md#schedule) with something
 like `0 */6 * * *` (every 6 hours).
+
+## Docker Hub rate limits
+
+Docker is now [enforcing Docker Hub pull rate limits](https://www.docker.com/increase-rate-limits). This means you can
+make 100 pull image requests per six hours for anonymous usage, and 200 pull image requests per six hours
+for free Docker accounts. But this rate limit is not necessarily an indicator on the number of times an image has
+actually been downloaded. In fact, their _pulls_ counter/metric is actually a representation of the number of times a
+manifest for a particular image has been retrieved.
+
+As you probably know, Diun download the manifest of an image from its registry through a `GET` request to be able to
+retrive its inside metadata. Fortunately Diun doesn't perform a `GET` request at each scan but only when an image
+has been updated and/or added on the registry. This allows us not to exceed this rate limit in our situation, but
+it also strongly depends on the number of images you scan. To increase your pull rate limits you can upgrade
+your account to a [Docker Pro or Team subscription](https://www.docker.com/pricing) or you can tweak the
+[`schedule` setting](config/watch.md#schedule) with something like `0 */6 * * *` (every 6 hours).
diff --git a/docs/install/docker.md b/docs/install/docker.md
index 23fc9d2f..f2f25aa5 100644
--- a/docs/install/docker.md
+++ b/docs/install/docker.md
@@ -24,7 +24,6 @@ Image: crazymax/diun:latest
    - linux/arm64
    - linux/386
    - linux/ppc64le
-   - linux/s390x
 ```
 
 ## Volumes
diff --git a/go.mod b/go.mod
index b004544b..124b10e1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,10 +1,10 @@
 module github.com/crazy-max/diun/v4
 
-go 1.13
+go 1.15
 
 require (
 	github.com/alecthomas/kong v0.2.11
-	github.com/containers/image/v5 v5.7.0
+	github.com/containers/image/v5 v5.7.1-0.20201112143342-1aef8ae0fec6
 	github.com/crazy-max/gohealthchecks v0.2.0
 	github.com/crazy-max/gonfig v0.4.0
 	github.com/docker/docker v1.4.2-0.20200204220554-5f6d6f3f2203
diff --git a/go.sum b/go.sum
index 60f309cd..5ba0db34 100644
--- a/go.sum
+++ b/go.sum
@@ -34,6 +34,7 @@ github.com/Masterminds/sprig v2.16.0+incompatible h1:QZbMUPxRQ50EKAq3LFMnxddMu88
 github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
 github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA=
 github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
+github.com/Microsoft/hcsshim v0.8.9 h1:VrfodqvztU8YSOvygU+DN1BGaSGxmrNfqOv5oOuX2Bk=
 github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
 github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
@@ -70,19 +71,20 @@ github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on
 github.com/containerd/console v1.0.0/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
 github.com/containerd/containerd v1.3.2 h1:ForxmXkA6tPIvffbrDAcPUIB32QgXkt2XFj+F0UxetA=
 github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
+github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8=
 github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
 github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
 github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
 github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
 github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
-github.com/containers/image/v5 v5.7.0 h1:fiTC8/Xbr+zEP6njGTZtPW/3UD7MC93nC9DbUoWdxkA=
-github.com/containers/image/v5 v5.7.0/go.mod h1:8aOy+YaItukxghRORkvhq5ibWttHErzDLy6egrKfKos=
+github.com/containers/image/v5 v5.7.1-0.20201112143342-1aef8ae0fec6 h1:9kFbRTAbQetdPu0L9t101KQW1FoUw/KccEjaIDPMysg=
+github.com/containers/image/v5 v5.7.1-0.20201112143342-1aef8ae0fec6/go.mod h1:Q4rhU5QLVQuKjJj6F1bF7uPXxdzpWXDI9mdIkztsvhQ=
 github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b h1:Q8ePgVfHDplZ7U33NwHZkrVELsZP5fYj9pM5WBZB2GE=
 github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
 github.com/containers/ocicrypt v1.0.3 h1:vYgl+RZ9Q3DPMuTfxmN+qp0X2Bj52uuY2vnt6GzVe1c=
 github.com/containers/ocicrypt v1.0.3/go.mod h1:CUBa+8MRNL/VkpxYIpaMtgn1WgXGyvPQj8jcy0EVG6g=
-github.com/containers/storage v1.23.6 h1:3rcZ1KTNv8q7SkZ75gcrFGYqTeiuI04Zg7m9X1sCg/s=
-github.com/containers/storage v1.23.6/go.mod h1:haFs0HRowKwyzvWEx9EgI3WsL8XCSnBDb5f8P5CAxJY=
+github.com/containers/storage v1.23.7 h1:43ImvG/npvQSZXRjaudVvKISIuZSfI6qvtSNQQSGO/A=
+github.com/containers/storage v1.23.7/go.mod h1:cUT2zHjtx+WlVri30obWmM2gpqpi8jfPsmIzP1TVpEI=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@@ -258,7 +260,8 @@ github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDE
 github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
 github.com/mistifyio/go-zfs v2.1.1+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
 github.com/moby/sys/mountinfo v0.1.3/go.mod h1:w2t2Avltqx8vE7gX5l+QiBKxODu2TX0+Syr3h52Tw4o=
-github.com/moby/sys/mountinfo v0.3.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
+github.com/moby/sys/mountinfo v0.4.0 h1:1KInV3Huv18akCu58V7lzNlt+jFmqlu1EaErnEHE/VM=
+github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -289,6 +292,7 @@ github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM
 github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6 h1:yN8BPXVwMBAm3Cuvh1L5XE8XpvYRMdsVLd82ILprhUU=
 github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
 github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
+github.com/opencontainers/runc v1.0.0-rc91 h1:Tp8LWs5G8rFpzTsbRjAtQkPVexhCu0bnANE5IfIhJ6g=
 github.com/opencontainers/runc v1.0.0-rc91/go.mod h1:3Sm6Dt7OT8z88EbdQqqcRN2oCT54jbi72tT/HqgflT8=
 github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
 github.com/opencontainers/runtime-spec v1.0.3-0.20200520003142-237cc4f519e2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
diff --git a/internal/app/job.go b/internal/app/job.go
index eb26d48d..9f03a9b1 100644
--- a/internal/app/job.go
+++ b/internal/app/job.go
@@ -80,14 +80,15 @@ func (di *Diun) createJob(job model.Job) {
 	}
 
 	job.Registry, err = registry.New(registry.Options{
-		Username:     regUser,
-		Password:     regPassword,
-		Timeout:      *reg.Timeout,
-		InsecureTLS:  *reg.InsecureTLS,
-		UserAgent:    di.meta.UserAgent,
-		ImageOs:      job.Image.Platform.Os,
-		ImageArch:    job.Image.Platform.Arch,
-		ImageVariant: job.Image.Platform.Variant,
+		Username:      regUser,
+		Password:      regPassword,
+		Timeout:       *reg.Timeout,
+		InsecureTLS:   *reg.InsecureTLS,
+		UserAgent:     di.meta.UserAgent,
+		CompareDigest: *di.cfg.Watch.CompareDigest,
+		ImageOs:       job.Image.Platform.Os,
+		ImageArch:     job.Image.Platform.Arch,
+		ImageVariant:  job.Image.Platform.Variant,
 	})
 	if err != nil {
 		sublog.Error().Err(err).Msg("Cannot create registry client")
@@ -162,15 +163,15 @@ func (di *Diun) runJob(job model.Job) (entry model.NotifEntry) {
 		return
 	}
 
-	entry.Manifest, err = job.Registry.Manifest(job.RegImage)
+	dbManifest, err := di.db.GetManifest(job.RegImage)
 	if err != nil {
-		sublog.Warn().Err(err).Msg("Cannot get remote manifest")
+		sublog.Error().Err(err).Msg("Cannot get manifest from db")
 		return
 	}
 
-	dbManifest, err := di.db.GetManifest(job.RegImage)
+	entry.Manifest, err = job.Registry.Manifest(job.RegImage, dbManifest)
 	if err != nil {
-		sublog.Error().Err(err).Msg("Cannot get manifest from db")
+		sublog.Warn().Err(err).Msg("Cannot get remote manifest")
 		return
 	}
 
diff --git a/internal/config/fixtures/config.validate.yml b/internal/config/fixtures/config.validate.yml
index 43a7a5d3..35e622d6 100644
--- a/internal/config/fixtures/config.validate.yml
+++ b/internal/config/fixtures/config.validate.yml
@@ -5,6 +5,7 @@ watch:
   workers: 100
   schedule: "*/30 * * * *"
   firstCheckNotif: false
+  compareDigest: true
   healthchecks:
     baseURL: https://hc-ping.com/
     uuid: 5bf66975-d4c7-4bf5-bcc8-b8d8a82ea278
diff --git a/internal/model/watch.go b/internal/model/watch.go
index 072606c9..5269ee7c 100644
--- a/internal/model/watch.go
+++ b/internal/model/watch.go
@@ -9,6 +9,7 @@ type Watch struct {
 	Workers         int           `yaml:"workers,omitempty" json:"workers,omitempty" validate:"required,min=1"`
 	Schedule        string        `yaml:"schedule,omitempty" json:"schedule,omitempty" validate:"required"`
 	FirstCheckNotif *bool         `yaml:"firstCheckNotif,omitempty" json:"firstCheckNotif,omitempty" validate:"required"`
+	CompareDigest   *bool         `yaml:"compareDigest,omitempty" json:"compareDigest,omitempty" validate:"required"`
 	Healthchecks    *Healthchecks `yaml:"healthchecks,omitempty" json:"healthchecks,omitempty"`
 }
 
@@ -24,4 +25,5 @@ func (s *Watch) SetDefaults() {
 	s.Workers = 10
 	s.Schedule = "0 * * * *"
 	s.FirstCheckNotif = utl.NewFalse()
+	s.CompareDigest = utl.NewTrue()
 }
diff --git a/pkg/registry/manifest.go b/pkg/registry/manifest.go
index 408fe60f..d0c774ff 100644
--- a/pkg/registry/manifest.go
+++ b/pkg/registry/manifest.go
@@ -4,7 +4,9 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/containers/image/v5/docker"
 	"github.com/containers/image/v5/manifest"
+	"github.com/containers/image/v5/types"
 	"github.com/opencontainers/go-digest"
 	"github.com/pkg/errors"
 )
@@ -23,11 +25,38 @@ type Manifest struct {
 }
 
 // Manifest returns the manifest for a specific image
-func (c *Client) Manifest(image Image) (Manifest, error) {
+func (c *Client) Manifest(image Image, dbManifest Manifest) (Manifest, error) {
 	ctx, cancel := c.timeoutContext()
 	defer cancel()
 
-	imgCloser, err := c.newImage(ctx, image.String())
+	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
+	}
+
+	imgRef, err := ParseReference(image.String())
+	if err != nil {
+		return Manifest{}, errors.Wrap(err, "Cannot parse reference")
+	}
+
+	var imgDigest digest.Digest
+	if c.opts.CompareDigest {
+		imgDigest, err = docker.GetDigest(ctx, c.sysCtx, imgRef)
+		if err != nil {
+			return Manifest{}, errors.Wrap(err, "Cannot get image digest from HEAD request")
+		}
+
+		if dbManifest.Digest != "" && dbManifest.Digest == imgDigest {
+			return dbManifest, nil
+		}
+	}
+
+	imgCloser, err := imgRef.NewImage(ctx, c.sysCtx)
 	if err != nil {
 		return Manifest{}, errors.Wrap(err, "Cannot create image closer")
 	}
@@ -38,14 +67,16 @@ func (c *Client) Manifest(image Image) (Manifest, error) {
 		return Manifest{}, errors.Wrap(err, "Cannot get raw manifest")
 	}
 
-	imgInspect, err := imgCloser.Inspect(ctx)
-	if err != nil {
-		return Manifest{}, errors.Wrap(err, "Cannot inspect")
+	if !c.opts.CompareDigest {
+		imgDigest, err = manifest.Digest(rawManifest)
+		if err != nil {
+			return Manifest{}, errors.Wrap(err, "Cannot get digest")
+		}
 	}
 
-	imgDigest, err := manifest.Digest(rawManifest)
+	imgInspect, err := imgCloser.Inspect(ctx)
 	if err != nil {
-		return Manifest{}, errors.Wrap(err, "Cannot get digest")
+		return Manifest{}, errors.Wrap(err, "Cannot inspect")
 	}
 
 	imgTag := imgInspect.Tag
diff --git a/pkg/registry/ref.go b/pkg/registry/ref.go
new file mode 100644
index 00000000..d7ff9b33
--- /dev/null
+++ b/pkg/registry/ref.go
@@ -0,0 +1,16 @@
+package registry
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/containers/image/v5/docker"
+	"github.com/containers/image/v5/types"
+)
+
+func ParseReference(imageStr string) (types.ImageReference, error) {
+	if !strings.HasPrefix(imageStr, "//") {
+		imageStr = fmt.Sprintf("//%s", imageStr)
+	}
+	return docker.ParseReference(imageStr)
+}
diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go
index 6b658406..98543d61 100644
--- a/pkg/registry/registry.go
+++ b/pkg/registry/registry.go
@@ -2,13 +2,9 @@ package registry
 
 import (
 	"context"
-	"fmt"
-	"strings"
 	"time"
 
-	"github.com/containers/image/v5/docker"
 	"github.com/containers/image/v5/types"
-	"github.com/pkg/errors"
 )
 
 // Client represents an active docker registry object
@@ -19,14 +15,15 @@ type Client struct {
 
 // Options holds docker registry object options
 type Options struct {
-	Username     string
-	Password     string
-	InsecureTLS  bool
-	Timeout      time.Duration
-	UserAgent    string
-	ImageOs      string
-	ImageArch    string
-	ImageVariant string
+	Username      string
+	Password      string
+	InsecureTLS   bool
+	Timeout       time.Duration
+	UserAgent     string
+	CompareDigest bool
+	ImageOs       string
+	ImageArch     string
+	ImageVariant  string
 }
 
 // New creates new docker registry client instance
@@ -52,6 +49,7 @@ func New(opts Options) (*Client, error) {
 	}
 
 	return &Client{
+		opts:   opts,
 		sysCtx: sysCtx,
 	}, nil
 }
@@ -64,31 +62,3 @@ func (c *Client) timeoutContext() (context.Context, context.CancelFunc) {
 	}
 	return ctx, cancel
 }
-
-func (c *Client) newImage(ctx context.Context, imageStr string) (types.ImageCloser, error) {
-	if !strings.HasPrefix(imageStr, "//") {
-		imageStr = fmt.Sprintf("//%s", imageStr)
-	}
-
-	ref, err := docker.ParseReference(imageStr)
-	if err != nil {
-		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
-	}
-
-	return img, nil
-}
diff --git a/pkg/registry/tags.go b/pkg/registry/tags.go
index a6eaef54..418c2f52 100644
--- a/pkg/registry/tags.go
+++ b/pkg/registry/tags.go
@@ -3,6 +3,7 @@ package registry
 import (
 	"github.com/containers/image/v5/docker"
 	"github.com/crazy-max/diun/v4/pkg/utl"
+	"github.com/pkg/errors"
 )
 
 // Tags holds information about image tags.
@@ -26,13 +27,18 @@ func (c *Client) Tags(opts TagsOptions) (*Tags, error) {
 	ctx, cancel := c.timeoutContext()
 	defer cancel()
 
-	imgCls, err := c.newImage(ctx, opts.Image.String())
+	imgRef, err := ParseReference(opts.Image.String())
 	if err != nil {
-		return nil, err
+		return nil, errors.Wrap(err, "Cannot parse reference")
+	}
+
+	imgCloser, err := imgRef.NewImage(ctx, c.sysCtx)
+	if err != nil {
+		return nil, errors.Wrap(err, "Cannot create image closer")
 	}
-	defer imgCls.Close()
+	defer imgCloser.Close()
 
-	tags, err := docker.GetRepositoryTags(ctx, c.sysCtx, imgCls.Reference())
+	tags, err := docker.GetRepositoryTags(ctx, c.sysCtx, imgCloser.Reference())
 	if err != nil {
 		return nil, err
 	}


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 379722c9..f91b743e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -31,7 +31,7 @@ jobs:
         name: Set up Go
         uses: actions/setup-go@v2
         with:
-          go-version: 1.13
+          go-version: 1.15
       -
         name: Cache Go modules
         uses: actions/cache@v2
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index cc04acb2..ea064a09 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -75,6 +75,7 @@ func TestLoadFile(t *testing.T) {
 					Workers:         100,
 					Schedule:        "*/30 * * * *",
 					FirstCheckNotif: utl.NewTrue(),
+					CompareDigest:   utl.NewTrue(),
 					Healthchecks: &model.Healthchecks{
 						BaseURL: "https://hc-ping.com/",
 						UUID:    "5bf66975-d4c7-4bf5-bcc8-b8d8a82ea278",
diff --git a/internal/config/fixtures/config.test.yml b/internal/config/fixtures/config.test.yml
index b9709387..f2573899 100644
--- a/internal/config/fixtures/config.test.yml
+++ b/internal/config/fixtures/config.test.yml
@@ -5,6 +5,7 @@ watch:
   workers: 100
   schedule: "*/30 * * * *"
   firstCheckNotif: true
+  compareDigest: true
   healthchecks:
     baseURL: https://hc-ping.com/
     uuid: 5bf66975-d4c7-4bf5-bcc8-b8d8a82ea278
diff --git a/pkg/registry/manifest_test.go b/pkg/registry/manifest_test.go
index 0252ac1b..89523b48 100644
--- a/pkg/registry/manifest_test.go
+++ b/pkg/registry/manifest_test.go
@@ -7,6 +7,49 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
+func TestCompareDigest(t *testing.T) {
+	rc, err := registry.New(registry.Options{
+		CompareDigest: true,
+	})
+	if err != nil {
+		t.Error(err)
+	}
+
+	img, err := registry.ParseImage(registry.ParseImageOptions{
+		Name: "crazymax/diun:2.5.0",
+	})
+	if err != nil {
+		t.Error(err)
+	}
+
+	manifest, err := rc.Manifest(img, registry.Manifest{
+		Name:          "docker.io/crazymax/diun",
+		Tag:           "2.5.0",
+		MIMEType:      "application/vnd.docker.distribution.manifest.list.v2+json",
+		Digest:        "sha256:db618981ef3d07699ff6cd8b9d2a81f51a021747bc08c85c1b0e8d11130c2be5",
+		DockerVersion: "",
+		Labels: map[string]string{
+			"maintainer":                      "CrazyMax",
+			"org.label-schema.build-date":     "2020-03-01T18:00:42Z",
+			"org.label-schema.description":    "Docker image update notifier",
+			"org.label-schema.name":           "Diun",
+			"org.label-schema.schema-version": "1.0",
+			"org.label-schema.url":            "https://github.com/crazy-max/diun",
+			"org.label-schema.vcs-ref":        "488ce441",
+			"org.label-schema.vcs-url":        "https://github.com/crazy-max/diun",
+			"org.label-schema.vendor":         "CrazyMax",
+			"org.label-schema.version":        "2.5.0",
+		},
+		Platform: "linux/amd64",
+	})
+	assert.NoError(t, err)
+	assert.Equal(t, "docker.io/crazymax/diun", manifest.Name)
+	assert.Equal(t, "2.5.0", manifest.Tag)
+	assert.Equal(t, "application/vnd.docker.distribution.manifest.list.v2+json", manifest.MIMEType)
+	assert.Equal(t, "linux/amd64", manifest.Platform)
+	assert.Empty(t, manifest.DockerVersion)
+}
+
 func TestManifestVariant(t *testing.T) {
 	rc, err := registry.New(registry.Options{
 		ImageOs:      "linux",
@@ -24,7 +67,7 @@ func TestManifestVariant(t *testing.T) {
 		t.Error(err)
 	}
 
-	manifest, err := rc.Manifest(img)
+	manifest, err := rc.Manifest(img, registry.Manifest{})
 	assert.NoError(t, err)
 	assert.Equal(t, "docker.io/crazymax/diun", manifest.Name)
 	assert.Equal(t, "2.5.0", manifest.Tag)
diff --git a/pkg/registry/ref_test.go b/pkg/registry/ref_test.go
new file mode 100644
index 00000000..7d947efd
--- /dev/null
+++ b/pkg/registry/ref_test.go
@@ -0,0 +1,65 @@
+package registry_test
+
+import (
+	"testing"
+
+	"github.com/crazy-max/diun/v4/pkg/registry"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+const (
+	sha256digestHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+	sha256digest    = "@sha256:" + sha256digestHex
+)
+
+func TestParseReference(t *testing.T) {
+	testCases := []struct {
+		input    string
+		expected string
+		wantErr  bool
+	}{
+		{
+			input:    "busybox",
+			expected: "docker.io/library/busybox:latest",
+		},
+		{
+			input:    "//busybox:notlatest",
+			expected: "docker.io/library/busybox:notlatest",
+		},
+		{
+			input:    "//busybox" + sha256digest,
+			expected: "docker.io/library/busybox" + sha256digest,
+		},
+		{
+			input:    "//busybox",
+			expected: "docker.io/library/busybox:latest",
+		},
+		{
+			input:    "//busybox:latest" + sha256digest,
+			expected: "",
+			wantErr:  true,
+		},
+		{
+			input:    "//docker.io/library/busybox:latest",
+			expected: "docker.io/library/busybox:latest",
+		},
+		{
+			input:    "//UPPERCASEISINVALID",
+			expected: "",
+			wantErr:  true,
+		},
+	}
+
+	for _, tt := range testCases {
+		t.Run(tt.input, func(t *testing.T) {
+			ref, err := registry.ParseReference(tt.input)
+			if tt.wantErr {
+				require.Error(t, err)
+				return
+			}
+			require.NoError(t, err)
+			assert.Equal(t, tt.expected, ref.DockerReference().String(), tt.input)
+		})
+	}
+}