Browse code

Add registry error handling for push and pull

Signed-off-by: Derek McGowan <derek@mcg.dev>

Derek McGowan authored on 2025/04/08 15:32:08
Showing 4 changed files
... ...
@@ -228,7 +228,7 @@ func (i *ImageService) pullTag(ctx context.Context, ref reference.Named, platfor
228 228
 				return errdefs.NotFound(fmt.Errorf("no matching manifest for %s in the manifest list entries: %w", platformStr, err))
229 229
 			}
230 230
 		}
231
-		return err
231
+		return translateRegistryError(ctx, err)
232 232
 	}
233 233
 
234 234
 	logger := log.G(ctx).WithFields(log.Fields{
... ...
@@ -191,7 +191,7 @@ func (i *ImageService) pushRef(ctx context.Context, targetRef reference.Named, p
191 191
 
192 192
 		if err != nil {
193 193
 			if !cerrdefs.IsNotFound(err) {
194
-				return errdefs.System(err)
194
+				return translateRegistryError(ctx, err)
195 195
 			}
196 196
 			progress.Aux(out, auxprogress.ContentMissing{
197 197
 				ContentMissing: true,
198 198
new file mode 100644
... ...
@@ -0,0 +1,78 @@
0
+package containerd
1
+
2
+import (
3
+	"context"
4
+	"encoding/json"
5
+	"errors"
6
+	"fmt"
7
+
8
+	"github.com/containerd/containerd/v2/core/remotes/docker"
9
+	remoteerrors "github.com/containerd/containerd/v2/core/remotes/errors"
10
+	cerrdefs "github.com/containerd/errdefs"
11
+	"github.com/containerd/log"
12
+)
13
+
14
+func translateRegistryError(ctx context.Context, err error) error {
15
+	// Check for registry specific error
16
+	var derrs docker.Errors
17
+	if !errors.As(err, &derrs) {
18
+		var remoteErr remoteerrors.ErrUnexpectedStatus
19
+		if errors.As(err, &remoteErr) {
20
+			if jerr := json.Unmarshal(remoteErr.Body, &derrs); jerr != nil {
21
+				log.G(ctx).WithError(derrs).Debug("unable to unmarshal registry error")
22
+				return fmt.Errorf("%w: %w", cerrdefs.ErrUnknown, err)
23
+			}
24
+		} else {
25
+			var derr docker.Error
26
+			if errors.As(err, &derr) {
27
+				derrs = append(derrs, derr)
28
+			} else {
29
+				return err
30
+			}
31
+		}
32
+	}
33
+	var errs []error
34
+	for _, err := range derrs {
35
+		var derr docker.Error
36
+		if errors.As(err, &derr) {
37
+			var message string
38
+
39
+			if derr.Message != "" {
40
+				message = derr.Message
41
+			} else {
42
+				message = derr.Code.Message()
43
+			}
44
+
45
+			if detail, ok := derr.Detail.(string); ok {
46
+				message = fmt.Sprintf("%s - %s", message, detail)
47
+			}
48
+
49
+			switch derr.Code {
50
+			case docker.ErrorCodeUnsupported:
51
+				err = cerrdefs.ErrNotImplemented.WithMessage(message)
52
+			case docker.ErrorCodeUnauthorized:
53
+				err = cerrdefs.ErrUnauthenticated.WithMessage(message)
54
+			case docker.ErrorCodeDenied:
55
+				err = cerrdefs.ErrPermissionDenied.WithMessage(message)
56
+			case docker.ErrorCodeUnavailable:
57
+				err = cerrdefs.ErrUnavailable.WithMessage(message)
58
+			case docker.ErrorCodeTooManyRequests:
59
+				err = cerrdefs.ErrResourceExhausted.WithMessage(message)
60
+			default:
61
+				err = cerrdefs.ErrUnknown.WithMessage(message)
62
+			}
63
+		} else {
64
+			errs = append(errs, cerrdefs.ErrUnknown.WithMessage(err.Error()))
65
+		}
66
+		errs = append(errs, err)
67
+	}
68
+	switch len(errs) {
69
+	case 0:
70
+		err = cerrdefs.ErrUnknown.WithMessage(err.Error())
71
+	case 1:
72
+		err = errs[0]
73
+	default:
74
+		err = errors.Join(errs...)
75
+	}
76
+	return fmt.Errorf("error from registry: %w", err)
77
+}
... ...
@@ -255,7 +255,7 @@ func getTestTokenService(status int, body string, retries int) *httptest.Server
255 255
 }
256 256
 
257 257
 func (s *DockerRegistryAuthTokenSuite) TestPushTokenServiceUnauthResponse(c *testing.T) {
258
-	ts := getTestTokenService(http.StatusUnauthorized, `{"errors": [{"Code":"UNAUTHORIZED", "message": "a message", "detail": null}]}`, 0)
258
+	ts := getTestTokenService(http.StatusUnauthorized, `{"errors": [{"Code":"UNAUTHORIZED", "message": "a message about not being authorized", "detail": null}]}`, 0)
259 259
 	defer ts.Close()
260 260
 	s.setupRegistryWithTokenService(c, ts.URL)
261 261
 
... ...
@@ -268,10 +268,9 @@ func (s *DockerRegistryAuthTokenSuite) TestPushTokenServiceUnauthResponse(c *tes
268 268
 
269 269
 	// Auth service errors are not part of the spec and containerd doesn't parse them.
270 270
 	if testEnv.UsingSnapshotter() {
271
-		assert.Check(c, is.Contains(out, "failed to authorize: failed to fetch anonymous token"))
272
-		assert.Check(c, is.Contains(out, "401 Unauthorized"))
271
+		assert.Check(c, is.Contains(out, "a message about not being authorized"))
273 272
 	} else {
274
-		assert.Check(c, is.Contains(out, "unauthorized: a message"))
273
+		assert.Check(c, is.Contains(out, "unauthorized: a message about not being authorized"))
275 274
 	}
276 275
 }
277 276
 
... ...
@@ -297,7 +296,7 @@ func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponse
297 297
 }
298 298
 
299 299
 func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponseError(c *testing.T) {
300
-	ts := getTestTokenService(http.StatusTooManyRequests, `{"errors": [{"code":"TOOMANYREQUESTS","message":"out of tokens"}]}`, 3)
300
+	ts := getTestTokenService(http.StatusTooManyRequests, `{"errors": [{"code":"TOOMANYREQUESTS","message":"out of tokens"}]}`, 0)
301 301
 	defer ts.Close()
302 302
 	s.setupRegistryWithTokenService(c, ts.URL)
303 303
 
... ...
@@ -311,8 +310,7 @@ func (s *DockerRegistryAuthTokenSuite) TestPushMisconfiguredTokenServiceResponse
311 311
 
312 312
 	// Auth service errors are not part of the spec and containerd doesn't parse them.
313 313
 	if testEnv.UsingSnapshotter() {
314
-		assert.Check(c, is.Contains(out, "failed to authorize: failed to fetch anonymous token"))
315
-		assert.Check(c, is.Contains(out, "503 Service Unavailable"))
314
+		assert.Check(c, is.Contains(out, "out of tokens"))
316 315
 	} else {
317 316
 		split := strings.Split(out, "\n")
318 317
 		assert.Check(c, is.Equal(split[len(split)-2], "toomanyrequests: out of tokens"))