Check digest from HEAD request (#217)

* Check digest from HEAD request
* Add FAQ note about Docker Hub rate limits
* Compare digest as watch setting

Co-authored-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2020-11-13 00:00:01 +01:00
committed by GitHub
parent 2fa4696f3a
commit b1953afdae
15 changed files with 244 additions and 72 deletions

View File

@@ -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,16 +67,18 @@ func (c *Client) Manifest(image Image) (Manifest, error) {
return Manifest{}, errors.Wrap(err, "Cannot get raw manifest")
}
if !c.opts.CompareDigest {
imgDigest, err = manifest.Digest(rawManifest)
if err != nil {
return Manifest{}, errors.Wrap(err, "Cannot get digest")
}
}
imgInspect, err := imgCloser.Inspect(ctx)
if err != nil {
return Manifest{}, errors.Wrap(err, "Cannot inspect")
}
imgDigest, err := manifest.Digest(rawManifest)
if err != nil {
return Manifest{}, errors.Wrap(err, "Cannot get digest")
}
imgTag := imgInspect.Tag
if len(imgTag) == 0 {
imgTag = image.Tag

View File

@@ -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)

16
pkg/registry/ref.go Normal file
View File

@@ -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)
}

65
pkg/registry/ref_test.go Normal file
View File

@@ -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)
})
}
}

View File

@@ -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
}

View File

@@ -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")
}
defer imgCls.Close()
tags, err := docker.GetRepositoryTags(ctx, c.sysCtx, imgCls.Reference())
imgCloser, err := imgRef.NewImage(ctx, c.sysCtx)
if err != nil {
return nil, errors.Wrap(err, "Cannot create image closer")
}
defer imgCloser.Close()
tags, err := docker.GetRepositoryTags(ctx, c.sysCtx, imgCloser.Reference())
if err != nil {
return nil, err
}