vendor: github.com/docker/docker 5cc3f1dab895 (master, v28.0.0-rc.2)

full diff: b570831cc3...5cc3f1dab8

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2025-02-14 17:23:37 +01:00
parent 591fcb273a
commit 1d3eb6f95b
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
90 changed files with 566 additions and 396 deletions

View File

@ -16,17 +16,17 @@ import (
type fakeClient struct { type fakeClient struct {
client.Client client.Client
imageTagFunc func(string, string) error imageTagFunc func(string, string) error
imageSaveFunc func(images []string, options image.SaveOptions) (io.ReadCloser, error) imageSaveFunc func(images []string, options ...client.ImageSaveOption) (io.ReadCloser, error)
imageRemoveFunc func(image string, options image.RemoveOptions) ([]image.DeleteResponse, error) imageRemoveFunc func(image string, options image.RemoveOptions) ([]image.DeleteResponse, error)
imagePushFunc func(ref string, options image.PushOptions) (io.ReadCloser, error) imagePushFunc func(ref string, options image.PushOptions) (io.ReadCloser, error)
infoFunc func() (system.Info, error) infoFunc func() (system.Info, error)
imagePullFunc func(ref string, options image.PullOptions) (io.ReadCloser, error) imagePullFunc func(ref string, options image.PullOptions) (io.ReadCloser, error)
imagesPruneFunc func(pruneFilter filters.Args) (image.PruneReport, error) imagesPruneFunc func(pruneFilter filters.Args) (image.PruneReport, error)
imageLoadFunc func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) imageLoadFunc func(input io.Reader, options ...client.ImageLoadOption) (image.LoadResponse, error)
imageListFunc func(options image.ListOptions) ([]image.Summary, error) imageListFunc func(options image.ListOptions) ([]image.Summary, error)
imageInspectFunc func(img string) (image.InspectResponse, error) imageInspectFunc func(img string) (image.InspectResponse, error)
imageImportFunc func(source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) imageImportFunc func(source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error)
imageHistoryFunc func(img string, options image.HistoryOptions) ([]image.HistoryResponseItem, error) imageHistoryFunc func(img string, options ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error)
imageBuildFunc func(context.Context, io.Reader, types.ImageBuildOptions) (types.ImageBuildResponse, error) imageBuildFunc func(context.Context, io.Reader, types.ImageBuildOptions) (types.ImageBuildResponse, error)
} }
@ -37,9 +37,9 @@ func (cli *fakeClient) ImageTag(_ context.Context, img, ref string) error {
return nil return nil
} }
func (cli *fakeClient) ImageSave(_ context.Context, images []string, options image.SaveOptions) (io.ReadCloser, error) { func (cli *fakeClient) ImageSave(_ context.Context, images []string, options ...client.ImageSaveOption) (io.ReadCloser, error) {
if cli.imageSaveFunc != nil { if cli.imageSaveFunc != nil {
return cli.imageSaveFunc(images, options) return cli.imageSaveFunc(images, options...)
} }
return io.NopCloser(strings.NewReader("")), nil return io.NopCloser(strings.NewReader("")), nil
} }
@ -81,9 +81,9 @@ func (cli *fakeClient) ImagesPrune(_ context.Context, pruneFilter filters.Args)
return image.PruneReport{}, nil return image.PruneReport{}, nil
} }
func (cli *fakeClient) ImageLoad(_ context.Context, input io.Reader, options image.LoadOptions) (image.LoadResponse, error) { func (cli *fakeClient) ImageLoad(_ context.Context, input io.Reader, options ...client.ImageLoadOption) (image.LoadResponse, error) {
if cli.imageLoadFunc != nil { if cli.imageLoadFunc != nil {
return cli.imageLoadFunc(input, options) return cli.imageLoadFunc(input, options...)
} }
return image.LoadResponse{}, nil return image.LoadResponse{}, nil
} }
@ -111,9 +111,9 @@ func (cli *fakeClient) ImageImport(_ context.Context, source image.ImportSource,
return io.NopCloser(strings.NewReader("")), nil return io.NopCloser(strings.NewReader("")), nil
} }
func (cli *fakeClient) ImageHistory(_ context.Context, img string, options image.HistoryOptions) ([]image.HistoryResponseItem, error) { func (cli *fakeClient) ImageHistory(_ context.Context, img string, options ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) {
if cli.imageHistoryFunc != nil { if cli.imageHistoryFunc != nil {
return cli.imageHistoryFunc(img, options) return cli.imageHistoryFunc(img, options...)
} }
return []image.HistoryResponseItem{{ID: img, Created: time.Now().Unix()}}, nil return []image.HistoryResponseItem{{ID: img, Created: time.Now().Unix()}}, nil
} }

View File

