Browse code

client: normalize and validate empty ID / name arguments to fail early

In situations where an empty ID was passed, the client would construct an
invalid API endpoint URL, which either resulted in the "not found" handler
being hit (resulting in a "page not found" error), or even the wrong endpoint
being hit if the client follows redirects.

For example, `/containers/<empty id>/json` (inspect) redirects to `/containers/json`
(docker ps))

Given that empty IDs should never be expected (especially if they're part of
the API URL path), we can validate these and return early.

Its worth noting that a few methods already had an error in place; those
methods were related to the situation mentioned above, where (e.g.) an
"inspect" would redirect to a "list" endpoint. The existing errors, for
convenience, mimicked a "not found" error; this patch changes such errors
to an "Invalid Parameter" instead, which is more correct, but it could be
a breaking change for some edge cases where users parsed the output;

git grep 'objectNotFoundError{'
client/config_inspect.go: return swarm.Config{}, nil, objectNotFoundError{object: "config", id: id}
client/container_inspect.go: return container.InspectResponse{}, nil, objectNotFoundError{object: "container", id: containerID}
client/container_inspect.go: return container.InspectResponse{}, objectNotFoundError{object: "container", id: containerID}
client/distribution_inspect.go: return distributionInspect, objectNotFoundError{object: "distribution", id: imageRef}
client/image_inspect.go: return image.InspectResponse{}, nil, objectNotFoundError{object: "image", id: imageID}
client/network_inspect.go: return network.Inspect{}, nil, objectNotFoundError{object: "network", id: networkID}
client/node_inspect.go: return swarm.Node{}, nil, objectNotFoundError{object: "node", id: nodeID}
client/plugin_inspect.go: return nil, nil, objectNotFoundError{object: "plugin", id: name}
client/secret_inspect.go: return swarm.Secret{}, nil, objectNotFoundError{object: "secret", id: id}
client/service_inspect.go: return swarm.Service{}, nil, objectNotFoundError{object: "service", id: serviceID}
client/task_inspect.go: return swarm.Task{}, nil, objectNotFoundError{object: "task", id: taskID}
client/volume_inspect.go: return volume.Volume{}, nil, objectNotFoundError{object: "volume", id: volumeID}

Two such errors are still left, as "ID or name" would probably be confusing,
but perhaps we can use a more generic error to include those as well (e.g.
"invalid <object> reference: value is empty");

client/distribution_inspect.go: return distributionInspect, objectNotFoundError{object: "distribution", id: imageRef}
client/image_inspect.go: return image.InspectResponse{}, nil, objectNotFoundError{object: "image", id: imageID}

Before this patch:

docker container start ""
Error response from daemon: page not found
Error: failed to start containers:

docker container start " "
Error response from daemon: No such container:
Error: failed to start containers:

With this patch:

docker container start ""
invalid container name or ID: value is empty
Error: failed to start containers:

docker container start " "
invalid container name or ID: value is empty
Error: failed to start containers:

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2025/02/03 18:35:25
Showing 100 changed files
... ...
@@ -7,8 +7,13 @@ import (
7 7
 )
8 8
 
9 9
 // CheckpointCreate creates a checkpoint from the given container with the given name
10
-func (cli *Client) CheckpointCreate(ctx context.Context, container string, options checkpoint.CreateOptions) error {
11
-	resp, err := cli.post(ctx, "/containers/"+container+"/checkpoints", nil, options, nil)
10
+func (cli *Client) CheckpointCreate(ctx context.Context, containerID string, options checkpoint.CreateOptions) error {
11
+	containerID, err := trimID("container", containerID)
12
+	if err != nil {
13
+		return err
14
+	}
15
+
16
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/checkpoints", nil, options, nil)
12 17
 	ensureReaderClosed(resp)
13 18
 	return err
14 19
 }
... ...
@@ -26,6 +26,14 @@ func TestCheckpointCreateError(t *testing.T) {
26 26
 	})
27 27
 
28 28
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
29
+
30
+	err = client.CheckpointCreate(context.Background(), "", checkpoint.CreateOptions{})
31
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
32
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
33
+
34
+	err = client.CheckpointCreate(context.Background(), "    ", checkpoint.CreateOptions{})
35
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
36
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
29 37
 }
30 38
 
