// Package registry looks up the digest a tag points at in a container // registry, using HEAD requests only. package registry import ( "context" "fmt" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/shcizo/package-updater/internal/imageref" ) // Client looks up tag digests with HEAD requests only. Docker Hub counts a // GET of a manifest as a pull against the rate limit and a HEAD as nothing, // so Client never issues a manifest GET. type Client struct { keychain authn.Keychain userAgent string } // New returns a Client that finds credentials the way the Docker CLI does: // in $DOCKER_CONFIG/config.json, else ~/.docker/config.json. Registries // without an entry are accessed anonymously. userAgent is added to the // User-Agent header of every request. func New(userAgent string) *Client { return &Client{keychain: authn.DefaultKeychain, userAgent: userAgent} } // Digest returns the digest that "repo:tag" points at, as the registry // reports it: for a multi-arch image that is the digest of the index, the // same one `docker inspect` shows in RepoDigests. A digest suffix in ref is // ignored. It sends HEAD /v2//manifests/ and nothing else to the // manifest endpoint; it does not fall back to GET when the registry omits // the digest header. func (c *Client) Digest(ctx context.Context, ref string) (string, error) { p := imageref.Parse(ref) if p.Tag == "" { return "", fmt.Errorf("image %q has no tag", ref) } r, err := name.ParseReference(p.Repo + ":" + p.Tag) if err != nil { return "", fmt.Errorf("parse %s: %w", ref, err) } desc, err := remote.Head(r, remote.WithContext(ctx), remote.WithAuthFromKeychain(c.keychain), remote.WithUserAgent(c.userAgent)) if err != nil { return "", fmt.Errorf("head %s: %w", r, err) } return desc.Digest.String(), nil }