...

Source file src/github.com/google/go-containerregistry/pkg/v1/remote/transport/error.go

Documentation: github.com/google/go-containerregistry/pkg/v1/remote/transport

     1  // Copyright 2018 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package transport
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"io"
    21  	"net/http"
    22  	"strings"
    23  
    24  	"github.com/google/go-containerregistry/internal/redact"
    25  )
    26  
    27  // Error implements error to support the following error specification:
    28  // https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors
    29  type Error struct {
    30  	Errors []Diagnostic `json:"errors,omitempty"`
    31  	// The http status code returned.
    32  	StatusCode int
    33  	// The request that failed.
    34  	Request *http.Request
    35  	// The raw body if we couldn't understand it.
    36  	rawBody string
    37  
    38  	// Bit of a hack to make it easier to force a retry.
    39  	temporary bool
    40  }
    41  
    42  // Check that Error implements error
    43  var _ error = (*Error)(nil)
    44  
    45  // Error implements error
    46  func (e *Error) Error() string {
    47  	prefix := ""
    48  	if e.Request != nil {
    49  		prefix = fmt.Sprintf("%s %s: ", e.Request.Method, redact.URL(e.Request.URL))
    50  	}
    51  	return prefix + e.responseErr()
    52  }
    53  
    54  func (e *Error) responseErr() string {
    55  	switch len(e.Errors) {
    56  	case 0:
    57  		if len(e.rawBody) == 0 {
    58  			if e.Request != nil && e.Request.Method == http.MethodHead {
    59  				return fmt.Sprintf("unexpected status code %d %s (HEAD responses have no body, use GET for details)", e.StatusCode, http.StatusText(e.StatusCode))
    60  			}
    61  			return fmt.Sprintf("unexpected status code %d %s", e.StatusCode, http.StatusText(e.StatusCode))
    62  		}
    63  		return fmt.Sprintf("unexpected status code %d %s: %s", e.StatusCode, http.StatusText(e.StatusCode), e.rawBody)
    64  	case 1:
    65  		return e.Errors[0].String()
    66  	default:
    67  		var errors []string
    68  		for _, d := range e.Errors {
    69  			errors = append(errors, d.String())
    70  		}
    71  		return fmt.Sprintf("multiple errors returned: %s",
    72  			strings.Join(errors, "; "))
    73  	}
    74  }
    75  
    76  // Temporary returns whether the request that preceded the error is temporary.
    77  func (e *Error) Temporary() bool {
    78  	if e.temporary {
    79  		return true
    80  	}
    81  
    82  	if len(e.Errors) == 0 {
    83  		_, ok := temporaryStatusCodes[e.StatusCode]
    84  		return ok
    85  	}
    86  	for _, d := range e.Errors {
    87  		if _, ok := temporaryErrorCodes[d.Code]; !ok {
    88  			return false
    89  		}
    90  	}
    91  	return true
    92  }
    93  
    94  // Diagnostic represents a single error returned by a Docker registry interaction.
    95  type Diagnostic struct {
    96  	Code    ErrorCode `json:"code"`
    97  	Message string    `json:"message,omitempty"`
    98  	Detail  any       `json:"detail,omitempty"`
    99  }
   100  
   101  // String stringifies the Diagnostic in the form: $Code: $Message[; $Detail]
   102  func (d Diagnostic) String() string {
   103  	msg := fmt.Sprintf("%s: %s", d.Code, d.Message)
   104  	if d.Detail != nil {
   105  		msg = fmt.Sprintf("%s; %v", msg, d.Detail)
   106  	}
   107  	return msg
   108  }
   109  
   110  // ErrorCode is an enumeration of supported error codes.
   111  type ErrorCode string
   112  
   113  // The set of error conditions a registry may return:
   114  // https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors-2
   115  const (
   116  	BlobUnknownErrorCode         ErrorCode = "BLOB_UNKNOWN"
   117  	BlobUploadInvalidErrorCode   ErrorCode = "BLOB_UPLOAD_INVALID"
   118  	BlobUploadUnknownErrorCode   ErrorCode = "BLOB_UPLOAD_UNKNOWN"
   119  	DigestInvalidErrorCode       ErrorCode = "DIGEST_INVALID"
   120  	ManifestBlobUnknownErrorCode ErrorCode = "MANIFEST_BLOB_UNKNOWN"
   121  	ManifestInvalidErrorCode     ErrorCode = "MANIFEST_INVALID"
   122  	ManifestUnknownErrorCode     ErrorCode = "MANIFEST_UNKNOWN"
   123  	ManifestUnverifiedErrorCode  ErrorCode = "MANIFEST_UNVERIFIED"
   124  	NameInvalidErrorCode         ErrorCode = "NAME_INVALID"
   125  	NameUnknownErrorCode         ErrorCode = "NAME_UNKNOWN"
   126  	SizeInvalidErrorCode         ErrorCode = "SIZE_INVALID"
   127  	TagInvalidErrorCode          ErrorCode = "TAG_INVALID"
   128  	UnauthorizedErrorCode        ErrorCode = "UNAUTHORIZED"
   129  	DeniedErrorCode              ErrorCode = "DENIED"
   130  	UnsupportedErrorCode         ErrorCode = "UNSUPPORTED"
   131  	TooManyRequestsErrorCode     ErrorCode = "TOOMANYREQUESTS"
   132  	UnknownErrorCode             ErrorCode = "UNKNOWN"
   133  
   134  	// This isn't defined by either docker or OCI spec, but is defined by docker/distribution:
   135  	// https://github.com/distribution/distribution/blob/6a977a5a754baa213041443f841705888107362a/registry/api/errcode/register.go#L60
   136  	UnavailableErrorCode ErrorCode = "UNAVAILABLE"
   137  )
   138  
   139  // TODO: Include other error types.
   140  var temporaryErrorCodes = map[ErrorCode]struct{}{
   141  	BlobUploadInvalidErrorCode: {},
   142  	TooManyRequestsErrorCode:   {},
   143  	UnknownErrorCode:           {},
   144  	UnavailableErrorCode:       {},
   145  }
   146  
   147  var temporaryStatusCodes = map[int]struct{}{
   148  	http.StatusRequestTimeout:      {},
   149  	http.StatusInternalServerError: {},
   150  	http.StatusBadGateway:          {},
   151  	http.StatusServiceUnavailable:  {},
   152  	http.StatusGatewayTimeout:      {},
   153  }
   154  
   155  // CheckError returns a structured error if the response status is not in codes.
   156  func CheckError(resp *http.Response, codes ...int) error {
   157  	for _, code := range codes {
   158  		if resp.StatusCode == code {
   159  			// This is one of the supported status codes.
   160  			return nil
   161  		}
   162  	}
   163  
   164  	b, err := io.ReadAll(resp.Body)
   165  	if err != nil {
   166  		return err
   167  	}
   168  
   169  	return makeError(resp, b)
   170  }
   171  
   172  func makeError(resp *http.Response, body []byte) *Error {
   173  	// https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors
   174  	structuredError := &Error{}
   175  
   176  	// This can fail if e.g. the response body is not valid JSON. That's fine,
   177  	// we'll construct an appropriate error string from the body and status code.
   178  	_ = json.Unmarshal(body, structuredError)
   179  
   180  	structuredError.rawBody = string(body)
   181  	structuredError.StatusCode = resp.StatusCode
   182  	structuredError.Request = resp.Request
   183  
   184  	return structuredError
   185  }
   186  
   187  func retryError(resp *http.Response) error {
   188  	b, err := io.ReadAll(resp.Body)
   189  	if err != nil {
   190  		return err
   191  	}
   192  
   193  	rerr := makeError(resp, b)
   194  	rerr.temporary = true
   195  	return rerr
   196  }
   197  

View as plain text