@ -9,7 +9,7 @@ import (
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags" flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/client"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -56,16 +56,16 @@ func NewHistoryCommand(dockerCli command.Cli) *cobra.Command {
} }
func runHistory(ctx context.Context, dockerCli command.Cli, opts historyOptions) error { func runHistory(ctx context.Context, dockerCli command.Cli, opts historyOptions) error {
var options image.HistoryOptions var options []client.ImageHistoryOption
if opts.platform != "" { if opts.platform != "" {
p, err := platforms.Parse(opts.platform) p, err := platforms.Parse(opts.platform)
if err != nil { if err != nil {
return errors.Wrap(err, "invalid platform") return errors.Wrap(err, "invalid platform")
} }
options.Platform = &p options = append(options, client.ImageHistoryWithPlatform(p))
} }
history, err := dockerCli.Client().ImageHistory(ctx, opts.image, options) history, err := dockerCli.Client().ImageHistory(ctx, opts.image, options...)
if err != nil { if err != nil {
return err return err
} }

View File

@ -9,9 +9,8 @@ import (
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/docker/docker/client"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
) )
@ -20,7 +19,7 @@ func TestNewHistoryCommandErrors(t *testing.T) {
name string name string
args []string args []string
expectedError string expectedError string
imageHistoryFunc func(img string, options image.HistoryOptions) ([]image.HistoryResponseItem, error) imageHistoryFunc func(img string, options ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error)
}{ }{
{ {
name: "wrong-args", name: "wrong-args",
@ -31,7 +30,7 @@ func TestNewHistoryCommandErrors(t *testing.T) {
name: "client-error", name: "client-error",
args: []string{"image:tag"}, args: []string{"image:tag"},
expectedError: "something went wrong", expectedError: "something went wrong",
imageHistoryFunc: func(img string, options image.HistoryOptions) ([]image.HistoryResponseItem, error) { imageHistoryFunc: func(string, ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) {
return []image.HistoryResponseItem{{}}, errors.New("something went wrong") return []image.HistoryResponseItem{{}}, errors.New("something went wrong")
}, },
}, },
@ -56,12 +55,12 @@ func TestNewHistoryCommandSuccess(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
args []string args []string
imageHistoryFunc func(img string, options image.HistoryOptions) ([]image.HistoryResponseItem, error) imageHistoryFunc func(img string, options ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error)
}{ }{
{ {
name: "simple", name: "simple",
args: []string{"image:tag"}, args: []string{"image:tag"},
imageHistoryFunc: func(img string, options image.HistoryOptions) ([]image.HistoryResponseItem, error) { imageHistoryFunc: func(string, ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) {
return []image.HistoryResponseItem{{ return []image.HistoryResponseItem{{
ID: "1234567890123456789", ID: "1234567890123456789",
Created: time.Now().Unix(), Created: time.Now().Unix(),
@ -76,7 +75,7 @@ func TestNewHistoryCommandSuccess(t *testing.T) {
{ {
name: "non-human", name: "non-human",
args: []string{"--human=false", "image:tag"}, args: []string{"--human=false", "image:tag"},
imageHistoryFunc: func(img string, options image.HistoryOptions) ([]image.HistoryResponseItem, error) { imageHistoryFunc: func(string, ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) {
return []image.HistoryResponseItem{{ return []image.HistoryResponseItem{{
ID: "abcdef", ID: "abcdef",
Created: time.Date(2017, 1, 1, 12, 0, 3, 0, time.UTC).Unix(), Created: time.Date(2017, 1, 1, 12, 0, 3, 0, time.UTC).Unix(),
@ -88,7 +87,7 @@ func TestNewHistoryCommandSuccess(t *testing.T) {
{ {
name: "quiet-no-trunc", name: "quiet-no-trunc",
args: []string{"--quiet", "--no-trunc", "image:tag"}, args: []string{"--quiet", "--no-trunc", "image:tag"},
imageHistoryFunc: func(img string, options image.HistoryOptions) ([]image.HistoryResponseItem, error) { imageHistoryFunc: func(string, ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) {
return []image.HistoryResponseItem{{ return []image.HistoryResponseItem{{
ID: "1234567890123456789", ID: "1234567890123456789",
Created: time.Now().Unix(), Created: time.Now().Unix(),
@ -98,8 +97,10 @@ func TestNewHistoryCommandSuccess(t *testing.T) {
{ {
name: "platform", name: "platform",
args: []string{"--platform", "linux/amd64", "image:tag"}, args: []string{"--platform", "linux/amd64", "image:tag"},
imageHistoryFunc: func(img string, options image.HistoryOptions) ([]image.HistoryResponseItem, error) { imageHistoryFunc: func(img string, options ...client.ImageHistoryOption) ([]image.HistoryResponseItem, error) {
assert.Check(t, is.DeepEqual(ocispec.Platform{OS: "linux", Architecture: "amd64"}, *options.Platform)) // FIXME(thaJeztah): need to find appropriate way to test the result of "ImageHistoryWithPlatform" being applied
assert.Check(t, len(options) > 0) // can be 1 or two depending on whether a terminal is attached :/
// assert.Check(t, is.Contains(options, client.ImageHistoryWithPlatform(ocispec.Platform{OS: "linux", Architecture: "amd64"})))
return []image.HistoryResponseItem{{ return []image.HistoryResponseItem{{
ID: "1234567890123456789", ID: "1234567890123456789",
Created: time.Now().Unix(), Created: time.Now().Unix(),

View File

@ -9,7 +9,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/internal/jsonstream" "github.com/docker/cli/cli/internal/jsonstream"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/client"
"github.com/moby/sys/sequential" "github.com/moby/sys/sequential"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -68,9 +68,9 @@ func runLoad(ctx context.Context, dockerCli command.Cli, opts loadOptions) error
return errors.Errorf("requested load from stdin, but stdin is empty") return errors.Errorf("requested load from stdin, but stdin is empty")
} }
var options image.LoadOptions var options []client.ImageLoadOption
if opts.quiet || !dockerCli.Out().IsTerminal() { if opts.quiet || !dockerCli.Out().IsTerminal() {
options.Quiet = true options = append(options, client.ImageLoadWithQuiet(true))
} }
if opts.platform != "" { if opts.platform != "" {
@ -79,10 +79,10 @@ func runLoad(ctx context.Context, dockerCli command.Cli, opts loadOptions) error
return errors.Wrap(err, "invalid platform") return errors.Wrap(err, "invalid platform")
} }
// TODO(thaJeztah): change flag-type to support multiple platforms. // TODO(thaJeztah): change flag-type to support multiple platforms.
options.Platforms = append(options.Platforms, p) options = append(options, client.ImageLoadWithPlatforms(p))
} }
response, err := dockerCli.Client().ImageLoad(ctx, input, options) response, err := dockerCli.Client().ImageLoad(ctx, input, options...)
if err != nil { if err != nil {
return err return err
} }

View File

@ -9,9 +9,8 @@ import (
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/docker/docker/client"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
) )
@ -21,7 +20,7 @@ func TestNewLoadCommandErrors(t *testing.T) {
args []string args []string
isTerminalIn bool isTerminalIn bool
expectedError string expectedError string
imageLoadFunc func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) imageLoadFunc func(input io.Reader, options ...client.ImageLoadOption) (image.LoadResponse, error)
}{ }{
{ {
name: "wrong-args", name: "wrong-args",
@ -38,7 +37,7 @@ func TestNewLoadCommandErrors(t *testing.T) {
name: "pull-error", name: "pull-error",
args: []string{}, args: []string{},
expectedError: "something went wrong", expectedError: "something went wrong",
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) { imageLoadFunc: func(io.Reader, ...client.ImageLoadOption) (image.LoadResponse, error) {
return image.LoadResponse{}, errors.New("something went wrong") return image.LoadResponse{}, errors.New("something went wrong")
}, },
}, },
@ -46,7 +45,7 @@ func TestNewLoadCommandErrors(t *testing.T) {
name: "invalid platform", name: "invalid platform",
args: []string{"--platform", "<invalid>"}, args: []string{"--platform", "<invalid>"},
expectedError: `invalid platform`, expectedError: `invalid platform`,
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) { imageLoadFunc: func(io.Reader, ...client.ImageLoadOption) (image.LoadResponse, error) {
return image.LoadResponse{}, nil return image.LoadResponse{}, nil
}, },
}, },
@ -78,22 +77,21 @@ func TestNewLoadCommandSuccess(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
args []string args []string
imageLoadFunc func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) imageLoadFunc func(input io.Reader, options ...client.ImageLoadOption) (image.LoadResponse, error)
}{ }{
{ {
name: "simple", name: "simple",
args: []string{}, args: []string{},
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) { imageLoadFunc: func(io.Reader, ...client.ImageLoadOption) (image.LoadResponse, error) {
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
}, },
}, },
{ {
name: "json", name: "json",
args: []string{}, args: []string{},
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) { imageLoadFunc: func(io.Reader, ...client.ImageLoadOption) (image.LoadResponse, error) {
json := "{\"ID\": \"1\"}"
return image.LoadResponse{ return image.LoadResponse{
Body: io.NopCloser(strings.NewReader(json)), Body: io.NopCloser(strings.NewReader(`{"ID": "1"}`)),
JSON: true, JSON: true,
}, nil }, nil
}, },
@ -101,15 +99,17 @@ func TestNewLoadCommandSuccess(t *testing.T) {
{ {
name: "input-file", name: "input-file",
args: []string{"--input", "testdata/load-command-success.input.txt"}, args: []string{"--input", "testdata/load-command-success.input.txt"},
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) { imageLoadFunc: func(input io.Reader, options ...client.ImageLoadOption) (image.LoadResponse, error) {
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
}, },
}, },
{ {
name: "with platform", name: "with platform",
args: []string{"--platform", "linux/amd64"}, args: []string{"--platform", "linux/amd64"},
imageLoadFunc: func(input io.Reader, options image.LoadOptions) (image.LoadResponse, error) { imageLoadFunc: func(input io.Reader, options ...client.ImageLoadOption) (image.LoadResponse, error) {
assert.Check(t, is.DeepEqual([]ocispec.Platform{{OS: "linux", Architecture: "amd64"}}, options.Platforms)) // FIXME(thaJeztah): need to find appropriate way to test the result of "ImageHistoryWithPlatform" being applied
assert.Check(t, len(options) > 0) // can be 1 or two depending on whether a terminal is attached :/
// assert.Check(t, is.Contains(options, client.ImageHistoryWithPlatform(ocispec.Platform{OS: "linux", Architecture: "amd64"})))
return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil return image.LoadResponse{Body: io.NopCloser(strings.NewReader("Success"))}, nil
}, },
}, },

View File

@ -8,7 +8,7 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/client"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -57,17 +57,17 @@ func RunSave(ctx context.Context, dockerCli command.Cli, opts saveOptions) error
return errors.Wrap(err, "failed to save image") return errors.Wrap(err, "failed to save image")
} }
var options image.SaveOptions var options []client.ImageSaveOption
if opts.platform != "" { if opts.platform != "" {
p, err := platforms.Parse(opts.platform) p, err := platforms.Parse(opts.platform)
if err != nil { if err != nil {
return errors.Wrap(err, "invalid platform") return errors.Wrap(err, "invalid platform")
} }
// TODO(thaJeztah): change flag-type to support multiple platforms. // TODO(thaJeztah): change flag-type to support multiple platforms.
options.Platforms = append(options.Platforms, p) options = append(options, client.ImageSaveWithPlatforms(p))
} }
responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, options) responseBody, err := dockerCli.Client().ImageSave(ctx, opts.images, options...)
if err != nil { if err != nil {
return err return err
} }

View File

@ -8,8 +8,7 @@ import (
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/client"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
) )
@ -20,7 +19,7 @@ func TestNewSaveCommandErrors(t *testing.T) {
args []string args []string
isTerminal bool isTerminal bool
expectedError string expectedError string
imageSaveFunc func(images []string, options image.SaveOptions) (io.ReadCloser, error) imageSaveFunc func(images []string, options ...client.ImageSaveOption) (io.ReadCloser, error)
}{ }{
{ {
name: "wrong args", name: "wrong args",
@ -38,7 +37,7 @@ func TestNewSaveCommandErrors(t *testing.T) {
args: []string{"arg1"}, args: []string{"arg1"},
isTerminal: false, isTerminal: false,
expectedError: "error saving image", expectedError: "error saving image",
imageSaveFunc: func(images []string, options image.SaveOptions) (io.ReadCloser, error) { imageSaveFunc: func([]string, ...client.ImageSaveOption) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), errors.New("error saving image") return io.NopCloser(strings.NewReader("")), errors.New("error saving image")
}, },
}, },
@ -75,13 +74,13 @@ func TestNewSaveCommandSuccess(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
isTerminal bool isTerminal bool
imageSaveFunc func(images []string, options image.SaveOptions) (io.ReadCloser, error) imageSaveFunc func(images []string, options ...client.ImageSaveOption) (io.ReadCloser, error)
deferredFunc func() deferredFunc func()
}{ }{
{ {
args: []string{"-o", "save_tmp_file", "arg1"}, args: []string{"-o", "save_tmp_file", "arg1"},
isTerminal: true, isTerminal: true,
imageSaveFunc: func(images []string, _ image.SaveOptions) (io.ReadCloser, error) { imageSaveFunc: func(images []string, _ ...client.ImageSaveOption) (io.ReadCloser, error) {
assert.Assert(t, is.Len(images, 1)) assert.Assert(t, is.Len(images, 1))
assert.Check(t, is.Equal("arg1", images[0])) assert.Check(t, is.Equal("arg1", images[0]))
return io.NopCloser(strings.NewReader("")), nil return io.NopCloser(strings.NewReader("")), nil
@ -93,7 +92,7 @@ func TestNewSaveCommandSuccess(t *testing.T) {
{ {
args: []string{"arg1", "arg2"}, args: []string{"arg1", "arg2"},
isTerminal: false, isTerminal: false,
imageSaveFunc: func(images []string, _ image.SaveOptions) (io.ReadCloser, error) { imageSaveFunc: func(images []string, _ ...client.ImageSaveOption) (io.ReadCloser, error) {
assert.Assert(t, is.Len(images, 2)) assert.Assert(t, is.Len(images, 2))
assert.Check(t, is.Equal("arg1", images[0])) assert.Check(t, is.Equal("arg1", images[0]))
assert.Check(t, is.Equal("arg2", images[1])) assert.Check(t, is.Equal("arg2", images[1]))
@ -103,10 +102,12 @@ func TestNewSaveCommandSuccess(t *testing.T) {
{ {
args: []string{"--platform", "linux/amd64", "arg1"}, args: []string{"--platform", "linux/amd64", "arg1"},
isTerminal: false, isTerminal: false,
imageSaveFunc: func(images []string, options image.SaveOptions) (io.ReadCloser, error) { imageSaveFunc: func(images []string, options ...client.ImageSaveOption) (io.ReadCloser, error) {
assert.Assert(t, is.Len(images, 1)) assert.Assert(t, is.Len(images, 1))
assert.Check(t, is.Equal("arg1", images[0])) assert.Check(t, is.Equal("arg1", images[0]))
assert.Check(t, is.DeepEqual([]ocispec.Platform{{OS: "linux", Architecture: "amd64"}}, options.Platforms)) // FIXME(thaJeztah): need to find appropriate way to test the result of "ImageHistoryWithPlatform" being applied
assert.Check(t, len(options) > 0) // can be 1 or two depending on whether a terminal is attached :/
// assert.Check(t, is.Contains(options, client.ImageHistoryWithPlatform(ocispec.Platform{OS: "linux", Architecture: "amd64"})))
return io.NopCloser(strings.NewReader("")), nil return io.NopCloser(strings.NewReader("")), nil
}, },
}, },

View File

@ -13,7 +13,7 @@ require (
github.com/distribution/reference v0.6.0 github.com/distribution/reference v0.6.0
github.com/docker/cli-docs-tool v0.9.0 github.com/docker/cli-docs-tool v0.9.0
github.com/docker/distribution v2.8.3+incompatible github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker v28.0.0-rc.1.0.20250211164921-b570831cc3a3+incompatible // master (v28.0.0-rc.2) github.com/docker/docker v28.0.0-rc.1.0.20250214181517-5cc3f1dab895+incompatible // master (v28.0.0-rc.2)
github.com/docker/docker-credential-helpers v0.8.2 github.com/docker/docker-credential-helpers v0.8.2
github.com/docker/go-connections v0.5.0 github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0 github.com/docker/go-units v0.5.0

View File

@ -51,8 +51,8 @@ github.com/docker/cli-docs-tool v0.9.0/go.mod h1:ClrwlNW+UioiRyH9GiAOe1o3J/TsY3T
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.0.0-rc.1.0.20250211164921-b570831cc3a3+incompatible h1:XHjzdPvMafmekjBHZDiS+aQUvfog6oSaCO/dtoZvFxM= github.com/docker/docker v28.0.0-rc.1.0.20250214181517-5cc3f1dab895+incompatible h1:jC2oKzxRv7Ji6AMyTJ/io92duH/G0NePvw6AAiSMQkc=
github.com/docker/docker v28.0.0-rc.1.0.20250211164921-b570831cc3a3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.0.0-rc.1.0.20250214181517-5cc3f1dab895+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0= github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=

View File

@ -5109,6 +5109,17 @@ definitions:
ImageID: ImageID:
description: "The ID of the image that this container was created from" description: "The ID of the image that this container was created from"
type: "string" type: "string"
ImageManifestDescriptor:
$ref: "#/definitions/OCIDescriptor"
x-nullable: true
description: |
OCI descriptor of the platform-specific manifest of the image
the container was created from.
Note: Only available if the daemon provides a multi-platform
image store.
This field is not populated in the `GET /system/df` endpoint.
Command: Command:
description: "Command to run when starting the container" description: "Command to run when starting the container"
type: "string" type: "string"

View File

@ -125,6 +125,7 @@ type Summary struct {
Names []string Names []string
Image string Image string
ImageID string ImageID string
ImageManifestDescriptor *ocispec.Descriptor `json:"ImageManifestDescriptor,omitempty"`
Command string Command string
Created int64 Created int64
Ports []Port Ports []Port
@ -183,5 +184,5 @@ type InspectResponse struct {
Config *Config Config *Config
NetworkSettings *NetworkSettings NetworkSettings *NetworkSettings
// ImageManifestDescriptor is the descriptor of a platform-specific manifest of the image used to create the container. // ImageManifestDescriptor is the descriptor of a platform-specific manifest of the image used to create the container.
ImageManifestDescriptor *ocispec.Descriptor `json:",omitempty"` ImageManifestDescriptor *ocispec.Descriptor `json:"ImageManifestDescriptor,omitempty"`
} }

View File

@ -10,7 +10,7 @@ func (cli *Client) BuildCancel(ctx context.Context, id string) error {
query := url.Values{} query := url.Values{}
query.Set("id", id) query.Set("id", id)
serverResp, err := cli.post(ctx, "/build/cancel", query, nil, nil) resp, err := cli.post(ctx, "/build/cancel", query, nil, nil)
ensureReaderClosed(serverResp) ensureReaderClosed(resp)
return err return err
} }

View File

@ -40,15 +40,15 @@ func (cli *Client) BuildCachePrune(ctx context.Context, opts types.BuildCachePru
} }
query.Set("filters", f) query.Set("filters", f)
serverResp, err := cli.post(ctx, "/build/prune", query, nil, nil) resp, err := cli.post(ctx, "/build/prune", query, nil, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
report := types.BuildCachePruneReport{} report := types.BuildCachePruneReport{}
if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
return nil, errors.Wrap(err, "error retrieving disk usage") return nil, errors.Wrap(err, "error retrieving disk usage")
} }

View File

@ -23,6 +23,6 @@ func (cli *Client) CheckpointList(ctx context.Context, container string, options
return checkpoints, err return checkpoints, err
} }
err = json.NewDecoder(resp.body).Decode(&checkpoints) err = json.NewDecoder(resp.Body).Decode(&checkpoints)
return checkpoints, err return checkpoints, err
} }

View File

@ -113,23 +113,30 @@ type ImageAPIClient interface {
BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) BuildCachePrune(ctx context.Context, opts types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error)
BuildCancel(ctx context.Context, id string) error BuildCancel(ctx context.Context, id string) error
ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error)
ImageHistory(ctx context.Context, image string, opts image.HistoryOptions) ([]image.HistoryResponseItem, error)
ImageImport(ctx context.Context, source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error) ImageImport(ctx context.Context, source image.ImportSource, ref string, options image.ImportOptions) (io.ReadCloser, error)
ImageList(ctx context.Context, options image.ListOptions) ([]image.Summary, error)
ImagePull(ctx context.Context, ref string, options image.PullOptions) (io.ReadCloser, error)
ImagePush(ctx context.Context, ref string, options image.PushOptions) (io.ReadCloser, error)
ImageRemove(ctx context.Context, image string, options image.RemoveOptions) ([]image.DeleteResponse, error)
ImageSearch(ctx context.Context, term string, options registry.SearchOptions) ([]registry.SearchResult, error)
ImageTag(ctx context.Context, image, ref string) error
ImagesPrune(ctx context.Context, pruneFilter filters.Args) (image.PruneReport, error)
ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (image.InspectResponse, error)
ImageHistory(ctx context.Context, image string, _ ...ImageHistoryOption) ([]image.HistoryResponseItem, error)
ImageLoad(ctx context.Context, input io.Reader, _ ...ImageLoadOption) (image.LoadResponse, error)
ImageSave(ctx context.Context, images []string, _ ...ImageSaveOption) (io.ReadCloser, error)
ImageAPIClientDeprecated
}
// ImageAPIClientDeprecated defines deprecated methods of the ImageAPIClient.
type ImageAPIClientDeprecated interface {
// ImageInspectWithRaw returns the image information and its raw representation. // ImageInspectWithRaw returns the image information and its raw representation.
// //
// Deprecated: Use [Client.ImageInspect] instead. Raw response can be obtained using the [ImageInspectWithRawResponse] option. // Deprecated: Use [Client.ImageInspect] instead. Raw response can be obtained using the [ImageInspectWithRawResponse] option.
ImageInspectWithRaw(ctx context.Context, image string) (image.InspectResponse, []byte, error) ImageInspectWithRaw(ctx context.Context, image string) (image.InspectResponse, []byte, error)
ImageInspect(ctx context.Context, image string, _ ...ImageInspectOption) (image.InspectResponse, error)
ImageList(ctx context.Context, options image.ListOptions) ([]image.Summary, error)
ImageLoad(ctx context.Context, input io.Reader, opts image.LoadOptions) (image.LoadResponse, error)
ImagePull(ctx context.Context, ref string, options image.PullOptions) (io.ReadCloser, error)
ImagePush(ctx context.Context, ref string, options image.PushOptions) (io.ReadCloser, error)
ImageRemove(ctx context.Context, image string, options image.RemoveOptions) ([]image.DeleteResponse, error)
ImageSave(ctx context.Context, images []string, opts image.SaveOptions) (io.ReadCloser, error)
ImageSearch(ctx context.Context, term string, options registry.SearchOptions) ([]registry.SearchResult, error)
ImageTag(ctx context.Context, image, ref string) error
ImagesPrune(ctx context.Context, pruneFilter filters.Args) (image.PruneReport, error)
} }
// NetworkAPIClient defines API client methods for the networks // NetworkAPIClient defines API client methods for the networks

View File

@ -20,6 +20,6 @@ func (cli *Client) ConfigCreate(ctx context.Context, config swarm.ConfigSpec) (t
return response, err return response, err
} }
err = json.NewDecoder(resp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
return response, err return response, err
} }

View File

@ -24,7 +24,7 @@ func (cli *Client) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.C
return swarm.Config{}, nil, err return swarm.Config{}, nil, err
} }
body, err := io.ReadAll(resp.body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return swarm.Config{}, nil, err return swarm.Config{}, nil, err
} }

View File

@ -33,6 +33,6 @@ func (cli *Client) ConfigList(ctx context.Context, options types.ConfigListOptio
} }
var configs []swarm.Config var configs []swarm.Config
err = json.NewDecoder(resp.body).Decode(&configs) err = json.NewDecoder(resp.Body).Decode(&configs)
return configs, err return configs, err
} }

View File

@ -55,6 +55,6 @@ func (cli *Client) ContainerCommit(ctx context.Context, containerID string, opti
return response, err return response, err
} }
err = json.NewDecoder(resp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
return response, err return response, err
} }

View File

@ -24,12 +24,12 @@ func (cli *Client) ContainerStatPath(ctx context.Context, containerID, path stri
query := url.Values{} query := url.Values{}
query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API. query.Set("path", filepath.ToSlash(path)) // Normalize the paths used in the API.
response, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil) resp, err := cli.head(ctx, "/containers/"+containerID+"/archive", query, nil)
defer ensureReaderClosed(response) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return container.PathStat{}, err return container.PathStat{}, err
} }
return getContainerPathStatFromHeader(response.header) return getContainerPathStatFromHeader(resp.Header)
} }
// CopyToContainer copies content into the container filesystem. // CopyToContainer copies content into the container filesystem.
@ -71,7 +71,7 @@ func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath s
query := make(url.Values, 1) query := make(url.Values, 1)
query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API. query.Set("path", filepath.ToSlash(srcPath)) // Normalize the paths used in the API.
response, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil) resp, err := cli.get(ctx, "/containers/"+containerID+"/archive", query, nil)
if err != nil { if err != nil {
return nil, container.PathStat{}, err return nil, container.PathStat{}, err
} }
@ -82,11 +82,11 @@ func (cli *Client) CopyFromContainer(ctx context.Context, containerID, srcPath s
// copy it locally. Along with the stat info about the local destination, // copy it locally. Along with the stat info about the local destination,
// we have everything we need to handle the multiple possibilities there // we have everything we need to handle the multiple possibilities there
// can be when copying a file/dir from one location to another file/dir. // can be when copying a file/dir from one location to another file/dir.
stat, err := getContainerPathStatFromHeader(response.header) stat, err := getContainerPathStatFromHeader(resp.Header)
if err != nil { if err != nil {
return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err) return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err)
} }
return response.body, stat, err return resp.Body, stat, err
} }
func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) { func getContainerPathStatFromHeader(header http.Header) (container.PathStat, error) {

View File

@ -79,13 +79,13 @@ func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config
NetworkingConfig: networkingConfig, NetworkingConfig: networkingConfig,
} }
serverResp, err := cli.post(ctx, "/containers/create", query, body, nil) resp, err := cli.post(ctx, "/containers/create", query, body, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return response, err return response, err
} }
err = json.NewDecoder(serverResp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
return response, err return response, err
} }

View File

@ -15,14 +15,14 @@ func (cli *Client) ContainerDiff(ctx context.Context, containerID string) ([]con
return nil, err return nil, err
} }
serverResp, err := cli.get(ctx, "/containers/"+containerID+"/changes", url.Values{}, nil) resp, err := cli.get(ctx, "/containers/"+containerID+"/changes", url.Values{}, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var changes []container.FilesystemChange var changes []container.FilesystemChange
err = json.NewDecoder(serverResp.body).Decode(&changes) err = json.NewDecoder(resp.Body).Decode(&changes)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -40,7 +40,7 @@ func (cli *Client) ContainerExecCreate(ctx context.Context, containerID string,
} }
var response container.ExecCreateResponse var response container.ExecCreateResponse
err = json.NewDecoder(resp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
return response, err return response, err
} }
@ -75,7 +75,7 @@ func (cli *Client) ContainerExecInspect(ctx context.Context, execID string) (con
return response, err return response, err
} }
err = json.NewDecoder(resp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
ensureReaderClosed(resp) ensureReaderClosed(resp)
return response, err return response, err
} }

View File

@ -15,10 +15,10 @@ func (cli *Client) ContainerExport(ctx context.Context, containerID string) (io.
return nil, err return nil, err
} }
serverResp, err := cli.get(ctx, "/containers/"+containerID+"/export", url.Values{}, nil) resp, err := cli.get(ctx, "/containers/"+containerID+"/export", url.Values{}, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return serverResp.body, nil return resp.Body, nil
} }

View File

@ -17,14 +17,14 @@ func (cli *Client) ContainerInspect(ctx context.Context, containerID string) (co
return container.InspectResponse{}, err return container.InspectResponse{}, err
} }
serverResp, err := cli.get(ctx, "/containers/"+containerID+"/json", nil, nil) resp, err := cli.get(ctx, "/containers/"+containerID+"/json", nil, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return container.InspectResponse{}, err return container.InspectResponse{}, err
} }
var response container.InspectResponse var response container.InspectResponse
err = json.NewDecoder(serverResp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
return response, err return response, err
} }
@ -39,13 +39,13 @@ func (cli *Client) ContainerInspectWithRaw(ctx context.Context, containerID stri
if getSize { if getSize {
query.Set("size", "1") query.Set("size", "1")
} }
serverResp, err := cli.get(ctx, "/containers/"+containerID+"/json", query, nil) resp, err := cli.get(ctx, "/containers/"+containerID+"/json", query, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return container.InspectResponse{}, nil, err return container.InspectResponse{}, nil, err
} }
body, err := io.ReadAll(serverResp.body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return container.InspectResponse{}, nil, err return container.InspectResponse{}, nil, err
} }

