mirror of
https://github.com/crazy-max/diun.git
synced 2025-12-21 13:23:09 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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
16
pkg/registry/ref.go
Normal 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
65
pkg/registry/ref_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user