31 39
 func TestCheckpointCreate(t *testing.T) {
... ...
@@ -36,7 +44,7 @@ func TestCheckpointCreate(t *testing.T) {
36 36
 	client := &Client{
37 37
 		client: newMockClient(func(req *http.Request) (*http.Response, error) {
38 38
 			if !strings.HasPrefix(req.URL.Path, expectedURL) {
39
-				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
39
+				return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
40 40
 			}
41 41
 
42 42
 			if req.Method != http.MethodPost {
... ...
@@ -9,6 +9,11 @@ import (
9 9
 
10 10
 // CheckpointDelete deletes the checkpoint with the given name from the given container
11 11
 func (cli *Client) CheckpointDelete(ctx context.Context, containerID string, options checkpoint.DeleteOptions) error {
12
+	containerID, err := trimID("container", containerID)
13
+	if err != nil {
14
+		return err
15
+	}
16
+
12 17
 	query := url.Values{}
13 18
 	if options.CheckpointDir != "" {
14 19
 		query.Set("dir", options.CheckpointDir)
... ...
@@ -25,6 +25,14 @@ func TestCheckpointDeleteError(t *testing.T) {
25 25
 	})
26 26
 
27 27
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
28
+
29
+	err = client.CheckpointDelete(context.Background(), "", checkpoint.DeleteOptions{})
30
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
31
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
32
+
33
+	err = client.CheckpointDelete(context.Background(), "    ", checkpoint.DeleteOptions{})
34
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
35
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
28 36
 }
29 37
 
30 38
 func TestCheckpointDelete(t *testing.T) {
... ...
@@ -11,8 +11,9 @@ import (
11 11
 
12 12
 // ConfigInspectWithRaw returns the config information with raw data
13 13
 func (cli *Client) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
14
-	if id == "" {
15
-		return swarm.Config{}, nil, objectNotFoundError{object: "config", id: id}
14
+	id, err := trimID("contig", id)
15
+	if err != nil {
16
+		return swarm.Config{}, nil, err
16 17
 	}
17 18
 	if err := cli.NewVersionError(ctx, "1.30", "config inspect"); err != nil {
18 19
 		return swarm.Config{}, nil, err
... ...
@@ -33,7 +33,12 @@ func TestConfigInspectWithEmptyID(t *testing.T) {
33 33
 		}),
34 34
 	}
35 35
 	_, _, err := client.ConfigInspectWithRaw(context.Background(), "")
36
-	assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
36
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
37
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
38
+
39
+	_, _, err = client.ConfigInspectWithRaw(context.Background(), "    ")
40
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
41
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
37 42
 }
38 43
 
39 44
 func TestConfigInspectUnsupported(t *testing.T) {
... ...
@@ -4,6 +4,10 @@ import "context"
4 4
 
5 5
 // ConfigRemove removes a config.
6 6
 func (cli *Client) ConfigRemove(ctx context.Context, id string) error {
7
+	id, err := trimID("config", id)
8
+	if err != nil {
9
+		return err
10
+	}
7 11
 	if err := cli.NewVersionError(ctx, "1.30", "config remove"); err != nil {
8 12
 		return err
9 13
 	}
... ...
@@ -31,6 +31,14 @@ func TestConfigRemoveError(t *testing.T) {
31 31
 
32 32
 	err := client.ConfigRemove(context.Background(), "config_id")
33 33
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
34
+
35
+	err = client.ConfigRemove(context.Background(), "")
36
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
37
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
38
+
39
+	err = client.ConfigRemove(context.Background(), "    ")
40
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
41
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
34 42
 }
35 43
 
36 44
 func TestConfigRemove(t *testing.T) {
... ...
@@ -9,6 +9,10 @@ import (
9 9
 
10 10
 // ConfigUpdate attempts to update a config
11 11
 func (cli *Client) ConfigUpdate(ctx context.Context, id string, version swarm.Version, config swarm.ConfigSpec) error {
12
+	id, err := trimID("config", id)
13
+	if err != nil {
14
+		return err
15
+	}
12 16
 	if err := cli.NewVersionError(ctx, "1.30", "config update"); err != nil {
13 17
 		return err
14 18
 	}
... ...
@@ -32,6 +32,14 @@ func TestConfigUpdateError(t *testing.T) {
32 32
 
33 33
 	err := client.ConfigUpdate(context.Background(), "config_id", swarm.Version{}, swarm.ConfigSpec{})
34 34
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
35
+
36
+	err = client.ConfigUpdate(context.Background(), "", swarm.Version{}, swarm.ConfigSpec{})
37
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
38
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
39
+
40
+	err = client.ConfigUpdate(context.Background(), "    ", swarm.Version{}, swarm.ConfigSpec{})
41
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
42
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
35 43
 }
36 44
 
37 45
 func TestConfigUpdate(t *testing.T) {
... ...
@@ -33,7 +33,12 @@ import (
33 33
 //
34 34
 // You can use github.com/docker/docker/pkg/stdcopy.StdCopy to demultiplex this
35 35
 // stream.
36
-func (cli *Client) ContainerAttach(ctx context.Context, container string, options container.AttachOptions) (types.HijackedResponse, error) {
36
+func (cli *Client) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
37
+	containerID, err := trimID("container", containerID)
38
+	if err != nil {
39
+		return types.HijackedResponse{}, err
40
+	}
41
+
37 42
 	query := url.Values{}
38 43
 	if options.Stream {
39 44
 		query.Set("stream", "1")
... ...
@@ -54,7 +59,7 @@ func (cli *Client) ContainerAttach(ctx context.Context, container string, option
54 54
 		query.Set("logs", "1")
55 55
 	}
56 56
 
57
-	return cli.postHijacked(ctx, "/containers/"+container+"/attach", query, nil, http.Header{
57
+	return cli.postHijacked(ctx, "/containers/"+containerID+"/attach", query, nil, http.Header{
58 58
 		"Content-Type": {"text/plain"},
59 59
 	})
60 60
 }
... ...
@@ -12,7 +12,12 @@ import (
12 12
 )
13 13
 
14 14
 // ContainerCommit applies changes to a container and creates a new tagged image.
15
-func (cli *Client) ContainerCommit(ctx context.Context, container string, options container.CommitOptions) (types.IDResponse, error) {
15
+func (cli *Client) ContainerCommit(ctx context.Context, containerID string, options container.CommitOptions) (types.IDResponse, error) {
16
+	containerID, err := trimID("container", containerID)
17
+	if err != nil {
18
+		return types.IDResponse{}, err
19
+	}
20
+
16 21
 	var repository, tag string
17 22
 	if options.Reference != "" {
18 23
 		ref, err := reference.ParseNormalizedNamed(options.Reference)
... ...
@@ -32,7 +37,7 @@ func (cli *Client) ContainerCommit(ctx context.Context, container string, option
32 32
 	}
33 33
 
34 34
 	query := url.Values{}
35
-	query.Set("container", container)
35
+	query.Set("container", containerID)
36 36
 	query.Set("repo", repository)
37 37
 	query.Set("tag", tag)
38 38
 	query.Set("comment", options.Comment)
... ...
@@ -23,6 +23,14 @@ func TestContainerCommitError(t *testing.T) {
23 23
 	}
24 24
 	_, err := client.ContainerCommit(context.Background(), "nothing", container.CommitOptions{})
25 25
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
26
+
27
+	_, err = client.ContainerCommit(context.Background(), "", container.CommitOptions{})
28
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
29
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
30
+
31
+	_, err = client.ContainerCommit(context.Background(), "    ", container.CommitOptions{})
32
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
33
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
26 34
 }
27 35
 
28 36
 func TestContainerCommit(t *testing.T) {
... ...
@@ -16,11 +16,15 @@ import (
16 16
 
17 17
 // ContainerStatPath returns stat information about a path inside the container filesystem.
18 18
 func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path string) (container.PathStat, error) {
19
+	containerID, err := trimID("container", containerID)
20
+	if err != nil {
21
+		return container.PathStat{}, err
22
+	}
23
+
19 24
 	query := url.Values{}
20 25
 	query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
21 26
 
22
-	urlStr := "/containers/" + containerID + "/archive"
23
-	response, err := cli.head(ctx, urlStr, query, nil)
27
+	response, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil)
24 28
 	defer ensureReaderClosed(response)
25 29
 	if err != nil {
26 30
 		return container.PathStat{}, err
... ...
@@ -31,6 +35,11 @@ func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path stri
31 31
 // CopyToContainer copies content into the container filesystem.
32 32
 // Note that `content` must be a Reader for a TAR archive
33 33
 func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options container.CopyToContainerOptions) error {
34
+	containerID, err := trimID("container", containerID)
35
+	if err != nil {
36
+		return err
37
+	}
38
+
34 39
 	query := url.Values{}
35 40
 	query.Set("path", filepath.ToSlash(dstPath)) // Normalize the paths used in the API.
36 41
 	// Do not allow for an existing directory to be overwritten by a non-directory and vice versa.
... ...
@@ -42,9 +51,7 @@ func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath str
42 42
 		query.Set("copyUIDGID", "true")
43 43
 	}
44 44
 
45
-	apiPath := "/containers/" + containerID + "/archive"
46
-
47
-	response, err := cli.putRaw(ctx, apiPath, query, content, nil)
45
+	response, err := cli.putRaw(ctx, "/containers/"+containerID+"/archive", query, content, nil)
48 46
 	defer ensureReaderClosed(response)
49 47
 	if err != nil {
50 48
 		return err
... ...
@@ -56,11 +63,15 @@ func (cli *Client) CopyToContainer(ctx context.Context, containerID, dstPath str
56 56
 // CopyFromContainer gets the content from the container and returns it as a Reader
57 57
 // for a TAR archive to manipulate it in the host. It's up to the caller to close the reader.
58 58
 func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
59
+	containerID, err := trimID("container", containerID)
60
+	if err != nil {
61
+		return nil, container.PathStat{}, err
62
+	}
63
+
59 64
 	query := make(url.Values, 1)
60 65
 	query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
61 66
 
62
-	apiPath := "/containers/" + containerID + "/archive"
63
-	response, err := cli.get(ctx, apiPath, query, nil)
67
+	response, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil)
64 68
 	if err != nil {
65 69
 		return nil, container.PathStat{}, err
66 70
 	}
... ...
@@ -23,6 +23,14 @@ func TestContainerStatPathError(t *testing.T) {
23 23
 	}
24 24
 	_, err := client.ContainerStatPath(context.Background(), "container_id", "path")
25 25
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
26
+
27
+	_, err = client.ContainerStatPath(context.Background(), "", "path")
28
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
29
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
30
+
31
+	_, err = client.ContainerStatPath(context.Background(), "    ", "path")
32
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
33
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
26 34
 }
27 35
 
28 36
 func TestContainerStatPathNotFoundError(t *testing.T) {
... ...
@@ -99,6 +107,14 @@ func TestCopyToContainerError(t *testing.T) {
99 99
 	}
100 100
 	err := client.CopyToContainer(context.Background(), "container_id", "path/to/file", bytes.NewReader([]byte("")), container.CopyToContainerOptions{})
101 101
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
102
+
103
+	err = client.CopyToContainer(context.Background(), "", "path/to/file", bytes.NewReader([]byte("")), container.CopyToContainerOptions{})
104
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
105
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
106
+
107
+	err = client.CopyToContainer(context.Background(), "    ", "path/to/file", bytes.NewReader([]byte("")), container.CopyToContainerOptions{})
108
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
109
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
102 110
 }
103 111
 
104 112
 func TestCopyToContainerNotFoundError(t *testing.T) {
... ...
@@ -173,6 +189,14 @@ func TestCopyFromContainerError(t *testing.T) {
173 173
 	}
174 174
 	_, _, err := client.CopyFromContainer(context.Background(), "container_id", "path/to/file")
175 175
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
176
+
177
+	_, _, err = client.CopyFromContainer(context.Background(), "", "path/to/file")
178
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
179
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
180
+
181
+	_, _, err = client.CopyFromContainer(context.Background(), "    ", "path/to/file")
182
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
183
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
176 184
 }
177 185
 
178 186
 func TestCopyFromContainerNotFoundError(t *testing.T) {
... ...
@@ -10,14 +10,21 @@ import (
10 10
 
11 11
 // ContainerDiff shows differences in a container filesystem since it was started.
12 12
 func (cli *Client) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) {
13
-	var changes []container.FilesystemChange
13
+	containerID, err := trimID("container", containerID)
14
+	if err != nil {
15
+		return nil, err
16
+	}
14 17
 
15 18
 	serverResp, err := cli.get(ctx, "/containers/"+containerID+"/changes", url.Values{}, nil)
16 19
 	defer ensureReaderClosed(serverResp)
17 20
 	if err != nil {
18
-		return changes, err
21
+		return nil, err
19 22
 	}
20 23
 
24
+	var changes []container.FilesystemChange
21 25
 	err = json.NewDecoder(serverResp.body).Decode(&changes)
26
+	if err != nil {
27
+		return nil, err
28
+	}
22 29
 	return changes, err
23 30
 }
... ...
@@ -22,6 +22,14 @@ func TestContainerDiffError(t *testing.T) {
22 22
 	}
23 23
 	_, err := client.ContainerDiff(context.Background(), "nothing")
24 24
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
25
+
26
+	_, err = client.ContainerDiff(context.Background(), "")
27
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
28
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
29
+
30
+	_, err = client.ContainerDiff(context.Background(), "    ")
31
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
32
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
25 33
 }
26 34
 
27 35
 func TestContainerDiff(t *testing.T) {
... ...
@@ -11,8 +11,11 @@ import (
11 11
 )
12 12
 
13 13
 // ContainerExecCreate creates a new exec configuration to run an exec process.
14
-func (cli *Client) ContainerExecCreate(ctx context.Context, container string, options container.ExecOptions) (types.IDResponse, error) {
15
-	var response types.IDResponse
14
+func (cli *Client) ContainerExecCreate(ctx context.Context, containerID string, options container.ExecOptions) (types.IDResponse, error) {
15
+	containerID, err := trimID("container", containerID)
16
+	if err != nil {
17
+		return types.IDResponse{}, err
18
+	}
16 19
 
17 20
 	// Make sure we negotiated (if the client is configured to do so),
18 21
 	// as code below contains API-version specific handling of options.
... ...
@@ -20,21 +23,23 @@ func (cli *Client) ContainerExecCreate(ctx context.Context, container string, op
20 20
 	// Normally, version-negotiation (if enabled) would not happen until
21 21
 	// the API request is made.
22 22
 	if err := cli.checkVersion(ctx); err != nil {
23
-		return response, err
23
+		return types.IDResponse{}, err
24 24
 	}
25 25
 
26 26
 	if err := cli.NewVersionError(ctx, "1.25", "env"); len(options.Env) != 0 && err != nil {
27
-		return response, err
27
+		return types.IDResponse{}, err
28 28
 	}
29 29
 	if versions.LessThan(cli.ClientVersion(), "1.42") {
30 30
 		options.ConsoleSize = nil
31 31
 	}
32 32
 
33
-	resp, err := cli.post(ctx, "/containers/"+container+"/exec", nil, options, nil)
33
+	resp, err := cli.post(ctx, "/containers/"+containerID+"/exec", nil, options, nil)
34 34
 	defer ensureReaderClosed(resp)
35 35
 	if err != nil {
36
-		return response, err
36
+		return types.IDResponse{}, err
37 37
 	}
38
+
39
+	var response types.IDResponse
38 40
 	err = json.NewDecoder(resp.body).Decode(&response)
39 41
 	return response, err
40 42
 }
... ...
@@ -21,8 +21,17 @@ func TestContainerExecCreateError(t *testing.T) {
21 21
 	client := &Client{
22 22
 		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
23 23
 	}
24
+
24 25
 	_, err := client.ContainerExecCreate(context.Background(), "container_id", container.ExecOptions{})
25 26
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
27
+
28
+	_, err = client.ContainerExecCreate(context.Background(), "", container.ExecOptions{})
29
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
30
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
31
+
32
+	_, err = client.ContainerExecCreate(context.Background(), "    ", container.ExecOptions{})
33
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
34
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
26 35
 }
27 36
 
28 37
 // TestContainerExecCreateConnectionError verifies that connection errors occurring
... ...
@@ -33,7 +42,7 @@ func TestContainerExecCreateConnectionError(t *testing.T) {
33 33
 	client, err := NewClientWithOpts(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid"))
34 34
 	assert.NilError(t, err)
35 35
 
36
-	_, err = client.ContainerExecCreate(context.Background(), "", container.ExecOptions{})
36
+	_, err = client.ContainerExecCreate(context.Background(), "container_id", container.ExecOptions{})
37 37
 	assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
38 38
 }
39 39
 
... ...
@@ -10,6 +10,11 @@ import (
10 10
 // and returns them as an io.ReadCloser. It's up to the caller
11 11
 // to close the stream.
12 12
 func (cli *Client) ContainerExport(ctx context.Context, containerID string) (io.ReadCloser, error) {
13
+	containerID, err := trimID("container", containerID)
14
+	if err != nil {
15
+		return nil, err
16
+	}
17
+
13 18
 	serverResp, err := cli.get(ctx, "/containers/"+containerID+"/export", url.Values{}, nil)
14 19
 	if err != nil {
15 20
 		return nil, err
... ...
@@ -20,6 +20,14 @@ func TestContainerExportError(t *testing.T) {
20 20
 	}
21 21
 	_, err := client.ContainerExport(context.Background(), "nothing")
22 22
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
23
+
24
+	_, err = client.ContainerExport(context.Background(), "")
25
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
26
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
27
+
28
+	_, err = client.ContainerExport(context.Background(), "    ")
29
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
30
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
23 31
 }
24 32
 
25 33
 func TestContainerExport(t *testing.T) {
... ...
@@ -12,9 +12,11 @@ import (
12 12
 
13 13
 // ContainerInspect returns the container information.
14 14
 func (cli *Client) ContainerInspect(ctx context.Context, containerID string) (container.InspectResponse, error) {
15
-	if containerID == "" {
16
-		return container.InspectResponse{}, objectNotFoundError{object: "container", id: containerID}
15
+	containerID, err := trimID("container", containerID)
16
+	if err != nil {
17
+		return container.InspectResponse{}, err
17 18
 	}
19
+
18 20
 	serverResp, err := cli.get(ctx, "/containers/"+containerID+"/json", nil, nil)
19 21
 	defer ensureReaderClosed(serverResp)
20 22
 	if err != nil {
... ...
@@ -28,9 +30,11 @@ func (cli *Client) ContainerInspect(ctx context.Context, containerID string) (co
28 28
 
29 29
 // ContainerInspectWithRaw returns the container information and its raw representation.
30 30
 func (cli *Client) ContainerInspectWithRaw(ctx context.Context, containerID string, getSize bool) (container.InspectResponse, []byte, error) {
31
-	if containerID == "" {
32
-		return container.InspectResponse{}, nil, objectNotFoundError{object: "container", id: containerID}
31
+	containerID, err := trimID("container", containerID)
32
+	if err != nil {
33
+		return container.InspectResponse{}, nil, err
33 34
 	}
35
+
34 36
 	query := url.Values{}
35 37
 	if getSize {
36 38
 		query.Set("size", "1")
... ...
@@ -24,6 +24,14 @@ func TestContainerInspectError(t *testing.T) {
24 24
 
25 25
 	_, err := client.ContainerInspect(context.Background(), "nothing")
26 26
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
27
+
28
+	_, err = client.ContainerInspect(context.Background(), "")
29
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
30
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
31
+
32
+	_, err = client.ContainerInspect(context.Background(), "    ")
33
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
34
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
27 35
 }
28 36
 
29 37
 func TestContainerInspectContainerNotFound(t *testing.T) {
... ...
@@ -42,7 +50,12 @@ func TestContainerInspectWithEmptyID(t *testing.T) {
42 42
 		}),
43 43
 	}
44 44
 	_, _, err := client.ContainerInspectWithRaw(context.Background(), "", true)
45
-	assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
45
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
46
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
47
+
48
+	_, _, err = client.ContainerInspectWithRaw(context.Background(), "    ", true)
49
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
50
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
46 51
 }
47 52
 
48 53
 func TestContainerInspect(t *testing.T) {
... ...
@@ -7,6 +7,11 @@ import (
7 7
 
8 8
 // ContainerKill terminates the container process but does not remove the container from the docker host.
9 9
 func (cli *Client) ContainerKill(ctx context.Context, containerID, signal string) error {
10
+	containerID, err := trimID("container", containerID)
11
+	if err != nil {
12
+		return err
13
+	}
14
+
10 15
 	query := url.Values{}
11 16
 	if signal != "" {
12 17
 		query.Set("signal", signal)
... ...
@@ -20,6 +20,14 @@ func TestContainerKillError(t *testing.T) {
20 20
 	}
21 21
 	err := client.ContainerKill(context.Background(), "nothing", "SIGKILL")
22 22
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
23
+
24
+	err = client.ContainerKill(context.Background(), "", "")
25
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
26
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
27
+
28
+	err = client.ContainerKill(context.Background(), "    ", "")
29
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
30
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
23 31
 }
24 32
 
25 33
 func TestContainerKill(t *testing.T) {
... ...
@@ -33,7 +33,12 @@ import (
33 33
 //
34 34
 // You can use github.com/docker/docker/pkg/stdcopy.StdCopy to demultiplex this
35 35
 // stream.
36
-func (cli *Client) ContainerLogs(ctx context.Context, container string, options container.LogsOptions) (io.ReadCloser, error) {
36
+func (cli *Client) ContainerLogs(ctx context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
37
+	containerID, err := trimID("container", containerID)
38
+	if err != nil {
39
+		return nil, err
40
+	}
41
+
37 42
 	query := url.Values{}
38 43
 	if options.ShowStdout {
39 44
 		query.Set("stdout", "1")
... ...
@@ -72,7 +77,7 @@ func (cli *Client) ContainerLogs(ctx context.Context, container string, options
72 72
 	}
73 73
 	query.Set("tail", options.Tail)
74 74
 
75
-	resp, err := cli.get(ctx, "/containers/"+container+"/logs", query, nil)
75
+	resp, err := cli.get(ctx, "/containers/"+containerID+"/logs", query, nil)
76 76
 	if err != nil {
77 77
 		return nil, err
78 78
 	}
... ...
@@ -24,6 +24,14 @@ func TestContainerLogsNotFoundError(t *testing.T) {
24 24
 	}
25 25
 	_, err := client.ContainerLogs(context.Background(), "container_id", container.LogsOptions{})
26 26
 	assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
27
+
28
+	_, err = client.ContainerLogs(context.Background(), "", container.LogsOptions{})
29
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
30
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
31
+
32
+	_, err = client.ContainerLogs(context.Background(), "    ", container.LogsOptions{})
33
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
34
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
27 35
 }
28 36
 
29 37
 func TestContainerLogsError(t *testing.T) {
... ...
@@ -4,6 +4,11 @@ import "context"
4 4
 
5 5
 // ContainerPause pauses the main process of a given container without terminating it.
6 6
 func (cli *Client) ContainerPause(ctx context.Context, containerID string) error {
7
+	containerID, err := trimID("container", containerID)
8
+	if err != nil {
9
+		return err
10
+	}
11
+
7 12
 	resp, err := cli.post(ctx, "/containers/"+containerID+"/pause", nil, nil, nil)
8 13
 	ensureReaderClosed(resp)
9 14
 	return err
... ...
@@ -9,6 +9,11 @@ import (
9 9
 
10 10
 // ContainerRemove kills and removes a container from the docker host.
11 11
 func (cli *Client) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error {
12
+	containerID, err := trimID("container", containerID)
13
+	if err != nil {
14
+		return err
15
+	}
16
+
12 17
 	query := url.Values{}
13 18
 	if options.RemoveVolumes {
14 19
 		query.Set("v", "1")
... ...
@@ -21,6 +21,14 @@ func TestContainerRemoveError(t *testing.T) {
21 21
 	}
22 22
 	err := client.ContainerRemove(context.Background(), "container_id", container.RemoveOptions{})
23 23
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
24
+
25
+	err = client.ContainerRemove(context.Background(), "", container.RemoveOptions{})
26
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
27
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
28
+
29
+	err = client.ContainerRemove(context.Background(), "    ", container.RemoveOptions{})
30
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
31
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
24 32
 }
25 33
 
26 34
 func TestContainerRemoveNotFoundError(t *testing.T) {
... ...
@@ -7,6 +7,11 @@ import (
7 7
 
8 8
 // ContainerRename changes the name of a given container.
9 9
 func (cli *Client) ContainerRename(ctx context.Context, containerID, newContainerName string) error {
10
+	containerID, err := trimID("container", containerID)
11
+	if err != nil {
12
+		return err
13
+	}
14
+
10 15
 	query := url.Values{}
11 16
 	query.Set("name", newContainerName)
12 17
 	resp, err := cli.post(ctx, "/containers/"+containerID+"/rename", query, nil, nil)
... ...
@@ -20,6 +20,14 @@ func TestContainerRenameError(t *testing.T) {
20 20
 	}
21 21
 	err := client.ContainerRename(context.Background(), "nothing", "newNothing")
22 22
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
23
+
24
+	err = client.ContainerRename(context.Background(), "", "newNothing")
25
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
26
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
27
+
28
+	err = client.ContainerRename(context.Background(), "    ", "newNothing")
29
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
30
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
23 31
 }
24 32
 
25 33
 func TestContainerRename(t *testing.T) {
... ...
@@ -10,11 +10,19 @@ import (
10 10
 
11 11
 // ContainerResize changes the size of the tty for a container.
12 12
 func (cli *Client) ContainerResize(ctx context.Context, containerID string, options container.ResizeOptions) error {
13
+	containerID, err := trimID("container", containerID)
14
+	if err != nil {
15
+		return err
16
+	}
13 17
 	return cli.resize(ctx, "/containers/"+containerID, options.Height, options.Width)
14 18
 }
15 19
 
16 20
 // ContainerExecResize changes the size of the tty for an exec process running inside a container.
17 21
 func (cli *Client) ContainerExecResize(ctx context.Context, execID string, options container.ResizeOptions) error {
22
+	execID, err := trimID("exec", execID)
23
+	if err != nil {
24
+		return err
25
+	}
18 26
 	return cli.resize(ctx, "/exec/"+execID, options.Height, options.Width)
19 27
 }
20 28
 
... ...
@@ -20,6 +20,14 @@ func TestContainerResizeError(t *testing.T) {
20 20
 	}
21 21
 	err := client.ContainerResize(context.Background(), "container_id", container.ResizeOptions{})
22 22
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
23
+
24
+	err = client.ContainerResize(context.Background(), "", container.ResizeOptions{})
25
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
26
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
27
+
28
+	err = client.ContainerResize(context.Background(), "    ", container.ResizeOptions{})
29
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
30
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
23 31
 }
24 32
 
25 33
 func TestContainerExecResizeError(t *testing.T) {
... ...
@@ -13,6 +13,11 @@ import (
13 13
 // It makes the daemon wait for the container to be up again for
14 14
 // a specific amount of time, given the timeout.
15 15
 func (cli *Client) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error {
16
+	containerID, err := trimID("container", containerID)
17
+	if err != nil {
18
+		return err
19
+	}
20
+
16 21
 	query := url.Values{}
17 22
 	if options.Timeout != nil {
18 23
 		query.Set("t", strconv.Itoa(*options.Timeout))
... ...
@@ -21,6 +21,14 @@ func TestContainerRestartError(t *testing.T) {
21 21
 	}
22 22
 	err := client.ContainerRestart(context.Background(), "nothing", container.StopOptions{})
23 23
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
24
+
25
+	err = client.ContainerRestart(context.Background(), "", container.StopOptions{})
26
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
27
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
28
+
29
+	err = client.ContainerRestart(context.Background(), "    ", container.StopOptions{})
30
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
31
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
24 32
 }
25 33
 
26 34
 // TestContainerRestartConnectionError verifies that connection errors occurring
... ...
@@ -9,6 +9,11 @@ import (
9 9
 
10 10
 // ContainerStart sends a request to the docker daemon to start a container.
11 11
 func (cli *Client) ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error {
12
+	containerID, err := trimID("container", containerID)
13
+	if err != nil {
14
+		return err
15
+	}
16
+
12 17
 	query := url.Values{}
13 18
 	if len(options.CheckpointID) != 0 {
14 19
 		query.Set("checkpoint", options.CheckpointID)
... ...
@@ -22,6 +22,14 @@ func TestContainerStartError(t *testing.T) {
22 22
 	}
23 23
 	err := client.ContainerStart(context.Background(), "nothing", container.StartOptions{})
24 24
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
25
+
26
+	err = client.ContainerStart(context.Background(), "", container.StartOptions{})
27
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
28
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
29
+
30
+	err = client.ContainerStart(context.Background(), "    ", container.StartOptions{})
31
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
32
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
25 33
 }
26 34
 
27 35
 func TestContainerStart(t *testing.T) {
... ...
@@ -10,6 +10,11 @@ import (
10 10
 // ContainerStats returns near realtime stats for a given container.
11 11
 // It's up to the caller to close the io.ReadCloser returned.
12 12
 func (cli *Client) ContainerStats(ctx context.Context, containerID string, stream bool) (container.StatsResponseReader, error) {
13
+	containerID, err := trimID("container", containerID)
14
+	if err != nil {
15
+		return container.StatsResponseReader{}, err
16
+	}
17
+
13 18
 	query := url.Values{}
14 19
 	query.Set("stream", "0")
15 20
 	if stream {
... ...
@@ -30,6 +35,11 @@ func (cli *Client) ContainerStats(ctx context.Context, containerID string, strea
30 30
 // ContainerStatsOneShot gets a single stat entry from a container.
31 31
 // It differs from `ContainerStats` in that the API should not wait to prime the stats
32 32
 func (cli *Client) ContainerStatsOneShot(ctx context.Context, containerID string) (container.StatsResponseReader, error) {
33
+	containerID, err := trimID("container", containerID)
34
+	if err != nil {
35
+		return container.StatsResponseReader{}, err
36
+	}
37
+
33 38
 	query := url.Values{}
34 39
 	query.Set("stream", "0")
35 40
 	query.Set("one-shot", "1")
... ...
@@ -20,6 +20,14 @@ func TestContainerStatsError(t *testing.T) {
20 20
 	}
21 21
 	_, err := client.ContainerStats(context.Background(), "nothing", false)
22 22
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
23
+
24
+	_, err = client.ContainerStats(context.Background(), "", false)
25
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
26
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
27
+
28
+	_, err = client.ContainerStats(context.Background(), "    ", false)
29
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
30
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
23 31
 }
24 32
 
25 33
 func TestContainerStats(t *testing.T) {
... ...
@@ -17,6 +17,11 @@ import (
17 17
 // otherwise the engine default. A negative timeout value can be specified,
18 18
 // meaning no timeout, i.e. no forceful termination is performed.
19 19
 func (cli *Client) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error {
20
+	containerID, err := trimID("container", containerID)
21
+	if err != nil {
22
+		return err
23
+	}
24
+
20 25
 	query := url.Values{}
21 26
 	if options.Timeout != nil {
22 27
 		query.Set("t", strconv.Itoa(*options.Timeout))
... ...
@@ -19,8 +19,16 @@ func TestContainerStopError(t *testing.T) {
19 19
 	client := &Client{
20 20
 		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
21 21
 	}
22
-	err := client.ContainerStop(context.Background(), "nothing", container.StopOptions{})
22
+	err := client.ContainerStop(context.Background(), "container_id", container.StopOptions{})
23 23
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
24
+
25
+	err = client.ContainerStop(context.Background(), "", container.StopOptions{})
26
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
27
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
28
+
29
+	err = client.ContainerStop(context.Background(), "    ", container.StopOptions{})
30
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
31
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
24 32
 }
25 33
 
26 34
 // TestContainerStopConnectionError verifies that connection errors occurring
... ...
@@ -31,7 +39,7 @@ func TestContainerStopConnectionError(t *testing.T) {
31 31
 	client, err := NewClientWithOpts(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid"))
32 32
 	assert.NilError(t, err)
33 33
 
34
-	err = client.ContainerStop(context.Background(), "nothing", container.StopOptions{})
34
+	err = client.ContainerStop(context.Background(), "container_id", container.StopOptions{})
35 35
 	assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
36 36
 }
37 37
 
... ...
@@ -40,7 +48,7 @@ func TestContainerStop(t *testing.T) {
40 40
 	client := &Client{
41 41
 		client: newMockClient(func(req *http.Request) (*http.Response, error) {
42 42
 			if !strings.HasPrefix(req.URL.Path, expectedURL) {
43
-				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
43
+				return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
44 44
 			}
45 45
 			s := req.URL.Query().Get("signal")
46 46
 			if s != "SIGKILL" {
... ...
@@ -11,7 +11,11 @@ import (
11 11
 
12 12
 // ContainerTop shows process information from within a container.
13 13
 func (cli *Client) ContainerTop(ctx context.Context, containerID string, arguments []string) (container.ContainerTopOKBody, error) {
14
-	var response container.ContainerTopOKBody
14
+	containerID, err := trimID("container", containerID)
15
+	if err != nil {
16
+		return container.ContainerTopOKBody{}, err
17
+	}
18
+
15 19
 	query := url.Values{}
16 20
 	if len(arguments) > 0 {
17 21
 		query.Set("ps_args", strings.Join(arguments, " "))
... ...
@@ -20,9 +24,10 @@ func (cli *Client) ContainerTop(ctx context.Context, containerID string, argumen
20 20
 	resp, err := cli.get(ctx, "/containers/"+containerID+"/top", query, nil)
21 21
 	defer ensureReaderClosed(resp)
22 22
 	if err != nil {
23
-		return response, err
23
+		return container.ContainerTopOKBody{}, err
24 24
 	}
25 25
 
26
+	var response container.ContainerTopOKBody
26 27
 	err = json.NewDecoder(resp.body).Decode(&response)
27 28
 	return response, err
28 29
 }
... ...
@@ -23,6 +23,14 @@ func TestContainerTopError(t *testing.T) {
23 23
 	}
24 24
 	_, err := client.ContainerTop(context.Background(), "nothing", []string{})
25 25
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
26
+
27
+	_, err = client.ContainerTop(context.Background(), "", []string{})
28
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
29
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
30
+
31
+	_, err = client.ContainerTop(context.Background(), "    ", []string{})
32
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
33
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
26 34
 }
27 35
 
28 36
 func TestContainerTop(t *testing.T) {
... ...
@@ -4,6 +4,11 @@ import "context"
4 4
 
5 5
 // ContainerUnpause resumes the process execution within a container
6 6
 func (cli *Client) ContainerUnpause(ctx context.Context, containerID string) error {
7
+	containerID, err := trimID("container", containerID)
8
+	if err != nil {
9
+		return err
10
+	}
11
+
7 12
 	resp, err := cli.post(ctx, "/containers/"+containerID+"/unpause", nil, nil, nil)
8 13
 	ensureReaderClosed(resp)
9 14
 	return err
... ...
@@ -20,6 +20,14 @@ func TestContainerUnpauseError(t *testing.T) {
20 20
 	}
21 21
 	err := client.ContainerUnpause(context.Background(), "nothing")
22 22
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
23
+
24
+	err = client.ContainerUnpause(context.Background(), "")
25
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
26
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
27
+
28
+	err = client.ContainerUnpause(context.Background(), "    ")
29
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
30
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
23 31
 }
24 32
 
25 33
 func TestContainerUnpause(t *testing.T) {
... ...
@@ -9,13 +9,18 @@ import (
9 9
 
10 10
 // ContainerUpdate updates the resources of a container.
11 11
 func (cli *Client) ContainerUpdate(ctx context.Context, containerID string, updateConfig container.UpdateConfig) (container.ContainerUpdateOKBody, error) {
12
-	var response container.ContainerUpdateOKBody
12
+	containerID, err := trimID("container", containerID)
13
+	if err != nil {
14
+		return container.ContainerUpdateOKBody{}, err
15
+	}
16
+
13 17
 	serverResp, err := cli.post(ctx, "/containers/"+containerID+"/update", nil, updateConfig, nil)
14 18
 	defer ensureReaderClosed(serverResp)
15 19
 	if err != nil {
16
-		return response, err
20
+		return container.ContainerUpdateOKBody{}, err
17 21
 	}
18 22
 
23
+	var response container.ContainerUpdateOKBody
19 24
 	err = json.NewDecoder(serverResp.body).Decode(&response)
20 25
 	return response, err
21 26
 }
... ...
@@ -22,6 +22,14 @@ func TestContainerUpdateError(t *testing.T) {
22 22
 	}
23 23
 	_, err := client.ContainerUpdate(context.Background(), "nothing", container.UpdateConfig{})
24 24
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
25
+
26
+	_, err = client.ContainerUpdate(context.Background(), "", container.UpdateConfig{})
27
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
28
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
29
+
30
+	_, err = client.ContainerUpdate(context.Background(), "    ", container.UpdateConfig{})
31
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
32
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
25 33
 }
26 34
 
27 35
 func TestContainerUpdate(t *testing.T) {
... ...
@@ -33,6 +33,12 @@ func (cli *Client) ContainerWait(ctx context.Context, containerID string, condit
33 33
 	resultC := make(chan container.WaitResponse)
34 34
 	errC := make(chan error, 1)
35 35
 
36
+	containerID, err := trimID("container", containerID)
37
+	if err != nil {
38
+		errC <- err
39
+		return resultC, errC
40
+	}
41
+
36 42
 	// Make sure we negotiated (if the client is configured to do so),
37 43
 	// as code below contains API-version specific handling of options.
38 44
 	//
... ...
@@ -8,6 +8,16 @@ import (
8 8
 
9 9
 // NetworkConnect connects a container to an existent network in the docker host.
10 10
 func (cli *Client) NetworkConnect(ctx context.Context, networkID, containerID string, config *network.EndpointSettings) error {
11
+	networkID, err := trimID("network", networkID)
12
+	if err != nil {
13
+		return err
14
+	}
15
+
16
+	containerID, err = trimID("container", containerID)
17
+	if err != nil {
18
+		return err
19
+	}
20
+
11 21
 	nc := network.ConnectOptions{
12 22
 		Container:      containerID,
13 23
 		EndpointConfig: config,
... ...
@@ -23,6 +23,15 @@ func TestNetworkConnectError(t *testing.T) {
23 23
 
24 24
 	err := client.NetworkConnect(context.Background(), "network_id", "container_id", nil)
25 25
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
26
+
27
+	// Empty network ID or container ID
28
+	err = client.NetworkConnect(context.Background(), "", "container_id", nil)
29
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
30
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
31
+
32
+	err = client.NetworkConnect(context.Background(), "network_id", "", nil)
33
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
34
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
26 35
 }
27 36
 
28 37
 func TestNetworkConnectEmptyNilEndpointSettings(t *testing.T) {
... ...
@@ -8,6 +8,16 @@ import (
8 8
 
9 9
 // NetworkDisconnect disconnects a container from an existent network in the docker host.
10 10
 func (cli *Client) NetworkDisconnect(ctx context.Context, networkID, containerID string, force bool) error {
11
+	networkID, err := trimID("network", networkID)
12
+	if err != nil {
13
+		return err
14
+	}
15
+
16
+	containerID, err = trimID("container", containerID)
17
+	if err != nil {
18
+		return err
19
+	}
20
+
11 21
 	nd := network.DisconnectOptions{
12 22
 		Container: containerID,
13 23
 		Force:     force,
... ...
@@ -23,6 +23,15 @@ func TestNetworkDisconnectError(t *testing.T) {
23 23
 
24 24
 	err := client.NetworkDisconnect(context.Background(), "network_id", "container_id", false)
25 25
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
26
+
27
+	// Empty network ID or container ID
28
+	err = client.NetworkDisconnect(context.Background(), "", "container_id", false)
29
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
30
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
31
+
32
+	err = client.NetworkDisconnect(context.Background(), "network_id", "", false)
33
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
34
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
26 35
 }
27 36
 
28 37
 func TestNetworkDisconnect(t *testing.T) {
... ...
@@ -18,8 +18,9 @@ func (cli *Client) NetworkInspect(ctx context.Context, networkID string, options
18 18
 
19 19
 // NetworkInspectWithRaw returns the information for a specific network configured in the docker host and its raw representation.
20 20
 func (cli *Client) NetworkInspectWithRaw(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, []byte, error) {
21
-	if networkID == "" {
22
-		return network.Inspect{}, nil, objectNotFoundError{object: "network", id: networkID}
21
+	networkID, err := trimID("network", networkID)
22
+	if err != nil {
23
+		return network.Inspect{}, nil, err
23 24
 	}
24 25
 	query := url.Values{}
25 26
 	if options.Verbose {
... ...
@@ -69,7 +69,12 @@ func TestNetworkInspect(t *testing.T) {
69 69
 	t.Run("empty ID", func(t *testing.T) {
70 70
 		// verify that the client does not create a request if the network-ID/name is empty.
71 71
 		_, err := client.NetworkInspect(context.Background(), "", network.InspectOptions{})
72
-		assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
72
+		assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
73
+		assert.Check(t, is.ErrorContains(err, "value is empty"))
74
+
75
+		_, err = client.NetworkInspect(context.Background(), "    ", network.InspectOptions{})
76
+		assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
77
+		assert.Check(t, is.ErrorContains(err, "value is empty"))
73 78
 	})
74 79
 	t.Run("no options", func(t *testing.T) {
75 80
 		r, err := client.NetworkInspect(context.Background(), "network_id", network.InspectOptions{})
... ...
@@ -4,6 +4,10 @@ import "context"
4 4
 
5 5
 // NetworkRemove removes an existent network from the docker host.
6 6
 func (cli *Client) NetworkRemove(ctx context.Context, networkID string) error {
7
+	networkID, err := trimID("network", networkID)
8
+	if err != nil {
9
+		return err
10
+	}
7 11
 	resp, err := cli.delete(ctx, "/networks/"+networkID, nil, nil)
8 12
 	defer ensureReaderClosed(resp)
9 13
 	return err
... ...
@@ -21,6 +21,14 @@ func TestNetworkRemoveError(t *testing.T) {
21 21
 
22 22
 	err := client.NetworkRemove(context.Background(), "network_id")
23 23
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
24
+
25
+	err = client.NetworkRemove(context.Background(), "")
26
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
27
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
28
+
29
+	err = client.NetworkRemove(context.Background(), "    ")
30
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
31
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
24 32
 }
25 33
 
26 34
 func TestNetworkRemove(t *testing.T) {
... ...
@@ -11,8 +11,9 @@ import (
11 11
 
12 12
 // NodeInspectWithRaw returns the node information.
13 13
 func (cli *Client) NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm.Node, []byte, error) {
14
-	if nodeID == "" {
15
-		return swarm.Node{}, nil, objectNotFoundError{object: "node", id: nodeID}
14
+	nodeID, err := trimID("node", nodeID)
15
+	if err != nil {
16
+		return swarm.Node{}, nil, err
16 17
 	}
17 18
 	serverResp, err := cli.get(ctx, "/nodes/"+nodeID, nil, nil)
18 19
 	defer ensureReaderClosed(serverResp)
... ...
@@ -42,7 +42,12 @@ func TestNodeInspectWithEmptyID(t *testing.T) {
42 42
 		}),
43 43
 	}
44 44
 	_, _, err := client.NodeInspectWithRaw(context.Background(), "")
45
-	assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
45
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
46
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
47
+
48
+	_, _, err = client.NodeInspectWithRaw(context.Background(), "    ")
49
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
50
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
46 51
 }
47 52
 
48 53
 func TestNodeInspect(t *testing.T) {
... ...
@@ -9,6 +9,11 @@ import (
9 9
 
10 10
 // NodeRemove removes a Node.
11 11
 func (cli *Client) NodeRemove(ctx context.Context, nodeID string, options types.NodeRemoveOptions) error {
12
+	nodeID, err := trimID("node", nodeID)
13
+	if err != nil {
14
+		return err
15
+	}
16
+
12 17
 	query := url.Values{}
13 18
 	if options.Force {
14 19
 		query.Set("force", "1")
... ...
@@ -22,6 +22,14 @@ func TestNodeRemoveError(t *testing.T) {
22 22
 
23 23
 	err := client.NodeRemove(context.Background(), "node_id", types.NodeRemoveOptions{Force: false})
24 24
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
25
+
26
+	err = client.NodeRemove(context.Background(), "", types.NodeRemoveOptions{Force: false})
27
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
28
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
29
+
30
+	err = client.NodeRemove(context.Background(), "    ", types.NodeRemoveOptions{Force: false})
31
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
32
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
25 33
 }
26 34
 
27 35
 func TestNodeRemove(t *testing.T) {
... ...
@@ -9,6 +9,11 @@ import (
9 9
 
10 10
 // NodeUpdate updates a Node.
11 11
 func (cli *Client) NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error {
12
+	nodeID, err := trimID("node", nodeID)
13
+	if err != nil {
14
+		return err
15
+	}
16
+
12 17
 	query := url.Values{}
13 18
 	query.Set("version", version.String())
14 19
 	resp, err := cli.post(ctx, "/nodes/"+nodeID+"/update", query, node, nil)
... ...
@@ -22,6 +22,14 @@ func TestNodeUpdateError(t *testing.T) {
22 22
 
23 23
 	err := client.NodeUpdate(context.Background(), "node_id", swarm.Version{}, swarm.NodeSpec{})
24 24
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
25
+
26
+	err = client.NodeUpdate(context.Background(), "", swarm.Version{}, swarm.NodeSpec{})
27
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
28
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
29
+
30
+	err = client.NodeUpdate(context.Background(), "    ", swarm.Version{}, swarm.NodeSpec{})
31
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
32
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
25 33
 }
26 34
 
27 35
 func TestNodeUpdate(t *testing.T) {
... ...
@@ -9,6 +9,10 @@ import (
9 9
 
10 10
 // PluginDisable disables a plugin
11 11
 func (cli *Client) PluginDisable(ctx context.Context, name string, options types.PluginDisableOptions) error {
12
+	name, err := trimID("plugin", name)
13
+	if err != nil {
14
+		return err
15
+	}
12 16
 	query := url.Values{}
13 17
 	if options.Force {
14 18
 		query.Set("force", "1")
... ...
@@ -22,6 +22,14 @@ func TestPluginDisableError(t *testing.T) {
22 22
 
23 23
 	err := client.PluginDisable(context.Background(), "plugin_name", types.PluginDisableOptions{})
24 24
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
25
+
26
+	err = client.PluginDisable(context.Background(), "", types.PluginDisableOptions{})
27
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
28
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
29
+
30
+	err = client.PluginDisable(context.Background(), "    ", types.PluginDisableOptions{})
31
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
32
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
25 33
 }
26 34
 
27 35
 func TestPluginDisable(t *testing.T) {
... ...
@@ -10,6 +10,10 @@ import (
10 10
 
11 11
 // PluginEnable enables a plugin
12 12
 func (cli *Client) PluginEnable(ctx context.Context, name string, options types.PluginEnableOptions) error {
13
+	name, err := trimID("plugin", name)
14
+	if err != nil {
15
+		return err
16
+	}
13 17
 	query := url.Values{}
14 18
 	query.Set("timeout", strconv.Itoa(options.Timeout))
15 19
 
... ...
@@ -22,6 +22,14 @@ func TestPluginEnableError(t *testing.T) {
22 22
 
23 23
 	err := client.PluginEnable(context.Background(), "plugin_name", types.PluginEnableOptions{})
24 24
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
25
+
26
+	err = client.PluginEnable(context.Background(), "", types.PluginEnableOptions{})
27
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
28
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
29
+
30
+	err = client.PluginEnable(context.Background(), "    ", types.PluginEnableOptions{})
31
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
32
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
25 33
 }
26 34
 
27 35
 func TestPluginEnable(t *testing.T) {
... ...
@@ -11,8 +11,9 @@ import (
11 11
 
12 12
 // PluginInspectWithRaw inspects an existing plugin
13 13
 func (cli *Client) PluginInspectWithRaw(ctx context.Context, name string) (*types.Plugin, []byte, error) {
14
-	if name == "" {
15
-		return nil, nil, objectNotFoundError{object: "plugin", id: name}
14
+	name, err := trimID("plugin", name)
15
+	if err != nil {
16
+		return nil, nil, err
16 17
 	}
17 18
 	resp, err := cli.get(ctx, "/plugins/"+name+"/json", nil, nil)
18 19
 	defer ensureReaderClosed(resp)
... ...
@@ -33,7 +33,12 @@ func TestPluginInspectWithEmptyID(t *testing.T) {
33 33
 		}),
34 34
 	}
35 35
 	_, _, err := client.PluginInspectWithRaw(context.Background(), "")
36
-	assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
36
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
37
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
38
+
39
+	_, _, err = client.PluginInspectWithRaw(context.Background(), "    ")
40
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
41
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
37 42
 }
38 43
 
39 44
 func TestPluginInspect(t *testing.T) {
... ...
@@ -10,6 +10,10 @@ import (
10 10
 
11 11
 // PluginPush pushes a plugin to a registry
12 12
 func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth string) (io.ReadCloser, error) {
13
+	name, err := trimID("plugin", name)
14
+	if err != nil {
15
+		return nil, err
16
+	}
13 17
 	resp, err := cli.post(ctx, "/plugins/"+name+"/push", nil, nil, http.Header{
14 18
 		registry.AuthHeader: {registryAuth},
15 19
 	})
... ...
@@ -22,6 +22,14 @@ func TestPluginPushError(t *testing.T) {
22 22
 
23 23
 	_, err := client.PluginPush(context.Background(), "plugin_name", "")
24 24
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
25
+
26
+	_, err = client.PluginPush(context.Background(), "", "")
27
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
28
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
29
+
30
+	_, err = client.PluginPush(context.Background(), "    ", "")
31
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
32
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
25 33
 }
26 34
 
27 35
 func TestPluginPush(t *testing.T) {
... ...
@@ -9,6 +9,11 @@ import (
9 9
 
10 10
 // PluginRemove removes a plugin
11 11
 func (cli *Client) PluginRemove(ctx context.Context, name string, options types.PluginRemoveOptions) error {
12
+	name, err := trimID("plugin", name)
13
+	if err != nil {
14
+		return err
15
+	}
16
+
12 17
 	query := url.Values{}
13 18
 	if options.Force {
14 19
 		query.Set("force", "1")
... ...
@@ -22,6 +22,14 @@ func TestPluginRemoveError(t *testing.T) {
22 22
 
23 23
 	err := client.PluginRemove(context.Background(), "plugin_name", types.PluginRemoveOptions{})
24 24
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
25
+
26
+	err = client.PluginRemove(context.Background(), "", types.PluginRemoveOptions{})
27
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
28
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
29
+
30
+	err = client.PluginRemove(context.Background(), "   ", types.PluginRemoveOptions{})
31
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
32
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
25 33
 }
26 34
 
27 35
 func TestPluginRemove(t *testing.T) {
... ...
@@ -6,6 +6,11 @@ import (
6 6
 
7 7
 // PluginSet modifies settings for an existing plugin
8 8
 func (cli *Client) PluginSet(ctx context.Context, name string, args []string) error {
9
+	name, err := trimID("plugin", name)
10
+	if err != nil {
11
+		return err
12
+	}
13
+
9 14
 	resp, err := cli.post(ctx, "/plugins/"+name+"/set", nil, args, nil)
10 15
 	ensureReaderClosed(resp)
11 16
 	return err
... ...
@@ -21,6 +21,14 @@ func TestPluginSetError(t *testing.T) {
21 21
 
22 22
 	err := client.PluginSet(context.Background(), "plugin_name", []string{})
23 23
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
24
+
25
+	err = client.PluginSet(context.Background(), "", []string{})
26
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
27
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
28
+
29
+	err = client.PluginSet(context.Background(), "    ", []string{})
30
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
31
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
24 32
 }
25 33
 
26 34
 func TestPluginSet(t *testing.T) {
... ...
@@ -13,7 +13,12 @@ import (
13 13
 )
14 14
 
15 15
 // PluginUpgrade upgrades a plugin
16
-func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (rc io.ReadCloser, err error) {
16
+func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types.PluginInstallOptions) (io.ReadCloser, error) {
17
+	name, err := trimID("plugin", name)
18
+	if err != nil {
19
+		return nil, err
20
+	}
21
+
17 22
 	if err := cli.NewVersionError(ctx, "1.26", "plugin upgrade"); err != nil {
18 23
 		return nil, err
19 24
 	}
... ...
@@ -11,11 +11,12 @@ import (
11 11
 
12 12
 // SecretInspectWithRaw returns the secret information with raw data
13 13
 func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) {
14
-	if err := cli.NewVersionError(ctx, "1.25", "secret inspect"); err != nil {
14
+	id, err := trimID("secret", id)
15
+	if err != nil {
15 16
 		return swarm.Secret{}, nil, err
16 17
 	}
17
-	if id == "" {
18
-		return swarm.Secret{}, nil, objectNotFoundError{object: "secret", id: id}
18
+	if err := cli.NewVersionError(ctx, "1.25", "secret inspect"); err != nil {
19
+		return swarm.Secret{}, nil, err
19 20
 	}
20 21
 	resp, err := cli.get(ctx, "/secrets/"+id, nil, nil)
21 22
 	defer ensureReaderClosed(resp)
... ...
@@ -53,7 +53,12 @@ func TestSecretInspectWithEmptyID(t *testing.T) {
53 53
 		}),
54 54
 	}
55 55
 	_, _, err := client.SecretInspectWithRaw(context.Background(), "")
56
-	assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
56
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
57
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
58
+
59
+	_, _, err = client.SecretInspectWithRaw(context.Background(), "    ")
60
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
61
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
57 62
 }
58 63
 
59 64
 func TestSecretInspect(t *testing.T) {
... ...
@@ -4,6 +4,10 @@ import "context"
4 4
 
5 5
 // SecretRemove removes a secret.
6 6
 func (cli *Client) SecretRemove(ctx context.Context, id string) error {
7
+	id, err := trimID("secret", id)
8
+	if err != nil {
9
+		return err
10
+	}
7 11
 	if err := cli.NewVersionError(ctx, "1.25", "secret remove"); err != nil {
8 12
 		return err
9 13
 	}
... ...
@@ -31,6 +31,14 @@ func TestSecretRemoveError(t *testing.T) {
31 31
 
32 32
 	err := client.SecretRemove(context.Background(), "secret_id")
33 33
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
34
+
35
+	err = client.SecretRemove(context.Background(), "")
36
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
37
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
38
+
39
+	err = client.SecretRemove(context.Background(), "   ")
40
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
41
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
34 42
 }
35 43
 
36 44
 func TestSecretRemove(t *testing.T) {
... ...
@@ -9,6 +9,10 @@ import (
9 9
 
10 10
 // SecretUpdate attempts to update a secret.
11 11
 func (cli *Client) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error {
12
+	id, err := trimID("secret", id)
13
+	if err != nil {
14
+		return err
15
+	}
12 16
 	if err := cli.NewVersionError(ctx, "1.25", "secret update"); err != nil {
13 17
 		return err
14 18
 	}
... ...
@@ -32,6 +32,14 @@ func TestSecretUpdateError(t *testing.T) {
32 32
 
33 33
 	err := client.SecretUpdate(context.Background(), "secret_id", swarm.Version{}, swarm.SecretSpec{})
34 34
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
35
+
36
+	err = client.SecretUpdate(context.Background(), "", swarm.Version{}, swarm.SecretSpec{})
37
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
38
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
39
+
40
+	err = client.SecretUpdate(context.Background(), "    ", swarm.Version{}, swarm.SecretSpec{})
41
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
42
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
35 43
 }
36 44
 
37 45
 func TestSecretUpdate(t *testing.T) {
... ...
@@ -41,7 +49,7 @@ func TestSecretUpdate(t *testing.T) {
41 41
 		version: "1.25",
42 42
 		client: newMockClient(func(req *http.Request) (*http.Response, error) {
43 43
 			if !strings.HasPrefix(req.URL.Path, expectedURL) {
44
-				return nil, fmt.Errorf("Expected URL '%s', got '%s'", expectedURL, req.URL)
44
+				return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedURL, req.URL)
45 45
 			}
46 46
 			if req.Method != http.MethodPost {
47 47
 				return nil, fmt.Errorf("expected POST method, got %s", req.Method)
... ...
@@ -14,9 +14,11 @@ import (
14 14
 
15 15
 // ServiceInspectWithRaw returns the service information and the raw data.
16 16
 func (cli *Client) ServiceInspectWithRaw(ctx context.Context, serviceID string, opts types.ServiceInspectOptions) (swarm.Service, []byte, error) {
17
-	if serviceID == "" {
18
-		return swarm.Service{}, nil, objectNotFoundError{object: "service", id: serviceID}
17
+	serviceID, err := trimID("service", serviceID)
18
+	if err != nil {
19
+		return swarm.Service{}, nil, err
19 20
 	}
21
+
20 22
 	query := url.Values{}
21 23
 	query.Set("insertDefaults", fmt.Sprintf("%v", opts.InsertDefaults))
22 24
 	serverResp, err := cli.get(ctx, "/services/"+serviceID, query, nil)
... ...
@@ -43,7 +43,12 @@ func TestServiceInspectWithEmptyID(t *testing.T) {
43 43
 		}),
44 44
 	}
45 45
 	_, _, err := client.ServiceInspectWithRaw(context.Background(), "", types.ServiceInspectOptions{})
46
-	assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
46
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
47
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
48
+
49
+	_, _, err = client.ServiceInspectWithRaw(context.Background(), "    ", types.ServiceInspectOptions{})
50
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
51
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
47 52
 }
48 53
 
49 54
 func TestServiceInspect(t *testing.T) {
... ...
@@ -14,6 +14,11 @@ import (
14 14
 // ServiceLogs returns the logs generated by a service in an io.ReadCloser.
15 15
 // It's up to the caller to close the stream.
16 16
 func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options container.LogsOptions) (io.ReadCloser, error) {
17
+	serviceID, err := trimID("service", serviceID)
18
+	if err != nil {
19
+		return nil, err
20
+	}
21
+
17 22
 	query := url.Values{}
18 23
 	if options.ShowStdout {
19 24
 		query.Set("stdout", "1")
... ...
@@ -29,6 +29,14 @@ func TestServiceLogsError(t *testing.T) {
29 29
 		Since: "2006-01-02TZ",
30 30
 	})
31 31
 	assert.Check(t, is.ErrorContains(err, `parsing time "2006-01-02TZ"`))
32
+
33
+	_, err = client.ServiceLogs(context.Background(), "", container.LogsOptions{})
34
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
35
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
36
+
37
+	_, err = client.ServiceLogs(context.Background(), "    ", container.LogsOptions{})
38
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
39
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
32 40
 }
33 41
 
34 42
 func TestServiceLogs(t *testing.T) {
... ...
@@ -4,6 +4,11 @@ import "context"
4 4
 
5 5
 // ServiceRemove kills and removes a service.
6 6
 func (cli *Client) ServiceRemove(ctx context.Context, serviceID string) error {
7
+	serviceID, err := trimID("service", serviceID)
8
+	if err != nil {
9
+		return err
10
+	}
11
+
7 12
 	resp, err := cli.delete(ctx, "/services/"+serviceID, nil, nil)
8 13
 	defer ensureReaderClosed(resp)
9 14
 	return err
... ...
@@ -21,6 +21,14 @@ func TestServiceRemoveError(t *testing.T) {
21 21
 
22 22
 	err := client.ServiceRemove(context.Background(), "service_id")
23 23
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
24
+
25
+	err = client.ServiceRemove(context.Background(), "")
26
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
27
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
28
+
29
+	err = client.ServiceRemove(context.Background(), "    ")
30
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
31
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
24 32
 }
25 33
 
26 34
 func TestServiceRemoveNotFoundError(t *testing.T) {
... ...
@@ -16,7 +16,10 @@ import (
16 16
 // It should be the value as set *before* the update. You can find this value in the Meta field
17 17
 // of swarm.Service, which can be found using ServiceInspectWithRaw.
18 18
 func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version swarm.Version, service swarm.ServiceSpec, options types.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {
19
-	response := swarm.ServiceUpdateResponse{}
19
+	serviceID, err := trimID("service", serviceID)
20
+	if err != nil {
21
+		return swarm.ServiceUpdateResponse{}, err
22
+	}
20 23
 
21 24
 	// Make sure we negotiated (if the client is configured to do so),
22 25
 	// as code below contains API-version specific handling of options.
... ...
@@ -24,7 +27,7 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
24 24
 	// Normally, version-negotiation (if enabled) would not happen until
25 25
 	// the API request is made.
26 26
 	if err := cli.checkVersion(ctx); err != nil {
27
-		return response, err
27
+		return swarm.ServiceUpdateResponse{}, err
28 28
 	}
29 29
 
30 30
 	query := url.Values{}
... ...
@@ -39,7 +42,7 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
39 39
 	query.Set("version", version.String())
40 40
 
41 41
 	if err := validateServiceSpec(service); err != nil {
42
-		return response, err
42
+		return swarm.ServiceUpdateResponse{}, err
43 43
 	}
44 44
 
45 45
 	// ensure that the image is tagged
... ...
@@ -74,9 +77,10 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
74 74
 	resp, err := cli.post(ctx, "/services/"+serviceID+"/update", query, service, headers)
75 75
 	defer ensureReaderClosed(resp)
76 76
 	if err != nil {
77
-		return response, err
77
+		return swarm.ServiceUpdateResponse{}, err
78 78
 	}
79 79
 
80
+	response := swarm.ServiceUpdateResponse{}
80 81
 	err = json.NewDecoder(resp.body).Decode(&response)
81 82
 	if resolveWarning != "" {
82 83
 		response.Warnings = append(response.Warnings, resolveWarning)
... ...
@@ -23,6 +23,14 @@ func TestServiceUpdateError(t *testing.T) {
23 23
 
24 24
 	_, err := client.ServiceUpdate(context.Background(), "service_id", swarm.Version{}, swarm.ServiceSpec{}, types.ServiceUpdateOptions{})
25 25
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
26
+
27
+	_, err = client.ServiceUpdate(context.Background(), "", swarm.Version{}, swarm.ServiceSpec{}, types.ServiceUpdateOptions{})
28
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
29
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
30
+
31
+	_, err = client.ServiceUpdate(context.Background(), "    ", swarm.Version{}, swarm.ServiceSpec{}, types.ServiceUpdateOptions{})
32
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
33
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
26 34
 }
27 35
 
28 36
 // TestServiceUpdateConnectionError verifies that connection errors occurring
... ...
@@ -11,9 +11,11 @@ import (
11 11
 
12 12
 // TaskInspectWithRaw returns the task information and its raw representation.
13 13
 func (cli *Client) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) {
14
-	if taskID == "" {
15
-		return swarm.Task{}, nil, objectNotFoundError{object: "task", id: taskID}
14
+	taskID, err := trimID("task", taskID)
15
+	if err != nil {
16
+		return swarm.Task{}, nil, err
16 17
 	}
18
+
17 19
 	serverResp, err := cli.get(ctx, "/tasks/"+taskID, nil, nil)
18 20
 	defer ensureReaderClosed(serverResp)
19 21
 	if err != nil {
... ...
@@ -33,7 +33,12 @@ func TestTaskInspectWithEmptyID(t *testing.T) {
33 33
 		}),
34 34
 	}
35 35
 	_, _, err := client.TaskInspectWithRaw(context.Background(), "")
36
-	assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
36
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
37
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
38
+
39
+	_, _, err = client.TaskInspectWithRaw(context.Background(), "    ")
40
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
41
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
37 42
 }
38 43
 
39 44
 func TestTaskInspect(t *testing.T) {
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"encoding/json"
5 5
 	"fmt"
6 6
 	"net/url"
7
+	"strings"
7 8
 
8 9
 	"github.com/docker/docker/api/types/filters"
9 10
 	"github.com/docker/docker/errdefs"
... ...
@@ -13,6 +14,23 @@ import (
13 13
 
14 14
 var headerRegexp = lazyregexp.New(`\ADocker/.+\s\((.+)\)\z`)
15 15
 
16
+type emptyIDError string
17
+
18
+func (e emptyIDError) InvalidParameter() {}
19
+
20
+func (e emptyIDError) Error() string {
21
+	return "invalid " + string(e) + " name or ID: value is empty"
22
+}
23
+
24
+// trimID trims the given object-ID / name, returning an error if it's empty.
25
+func trimID(objType, id string) (string, error) {
26
+	id = strings.TrimSpace(id)
27
+	if len(id) == 0 {
28
+		return "", emptyIDError(objType)
29
+	}
30
+	return id, nil
31
+}
32
+
16 33
 // getDockerOS returns the operating system based on the server header from the daemon.
17 34
 func getDockerOS(serverHeader string) string {
18 35
 	var osType string
... ...
@@ -17,8 +17,9 @@ func (cli *Client) VolumeInspect(ctx context.Context, volumeID string) (volume.V
17 17
 
18 18
 // VolumeInspectWithRaw returns the information about a specific volume in the docker host and its raw representation
19 19
 func (cli *Client) VolumeInspectWithRaw(ctx context.Context, volumeID string) (volume.Volume, []byte, error) {
20
-	if volumeID == "" {
21
-		return volume.Volume{}, nil, objectNotFoundError{object: "volume", id: volumeID}
20
+	volumeID, err := trimID("volume", volumeID)
21
+	if err != nil {
22
+		return volume.Volume{}, nil, err
22 23
 	}
23 24
 
24 25
 	var vol volume.Volume
... ...
@@ -42,7 +42,12 @@ func TestVolumeInspectWithEmptyID(t *testing.T) {
42 42
 		}),
43 43
 	}
44 44
 	_, _, err := client.VolumeInspectWithRaw(context.Background(), "")
45
-	assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
45
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
46
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
47
+
48
+	_, _, err = client.VolumeInspectWithRaw(context.Background(), "    ")
49
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
50
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
46 51
 }
47 52
 
48 53
 func TestVolumeInspect(t *testing.T) {
... ...
@@ -9,6 +9,11 @@ import (
9 9
 
10 10
 // VolumeRemove removes a volume from the docker host.
11 11
 func (cli *Client) VolumeRemove(ctx context.Context, volumeID string, force bool) error {
12
+	volumeID, err := trimID("volume", volumeID)
13
+	if err != nil {
14
+		return err
15
+	}
16
+
12 17
 	query := url.Values{}
13 18
 	if force {
14 19
 		// Make sure we negotiated (if the client is configured to do so),
... ...
@@ -21,6 +21,14 @@ func TestVolumeRemoveError(t *testing.T) {
21 21
 
22 22
 	err := client.VolumeRemove(context.Background(), "volume_id", false)
23 23
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
24
+
25
+	err = client.VolumeRemove(context.Background(), "", false)
26
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
27
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
28
+
29
+	err = client.VolumeRemove(context.Background(), "    ", false)
30
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
31
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
24 32
 }
25 33
 
26 34
 // TestVolumeRemoveConnectionError verifies that connection errors occurring
... ...
@@ -11,6 +11,10 @@ import (
11 11
 // VolumeUpdate updates a volume. This only works for Cluster Volumes, and
12 12
 // only some fields can be updated.
13 13
 func (cli *Client) VolumeUpdate(ctx context.Context, volumeID string, version swarm.Version, options volume.UpdateOptions) error {
14
+	volumeID, err := trimID("volume", volumeID)
15
+	if err != nil {
16
+		return err
17
+	}
14 18
 	if err := cli.NewVersionError(ctx, "1.42", "volume update"); err != nil {
15 19
 		return err
16 20
 	}
... ...
@@ -21,8 +21,16 @@ func TestVolumeUpdateError(t *testing.T) {
21 21
 		client: newMockClient(errorMock(http.StatusInternalServerError, "Server error")),
22 22
 	}
23 23
 
24
-	err := client.VolumeUpdate(context.Background(), "", swarm.Version{}, volumetypes.UpdateOptions{})
24
+	err := client.VolumeUpdate(context.Background(), "volume", swarm.Version{}, volumetypes.UpdateOptions{})
25 25
 	assert.Check(t, is.ErrorType(err, errdefs.IsSystem))
26
+
27
+	err = client.VolumeUpdate(context.Background(), "", swarm.Version{}, volumetypes.UpdateOptions{})
28
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
29
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
30
+
31
+	err = client.VolumeUpdate(context.Background(), "    ", swarm.Version{}, volumetypes.UpdateOptions{})
32
+	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidParameter))
33
+	assert.Check(t, is.ErrorContains(err, "value is empty"))
26 34
 }
27 35
 
28 36
 func TestVolumeUpdate(t *testing.T) {
... ...
@@ -1391,7 +1391,8 @@ func (s *DockerAPISuite) TestContainerAPIDeleteWithEmptyName(c *testing.T) {
1391 1391
 	defer apiClient.Close()
1392 1392
 
1393 1393
 	err = apiClient.ContainerRemove(testutil.GetContext(c), "", container.RemoveOptions{})
1394
-	assert.Check(c, errdefs.IsNotFound(err))
1394
+	assert.Check(c, is.ErrorType(err, errdefs.IsInvalidParameter))
1395
+	assert.Check(c, is.ErrorContains(err, "value is empty"))
1395 1396
 }
1396 1397
 
1397 1398
 func (s *DockerAPISuite) TestContainerAPIStatsWithNetworkDisabled(c *testing.T) {