View File

@ -51,6 +51,6 @@ func (cli *Client) ContainerList(ctx context.Context, options container.ListOpti
} }
var containers []container.Summary var containers []container.Summary
err = json.NewDecoder(resp.body).Decode(&containers) err = json.NewDecoder(resp.Body).Decode(&containers)
return containers, err return containers, err
} }

View File

@ -81,5 +81,5 @@ func (cli *Client) ContainerLogs(ctx context.Context, containerID string, option
if err != nil { if err != nil {
return nil, err return nil, err
} }
return resp.body, nil return resp.Body, nil
} }

View File

@ -20,14 +20,14 @@ func (cli *Client) ContainersPrune(ctx context.Context, pruneFilters filters.Arg
return container.PruneReport{}, err return container.PruneReport{}, err
} }
serverResp, err := cli.post(ctx, "/containers/prune", query, nil, nil) resp, err := cli.post(ctx, "/containers/prune", query, nil, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return container.PruneReport{}, err return container.PruneReport{}, err
} }
var report container.PruneReport var report container.PruneReport
if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
return container.PruneReport{}, fmt.Errorf("Error retrieving disk usage: %v", err) return container.PruneReport{}, fmt.Errorf("Error retrieving disk usage: %v", err)
} }

View File

@ -27,8 +27,8 @@ func (cli *Client) ContainerStats(ctx context.Context, containerID string, strea
} }
return container.StatsResponseReader{ return container.StatsResponseReader{
Body: resp.body, Body: resp.Body,
OSType: getDockerOS(resp.header.Get("Server")), OSType: getDockerOS(resp.Header.Get("Server")),
}, nil }, nil
} }
@ -50,7 +50,7 @@ func (cli *Client) ContainerStatsOneShot(ctx context.Context, containerID string
} }
return container.StatsResponseReader{ return container.StatsResponseReader{
Body: resp.body, Body: resp.Body,
OSType: getDockerOS(resp.header.Get("Server")), OSType: getDockerOS(resp.Header.Get("Server")),
}, nil }, nil
} }

View File

@ -28,6 +28,6 @@ func (cli *Client) ContainerTop(ctx context.Context, containerID string, argumen
} }
var response container.TopResponse var response container.TopResponse
err = json.NewDecoder(resp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
return response, err return response, err
} }

