Repository: moby/moby
File: daemon/containerd/registry_errors.go (Go)

Code before:
package containerd

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	"github.com/containerd/containerd/v2/core/remotes/docker"
	remoteerrors "github.com/containerd/containerd/v2/core/remotes/errors"
	cerrdefs "github.com/containerd/errdefs"
	"github.com/containerd/log"
)

type tokenServerError struct {
	Details string `json:"details"`
}

func translateRegistryError(ctx context.Context, err error) error {
	// Check for registry specific error
	var derrs docker.Errors
	if !errors.As(err, &derrs) {
		var remoteErr remoteerrors.ErrUnexpectedStatus
		if errors.As(err, &remoteErr) {
			if jerr := json.Unmarshal(remoteErr.Body, &derrs); jerr != nil {
				log.G(ctx).WithError(derrs).Debug("unable to unmarshal registry error")
				return fmt.Errorf("%w: %w", cerrdefs.ErrUnknown, err)
			}
			if len(derrs) == 0 {
				if remoteErr.StatusCode == http.StatusUnauthorized || remoteErr.StatusCode == http.StatusForbidden {
					// Token server may have a unique response
					var tokenErr tokenServerError
					if jerr := json.Unmarshal(remoteErr.Body, &tokenErr); jerr == nil && tokenErr.Details != "" {
						if remoteErr.StatusCode == http.StatusUnauthorized {
							return cerrdefs.ErrUnauthenticated.WithMessage(fmt.Sprintf("%s - %s", docker.ErrorCodeUnauthorized.Message(), tokenErr.Details))
						} else {
							return cerrdefs.ErrPermissionDenied.WithMessage(fmt.Sprintf("%s - %s", docker.ErrorCodeDenied.Message(), tokenErr.Details))

						}
					}

				}

Review comment (bug):
I was curious if there wasn't a definition for this, but looking at the old code from docker/distribution, it looks like this type was added for incorrectly formatted responses that are missing some of the other fields (?); perhaps adding a similar comment here could be useful. https://github.com/moby/moby/blob/e1d0aacb6a05663eb5f97ca485a1aebc08c0be22/vendor/github.com/docker/distribution/registry/client/errors.go#L64-L72

Looks like the old code also looks for mediatype, and if it doesn't match, it returns the whole body, but not sure if that's a good idea (at least not without a limit-reader; https://github.com/moby/moby/blob/e1d0aacb6a05663eb5f97ca485a1aebc08c0be22/vendor/github.com/docker/distribution/registry/client/errors.go#L60-L62

I'm slightly curious though if the _current_ type would work for this; I see code further down handling the `docker.Errors` type (which awkwardly uses a `interface{}` for details, not a string); https://github.com/moby/moby/blob/3b1d2f746d0ef4ccc695353ffd54dc7df37220b6/daemon/containerd/registry_errors.go#L36-L37

https://github.com/moby/moby/blob/0aa8fe0bf949cbccab74414cb091728854be0d7d/vendor/github.com/containerd/containerd/v2/core/remotes/docker/errcode.go#L114-L118

☝️ that error looks to be what's now in the distribution-spec (perhaps containerd should use that as well, but not really important); https://github.com/opencontainers/distribution-spec/blob/583e014d15418d839d67f68152bc2c83821770e0/specs-go/v1/error.go#L35-L40



Code after:
package containerd

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	"github.com/containerd/containerd/v2/core/remotes/docker"
	remoteerrors "github.com/containerd/containerd/v2/core/remotes/errors"
	cerrdefs "github.com/containerd/errdefs"
	"github.com/containerd/log"
)

func translateRegistryError(ctx context.Context, err error) error {
	// Check for registry specific error
	var derrs docker.Errors
	if !errors.As(err, &derrs) {
		var remoteErr remoteerrors.ErrUnexpectedStatus
		if errors.As(err, &remoteErr) {
			if jerr := json.Unmarshal(remoteErr.Body, &derrs); jerr != nil {
				log.G(ctx).WithError(derrs).Debug("unable to unmarshal registry error")
				return fmt.Errorf("%w: %w", cerrdefs.ErrUnknown, err)
			}
			if len(derrs) == 0 {
				if remoteErr.StatusCode == http.StatusUnauthorized || remoteErr.StatusCode == http.StatusForbidden {
					// Some registries or token servers may use an old deprecated error format
					// which only has a "details" field and not the OCI defined "errors" array.
					var tokenErr struct {
						Details string `json:"details"`
					}
					if jerr := json.Unmarshal(remoteErr.Body, &tokenErr); jerr == nil && tokenErr.Details != "" {
						if remoteErr.StatusCode == http.StatusUnauthorized {
							return cerrdefs.ErrUnauthenticated.WithMessage(fmt.Sprintf("%s - %s", docker.ErrorCodeUnauthorized.Message(), tokenErr.Details))
						}
						return cerrdefs.ErrPermissionDenied.WithMessage(fmt.Sprintf("%s - %s", docker.ErrorCodeDenied.Message(), tokenErr.Details))
					}
				}
			}
		} else {
			var derr docker.Error
			if errors.As(err, &derr) {