Repository: crazy-max/diun
Pull request #410 (2021-06-17 06:15:22)

Bump github.com/microcosm-cc/bluemonday from 1.0.10 to 1.0.13
Bumps [github.com/microcosm-cc/bluemonday](https://github.com/microcosm-cc/bluemonday) from 1.0.10 to 1.0.13.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/microcosm-cc/bluemonday/releases">github.com/microcosm-cc/bluemonday's releases</a>.</em></p>
<blockquote>
<h2>Policies that accept regexps for matching are now additive</h2>
<p>Thanks to <a href="https://github.com/KN4CK3R"><code>@​KN4CK3R</code></a> for the contribution of a PR that results in multiple Matching() policies on the same attr and element no longer clobber the previous regexps.</p>
<h2>Improve data-uri base64 handling, and improve docs structure</h2>
<p>No release notes provided.</p>
<h2>Improve support for links on all elements</h2>
<p>Originally I had only concentrated the link validation on the elements that were safe to link. However people do want to allow some unsafe elements and yet still have the benefits of link validation and sanitization, i.e. allow <code>iframe</code> but still have the <code>src</code> safely validated... these changes allow that.</p>
<p>Additionally I have added tests showing how <code>AllowSchemesWithCustomPolicy</code> can be used to globally allow only links to certain domains, and a test that shows how to apply the <code>AllowAttributes().Matching().OnElements</code> to only allow a given domain on specific elements (i.e. only allow an <code>iframe</code> if is is a YouTube embed).</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/microcosm-cc/bluemonday/commit/7f2aa2d05360bab081fd6d78e20a54097e3c7576"><code>7f2aa2d</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/microcosm-cc/bluemonday/issues/125">#125</a> from KN4CK3R/additive-policies</li>
<li><a href="https://github.com/microcosm-cc/bluemonday/commit/eb9cdddfafdb55efbfe8b29aa03178e7c0f47f99"><code>eb9cddd</code></a> Merge branch 'master' of <a href="https://github.com/microcosm-cc/bluemonday">https://github.com/microcosm-cc/bluemonday</a> into addi...</li>
<li><a href="https://github.com/microcosm-cc/bluemonday/commit/0310fe1893889311043063c7072d93215f21ae25"><code>0310fe1</code></a> Added additive policies.</li>
<li><a href="https://github.com/microcosm-cc/bluemonday/commit/75e91cc9e95a0b83b3345e794e99013374849810"><code>75e91cc</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/microcosm-cc/bluemonday/issues/124">#124</a> from microcosm-cc/buro9/reorg</li>
<li><a href="https://github.com/microcosm-cc/bluemonday/commit/7107ed5e32d1310bd23c999a5dacdec2401be504"><code>7107ed5</code></a> Minor re-org to improve documentation readability</li>
<li><a href="https://github.com/microcosm-cc/bluemonday/commit/548985b4dcd3b9c41d36fba10b7646cc7879f550"><code>548985b</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/microcosm-cc/bluemonday/issues/123">#123</a> from KN4CK3R/whitespace</li>
<li><a href="https://github.com/microcosm-cc/bluemonday/commit/7806a76ad8b3ffb6d344a15d44495dd54549ac90"><code>7806a76</code></a> Use Replace for compatibility with Go 1.11.</li>
<li><a href="https://github.com/microcosm-cc/bluemonday/commit/21898517df3d8267f709fb4b76b5e4e0da101fe8"><code>2189851</code></a> Restored old test.</li>
<li><a href="https://github.com/microcosm-cc/bluemonday/commit/79fd0920bbb61d61c51d335d9d24160c7d66c660"><code>79fd092</code></a> Remove \r \n from base64 encoded data.</li>
<li><a href="https://github.com/microcosm-cc/bluemonday/commit/cb614698bfbcbdca4b8e177e17b9a9aaf65109d7"><code>cb61469</code></a> Better support for linkable elements that aren't by default safe</li>
<li>See full diff in <a href="https://github.com/microcosm-cc/bluemonday/compare/v1.0.10...v1.0.13">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/microcosm-cc/bluemonday&package-manager=go_modules&previous-version=1.0.10&new-version=1.0.13)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