View File

@ -14,13 +14,13 @@ func (cli *Client) ContainerUpdate(ctx context.Context, containerID string, upda
return container.UpdateResponse{}, err return container.UpdateResponse{}, err
} }
serverResp, err := cli.post(ctx, "/containers/"+containerID+"/update", nil, updateConfig, nil) resp, err := cli.post(ctx, "/containers/"+containerID+"/update", nil, updateConfig, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return container.UpdateResponse{}, err return container.UpdateResponse{}, err
} }
var response container.UpdateResponse var response container.UpdateResponse
err = json.NewDecoder(serverResp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
return response, err return response, err
} }

View File

@ -67,9 +67,8 @@ func (cli *Client) ContainerWait(ctx context.Context, containerID string, condit
go func() { go func() {
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
body := resp.body
responseText := bytes.NewBuffer(nil) responseText := bytes.NewBuffer(nil)
stream := io.TeeReader(body, responseText) stream := io.TeeReader(resp.Body, responseText)
var res container.WaitResponse var res container.WaitResponse
if err := json.NewDecoder(stream).Decode(&res); err != nil { if err := json.NewDecoder(stream).Decode(&res); err != nil {
@ -111,7 +110,7 @@ func (cli *Client) legacyContainerWait(ctx context.Context, containerID string)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
var res container.WaitResponse var res container.WaitResponse
if err := json.NewDecoder(resp.body).Decode(&res); err != nil { if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
errC <- err errC <- err
return return
} }

View File

@ -19,14 +19,14 @@ func (cli *Client) DiskUsage(ctx context.Context, options types.DiskUsageOptions
} }
} }
serverResp, err := cli.get(ctx, "/system/df", query, nil) resp, err := cli.get(ctx, "/system/df", query, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return types.DiskUsage{}, err return types.DiskUsage{}, err
} }
var du types.DiskUsage var du types.DiskUsage
if err := json.NewDecoder(serverResp.body).Decode(&du); err != nil { if err := json.NewDecoder(resp.Body).Decode(&du); err != nil {
return types.DiskUsage{}, fmt.Errorf("Error retrieving disk usage: %v", err) return types.DiskUsage{}, fmt.Errorf("Error retrieving disk usage: %v", err)
} }
return du, nil return du, nil

View File

@ -11,14 +11,12 @@ import (
// DistributionInspect returns the image digest with the full manifest. // DistributionInspect returns the image digest with the full manifest.
func (cli *Client) DistributionInspect(ctx context.Context, imageRef, encodedRegistryAuth string) (registry.DistributionInspect, error) { func (cli *Client) DistributionInspect(ctx context.Context, imageRef, encodedRegistryAuth string) (registry.DistributionInspect, error) {
// Contact the registry to retrieve digest and platform information
var distributionInspect registry.DistributionInspect
if imageRef == "" { if imageRef == "" {
return distributionInspect, objectNotFoundError{object: "distribution", id: imageRef} return registry.DistributionInspect{}, objectNotFoundError{object: "distribution", id: imageRef}
} }
if err := cli.NewVersionError(ctx, "1.30", "distribution inspect"); err != nil { if err := cli.NewVersionError(ctx, "1.30", "distribution inspect"); err != nil {
return distributionInspect, err return registry.DistributionInspect{}, err
} }
var headers http.Header var headers http.Header
@ -28,12 +26,14 @@ func (cli *Client) DistributionInspect(ctx context.Context, imageRef, encodedReg
} }
} }
// Contact the registry to retrieve digest and platform information
resp, err := cli.get(ctx, "/distribution/"+imageRef+"/json", url.Values{}, headers) resp, err := cli.get(ctx, "/distribution/"+imageRef+"/json", url.Values{}, headers)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return distributionInspect, err return registry.DistributionInspect{}, err
} }
err = json.NewDecoder(resp.body).Decode(&distributionInspect) var distributionInspect registry.DistributionInspect
err = json.NewDecoder(resp.Body).Decode(&distributionInspect)
return distributionInspect, err return distributionInspect, err
} }

View File

@ -36,9 +36,9 @@ func (cli *Client) Events(ctx context.Context, options events.ListOptions) (<-ch
errs <- err errs <- err
return return
} }
defer resp.body.Close() defer resp.Body.Close()
decoder := json.NewDecoder(resp.body) decoder := json.NewDecoder(resp.Body)
close(started) close(started)
for { for {

View File

@ -33,14 +33,14 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio
headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf)) headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf))
headers.Set("Content-Type", "application/x-tar") headers.Set("Content-Type", "application/x-tar")
serverResp, err := cli.postRaw(ctx, "/build", query, buildContext, headers) resp, err := cli.postRaw(ctx, "/build", query, buildContext, headers)
if err != nil { if err != nil {
return types.ImageBuildResponse{}, err return types.ImageBuildResponse{}, err
} }
return types.ImageBuildResponse{ return types.ImageBuildResponse{
Body: serverResp.body, Body: resp.Body,
OSType: getDockerOS(serverResp.header.Get("Server")), OSType: getDockerOS(resp.Header.Get("Server")),
}, nil }, nil
} }

View File

@ -30,10 +30,10 @@ func (cli *Client) ImageCreate(ctx context.Context, parentReference string, opti
if err != nil { if err != nil {
return nil, err return nil, err
} }
return resp.body, nil return resp.Body, nil
} }
func (cli *Client) tryImageCreate(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) { func (cli *Client) tryImageCreate(ctx context.Context, query url.Values, registryAuth string) (*http.Response, error) {
return cli.post(ctx, "/images/create", query, nil, http.Header{ return cli.post(ctx, "/images/create", query, nil, http.Header{
registry.AuthHeader: {registryAuth}, registry.AuthHeader: {registryAuth},
}) })

View File

@ -3,33 +3,54 @@ package client // import "github.com/docker/docker/client"
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net/url" "net/url"
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
) )
// ImageHistoryWithPlatform sets the platform for the image history operation.
func ImageHistoryWithPlatform(platform ocispec.Platform) ImageHistoryOption {
return imageHistoryOptionFunc(func(opt *imageHistoryOpts) error {
if opt.apiOptions.Platform != nil {
return fmt.Errorf("platform already set to %s", *opt.apiOptions.Platform)
}
opt.apiOptions.Platform = &platform
return nil
})
}
// ImageHistory returns the changes in an image in history format. // ImageHistory returns the changes in an image in history format.
func (cli *Client) ImageHistory(ctx context.Context, imageID string, opts image.HistoryOptions) ([]image.HistoryResponseItem, error) { func (cli *Client) ImageHistory(ctx context.Context, imageID string, historyOpts ...ImageHistoryOption) ([]image.HistoryResponseItem, error) {
query := url.Values{} query := url.Values{}
if opts.Platform != nil {
var opts imageHistoryOpts
for _, o := range historyOpts {
if err := o.Apply(&opts); err != nil {
return nil, err
}
}
if opts.apiOptions.Platform != nil {
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
return nil, err return nil, err
} }
p, err := encodePlatform(opts.Platform) p, err := encodePlatform(opts.apiOptions.Platform)
if err != nil { if err != nil {
return nil, err return nil, err
} }
query.Set("platform", p) query.Set("platform", p)
} }
serverResp, err := cli.get(ctx, "/images/"+imageID+"/history", query, nil) resp, err := cli.get(ctx, "/images/"+imageID+"/history", query, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var history []image.HistoryResponseItem var history []image.HistoryResponseItem
err = json.NewDecoder(serverResp.body).Decode(&history) err = json.NewDecoder(resp.Body).Decode(&history)
return history, err return history, err
} }

View File

@ -0,0 +1,19 @@
package client
import (
"github.com/docker/docker/api/types/image"
)
// ImageHistoryOption is a type representing functional options for the image history operation.
type ImageHistoryOption interface {
Apply(*imageHistoryOpts) error
}
type imageHistoryOptionFunc func(opt *imageHistoryOpts) error
func (f imageHistoryOptionFunc) Apply(o *imageHistoryOpts) error {
return f(o)
}
type imageHistoryOpts struct {
apiOptions image.HistoryOptions
}

View File

@ -44,5 +44,5 @@ func (cli *Client) ImageImport(ctx context.Context, source image.ImportSource, r
if err != nil { if err != nil {
return nil, err return nil, err
} }
return resp.body, nil return resp.Body, nil
} }

View File

@ -11,49 +11,6 @@ import (
"github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/image"
) )
// ImageInspectOption is a type representing functional options for the image inspect operation.
type ImageInspectOption interface {
Apply(*imageInspectOpts) error
}
type imageInspectOptionFunc func(opt *imageInspectOpts) error
func (f imageInspectOptionFunc) Apply(o *imageInspectOpts) error {
return f(o)
}
// ImageInspectWithRawResponse instructs the client to additionally store the
// raw inspect response in the provided buffer.
func ImageInspectWithRawResponse(raw *bytes.Buffer) ImageInspectOption {
return imageInspectOptionFunc(func(opts *imageInspectOpts) error {
opts.raw = raw
return nil
})
}
// ImageInspectWithManifests sets manifests API option for the image inspect operation.
// This option is only available for API version 1.48 and up.
// With this option set, the image inspect operation response will have the
// [image.InspectResponse.Manifests] field populated if the server is multi-platform capable.
func ImageInspectWithManifests(manifests bool) ImageInspectOption {
return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error {
clientOpts.apiOptions.Manifests = manifests
return nil
})
}
// ImageInspectWithAPIOpts sets the API options for the image inspect operation.
func ImageInspectWithAPIOpts(opts image.InspectOptions) ImageInspectOption {
return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error {
clientOpts.apiOptions = opts
return nil
})
}
type imageInspectOpts struct {
raw *bytes.Buffer
apiOptions image.InspectOptions
}
// ImageInspect returns the image information. // ImageInspect returns the image information.
func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts ...ImageInspectOption) (image.InspectResponse, error) { func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts ...ImageInspectOption) (image.InspectResponse, error) {
if imageID == "" { if imageID == "" {
@ -75,8 +32,8 @@ func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts
query.Set("manifests", "1") query.Set("manifests", "1")
} }
serverResp, err := cli.get(ctx, "/images/"+imageID+"/json", query, nil) resp, err := cli.get(ctx, "/images/"+imageID+"/json", query, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return image.InspectResponse{}, err return image.InspectResponse{}, err
} }
@ -86,7 +43,7 @@ func (cli *Client) ImageInspect(ctx context.Context, imageID string, inspectOpts
buf = &bytes.Buffer{} buf = &bytes.Buffer{}
} }
if _, err := io.Copy(buf, serverResp.body); err != nil { if _, err := io.Copy(buf, resp.Body); err != nil {
return image.InspectResponse{}, err return image.InspectResponse{}, err
} }

View File

@ -0,0 +1,50 @@
package client
import (
"bytes"
"github.com/docker/docker/api/types/image"
)
// ImageInspectOption is a type representing functional options for the image inspect operation.
type ImageInspectOption interface {
Apply(*imageInspectOpts) error
}
type imageInspectOptionFunc func(opt *imageInspectOpts) error
func (f imageInspectOptionFunc) Apply(o *imageInspectOpts) error {
return f(o)
}
// ImageInspectWithRawResponse instructs the client to additionally store the
// raw inspect response in the provided buffer.
func ImageInspectWithRawResponse(raw *bytes.Buffer) ImageInspectOption {
return imageInspectOptionFunc(func(opts *imageInspectOpts) error {
opts.raw = raw
return nil
})
}
// ImageInspectWithManifests sets manifests API option for the image inspect operation.
// This option is only available for API version 1.48 and up.
// With this option set, the image inspect operation response will have the
// [image.InspectResponse.Manifests] field populated if the server is multi-platform capable.
func ImageInspectWithManifests(manifests bool) ImageInspectOption {
return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error {
clientOpts.apiOptions.Manifests = manifests
return nil
})
}
// ImageInspectWithAPIOpts sets the API options for the image inspect operation.
func ImageInspectWithAPIOpts(opts image.InspectOptions) ImageInspectOption {
return imageInspectOptionFunc(func(clientOpts *imageInspectOpts) error {
clientOpts.apiOptions = opts
return nil
})
}
type imageInspectOpts struct {
raw *bytes.Buffer
apiOptions image.InspectOptions
}

