Skip to content

Commit e37a37a

Browse files
committed
manifest: rework error handling
This patch reworks and updates the error handling in the docker manifest command to be more inline with modern go principles. Specifically, we use the new errors.Is function, along with errors.Wrap, to preserve error hierarchy instead of using manual string operations. This allows us to simplify various testing components, as well as simplifying the implementation of manifest merging later. Signed-off-by: Justin Chadwell <me@jedevc.com>
1 parent 67b9617 commit e37a37a

10 files changed

Lines changed: 65 additions & 84 deletions

File tree

cli/command/manifest/annotate.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55

66
"github.com/docker/cli/cli"
77
"github.com/docker/cli/cli/command"
8-
"github.com/docker/cli/cli/manifest/store"
8+
"github.com/docker/cli/cli/manifest/types"
99
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1010
"github.com/pkg/errors"
1111
"github.com/spf13/cobra"
@@ -60,7 +60,7 @@ func runManifestAnnotate(dockerCli command.Cli, opts annotateOptions) error {
6060
manifestStore := dockerCli.ManifestStore()
6161
imageManifest, err := manifestStore.Get(targetRef, imgRef)
6262
switch {
63-
case store.IsNotFound(err):
63+
case errors.Is(err, types.ErrManifestNotFound):
6464
return fmt.Errorf("manifest for image %s does not exist in %s", opts.image, opts.target)
6565
case err != nil:
6666
return err

cli/command/manifest/create_list.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66

77
"github.com/docker/cli/cli"
88
"github.com/docker/cli/cli/command"
9-
"github.com/docker/cli/cli/manifest/store"
9+
"github.com/docker/cli/cli/manifest/types"
1010
"github.com/docker/docker/registry"
1111
"github.com/pkg/errors"
1212
"github.com/spf13/cobra"
@@ -50,7 +50,7 @@ func createManifestList(dockerCli command.Cli, args []string, opts createOpts) e
5050
manifestStore := dockerCli.ManifestStore()
5151
_, err = manifestStore.GetList(targetRef)
5252
switch {
53-
case store.IsNotFound(err):
53+
case errors.Is(err, types.ErrManifestNotFound):
5454
// New manifest list
5555
case err != nil:
5656
return err

cli/command/manifest/create_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,16 @@ func TestManifestCreateNoManifest(t *testing.T) {
9999
cli.SetManifestStore(store)
100100
cli.SetRegistryClient(&fakeRegistryClient{
101101
getManifestFunc: func(_ context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
102-
return manifesttypes.ImageManifest{}, errors.Errorf("No such image: %v", ref)
102+
return manifesttypes.ImageManifest{}, errors.Wrapf(manifesttypes.ErrManifestNotFound, "%q does not exist", ref)
103103
},
104104
getManifestListFunc: func(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
105-
return nil, errors.Errorf("No such manifest: %s", ref)
105+
return nil, errors.Wrapf(manifesttypes.ErrManifestNotFound, "%q does not exist", ref)
106106
},
107107
})
108108

109109
cmd := newCreateListCommand(cli)
110110
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
111111
cmd.SetOut(io.Discard)
112112
err := cmd.Execute()
113-
assert.Error(t, err, "No such image: example.com/alpine:3.0")
113+
assert.Error(t, err, `"example.com/alpine:3.0" does not exist: manifest not found`)
114114
}

cli/command/manifest/inspect_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func TestInspectCommandLocalManifestNotFound(t *testing.T) {
7070
cmd.SetOut(io.Discard)
7171
cmd.SetArgs([]string{"example.com/list:v1", "example.com/alpine:3.0"})
7272
err := cmd.Execute()
73-
assert.Error(t, err, "No such manifest: example.com/alpine:3.0")
73+
assert.Error(t, err, `"example.com/alpine:3.0" does not exist: manifest not found`)
7474
}
7575

7676
func TestInspectCommandNotFound(t *testing.T) {
@@ -79,19 +79,19 @@ func TestInspectCommandNotFound(t *testing.T) {
7979
cli := test.NewFakeCli(nil)
8080
cli.SetManifestStore(store)
8181
cli.SetRegistryClient(&fakeRegistryClient{
82-
getManifestFunc: func(_ context.Context, _ reference.Named) (types.ImageManifest, error) {
83-
return types.ImageManifest{}, errors.New("missing")
82+
getManifestFunc: func(ctx context.Context, ref reference.Named) (types.ImageManifest, error) {
83+
return types.ImageManifest{}, errors.Wrapf(types.ErrManifestNotFound, "%q does not exist", ref)
8484
},
8585
getManifestListFunc: func(ctx context.Context, ref reference.Named) ([]types.ImageManifest, error) {
86-
return nil, errors.Errorf("No such manifest: %s", ref)
86+
return nil, errors.Wrapf(types.ErrManifestNotFound, "%q does not exist", ref)
8787
},
8888
})
8989

9090
cmd := newInspectCommand(cli)
9191
cmd.SetOut(io.Discard)
9292
cmd.SetArgs([]string{"example.com/alpine:3.0"})
9393
err := cmd.Execute()
94-
assert.Error(t, err, "No such manifest: example.com/alpine:3.0")
94+
assert.Error(t, err, `"example.com/alpine:3.0" does not exist: manifest not found`)
9595
}
9696

9797
func TestInspectCommandLocalManifest(t *testing.T) {

cli/command/manifest/rm_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ func TestRmSeveralManifests(t *testing.T) {
3737

3838
_, search1 := cli.ManifestStore().GetList(list1)
3939
_, search2 := cli.ManifestStore().GetList(list2)
40-
assert.Error(t, search1, "No such manifest: example.com/first:1")
41-
assert.Error(t, search2, "No such manifest: example.com/second:2")
40+
assert.Error(t, search1, `"example.com/first:1" does not exist: manifest not found`)
41+
assert.Error(t, search2, `"example.com/second:2" does not exist: manifest not found`)
4242
}
4343

4444
// attempt to remove a manifest list which was never created
@@ -57,8 +57,8 @@ func TestRmManifestNotCreated(t *testing.T) {
5757
cmd.SetArgs([]string{"example.com/first:1", "example.com/second:2"})
5858
cmd.SetOut(io.Discard)
5959
err = cmd.Execute()
60-
assert.Error(t, err, "No such manifest: example.com/first:1")
60+
assert.Error(t, err, `"example.com/first:1" does not exist: manifest not found`)
6161

6262
_, err = cli.ManifestStore().GetList(list2)
63-
assert.Error(t, err, "No such manifest: example.com/second:2")
63+
assert.Error(t, err, `"example.com/second:2" does not exist: manifest not found`)
6464
}

cli/command/manifest/util.go

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import (
44
"context"
55

66
"github.com/docker/cli/cli/command"
7-
"github.com/docker/cli/cli/manifest/store"
87
"github.com/docker/cli/cli/manifest/types"
98
"github.com/docker/distribution/reference"
9+
"github.com/pkg/errors"
1010
)
1111

1212
type osArch struct {
@@ -70,15 +70,26 @@ func normalizeReference(ref string) (reference.Named, error) {
7070
// getManifest from the local store, and fallback to the remote registry if it
7171
// doesn't exist locally
7272
func getManifest(ctx context.Context, dockerCli command.Cli, listRef, namedRef reference.Named, insecure bool) (types.ImageManifest, error) {
73-
data, err := dockerCli.ManifestStore().Get(listRef, namedRef)
74-
switch {
75-
case store.IsNotFound(err):
76-
return dockerCli.RegistryClient(insecure).GetManifest(ctx, namedRef)
77-
case err != nil:
78-
return types.ImageManifest{}, err
79-
case len(data.Raw) == 0:
80-
return dockerCli.RegistryClient(insecure).GetManifest(ctx, namedRef)
81-
default:
82-
return data, nil
73+
// load from the local store
74+
if listRef != nil {
75+
data, err := dockerCli.ManifestStore().Get(listRef, namedRef)
76+
if err == nil {
77+
return data, nil
78+
} else if !errors.Is(err, types.ErrManifestNotFound) {
79+
return types.ImageManifest{}, err
80+
}
8381
}
82+
83+
// load from the remote registry
84+
client := dockerCli.RegistryClient(insecure)
85+
if client != nil {
86+
data, err := client.GetManifest(ctx, namedRef)
87+
if err == nil {
88+
return data, nil
89+
} else if !errors.Is(err, types.ErrManifestNotFound) {
90+
return types.ImageManifest{}, err
91+
}
92+
}
93+
94+
return types.ImageManifest{}, errors.Wrapf(types.ErrManifestNotFound, "%q does not exist", namedRef)
8495
}

cli/manifest/store/store.go

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package store
22

33
import (
44
"encoding/json"
5-
"fmt"
65
"os"
76
"path/filepath"
87
"strings"
@@ -49,7 +48,7 @@ func (s *fsStore) getFromFilename(ref reference.Reference, filename string) (typ
4948
bytes, err := os.ReadFile(filename)
5049
switch {
5150
case os.IsNotExist(err):
52-
return types.ImageManifest{}, newNotFoundError(ref.String())
51+
return types.ImageManifest{}, errors.Wrapf(types.ErrManifestNotFound, "%q does not exist", ref.String())
5352
case err != nil:
5453
return types.ImageManifest{}, err
5554
}
@@ -93,7 +92,7 @@ func (s *fsStore) GetList(listRef reference.Reference) ([]types.ImageManifest, e
9392
case err != nil:
9493
return nil, err
9594
case filenames == nil:
96-
return nil, newNotFoundError(listRef.String())
95+
return nil, errors.Wrapf(types.ErrManifestNotFound, "%q does not exist", listRef.String())
9796
}
9897

9998
manifests := []types.ImageManifest{}
@@ -152,28 +151,3 @@ func makeFilesafeName(ref string) string {
152151
fileName := strings.Replace(ref, ":", "-", -1)
153152
return strings.Replace(fileName, "/", "_", -1)
154153
}
155-
156-
type notFoundError struct {
157-
object string
158-
}
159-
160-
func newNotFoundError(ref string) *notFoundError {
161-
return &notFoundError{object: ref}
162-
}
163-
164-
func (n *notFoundError) Error() string {
165-
return fmt.Sprintf("No such manifest: %s", n.object)
166-
}
167-
168-
// NotFound interface
169-
func (n *notFoundError) NotFound() {}
170-
171-
// IsNotFound returns true if the error is a not found error
172-
func IsNotFound(err error) bool {
173-
_, ok := err.(notFound)
174-
return ok
175-
}
176-
177-
type notFound interface {
178-
NotFound()
179-
}

cli/manifest/store/store_test.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/docker/cli/cli/manifest/types"
88
"github.com/docker/distribution/reference"
99
"github.com/google/go-cmp/cmp"
10+
"github.com/pkg/errors"
1011
"gotest.tools/v3/assert"
1112
is "gotest.tools/v3/assert/cmp"
1213
)
@@ -62,7 +63,7 @@ func TestStoreSaveAndGet(t *testing.T) {
6263
listRef reference.Reference
6364
manifestRef reference.Reference
6465
expected types.ImageManifest
65-
expectedErr string
66+
expectedErr error
6667
}{
6768
{
6869
listRef: listRef,
@@ -72,22 +73,21 @@ func TestStoreSaveAndGet(t *testing.T) {
7273
{
7374
listRef: listRef,
7475
manifestRef: ref("exist:does-not"),
75-
expectedErr: "No such manifest: exist:does-not",
76+
expectedErr: types.ErrManifestNotFound,
7677
},
7778
{
7879
listRef: ref("list:does-not-exist"),
7980
manifestRef: ref("manifest:does-not-exist"),
80-
expectedErr: "No such manifest: manifest:does-not-exist",
81+
expectedErr: types.ErrManifestNotFound,
8182
},
8283
}
8384

8485
for _, testcase := range testcases {
8586
testcase := testcase
8687
t.Run(testcase.manifestRef.String(), func(t *testing.T) {
8788
actual, err := store.Get(testcase.listRef, testcase.manifestRef)
88-
if testcase.expectedErr != "" {
89-
assert.Error(t, err, testcase.expectedErr)
90-
assert.Check(t, IsNotFound(err))
89+
if testcase.expectedErr != nil {
90+
assert.Check(t, errors.Is(err, types.ErrManifestNotFound))
9191
return
9292
}
9393
assert.NilError(t, err)
@@ -117,6 +117,5 @@ func TestStoreGetListDoesNotExist(t *testing.T) {
117117
store := NewStore(t.TempDir())
118118
listRef := ref("list")
119119
_, err := store.GetList(listRef)
120-
assert.Error(t, err, "No such manifest: list")
121-
assert.Check(t, IsNotFound(err))
120+
assert.Check(t, errors.Is(err, types.ErrManifestNotFound))
122121
}

cli/manifest/types/types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ import (
1313
"github.com/pkg/errors"
1414
)
1515

16+
var (
17+
// ErrManifestNotFound is an error returned when a requested manifest or
18+
// manifest list cannot be found in the store or the registry
19+
ErrManifestNotFound = errors.New("manifest not found")
20+
21+
// ErrUnknownType is an error returned when a manifest was found, but it
22+
// was of an unrecognized type
23+
ErrUnknownType = errors.New("unknown manifest type")
24+
)
25+
1626
// ImageManifest contains info to output for a manifest object.
1727
type ImageManifest struct {
1828
Ref *SerializableNamed

cli/registry/client/fetcher.go

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ func fetchManifest(ctx context.Context, repo distribution.Repository, ref refere
4343
}
4444
return imageManifest, nil
4545
case *manifestlist.DeserializedManifestList:
46-
return types.ImageManifest{}, errors.Errorf("%s is a manifest list", ref)
46+
return types.ImageManifest{}, errors.Wrapf(types.ErrManifestNotFound, "%q is a manifest list", ref)
4747
}
48-
return types.ImageManifest{}, errors.Errorf("%s is not a manifest", ref)
48+
return types.ImageManifest{}, errors.Wrapf(types.ErrUnknownType, "%q does not exist", ref)
4949
}
5050

5151
func fetchList(ctx context.Context, repo distribution.Repository, ref reference.Named) ([]types.ImageManifest, error) {
@@ -55,14 +55,16 @@ func fetchList(ctx context.Context, repo distribution.Repository, ref reference.
5555
}
5656

5757
switch v := manifest.(type) {
58+
case *schema2.DeserializedManifest, *ocischema.DeserializedManifest:
59+
return nil, errors.Wrapf(types.ErrManifestNotFound, "%q is not a manifest list", ref)
5860
case *manifestlist.DeserializedManifestList:
5961
imageManifests, err := pullManifestList(ctx, ref, repo, *v)
6062
if err != nil {
6163
return nil, err
6264
}
6365
return imageManifests, nil
6466
default:
65-
return nil, errors.Errorf("unsupported manifest format: %v", v)
67+
return nil, types.ErrUnknownType
6668
}
6769
}
6870

@@ -74,7 +76,7 @@ func getManifest(ctx context.Context, repo distribution.Repository, ref referenc
7476

7577
dgst, opts, err := getManifestOptionsFromReference(ref)
7678
if err != nil {
77-
return nil, errors.Errorf("image manifest for %q does not exist", ref)
79+
return nil, errors.Wrapf(types.ErrManifestNotFound, "%q does not exist", ref)
7880
}
7981
return manSvc.Get(ctx, dgst, opts...)
8082
}
@@ -195,7 +197,7 @@ func pullManifestList(ctx context.Context, ref reference.Named, repo distributio
195197
case *ocischema.DeserializedManifest:
196198
imageManifest, err = pullManifestOCISchema(ctx, manifestRef, repo, *v)
197199
default:
198-
err = errors.Errorf("unsupported manifest type: %T", manifest)
200+
err = types.ErrUnknownType
199201
}
200202
if err != nil {
201203
return nil, err
@@ -287,7 +289,7 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named,
287289
return nil
288290
}
289291
}
290-
return newNotFoundError(namedRef.String())
292+
return errors.Wrapf(types.ErrManifestNotFound, "%q does not exist", namedRef)
291293
}
292294

293295
// allEndpoints returns a list of endpoints ordered by priority (v2, https, v1).
@@ -310,18 +312,3 @@ func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoi
310312
logrus.Debugf("endpoints for %s: %v", namedRef, endpoints)
311313
return endpoints, err
312314
}
313-
314-
func newNotFoundError(ref string) *notFoundError {
315-
return &notFoundError{err: errors.New("no such manifest: " + ref)}
316-
}
317-
318-
type notFoundError struct {
319-
err error
320-
}
321-
322-
func (n *notFoundError) Error() string {
323-
return n.err.Error()
324-
}
325-
326-
// NotFound satisfies interface github.com/docker/docker/errdefs.ErrNotFound
327-
func (n *notFoundError) NotFound() {}

0 commit comments

Comments
 (0)