fix(master): seed the tag digest from running services and hide registry error text (#9)
This commit is contained in:
@@ -6,7 +6,10 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -24,14 +27,16 @@ import (
|
||||
type Client struct {
|
||||
keychain authn.Keychain
|
||||
userAgent string
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// 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}
|
||||
// User-Agent header of every request. log receives the full error of a
|
||||
// failed lookup, which Digest itself returns only in a sanitised form.
|
||||
func New(userAgent string, log *slog.Logger) *Client {
|
||||
return &Client{keychain: authn.DefaultKeychain, userAgent: userAgent, log: log}
|
||||
}
|
||||
|
||||
// Digest returns the digest that "repo:tag" points at, as the registry
|
||||
@@ -49,20 +54,43 @@ func (c *Client) Digest(ctx context.Context, ref string) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse %s: %w", ref, err)
|
||||
}
|
||||
// Credentials are resolved here, not by ggcr, so that a broken credential
|
||||
// helper keeps its own message: that text comes from the local config.
|
||||
auth, err := c.keychain.Resolve(r.Context())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("lookup %s: %w", r, err)
|
||||
}
|
||||
desc, err := remote.Head(r,
|
||||
remote.WithContext(ctx),
|
||||
remote.WithAuthFromKeychain(c.keychain),
|
||||
remote.WithAuth(auth),
|
||||
remote.WithUserAgent(c.userAgent))
|
||||
if err != nil {
|
||||
var terr *transport.Error
|
||||
if errors.As(err, &terr) {
|
||||
return "", fmt.Errorf("lookup %s: %s", r, describe(terr))
|
||||
}
|
||||
return "", fmt.Errorf("lookup %s: %w", r, err)
|
||||
c.log.Warn("registry_lookup_failed", "ref", ref, "error", err)
|
||||
return "", fmt.Errorf("lookup %s: %s", r, classify(err))
|
||||
}
|
||||
return desc.Digest.String(), nil
|
||||
}
|
||||
|
||||
// classify returns a fixed text for an error that is not a registry answer.
|
||||
// ggcr copies registry-controlled text (token responses, authentication
|
||||
// challenges, redirect URLs, certificate names) into such errors, so none of
|
||||
// it is passed on.
|
||||
func classify(err error) string {
|
||||
var netErr net.Error
|
||||
var urlErr *url.Error
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded), errors.Is(err, context.Canceled):
|
||||
return "timeout"
|
||||
case errors.As(err, &netErr), errors.As(err, &urlErr):
|
||||
return "cannot reach registry"
|
||||
}
|
||||
return "lookup failed"
|
||||
}
|
||||
|
||||
// errorCodePattern matches the registry error codes of the distribution
|
||||
// spec ("MANIFEST_UNKNOWN"). Anything else is dropped from error text.
|
||||
var errorCodePattern = regexp.MustCompile(`^[A-Z_]{1,40}$`)
|
||||
|
||||
Reference in New Issue
Block a user