View File

@ -56,12 +56,12 @@ func (cli *Client) ImageList(ctx context.Context, options image.ListOptions) ([]
query.Set("manifests", "1") query.Set("manifests", "1")
} }
serverResp, err := cli.get(ctx, "/images/json", query, nil) resp, err := cli.get(ctx, "/images/json", query, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return images, err return images, err
} }
err = json.NewDecoder(serverResp.body).Decode(&images) err = json.NewDecoder(resp.Body).Decode(&images)
return images, err return images, err
} }

View File

@ -16,18 +16,25 @@ import (
// Platform is an optional parameter that specifies the platform to load from // Platform is an optional parameter that specifies the platform to load from
// the provided multi-platform image. This is only has effect if the input image // the provided multi-platform image. This is only has effect if the input image
// is a multi-platform image. // is a multi-platform image.
func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, opts image.LoadOptions) (image.LoadResponse, error) { func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, loadOpts ...ImageLoadOption) (image.LoadResponse, error) {
var opts imageLoadOpts
for _, opt := range loadOpts {
if err := opt.Apply(&opts); err != nil {
return image.LoadResponse{}, err
}
}
query := url.Values{} query := url.Values{}
query.Set("quiet", "0") query.Set("quiet", "0")
if opts.Quiet { if opts.apiOptions.Quiet {
query.Set("quiet", "1") query.Set("quiet", "1")
} }
if len(opts.Platforms) > 0 { if len(opts.apiOptions.Platforms) > 0 {
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
return image.LoadResponse{}, err return image.LoadResponse{}, err
} }
p, err := encodePlatforms(opts.Platforms...) p, err := encodePlatforms(opts.apiOptions.Platforms...)
if err != nil { if err != nil {
return image.LoadResponse{}, err return image.LoadResponse{}, err
} }
@ -41,7 +48,7 @@ func (cli *Client) ImageLoad(ctx context.Context, input io.Reader, opts image.Lo
return image.LoadResponse{}, err return image.LoadResponse{}, err
} }
return image.LoadResponse{ return image.LoadResponse{
Body: resp.body, Body: resp.Body,
JSON: resp.header.Get("Content-Type") == "application/json", JSON: resp.Header.Get("Content-Type") == "application/json",
}, nil }, nil
} }

View File

@ -0,0 +1,41 @@
package client
import (
"fmt"
"github.com/docker/docker/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// ImageLoadOption is a type representing functional options for the image load operation.
type ImageLoadOption interface {
Apply(*imageLoadOpts) error
}
type imageLoadOptionFunc func(opt *imageLoadOpts) error
func (f imageLoadOptionFunc) Apply(o *imageLoadOpts) error {
return f(o)
}
type imageLoadOpts struct {
apiOptions image.LoadOptions
}
// ImageLoadWithQuiet sets the quiet option for the image load operation.
func ImageLoadWithQuiet(quiet bool) ImageLoadOption {
return imageLoadOptionFunc(func(opt *imageLoadOpts) error {
opt.apiOptions.Quiet = quiet
return nil
})
}
// ImageLoadWithPlatforms sets the platforms to be loaded from the image.
func ImageLoadWithPlatforms(platforms ...ocispec.Platform) ImageLoadOption {
return imageLoadOptionFunc(func(opt *imageLoadOpts) error {
if opt.apiOptions.Platforms != nil {
return fmt.Errorf("platforms already set to %v", opt.apiOptions.Platforms)
}
opt.apiOptions.Platforms = platforms
return nil
})
}

View File

@ -20,14 +20,14 @@ func (cli *Client) ImagesPrune(ctx context.Context, pruneFilters filters.Args) (
return image.PruneReport{}, err return image.PruneReport{}, err
} }
serverResp, err := cli.post(ctx, "/images/prune", query, nil, nil) resp, err := cli.post(ctx, "/images/prune", query, nil, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return image.PruneReport{}, err return image.PruneReport{}, err
} }
var report image.PruneReport var report image.PruneReport
if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
return image.PruneReport{}, fmt.Errorf("Error retrieving disk usage: %v", err) return image.PruneReport{}, fmt.Errorf("Error retrieving disk usage: %v", err)
} }

View File

@ -45,7 +45,7 @@ func (cli *Client) ImagePull(ctx context.Context, refStr string, options image.P
if err != nil { if err != nil {
return nil, err return nil, err
} }
return resp.body, nil return resp.Body, nil
} }
// getAPITagFromNamedRef returns a tag from the specified reference. // getAPITagFromNamedRef returns a tag from the specified reference.

View File

@ -63,10 +63,10 @@ func (cli *Client) ImagePush(ctx context.Context, image string, options image.Pu
if err != nil { if err != nil {
return nil, err return nil, err
} }
return resp.body, nil return resp.Body, nil
} }
func (cli *Client) tryImagePush(ctx context.Context, imageID string, query url.Values, registryAuth string) (serverResponse, error) { func (cli *Client) tryImagePush(ctx context.Context, imageID string, query url.Values, registryAuth string) (*http.Response, error) {
return cli.post(ctx, "/images/"+imageID+"/push", query, nil, http.Header{ return cli.post(ctx, "/images/"+imageID+"/push", query, nil, http.Header{
registry.AuthHeader: {registryAuth}, registry.AuthHeader: {registryAuth},
}) })

View File

@ -19,13 +19,13 @@ func (cli *Client) ImageRemove(ctx context.Context, imageID string, options imag
query.Set("noprune", "1") query.Set("noprune", "1")
} }
var dels []image.DeleteResponse
resp, err := cli.delete(ctx, "/images/"+imageID, query, nil) resp, err := cli.delete(ctx, "/images/"+imageID, query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return dels, err return nil, err
} }
err = json.NewDecoder(resp.body).Decode(&dels) var dels []image.DeleteResponse
err = json.NewDecoder(resp.Body).Decode(&dels)
return dels, err return dels, err
} }

View File

@ -4,22 +4,29 @@ import (
"context" "context"
"io" "io"
"net/url" "net/url"
"github.com/docker/docker/api/types/image"
) )
// ImageSave retrieves one or more images from the docker host as an io.ReadCloser. // ImageSave retrieves one or more images from the docker host as an io.ReadCloser.
// It's up to the caller to store the images and close the stream. //
func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, opts image.SaveOptions) (io.ReadCloser, error) { // Platforms is an optional parameter that specifies the platforms to save from the image.
// This is only has effect if the input image is a multi-platform image.
func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, saveOpts ...ImageSaveOption) (io.ReadCloser, error) {
var opts imageSaveOpts
for _, opt := range saveOpts {
if err := opt.Apply(&opts); err != nil {
return nil, err
}
}
query := url.Values{ query := url.Values{
"names": imageIDs, "names": imageIDs,
} }
if len(opts.Platforms) > 0 { if len(opts.apiOptions.Platforms) > 0 {
if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil { if err := cli.NewVersionError(ctx, "1.48", "platform"); err != nil {
return nil, err return nil, err
} }
p, err := encodePlatforms(opts.Platforms...) p, err := encodePlatforms(opts.apiOptions.Platforms...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -30,5 +37,5 @@ func (cli *Client) ImageSave(ctx context.Context, imageIDs []string, opts image.
if err != nil { if err != nil {
return nil, err return nil, err
} }
return resp.body, nil return resp.Body, nil
} }

View File

@ -0,0 +1,33 @@
package client
import (
"fmt"
"github.com/docker/docker/api/types/image"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
type ImageSaveOption interface {
Apply(*imageSaveOpts) error
}
type imageSaveOptionFunc func(opt *imageSaveOpts) error
func (f imageSaveOptionFunc) Apply(o *imageSaveOpts) error {
return f(o)
}
// ImageSaveWithPlatforms sets the platforms to be saved from the image.
func ImageSaveWithPlatforms(platforms ...ocispec.Platform) ImageSaveOption {
return imageSaveOptionFunc(func(opt *imageSaveOpts) error {
if opt.apiOptions.Platforms != nil {
return fmt.Errorf("platforms already set to %v", opt.apiOptions.Platforms)
}
opt.apiOptions.Platforms = platforms
return nil
})
}
type imageSaveOpts struct {
apiOptions image.SaveOptions
}

View File

@ -43,11 +43,11 @@ func (cli *Client) ImageSearch(ctx context.Context, term string, options registr
return results, err return results, err
} }
err = json.NewDecoder(resp.body).Decode(&results) err = json.NewDecoder(resp.Body).Decode(&results)
return results, err return results, err
} }
func (cli *Client) tryImageSearch(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) { func (cli *Client) tryImageSearch(ctx context.Context, query url.Values, registryAuth string) (*http.Response, error) {
return cli.get(ctx, "/images/search", query, http.Header{ return cli.get(ctx, "/images/search", query, http.Header{
registry.AuthHeader: {registryAuth}, registry.AuthHeader: {registryAuth},
}) })

View File

@ -12,13 +12,13 @@ import (
// Info returns information about the docker server. // Info returns information about the docker server.
func (cli *Client) Info(ctx context.Context) (system.Info, error) { func (cli *Client) Info(ctx context.Context) (system.Info, error) {
var info system.Info var info system.Info
serverResp, err := cli.get(ctx, "/info", url.Values{}, nil) resp, err := cli.get(ctx, "/info", url.Values{}, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return info, err return info, err
} }
if err := json.NewDecoder(serverResp.body).Decode(&info); err != nil { if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return info, fmt.Errorf("Error reading remote info: %v", err) return info, fmt.Errorf("Error reading remote info: %v", err)
} }

View File

@ -19,6 +19,6 @@ func (cli *Client) RegistryLogin(ctx context.Context, auth registry.AuthConfig)
} }
var response registry.AuthenticateOKBody var response registry.AuthenticateOKBody
err = json.NewDecoder(resp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
return response, err return response, err
} }

View File

@ -10,15 +10,13 @@ import (
// NetworkCreate creates a new network in the docker host. // NetworkCreate creates a new network in the docker host.
func (cli *Client) NetworkCreate(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) { func (cli *Client) NetworkCreate(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) {
var response network.CreateResponse
// Make sure we negotiated (if the client is configured to do so), // Make sure we negotiated (if the client is configured to do so),
// as code below contains API-version specific handling of options. // as code below contains API-version specific handling of options.
// //
// Normally, version-negotiation (if enabled) would not happen until // Normally, version-negotiation (if enabled) would not happen until
// the API request is made. // the API request is made.
if err := cli.checkVersion(ctx); err != nil { if err := cli.checkVersion(ctx); err != nil {
return response, err return network.CreateResponse{}, err
} }
networkCreateRequest := network.CreateRequest{ networkCreateRequest := network.CreateRequest{
@ -30,12 +28,13 @@ func (cli *Client) NetworkCreate(ctx context.Context, name string, options netwo
networkCreateRequest.CheckDuplicate = &enabled //nolint:staticcheck // ignore SA1019: CheckDuplicate is deprecated since API v1.44. networkCreateRequest.CheckDuplicate = &enabled //nolint:staticcheck // ignore SA1019: CheckDuplicate is deprecated since API v1.44.
} }
serverResp, err := cli.post(ctx, "/networks/create", nil, networkCreateRequest, nil) resp, err := cli.post(ctx, "/networks/create", nil, networkCreateRequest, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return response, err return network.CreateResponse{}, err
} }
err = json.NewDecoder(serverResp.body).Decode(&response) var response network.CreateResponse
err = json.NewDecoder(resp.Body).Decode(&response)
return response, err return response, err
} }

View File

@ -36,7 +36,7 @@ func (cli *Client) NetworkInspectWithRaw(ctx context.Context, networkID string,
return network.Inspect{}, nil, err return network.Inspect{}, nil, err
} }
raw, err := io.ReadAll(resp.body) raw, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return network.Inspect{}, nil, err return network.Inspect{}, nil, err
} }

View File

@ -27,6 +27,6 @@ func (cli *Client) NetworkList(ctx context.Context, options network.ListOptions)
if err != nil { if err != nil {
return networkResources, err return networkResources, err
} }
err = json.NewDecoder(resp.body).Decode(&networkResources) err = json.NewDecoder(resp.Body).Decode(&networkResources)
return networkResources, err return networkResources, err
} }