codecov[bot]: # [Codecov](https://codecov.io/gh/crazy-max/diun/pull/410?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CrazyMax) Report
> Merging [#410](https://codecov.io/gh/crazy-max/diun/pull/410?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CrazyMax) (e501a95) into [master](https://codecov.io/gh/crazy-max/diun/commit/dcab3a840c59d1d115e0f8a97918cdaf0d6d0ebc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CrazyMax) (dcab3a8) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/crazy-max/diun/pull/410/graphs/tree.svg?width=650&height=150&src=pr&token=phc5KYIjsV&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CrazyMax)](https://codecov.io/gh/crazy-max/diun/pull/410?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CrazyMax)

```diff
@@           Coverage Diff           @@
##           master     #410   +/-   ##
=======================================
  Coverage   70.83%   70.83%           
=======================================
  Files           8        8           
  Lines         240      240           
=======================================
  Hits          170      170           
  Misses         44       44           
  Partials       26       26           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/crazy-max/diun/pull/410?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CrazyMax).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CrazyMax)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/crazy-max/diun/pull/410?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CrazyMax). Last update [dcab3a8...e501a95](https://codecov.io/gh/crazy-max/diun/pull/410?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CrazyMax). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CrazyMax).


Patch:
diff --git a/go.mod b/go.mod
index ca6f6a0a..81bbf3ca 100644
--- a/go.mod
+++ b/go.mod
@@ -22,11 +22,12 @@ require (
 	github.com/jedib0t/go-pretty/v6 v6.2.2
 	github.com/matcornic/hermes/v2 v2.1.0
 	github.com/matrix-org/gomatrix v0.0.0-20200501121722-e5578b12c752
-	github.com/microcosm-cc/bluemonday v1.0.10
+	github.com/microcosm-cc/bluemonday v1.0.13
 	github.com/moby/buildkit v0.8.3
 	github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
 	github.com/nlopes/slack v0.6.0
 	github.com/opencontainers/go-digest v1.0.0
+	github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6
 	github.com/panjf2000/ants/v2 v2.4.5
 	github.com/pkg/errors v0.9.1
 	github.com/pkg/profile v1.6.0
diff --git a/go.sum b/go.sum
index 3ec65dcc..60d7bff4 100644
--- a/go.sum
+++ b/go.sum
@@ -821,8 +821,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff
 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
-github.com/microcosm-cc/bluemonday v1.0.10 h1:hsW4ch5StWWscflW8orGkX3TP2AVelTmwYO26qwGjqg=
-github.com/microcosm-cc/bluemonday v1.0.10/go.mod h1:beubO5lmWoy1tU8niaMyXNriNgROO37H3U/tsrcZsy0=
+github.com/microcosm-cc/bluemonday v1.0.13 h1:PBMD+579GvGysq8YLZdeOpHfzhhzAnzDQlXOPFU77cM=
+github.com/microcosm-cc/bluemonday v1.0.13/go.mod h1:beubO5lmWoy1tU8niaMyXNriNgROO37H3U/tsrcZsy0=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
 github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
diff --git a/internal/app/job.go b/internal/app/job.go
index 2a1b0e97..bd48fd0d 100644
--- a/internal/app/job.go
+++ b/internal/app/job.go
@@ -176,7 +176,8 @@ func (di *Diun) runJob(job model.Job) (entry model.NotifEntry) {
 		return
 	}
 
-	entry.Manifest, err = job.Registry.Manifest(job.RegImage, dbManifest)
+	var updated bool
+	entry.Manifest, updated, err = job.Registry.Manifest(job.RegImage, dbManifest)
 	if err != nil {
 		sublog.Warn().Err(err).Msg("Cannot get remote manifest")
 		return
@@ -185,13 +186,12 @@ func (di *Diun) runJob(job model.Job) (entry model.NotifEntry) {
 	if len(dbManifest.Name) == 0 {
 		entry.Status = model.ImageStatusNew
 		sublog.Info().Msg("New image found")
-	} else if entry.Manifest.Digest.String() != dbManifest.Digest.String() {
+	} else if updated {
 		entry.Status = model.ImageStatusUpdate
 		sublog.Info().Msg("Image update found")
 	} else {
 		entry.Status = model.ImageStatusUnchange
 		sublog.Debug().Msg("No changes")
-		return
 	}
 
 	if err := di.db.PutManifest(job.RegImage, entry.Manifest); err != nil {
@@ -199,6 +199,9 @@ func (di *Diun) runJob(job model.Job) (entry model.NotifEntry) {
 		return
 	}
 	sublog.Debug().Msg("Manifest saved to database")
+	if entry.Status == model.ImageStatusUnchange {
+		return
+	}
 
 	if job.FirstCheck && !*di.cfg.Watch.FirstCheckNotif {
 		sublog.Debug().Msg("Skipping notification (first check)")
diff --git a/pkg/registry/manifest.go b/pkg/registry/manifest.go
index 25fc580c..6d32ae8a 100644
--- a/pkg/registry/manifest.go
+++ b/pkg/registry/manifest.go
@@ -6,8 +6,8 @@ import (
 
 	"github.com/containers/image/v5/docker"
 	"github.com/containers/image/v5/manifest"
-	"github.com/containers/image/v5/types"
 	"github.com/opencontainers/go-digest"
+	imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/pkg/errors"
 )
 
@@ -26,80 +26,91 @@ type Manifest struct {
 }
 
 // Manifest returns the manifest for a specific image
-func (c *Client) Manifest(image Image, dbManifest Manifest) (Manifest, error) {
+func (c *Client) Manifest(image Image, dbManifest Manifest) (Manifest, bool, error) {
 	ctx, cancel := c.timeoutContext()
 	defer cancel()
 
-	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
+	rmRef, err := ParseReference(image.String())
+	if err != nil {
+		return Manifest{}, false, errors.Wrap(err, "Cannot parse reference")
 	}
 
-	imgRef, err := ParseReference(image.String())
+	// Retrieve remote digest through HEAD request
+	rmDigest, err := docker.GetDigest(ctx, c.sysCtx, rmRef)
 	if err != nil {
-		return Manifest{}, errors.Wrap(err, "Cannot parse reference")
+		return Manifest{}, false, errors.Wrap(err, "Cannot get image digest from HEAD request")
 	}
 
-	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
-		}
+	// Digest match, returns db manifest
+	if c.opts.CompareDigest && len(dbManifest.Digest) > 0 && dbManifest.Digest == rmDigest {
+		return dbManifest, false, nil
 	}
 
-	imgCloser, err := imgRef.NewImage(ctx, c.sysCtx)
+	rmCloser, err := rmRef.NewImage(ctx, c.sysCtx)
 	if err != nil {
-		return Manifest{}, errors.Wrap(err, "Cannot create image closer")
+		return Manifest{}, false, errors.Wrap(err, "Cannot create image closer")
 	}
-	defer imgCloser.Close()
+	defer rmCloser.Close()
 
-	rawManifest, _, err := imgCloser.Manifest(ctx)
+	rmRawManifest, rmManifestMimeType, err := rmCloser.Manifest(ctx)
 	if err != nil {
-		return Manifest{}, errors.Wrap(err, "Cannot get raw manifest")
+		return Manifest{}, false, errors.Wrap(err, "Cannot get raw manifest")
 	}
 
-	if !c.opts.CompareDigest {
-		imgDigest, err = manifest.Digest(rawManifest)
+	// For manifests list compare also digest matching the platform
+	updated := dbManifest.Digest != rmDigest
+	if c.opts.CompareDigest && len(dbManifest.Raw) > 0 && dbManifest.isManifestList() && isManifestList(rmManifestMimeType) {
+		dbManifestList, err := manifest.ListFromBlob(dbManifest.Raw, dbManifest.MIMEType)
+		if err != nil {
+			return Manifest{}, false, errors.Wrap(err, "Cannot parse manifest list")
+		}
+		dbManifestPlatformDigest, err := dbManifestList.ChooseInstance(c.sysCtx)
+		if err != nil {
+			return Manifest{}, false, errors.Wrapf(err, "Error choosing image instance")
+		}
+		rmManifestList, err := manifest.ListFromBlob(rmRawManifest, rmManifestMimeType)
 		if err != nil {
-			return Manifest{}, errors.Wrap(err, "Cannot get digest")
+			return Manifest{}, false, errors.Wrap(err, "Cannot parse manifest list")
 		}
+		rmManifestPlatformDigest, err := rmManifestList.ChooseInstance(c.sysCtx)
+		if err != nil {
+			return Manifest{}, false, errors.Wrapf(err, "Error choosing image instance")
+		}
+		updated = dbManifestPlatformDigest != rmManifestPlatformDigest
 	}
 
-	imgInspect, err := imgCloser.Inspect(ctx)
+	// Metadata describing the Docker image
+	rmInspect, err := rmCloser.Inspect(ctx)
 	if err != nil {
-		return Manifest{}, errors.Wrap(err, "Cannot inspect")
+		return Manifest{}, false, errors.Wrap(err, "Cannot inspect")
 	}
-
-	imgTag := imgInspect.Tag
-	if len(imgTag) == 0 {
-		imgTag = image.Tag
+	rmTag := rmInspect.Tag
+	if len(rmTag) == 0 {
+		rmTag = image.Tag
 	}
-
-	imgPlatform := fmt.Sprintf("%s/%s", imgInspect.Os, imgInspect.Architecture)
-	if imgInspect.Variant != "" {
-		imgPlatform = fmt.Sprintf("%s/%s", imgPlatform, imgInspect.Variant)
+	rmPlatform := fmt.Sprintf("%s/%s", rmInspect.Os, rmInspect.Architecture)
+	if rmInspect.Variant != "" {
+		rmPlatform = fmt.Sprintf("%s/%s", rmPlatform, rmInspect.Variant)
 	}
 
 	return Manifest{
-		Name:          imgCloser.Reference().DockerReference().Name(),
-		Tag:           imgTag,
-		MIMEType:      manifest.GuessMIMEType(rawManifest),
-		Digest:        imgDigest,
-		Created:       imgInspect.Created,
-		DockerVersion: imgInspect.DockerVersion,
-		Labels:        imgInspect.Labels,
-		Layers:        imgInspect.Layers,
-		Platform:      imgPlatform,
-		Raw:           rawManifest,
-	}, nil
+		Name:          rmCloser.Reference().DockerReference().Name(),
+		Tag:           rmTag,
+		MIMEType:      rmManifestMimeType,
+		Digest:        rmDigest,
+		Created:       rmInspect.Created,
+		DockerVersion: rmInspect.DockerVersion,
+		Labels:        rmInspect.Labels,
+		Layers:        rmInspect.Layers,
+		Platform:      rmPlatform,
+		Raw:           rmRawManifest,
+	}, updated, nil
+}
+
+func (m Manifest) isManifestList() bool {
+	return isManifestList(m.MIMEType)
+}
+
+func isManifestList(mimeType string) bool {
+	return mimeType == manifest.DockerV2ListMediaType || mimeType == imgspecv1.MediaTypeImageIndex
 }
diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go
index 98543d61..f1ade4dd 100644
--- a/pkg/registry/registry.go
+++ b/pkg/registry/registry.go
@@ -37,6 +37,16 @@ func New(opts Options) (*Client, error) {
 		}
 	}
 
+	if auth == nil {
+		auth = &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
+	}
+
 	// Sys context
 	sysCtx := &types.SystemContext{
 		DockerAuthConfig:                  auth,


Test patch:
diff --git a/pkg/registry/manifest_test.go b/pkg/registry/manifest_test.go
index 89523b48..26d062d8 100644
--- a/pkg/registry/manifest_test.go
+++ b/pkg/registry/manifest_test.go
@@ -22,24 +22,11 @@ func TestCompareDigest(t *testing.T) {
 		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",
-		},
+	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",
 		Platform: "linux/amd64",
 	})
 	assert.NoError(t, err)
@@ -50,6 +37,224 @@ func TestCompareDigest(t *testing.T) {
 	assert.Empty(t, manifest.DockerVersion)
 }
 
+func TestManifest(t *testing.T) {
+	rc, err := registry.New(registry.Options{
+		CompareDigest: true,
+		ImageOs:       "linux",
+		ImageArch:     "amd64",
+	})
+	if err != nil {
+		t.Error(err)
+	}
+
+	img, err := registry.ParseImage(registry.ParseImageOptions{
+		Name: "portainer/portainer-ce:linux-amd64-2.5.1",
+	})
+	if err != nil {
+		t.Error(err)
+	}
+
+	manifest, updated, err := rc.Manifest(img, registry.Manifest{
+		Name:     "docker.io/portainer/portainer-ce",
+		Tag:      "linux-amd64-2.5.1",
+		MIMEType: "application/vnd.docker.distribution.manifest.v2+json",
+		Digest:   "sha256:653057af0d2d961f436c75deda1ca7fe3defc89664bed6bd3da8c91c88c1ce05",
+		Platform: "linux/amd64",
+		Raw: []byte(`{
+   "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+   "schemaVersion": 2,
+   "config": {
+      "mediaType": "application/vnd.docker.container.image.v1+json",
+      "digest": "sha256:45be17a5903a1129362792537fc6b18bc91fe03e2581501b514ac5d45ede128e",
+      "size": 1704
+   },
+   "layers": [
+      {
+         "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+         "digest": "sha256:94cfa856b2b17d5e36c7df9875ebbbed7e939a8292df5fe22d2dfce0434330f2",
+         "size": 122403
+      },
+      {
+         "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+         "digest": "sha256:49d59ee0881a4f04166d438b27055e2b29327abbbb0f274951255ee880912056",
+         "size": 92
+      },
+      {
+         "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+         "digest": "sha256:3fc1bc38fb56bce4b3913d0fab6072822541142793a6d997a4a69d5d81fa46e0",
+         "size": 74850629
+      }
+   ]
+}`),
+	})
+
+	assert.NoError(t, err)
+	assert.Equal(t, false, updated)
+	assert.Equal(t, "docker.io/portainer/portainer-ce", manifest.Name)
+	assert.Equal(t, "linux-amd64-2.5.1", manifest.Tag)
+	assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", manifest.MIMEType)
+	assert.Equal(t, "sha256:653057af0d2d961f436c75deda1ca7fe3defc89664bed6bd3da8c91c88c1ce05", manifest.Digest.String())
+	assert.Equal(t, "linux/amd64", manifest.Platform)
+}
+
+func TestManifestMultiUpdatedPlatform(t *testing.T) {
+	rc, err := registry.New(registry.Options{
+		CompareDigest: true,
+		ImageOs:       "linux",
+		ImageArch:     "amd64",
+	})
+	if err != nil {
+		t.Error(err)
+	}
+
+	img, err := registry.ParseImage(registry.ParseImageOptions{
+		Name: "mongo:3.6.21",
+	})
+	if err != nil {
+		t.Error(err)
+	}
+
+	manifest, updated, err := rc.Manifest(img, registry.Manifest{
+		Name:     "docker.io/library/mongo",
+		Tag:      "3.6.21",
+		MIMEType: "application/vnd.docker.distribution.manifest.list.v2+json",
+		Digest:   "sha256:61f5dce8422d36b2a4ad0077bc499b1b68320e13fd30aa0b201c080fef42a39a",
+		Platform: "linux/amd64",
+		Raw: []byte(`{
+  "manifests": [
+    {
+      "digest": "sha256:98f22b0bf33479e2c34d99c820d9ded79cdf46b2c6f54af5a11191a90ff369ed",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "amd64",
+        "os": "linux"
+      },
+      "size": 3030
+    },
+    {
+      "digest": "sha256:8226c9734c19533d5cc52748e35ae10085f3b4ef0a3bd4537017bc2484589511",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "arm64",
+        "os": "linux",
+        "variant": "v8"
+      },
+      "size": 3030
+    },
+    {
+      "digest": "sha256:fb9e9376b228ba8d75d62b10aadaa3ed445266f85e27af3da531666d992f9621",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "amd64",
+        "os": "windows",
+        "os.version": "10.0.17763.1697"
+      },
+      "size": 2771
+    },
+    {
+      "digest": "sha256:f0534dfb20d90f152a7b4ae8812c61381cff7de983c2b17fc1fe3558a237fdac",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "amd64",
+        "os": "windows",
+        "os.version": "10.0.14393.4169"
+      },
+      "size": 2693
+    }
+  ],
+  "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
+  "schemaVersion": 2
+}`),
+	})
+
+	assert.NoError(t, err)
+	assert.Equal(t, true, updated)
+	assert.Equal(t, "docker.io/library/mongo", manifest.Name)
+	assert.Equal(t, "3.6.21", manifest.Tag)
+	assert.Equal(t, "application/vnd.docker.distribution.manifest.list.v2+json", manifest.MIMEType)
+	assert.Equal(t, "sha256:3cff2069adb34a330552695659c261bca69148e325863763b78b0285dd1a25c9", manifest.Digest.String())
+	assert.Equal(t, "linux/amd64", manifest.Platform)
+}
+
+func TestManifestMultiNotUpdatedPlatform(t *testing.T) {
+	rc, err := registry.New(registry.Options{
+		CompareDigest: true,
+		ImageOs:       "linux",
+		ImageArch:     "amd64",
+	})
+	if err != nil {
+		t.Error(err)
+	}
+
+	img, err := registry.ParseImage(registry.ParseImageOptions{
+		Name: "mongo:3.6.21",
+	})
+	if err != nil {
+		t.Error(err)
+	}
+
+	manifest, updated, err := rc.Manifest(img, registry.Manifest{
+		Name:     "docker.io/library/mongo",
+		Tag:      "3.6.21",
+		MIMEType: "application/vnd.docker.distribution.manifest.list.v2+json",
+		Digest:   "sha256:61f5dce8422d36b2a4ad0077bc499b1b68320e13fd30aa0b201c080fef42a39a",
+		Platform: "linux/amd64",
+		Raw: []byte(`{
+  "manifests": [
+    {
+      "digest": "sha256:6e5d3405a510988d96f0fa3ec7220040be27ce783eb4cd576feb1a69b382ea20",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "amd64",
+        "os": "linux"
+      },
+      "size": 3030
+    },
+    {
+      "digest": "sha256:8226c9734c19533d5cc52748e35ae10085f3b4ef0a3bd4537017bc2484589511",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "arm64",
+        "os": "linux",
+        "variant": "v8"
+      },
+      "size": 3030
+    },
+    {
+      "digest": "sha256:0fcde35d138739e27b79a8b9863dedc1fdd65fd3a82a319842f86edc87d11594",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "amd64",
+        "os": "windows",
+        "os.version": "10.0.17763.1817"
+      },
+      "size": 2771
+    },
+    {
+      "digest": "sha256:6f54fda6a88a56c0953e901f0285a74a16b4cf1bec021b2434e3bfe78cabfada",
+      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+      "platform": {
+        "architecture": "amd64",
+        "os": "windows",
+        "os.version": "10.0.14393.4283"
+      },
+      "size": 2693
+    }
+  ],
+  "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
+  "schemaVersion": 2
+}`),
+	})
+
+	assert.NoError(t, err)
+	assert.Equal(t, false, updated)
+	assert.Equal(t, "docker.io/library/mongo", manifest.Name)
+	assert.Equal(t, "3.6.21", manifest.Tag)
+	assert.Equal(t, "application/vnd.docker.distribution.manifest.list.v2+json", manifest.MIMEType)
+	assert.Equal(t, "sha256:3cff2069adb34a330552695659c261bca69148e325863763b78b0285dd1a25c9", manifest.Digest.String())
+	assert.Equal(t, "linux/amd64", manifest.Platform)
+}
+
 func TestManifestVariant(t *testing.T) {
 	rc, err := registry.New(registry.Options{
 		ImageOs:      "linux",
@@ -67,7 +272,7 @@ func TestManifestVariant(t *testing.T) {
 		t.Error(err)
 	}
 
-	manifest, err := rc.Manifest(img, registry.Manifest{})
+	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)