View File

@ -20,14 +20,14 @@ func (cli *Client) NetworksPrune(ctx context.Context, pruneFilters filters.Args)
return network.PruneReport{}, err return network.PruneReport{}, err
} }
serverResp, err := cli.post(ctx, "/networks/prune", query, nil, nil) resp, err := cli.post(ctx, "/networks/prune", query, nil, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return network.PruneReport{}, err return network.PruneReport{}, err
} }
var report network.PruneReport var report network.PruneReport
if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
return network.PruneReport{}, fmt.Errorf("Error retrieving network prune report: %v", err) return network.PruneReport{}, fmt.Errorf("Error retrieving network prune report: %v", err)
} }

View File

@ -15,13 +15,13 @@ func (cli *Client) NodeInspectWithRaw(ctx context.Context, nodeID string) (swarm
if err != nil { if err != nil {
return swarm.Node{}, nil, err return swarm.Node{}, nil, err
} }
serverResp, err := cli.get(ctx, "/nodes/"+nodeID, nil, nil) resp, err := cli.get(ctx, "/nodes/"+nodeID, nil, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return swarm.Node{}, nil, err return swarm.Node{}, nil, err
} }
body, err := io.ReadAll(serverResp.body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return swarm.Node{}, nil, err return swarm.Node{}, nil, err
} }

View File

@ -30,6 +30,6 @@ func (cli *Client) NodeList(ctx context.Context, options types.NodeListOptions)
} }
var nodes []swarm.Node var nodes []swarm.Node
err = json.NewDecoder(resp.body).Decode(&nodes) err = json.NewDecoder(resp.Body).Decode(&nodes)
return nodes, err return nodes, err
} }

View File

@ -8,7 +8,6 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/errdefs"
) )
// Ping pings the server and returns the value of the "Docker-Experimental", // Ping pings the server and returns the value of the "Docker-Experimental",
@ -28,49 +27,54 @@ func (cli *Client) Ping(ctx context.Context) (types.Ping, error) {
if err != nil { if err != nil {
return ping, err return ping, err
} }
serverResp, err := cli.doRequest(req) resp, err := cli.doRequest(req)
if err == nil { if err != nil {
defer ensureReaderClosed(serverResp) if IsErrConnectionFailed(err) {
switch serverResp.statusCode { return ping, err
}
// We managed to connect, but got some error; continue and try GET request.
} else {
defer ensureReaderClosed(resp)
switch resp.StatusCode {
case http.StatusOK, http.StatusInternalServerError: case http.StatusOK, http.StatusInternalServerError:
// Server handled the request, so parse the response // Server handled the request, so parse the response
return parsePingResponse(cli, serverResp) return parsePingResponse(cli, resp)
} }
} else if IsErrConnectionFailed(err) {
return ping, err
} }
// HEAD failed; fallback to GET. // HEAD failed; fallback to GET.
req.Method = http.MethodGet req.Method = http.MethodGet
serverResp, err = cli.doRequest(req) resp, err = cli.doRequest(req)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return ping, err return ping, err
} }
return parsePingResponse(cli, serverResp) return parsePingResponse(cli, resp)
} }
func parsePingResponse(cli *Client, resp serverResponse) (types.Ping, error) { func parsePingResponse(cli *Client, resp *http.Response) (types.Ping, error) {
var ping types.Ping if resp == nil {
if resp.header == nil { return types.Ping{}, nil
err := cli.checkResponseErr(resp)
return ping, errdefs.FromStatusCode(err, resp.statusCode)
} }
ping.APIVersion = resp.header.Get("Api-Version")
ping.OSType = resp.header.Get("Ostype") var ping types.Ping
if resp.header.Get("Docker-Experimental") == "true" { if resp.Header == nil {
return ping, cli.checkResponseErr(resp)
}
ping.APIVersion = resp.Header.Get("Api-Version")
ping.OSType = resp.Header.Get("Ostype")
if resp.Header.Get("Docker-Experimental") == "true" {
ping.Experimental = true ping.Experimental = true
} }
if bv := resp.header.Get("Builder-Version"); bv != "" { if bv := resp.Header.Get("Builder-Version"); bv != "" {
ping.BuilderVersion = types.BuilderVersion(bv) ping.BuilderVersion = types.BuilderVersion(bv)
} }
if si := resp.header.Get("Swarm"); si != "" { if si := resp.Header.Get("Swarm"); si != "" {
state, role, _ := strings.Cut(si, "/") state, role, _ := strings.Cut(si, "/")
ping.SwarmStatus = &swarm.Status{ ping.SwarmStatus = &swarm.Status{
NodeState: swarm.LocalNodeState(state), NodeState: swarm.LocalNodeState(state),
ControlAvailable: role == "manager", ControlAvailable: role == "manager",
} }
} }
err := cli.checkResponseErr(resp) return ping, cli.checkResponseErr(resp)
return ping, errdefs.FromStatusCode(err, resp.statusCode)
} }

View File

@ -21,7 +21,7 @@ func (cli *Client) PluginInspectWithRaw(ctx context.Context, name string) (*type
return nil, nil, err return nil, nil, err
} }
body, err := io.ReadAll(resp.body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@ -35,13 +35,13 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types
return nil, err return nil, err
} }
name = resp.header.Get("Docker-Plugin-Name") name = resp.Header.Get("Docker-Plugin-Name")
pr, pw := io.Pipe() pr, pw := io.Pipe()
go func() { // todo: the client should probably be designed more around the actual api go func() { // todo: the client should probably be designed more around the actual api
_, err := io.Copy(pw, resp.body) _, err := io.Copy(pw, resp.Body)
if err != nil { if err != nil {
pw.CloseWithError(err) _ = pw.CloseWithError(err)
return return
} }
defer func() { defer func() {
@ -52,29 +52,29 @@ func (cli *Client) PluginInstall(ctx context.Context, name string, options types
}() }()
if len(options.Args) > 0 { if len(options.Args) > 0 {
if err := cli.PluginSet(ctx, name, options.Args); err != nil { if err := cli.PluginSet(ctx, name, options.Args); err != nil {
pw.CloseWithError(err) _ = pw.CloseWithError(err)
return return
} }
} }
if options.Disabled { if options.Disabled {
pw.Close() _ = pw.Close()
return return
} }
enableErr := cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0}) enableErr := cli.PluginEnable(ctx, name, types.PluginEnableOptions{Timeout: 0})
pw.CloseWithError(enableErr) _ = pw.CloseWithError(enableErr)
}() }()
return pr, nil return pr, nil
} }
func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) { func (cli *Client) tryPluginPrivileges(ctx context.Context, query url.Values, registryAuth string) (*http.Response, error) {
return cli.get(ctx, "/plugins/privileges", query, http.Header{ return cli.get(ctx, "/plugins/privileges", query, http.Header{
registry.AuthHeader: {registryAuth}, registry.AuthHeader: {registryAuth},
}) })
} }
func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileges types.PluginPrivileges, registryAuth string) (serverResponse, error) { func (cli *Client) tryPluginPull(ctx context.Context, query url.Values, privileges types.PluginPrivileges, registryAuth string) (*http.Response, error) {
return cli.post(ctx, "/plugins/pull", query, privileges, http.Header{ return cli.post(ctx, "/plugins/pull", query, privileges, http.Header{
registry.AuthHeader: {registryAuth}, registry.AuthHeader: {registryAuth},
}) })
@ -98,7 +98,7 @@ func (cli *Client) checkPluginPermissions(ctx context.Context, query url.Values,
} }
var privileges types.PluginPrivileges var privileges types.PluginPrivileges
if err := json.NewDecoder(resp.body).Decode(&privileges); err != nil { if err := json.NewDecoder(resp.Body).Decode(&privileges); err != nil {
ensureReaderClosed(resp) ensureReaderClosed(resp)
return nil, err return nil, err
} }

View File

@ -28,6 +28,6 @@ func (cli *Client) PluginList(ctx context.Context, filter filters.Args) (types.P
return plugins, err return plugins, err
} }
err = json.NewDecoder(resp.body).Decode(&plugins) err = json.NewDecoder(resp.Body).Decode(&plugins)
return plugins, err return plugins, err
} }

View File

@ -20,5 +20,5 @@ func (cli *Client) PluginPush(ctx context.Context, name string, registryAuth str
if err != nil { if err != nil {
return nil, err return nil, err
} }
return resp.body, nil return resp.Body, nil
} }

View File

@ -37,10 +37,10 @@ func (cli *Client) PluginUpgrade(ctx context.Context, name string, options types
if err != nil { if err != nil {
return nil, err return nil, err
} }
return resp.body, nil return resp.Body, nil
} }
func (cli *Client) tryPluginUpgrade(ctx context.Context, query url.Values, privileges types.PluginPrivileges, name, registryAuth string) (serverResponse, error) { func (cli *Client) tryPluginUpgrade(ctx context.Context, query url.Values, privileges types.PluginPrivileges, name, registryAuth string) (*http.Response, error) {
return cli.post(ctx, "/plugins/"+name+"/upgrade", query, privileges, http.Header{ return cli.post(ctx, "/plugins/"+name+"/upgrade", query, privileges, http.Header{
registry.AuthHeader: {registryAuth}, registry.AuthHeader: {registryAuth},
}) })

View File

@ -19,47 +19,39 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// serverResponse is a wrapper for http API responses.
type serverResponse struct {
body io.ReadCloser
header http.Header
statusCode int
reqURL *url.URL
}
// head sends an http request to the docker API using the method HEAD. // head sends an http request to the docker API using the method HEAD.
func (cli *Client) head(ctx context.Context, path string, query url.Values, headers http.Header) (serverResponse, error) { func (cli *Client) head(ctx context.Context, path string, query url.Values, headers http.Header) (*http.Response, error) {
return cli.sendRequest(ctx, http.MethodHead, path, query, nil, headers) return cli.sendRequest(ctx, http.MethodHead, path, query, nil, headers)
} }
// get sends an http request to the docker API using the method GET with a specific Go context. // get sends an http request to the docker API using the method GET with a specific Go context.
func (cli *Client) get(ctx context.Context, path string, query url.Values, headers http.Header) (serverResponse, error) { func (cli *Client) get(ctx context.Context, path string, query url.Values, headers http.Header) (*http.Response, error) {
return cli.sendRequest(ctx, http.MethodGet, path, query, nil, headers) return cli.sendRequest(ctx, http.MethodGet, path, query, nil, headers)
} }
// post sends an http request to the docker API using the method POST with a specific Go context. // post sends an http request to the docker API using the method POST with a specific Go context.
func (cli *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers http.Header) (serverResponse, error) { func (cli *Client) post(ctx context.Context, path string, query url.Values, obj interface{}, headers http.Header) (*http.Response, error) {
body, headers, err := encodeBody(obj, headers) body, headers, err := encodeBody(obj, headers)
if err != nil { if err != nil {
return serverResponse{}, err return nil, err
} }
return cli.sendRequest(ctx, http.MethodPost, path, query, body, headers) return cli.sendRequest(ctx, http.MethodPost, path, query, body, headers)
} }
func (cli *Client) postRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers http.Header) (serverResponse, error) { func (cli *Client) postRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers http.Header) (*http.Response, error) {
return cli.sendRequest(ctx, http.MethodPost, path, query, body, headers) return cli.sendRequest(ctx, http.MethodPost, path, query, body, headers)
} }
func (cli *Client) put(ctx context.Context, path string, query url.Values, obj interface{}, headers http.Header) (serverResponse, error) { func (cli *Client) put(ctx context.Context, path string, query url.Values, obj interface{}, headers http.Header) (*http.Response, error) {
body, headers, err := encodeBody(obj, headers) body, headers, err := encodeBody(obj, headers)
if err != nil { if err != nil {
return serverResponse{}, err return nil, err
} }
return cli.putRaw(ctx, path, query, body, headers) return cli.putRaw(ctx, path, query, body, headers)
} }
// putRaw sends an http request to the docker API using the method PUT. // putRaw sends an http request to the docker API using the method PUT.
func (cli *Client) putRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers http.Header) (serverResponse, error) { func (cli *Client) putRaw(ctx context.Context, path string, query url.Values, body io.Reader, headers http.Header) (*http.Response, error) {
// PUT requests are expected to always have a body (apparently) // PUT requests are expected to always have a body (apparently)
// so explicitly pass an empty body to sendRequest to signal that // so explicitly pass an empty body to sendRequest to signal that
// it should set the Content-Type header if not already present. // it should set the Content-Type header if not already present.
@ -70,7 +62,7 @@ func (cli *Client) putRaw(ctx context.Context, path string, query url.Values, bo
} }
// delete sends an http request to the docker API using the method DELETE. // delete sends an http request to the docker API using the method DELETE.
func (cli *Client) delete(ctx context.Context, path string, query url.Values, headers http.Header) (serverResponse, error) { func (cli *Client) delete(ctx context.Context, path string, query url.Values, headers http.Header) (*http.Response, error) {
return cli.sendRequest(ctx, http.MethodDelete, path, query, nil, headers) return cli.sendRequest(ctx, http.MethodDelete, path, query, nil, headers)
} }
@ -116,42 +108,40 @@ func (cli *Client) buildRequest(ctx context.Context, method, path string, body i
return req, nil return req, nil
} }
func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers http.Header) (serverResponse, error) { func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers http.Header) (*http.Response, error) {
req, err := cli.buildRequest(ctx, method, cli.getAPIPath(ctx, path, query), body, headers) req, err := cli.buildRequest(ctx, method, cli.getAPIPath(ctx, path, query), body, headers)
if err != nil { if err != nil {
return serverResponse{}, err return nil, err
} }
resp, err := cli.doRequest(req) resp, err := cli.doRequest(req)
switch { switch {
case errors.Is(err, context.Canceled): case errors.Is(err, context.Canceled):
return serverResponse{}, errdefs.Cancelled(err) return nil, errdefs.Cancelled(err)
case errors.Is(err, context.DeadlineExceeded): case errors.Is(err, context.DeadlineExceeded):
return serverResponse{}, errdefs.Deadline(err) return nil, errdefs.Deadline(err)
case err == nil: case err == nil:
err = cli.checkResponseErr(resp) return resp, cli.checkResponseErr(resp)
default:
return resp, err
} }
return resp, errdefs.FromStatusCode(err, resp.statusCode)
} }
// FIXME(thaJeztah): Should this actually return a serverResp when a connection error occurred? func (cli *Client) doRequest(req *http.Request) (*http.Response, error) {
func (cli *Client) doRequest(req *http.Request) (serverResponse, error) {
serverResp := serverResponse{statusCode: -1, reqURL: req.URL}
resp, err := cli.client.Do(req) resp, err := cli.client.Do(req)
if err != nil { if err != nil {
if cli.scheme != "https" && strings.Contains(err.Error(), "malformed HTTP response") { if cli.scheme != "https" && strings.Contains(err.Error(), "malformed HTTP response") {
return serverResp, errConnectionFailed{fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err)} return nil, errConnectionFailed{fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err)}
} }
if cli.scheme == "https" && strings.Contains(err.Error(), "bad certificate") { if cli.scheme == "https" && strings.Contains(err.Error(), "bad certificate") {
return serverResp, errConnectionFailed{errors.Wrap(err, "the server probably has client authentication (--tlsverify) enabled; check your TLS client certification settings")} return nil, errConnectionFailed{errors.Wrap(err, "the server probably has client authentication (--tlsverify) enabled; check your TLS client certification settings")}
} }
// Don't decorate context sentinel errors; users may be comparing to // Don't decorate context sentinel errors; users may be comparing to
// them directly. // them directly.
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return serverResp, err return nil, err
} }
var uErr *url.Error var uErr *url.Error
@ -159,7 +149,7 @@ func (cli *Client) doRequest(req *http.Request) (serverResponse, error) {
var nErr *net.OpError var nErr *net.OpError
if errors.As(uErr.Err, &nErr) { if errors.As(uErr.Err, &nErr) {
if os.IsPermission(nErr.Err) { if os.IsPermission(nErr.Err) {
return serverResp, errConnectionFailed{errors.Wrapf(err, "permission denied while trying to connect to the Docker daemon socket at %v", cli.host)} return nil, errConnectionFailed{errors.Wrapf(err, "permission denied while trying to connect to the Docker daemon socket at %v", cli.host)}
} }
} }
} }
@ -168,10 +158,10 @@ func (cli *Client) doRequest(req *http.Request) (serverResponse, error) {
if errors.As(err, &nErr) { if errors.As(err, &nErr) {
// FIXME(thaJeztah): any net.Error should be considered a connection error (but we should include the original error)? // FIXME(thaJeztah): any net.Error should be considered a connection error (but we should include the original error)?
if nErr.Timeout() { if nErr.Timeout() {
return serverResp, connectionFailed(cli.host) return nil, connectionFailed(cli.host)
} }
if strings.Contains(nErr.Error(), "connection refused") || strings.Contains(nErr.Error(), "dial unix") { if strings.Contains(nErr.Error(), "connection refused") || strings.Contains(nErr.Error(), "dial unix") {
return serverResp, connectionFailed(cli.host) return nil, connectionFailed(cli.host)
} }
} }
@ -195,28 +185,37 @@ func (cli *Client) doRequest(req *http.Request) (serverResponse, error) {
} }
} }
return serverResp, errConnectionFailed{errors.Wrap(err, "error during connect")} return nil, errConnectionFailed{errors.Wrap(err, "error during connect")}
} }
if resp != nil { return resp, nil
serverResp.statusCode = resp.StatusCode
serverResp.body = resp.Body
serverResp.header = resp.Header
}
return serverResp, nil
} }
func (cli *Client) checkResponseErr(serverResp serverResponse) error { func (cli *Client) checkResponseErr(serverResp *http.Response) (retErr error) {
if serverResp.statusCode >= 200 && serverResp.statusCode < 400 { if serverResp == nil {
return nil return nil
} }
if serverResp.StatusCode >= 200 && serverResp.StatusCode < 400 {
return nil
}
defer func() {
retErr = errdefs.FromStatusCode(retErr, serverResp.StatusCode)
}()
var body []byte var body []byte
var err error var err error
if serverResp.body != nil { var reqURL string
if serverResp.Request != nil {
reqURL = serverResp.Request.URL.String()
}
statusMsg := serverResp.Status
if statusMsg == "" {
statusMsg = http.StatusText(serverResp.StatusCode)
}
if serverResp.Body != nil {
bodyMax := 1 * 1024 * 1024 // 1 MiB bodyMax := 1 * 1024 * 1024 // 1 MiB
bodyR := &io.LimitedReader{ bodyR := &io.LimitedReader{
R: serverResp.body, R: serverResp.Body,
N: int64(bodyMax), N: int64(bodyMax),
} }
body, err = io.ReadAll(bodyR) body, err = io.ReadAll(bodyR)
@ -224,15 +223,21 @@ func (cli *Client) checkResponseErr(serverResp serverResponse) error {
return err return err
} }
if bodyR.N == 0 { if bodyR.N == 0 {
return fmt.Errorf("request returned %s with a message (> %d bytes) for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), bodyMax, serverResp.reqURL) if reqURL != "" {
return fmt.Errorf("request returned %s with a message (> %d bytes) for API route and version %s, check if the server supports the requested API version", statusMsg, bodyMax, reqURL)
}
return fmt.Errorf("request returned %s with a message (> %d bytes); check if the server supports the requested API version", statusMsg, bodyMax)
} }
} }
if len(body) == 0 { if len(body) == 0 {
return fmt.Errorf("request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), serverResp.reqURL) if reqURL != "" {
return fmt.Errorf("request returned %s for API route and version %s, check if the server supports the requested API version", statusMsg, reqURL)
}
return fmt.Errorf("request returned %s; check if the server supports the requested API version", statusMsg)
} }
var daemonErr error var daemonErr error
if serverResp.header.Get("Content-Type") == "application/json" && (cli.version == "" || versions.GreaterThan(cli.version, "1.23")) { if serverResp.Header.Get("Content-Type") == "application/json" && (cli.version == "" || versions.GreaterThan(cli.version, "1.23")) {
var errorResponse types.ErrorResponse var errorResponse types.ErrorResponse
if err := json.Unmarshal(body, &errorResponse); err != nil { if err := json.Unmarshal(body, &errorResponse); err != nil {
return errors.Wrap(err, "Error reading JSON") return errors.Wrap(err, "Error reading JSON")
@ -255,8 +260,8 @@ func (cli *Client) checkResponseErr(serverResp serverResponse) error {
// //
// TODO(thaJeztah): consider adding a log.Debug to allow clients to debug the actual response when enabling debug logging. // TODO(thaJeztah): consider adding a log.Debug to allow clients to debug the actual response when enabling debug logging.
daemonErr = fmt.Errorf(`API returned a %d (%s) but provided no error-message`, daemonErr = fmt.Errorf(`API returned a %d (%s) but provided no error-message`,
serverResp.statusCode, serverResp.StatusCode,
http.StatusText(serverResp.statusCode), http.StatusText(serverResp.StatusCode),
) )
} else { } else {
daemonErr = errors.New(strings.TrimSpace(errorResponse.Message)) daemonErr = errors.New(strings.TrimSpace(errorResponse.Message))
@ -305,10 +310,16 @@ func encodeData(data interface{}) (*bytes.Buffer, error) {
return params, nil return params, nil
} }
func ensureReaderClosed(response serverResponse) { func ensureReaderClosed(response *http.Response) {
if response.body != nil { if response != nil && response.Body != nil {
// Drain up to 512 bytes and close the body to let the Transport reuse the connection // Drain up to 512 bytes and close the body to let the Transport reuse the connection
_, _ = io.CopyN(io.Discard, response.body, 512) // see https://github.com/google/go-github/pull/317/files#r57536827
_ = response.body.Close() //
// TODO(thaJeztah): see if this optimization is still needed, or already implemented in stdlib,
// and check if context-cancellation should handle this as well. If still needed, consider
// wrapping response.Body, or returning a "closer()" from [Client.sendRequest] and related
// methods.
_, _ = io.CopyN(io.Discard, response.Body, 512)
_ = response.Body.Close()
} }
} }

View File

@ -10,16 +10,16 @@ import (
// SecretCreate creates a new secret. // SecretCreate creates a new secret.
func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) { func (cli *Client) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) {
var response types.SecretCreateResponse
if err := cli.NewVersionError(ctx, "1.25", "secret create"); err != nil { if err := cli.NewVersionError(ctx, "1.25", "secret create"); err != nil {
return response, err return types.SecretCreateResponse{}, err
} }
resp, err := cli.post(ctx, "/secrets/create", nil, secret, nil) resp, err := cli.post(ctx, "/secrets/create", nil, secret, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return response, err return types.SecretCreateResponse{}, err
} }
err = json.NewDecoder(resp.body).Decode(&response) var response types.SecretCreateResponse
err = json.NewDecoder(resp.Body).Decode(&response)
return response, err return response, err
} }

View File

@ -24,7 +24,7 @@ func (cli *Client) SecretInspectWithRaw(ctx context.Context, id string) (swarm.S
return swarm.Secret{}, nil, err return swarm.Secret{}, nil, err
} }
body, err := io.ReadAll(resp.body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return swarm.Secret{}, nil, err return swarm.Secret{}, nil, err
} }

View File

@ -33,6 +33,6 @@ func (cli *Client) SecretList(ctx context.Context, options types.SecretListOptio
} }
var secrets []swarm.Secret var secrets []swarm.Secret
err = json.NewDecoder(resp.body).Decode(&secrets) err = json.NewDecoder(resp.Body).Decode(&secrets)
return secrets, err return secrets, err
} }

View File

@ -73,7 +73,7 @@ func (cli *Client) ServiceCreate(ctx context.Context, service swarm.ServiceSpec,
return response, err return response, err
} }
err = json.NewDecoder(resp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
if resolveWarning != "" { if resolveWarning != "" {
response.Warnings = append(response.Warnings, resolveWarning) response.Warnings = append(response.Warnings, resolveWarning)
} }

View File

@ -21,13 +21,13 @@ func (cli *Client) ServiceInspectWithRaw(ctx context.Context, serviceID string,
query := url.Values{} query := url.Values{}
query.Set("insertDefaults", fmt.Sprintf("%v", opts.InsertDefaults)) query.Set("insertDefaults", fmt.Sprintf("%v", opts.InsertDefaults))
serverResp, err := cli.get(ctx, "/services/"+serviceID, query, nil) resp, err := cli.get(ctx, "/services/"+serviceID, query, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return swarm.Service{}, nil, err return swarm.Service{}, nil, err
} }
body, err := io.ReadAll(serverResp.body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return swarm.Service{}, nil, err return swarm.Service{}, nil, err
} }

View File

@ -34,6 +34,6 @@ func (cli *Client) ServiceList(ctx context.Context, options types.ServiceListOpt
} }
var services []swarm.Service var services []swarm.Service
err = json.NewDecoder(resp.body).Decode(&services) err = json.NewDecoder(resp.Body).Decode(&services)
return services, err return services, err
} }

View File

@ -53,5 +53,5 @@ func (cli *Client) ServiceLogs(ctx context.Context, serviceID string, options co
if err != nil { if err != nil {
return nil, err return nil, err
} }
return resp.body, nil return resp.Body, nil
} }

View File

@ -80,8 +80,8 @@ func (cli *Client) ServiceUpdate(ctx context.Context, serviceID string, version
return swarm.ServiceUpdateResponse{}, err return swarm.ServiceUpdateResponse{}, err
} }
response := swarm.ServiceUpdateResponse{} var response swarm.ServiceUpdateResponse
err = json.NewDecoder(resp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
if resolveWarning != "" { if resolveWarning != "" {
response.Warnings = append(response.Warnings, resolveWarning) response.Warnings = append(response.Warnings, resolveWarning)
} }

View File

@ -9,13 +9,13 @@ import (
// SwarmGetUnlockKey retrieves the swarm's unlock key. // SwarmGetUnlockKey retrieves the swarm's unlock key.
func (cli *Client) SwarmGetUnlockKey(ctx context.Context) (types.SwarmUnlockKeyResponse, error) { func (cli *Client) SwarmGetUnlockKey(ctx context.Context) (types.SwarmUnlockKeyResponse, error) {
serverResp, err := cli.get(ctx, "/swarm/unlockkey", nil, nil) resp, err := cli.get(ctx, "/swarm/unlockkey", nil, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return types.SwarmUnlockKeyResponse{}, err return types.SwarmUnlockKeyResponse{}, err
} }
var response types.SwarmUnlockKeyResponse var response types.SwarmUnlockKeyResponse
err = json.NewDecoder(serverResp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
return response, err return response, err
} }

View File

@ -9,13 +9,13 @@ import (
// SwarmInit initializes the swarm. // SwarmInit initializes the swarm.
func (cli *Client) SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) { func (cli *Client) SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) {
serverResp, err := cli.post(ctx, "/swarm/init", nil, req, nil) resp, err := cli.post(ctx, "/swarm/init", nil, req, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return "", err return "", err
} }
var response string var response string
err = json.NewDecoder(serverResp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
return response, err return response, err
} }

View File

@ -9,13 +9,13 @@ import (
// SwarmInspect inspects the swarm. // SwarmInspect inspects the swarm.
func (cli *Client) SwarmInspect(ctx context.Context) (swarm.Swarm, error) { func (cli *Client) SwarmInspect(ctx context.Context) (swarm.Swarm, error) {
serverResp, err := cli.get(ctx, "/swarm", nil, nil) resp, err := cli.get(ctx, "/swarm", nil, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return swarm.Swarm{}, err return swarm.Swarm{}, err
} }
var response swarm.Swarm var response swarm.Swarm
err = json.NewDecoder(serverResp.body).Decode(&response) err = json.NewDecoder(resp.Body).Decode(&response)
return response, err return response, err
} }

View File

@ -8,7 +8,7 @@ import (
// SwarmUnlock unlocks locked swarm. // SwarmUnlock unlocks locked swarm.
func (cli *Client) SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error { func (cli *Client) SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error {
serverResp, err := cli.post(ctx, "/swarm/unlock", nil, req, nil) resp, err := cli.post(ctx, "/swarm/unlock", nil, req, nil)
ensureReaderClosed(serverResp) ensureReaderClosed(resp)
return err return err
} }

View File

@ -16,13 +16,13 @@ func (cli *Client) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm
return swarm.Task{}, nil, err return swarm.Task{}, nil, err
} }
serverResp, err := cli.get(ctx, "/tasks/"+taskID, nil, nil) resp, err := cli.get(ctx, "/tasks/"+taskID, nil, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return swarm.Task{}, nil, err return swarm.Task{}, nil, err
} }
body, err := io.ReadAll(serverResp.body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return swarm.Task{}, nil, err return swarm.Task{}, nil, err
} }

View File

@ -30,6 +30,6 @@ func (cli *Client) TaskList(ctx context.Context, options types.TaskListOptions)
} }
var tasks []swarm.Task var tasks []swarm.Task
err = json.NewDecoder(resp.body).Decode(&tasks) err = json.NewDecoder(resp.Body).Decode(&tasks)
return tasks, err return tasks, err
} }

View File

@ -47,5 +47,5 @@ func (cli *Client) TaskLogs(ctx context.Context, taskID string, options containe
if err != nil { if err != nil {
return nil, err return nil, err
} }
return resp.body, nil return resp.Body, nil
} }

View File

@ -16,6 +16,6 @@ func (cli *Client) ServerVersion(ctx context.Context) (types.Version, error) {
} }
var server types.Version var server types.Version
err = json.NewDecoder(resp.body).Decode(&server) err = json.NewDecoder(resp.Body).Decode(&server)
return server, err return server, err
} }

View File

@ -9,12 +9,13 @@ import (
// VolumeCreate creates a volume in the docker host. // VolumeCreate creates a volume in the docker host.
func (cli *Client) VolumeCreate(ctx context.Context, options volume.CreateOptions) (volume.Volume, error) { func (cli *Client) VolumeCreate(ctx context.Context, options volume.CreateOptions) (volume.Volume, error) {
var vol volume.Volume
resp, err := cli.post(ctx, "/volumes/create", nil, options, nil) resp, err := cli.post(ctx, "/volumes/create", nil, options, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return vol, err return volume.Volume{}, err
} }
err = json.NewDecoder(resp.body).Decode(&vol)
var vol volume.Volume
err = json.NewDecoder(resp.Body).Decode(&vol)
return vol, err return vol, err
} }

View File

@ -22,17 +22,18 @@ func (cli *Client) VolumeInspectWithRaw(ctx context.Context, volumeID string) (v
return volume.Volume{}, nil, err return volume.Volume{}, nil, err
} }
var vol volume.Volume
resp, err := cli.get(ctx, "/volumes/"+volumeID, nil, nil) resp, err := cli.get(ctx, "/volumes/"+volumeID, nil, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return vol, nil, err return volume.Volume{}, nil, err
} }
body, err := io.ReadAll(resp.body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return vol, nil, err return volume.Volume{}, nil, err
} }
var vol volume.Volume
rdr := bytes.NewReader(body) rdr := bytes.NewReader(body)
err = json.NewDecoder(rdr).Decode(&vol) err = json.NewDecoder(rdr).Decode(&vol)
return vol, body, err return vol, body, err

View File

@ -11,23 +11,23 @@ import (
// VolumeList returns the volumes configured in the docker host. // VolumeList returns the volumes configured in the docker host.
func (cli *Client) VolumeList(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) { func (cli *Client) VolumeList(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) {
var volumes volume.ListResponse
query := url.Values{} query := url.Values{}
if options.Filters.Len() > 0 { if options.Filters.Len() > 0 {
//nolint:staticcheck // ignore SA1019 for old code //nolint:staticcheck // ignore SA1019 for old code
filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters) filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters)
if err != nil { if err != nil {
return volumes, err return volume.ListResponse{}, err
} }
query.Set("filters", filterJSON) query.Set("filters", filterJSON)
} }
resp, err := cli.get(ctx, "/volumes", query, nil) resp, err := cli.get(ctx, "/volumes", query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return volumes, err return volume.ListResponse{}, err
} }
err = json.NewDecoder(resp.body).Decode(&volumes) var volumes volume.ListResponse
err = json.NewDecoder(resp.Body).Decode(&volumes)
return volumes, err return volumes, err
} }

View File

@ -20,14 +20,14 @@ func (cli *Client) VolumesPrune(ctx context.Context, pruneFilters filters.Args)
return volume.PruneReport{}, err return volume.PruneReport{}, err
} }
serverResp, err := cli.post(ctx, "/volumes/prune", query, nil, nil) resp, err := cli.post(ctx, "/volumes/prune", query, nil, nil)
defer ensureReaderClosed(serverResp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return volume.PruneReport{}, err return volume.PruneReport{}, err
} }
var report volume.PruneReport var report volume.PruneReport
if err := json.NewDecoder(serverResp.body).Decode(&report); err != nil { if err := json.NewDecoder(resp.Body).Decode(&report); err != nil {
return volume.PruneReport{}, fmt.Errorf("Error retrieving volume prune report: %v", err) return volume.PruneReport{}, fmt.Errorf("Error retrieving volume prune report: %v", err)
} }

View File

@ -1,11 +1,11 @@
package archive package archive
import ( import (
"bytes"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strings"
"syscall" "syscall"
"unsafe" "unsafe"
@ -143,7 +143,7 @@ func (w *walker) walk(path string, i1, i2 os.FileInfo) (err error) {
ni1 := names1[ix1] ni1 := names1[ix1]
ni2 := names2[ix2] ni2 := names2[ix2]
switch bytes.Compare([]byte(ni1.name), []byte(ni2.name)) { switch strings.Compare(ni1.name, ni2.name) {
case -1: // ni1 < ni2 -- advance ni1 case -1: // ni1 < ni2 -- advance ni1
// we will not encounter ni1 in names2 // we will not encounter ni1 in names2
names = append(names, ni1.name) names = append(names, ni1.name)

View File

@ -56,7 +56,6 @@ func (p *JSONProgress) String() string {
width = p.width() width = p.width()
pbBox string pbBox string
numbersBox string numbersBox string
timeLeftBox string
) )
if p.Current <= 0 && p.Total <= 0 { if p.Current <= 0 && p.Total <= 0 {
return "" return ""
@ -104,14 +103,14 @@ func (p *JSONProgress) String() string {
} }
} }
// Show approximation of remaining time if there's enough width.
var timeLeftBox string
if width > 50 {
if p.Current > 0 && p.Start > 0 && percentage < 50 { if p.Current > 0 && p.Start > 0 && percentage < 50 {
fromStart := p.now().Sub(time.Unix(p.Start, 0)) fromStart := p.now().Sub(time.Unix(p.Start, 0))
perEntry := fromStart / time.Duration(p.Current) perEntry := fromStart / time.Duration(p.Current)
left := time.Duration(p.Total-p.Current) * perEntry left := time.Duration(p.Total-p.Current) * perEntry
left = (left / time.Second) * time.Second timeLeftBox = " " + left.Round(time.Second).String()
if width > 50 {
timeLeftBox = " " + left.String()
} }
} }
return pbBox + numbersBox + timeLeftBox return pbBox + numbersBox + timeLeftBox

2
vendor/modules.txt vendored
View File

@ -55,7 +55,7 @@ github.com/docker/distribution/registry/client/transport
github.com/docker/distribution/registry/storage/cache github.com/docker/distribution/registry/storage/cache
github.com/docker/distribution/registry/storage/cache/memory github.com/docker/distribution/registry/storage/cache/memory
github.com/docker/distribution/uuid github.com/docker/distribution/uuid
# github.com/docker/docker v28.0.0-rc.1.0.20250211164921-b570831cc3a3+incompatible # github.com/docker/docker v28.0.0-rc.1.0.20250214181517-5cc3f1dab895+incompatible
## explicit ## explicit
github.com/docker/docker/api github.com/docker/docker/api
github.com/docker/docker/api/types github.com/docker/docker/api/types