Merge pull request #51278 from vvoland/client-container-opts2

client_(attach,commit,create,diff): Wrap result and options
This commit is contained in:
Sebastiaan van Stijn
2025-10-24 13:41:23 +02:00
committed by GitHub
32 changed files with 498 additions and 249 deletions

View File

@@ -12,7 +12,6 @@ import (
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/system"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// APIClient is an interface that clients that talk with a docker server must implement.
@@ -58,10 +57,10 @@ type HijackDialer interface {
// ContainerAPIClient defines API client methods for the containers
type ContainerAPIClient interface {
ContainerAttach(ctx context.Context, container string, options ContainerAttachOptions) (HijackedResponse, error)
ContainerCommit(ctx context.Context, container string, options ContainerCommitOptions) (container.CommitResponse, error)
ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error)
ContainerDiff(ctx context.Context, container string) ([]container.FilesystemChange, error)
ContainerAttach(ctx context.Context, container string, options ContainerAttachOptions) (ContainerAttachResult, error)
ContainerCommit(ctx context.Context, container string, options ContainerCommitOptions) (ContainerCommitResult, error)
ContainerCreate(ctx context.Context, options ContainerCreateOptions) (ContainerCreateResult, error)
ContainerDiff(ctx context.Context, container string, options ContainerDiffOptions) (ContainerDiffResult, error)
ExecAPIClient
ContainerExport(ctx context.Context, container string) (io.ReadCloser, error)
ContainerInspect(ctx context.Context, container string) (container.InspectResponse, error)

View File

@@ -16,6 +16,11 @@ type ContainerAttachOptions struct {
Logs bool
}
// ContainerAttachResult is the result from attaching to a container.
type ContainerAttachResult struct {
HijackedResponse
}
// ContainerAttach attaches a connection to a container in the server.
// It returns a [HijackedResponse] with the hijacked connection
// and a reader to get output. It's up to the called to close
@@ -44,10 +49,10 @@ type ContainerAttachOptions struct {
// [stdcopy.StdType]: https://pkg.go.dev/github.com/moby/moby/api/pkg/stdcopy#StdType
// [Stdout]: https://pkg.go.dev/github.com/moby/moby/api/pkg/stdcopy#Stdout
// [Stderr]: https://pkg.go.dev/github.com/moby/moby/api/pkg/stdcopy#Stderr
func (cli *Client) ContainerAttach(ctx context.Context, containerID string, options ContainerAttachOptions) (HijackedResponse, error) {
func (cli *Client) ContainerAttach(ctx context.Context, containerID string, options ContainerAttachOptions) (ContainerAttachResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
return HijackedResponse{}, err
return ContainerAttachResult{}, err
}
query := url.Values{}
@@ -70,7 +75,12 @@ func (cli *Client) ContainerAttach(ctx context.Context, containerID string, opti
query.Set("logs", "1")
}
return cli.postHijacked(ctx, "/containers/"+containerID+"/attach", query, nil, http.Header{
hijacked, err := cli.postHijacked(ctx, "/containers/"+containerID+"/attach", query, nil, http.Header{
"Content-Type": {"text/plain"},
})
if err != nil {
return ContainerAttachResult{}, err
}
return ContainerAttachResult{HijackedResponse: hijacked}, nil
}

View File

@@ -20,22 +20,27 @@ type ContainerCommitOptions struct {
Config *container.Config
}
// ContainerCommitResult is the result from committing a container.
type ContainerCommitResult struct {
ID string
}
// ContainerCommit applies changes to a container and creates a new tagged image.
func (cli *Client) ContainerCommit(ctx context.Context, containerID string, options ContainerCommitOptions) (container.CommitResponse, error) {
func (cli *Client) ContainerCommit(ctx context.Context, containerID string, options ContainerCommitOptions) (ContainerCommitResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
return container.CommitResponse{}, err
return ContainerCommitResult{}, err
}
var repository, tag string
if options.Reference != "" {
ref, err := reference.ParseNormalizedNamed(options.Reference)
if err != nil {
return container.CommitResponse{}, err
return ContainerCommitResult{}, err
}
if _, ok := ref.(reference.Digested); ok {
return container.CommitResponse{}, errors.New("refusing to create a tag with a digest reference")
return ContainerCommitResult{}, errors.New("refusing to create a tag with a digest reference")
}
ref = reference.TagNameOnly(ref)
@@ -62,9 +67,9 @@ func (cli *Client) ContainerCommit(ctx context.Context, containerID string, opti
resp, err := cli.post(ctx, "/commit", query, options.Config, nil)
defer ensureReaderClosed(resp)
if err != nil {
return response, err
return ContainerCommitResult{}, err
}
err = json.NewDecoder(resp.Body).Decode(&response)
return response, err
return ContainerCommitResult{ID: response.ID}, err
}

View File

@@ -10,49 +10,63 @@ import (
cerrdefs "github.com/containerd/errdefs"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// ContainerCreate creates a new container based on the given configuration.
// It can be associated with a name, but it's not mandatory.
func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
if config == nil {
return container.CreateResponse{}, cerrdefs.ErrInvalidArgument.WithMessage("config is nil")
func (cli *Client) ContainerCreate(ctx context.Context, options ContainerCreateOptions) (ContainerCreateResult, error) {
cfg := options.Config
if cfg == nil {
cfg = &container.Config{}
}
if options.Image != "" {
if cfg.Image != "" {
return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("either Image or config.Image should be set")
}
newCfg := *cfg
newCfg.Image = options.Image
cfg = &newCfg
}
if cfg.Image == "" {
return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("config.Image or Image is required")
}
var response container.CreateResponse
if hostConfig != nil {
hostConfig.CapAdd = normalizeCapabilities(hostConfig.CapAdd)
hostConfig.CapDrop = normalizeCapabilities(hostConfig.CapDrop)
if options.HostConfig != nil {
options.HostConfig.CapAdd = normalizeCapabilities(options.HostConfig.CapAdd)
options.HostConfig.CapDrop = normalizeCapabilities(options.HostConfig.CapDrop)
}
query := url.Values{}
if platform != nil {
if p := formatPlatform(*platform); p != "unknown" {
if options.Platform != nil {
if p := formatPlatform(*options.Platform); p != "unknown" {
query.Set("platform", p)
}
}
if containerName != "" {
query.Set("name", containerName)
if options.Name != "" {
query.Set("name", options.Name)
}
body := container.CreateRequest{
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
Config: cfg,
HostConfig: options.HostConfig,
NetworkingConfig: options.NetworkingConfig,
}
resp, err := cli.post(ctx, "/containers/create", query, body, nil)
defer ensureReaderClosed(resp)
if err != nil {
return response, err
return ContainerCreateResult{}, err
}
err = json.NewDecoder(resp.Body).Decode(&response)
return response, err
return ContainerCreateResult{ID: response.ID, Warnings: response.Warnings}, err
}
// formatPlatform returns a formatted string representing platform (e.g., "linux/arm/v7").

View File

@@ -0,0 +1,25 @@
package client
import (
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// ContainerCreateOptions holds parameters to create a container.
type ContainerCreateOptions struct {
Config *container.Config
HostConfig *container.HostConfig
NetworkingConfig *network.NetworkingConfig
Platform *ocispec.Platform
Name string
// Image is a shortcut for Config.Image - only one of Image or Config.Image should be set.
Image string
}
// ContainerCreateResult is the result from creating a container.
type ContainerCreateResult struct {
ID string
Warnings []string
}

View File

@@ -20,21 +20,12 @@ func TestContainerCreateError(t *testing.T) {
)
assert.NilError(t, err)
_, err = client.ContainerCreate(context.Background(), nil, nil, nil, nil, "nothing")
assert.Error(t, err, "config is nil")
_, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: nil, Name: "nothing"})
assert.Error(t, err, "config.Image or Image is required")
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
_, err = client.ContainerCreate(context.Background(), &container.Config{}, nil, nil, nil, "nothing")
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
// 404 doesn't automatically means an unknown image
client, err = NewClientWithOpts(
WithMockClient(errorMock(http.StatusNotFound, "Server error")),
)
assert.NilError(t, err)
_, err = client.ContainerCreate(context.Background(), &container.Config{}, nil, nil, nil, "nothing")
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
_, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{}, Name: "nothing"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
}
func TestContainerCreateImageNotFound(t *testing.T) {
@@ -43,7 +34,7 @@ func TestContainerCreateImageNotFound(t *testing.T) {
)
assert.NilError(t, err)
_, err = client.ContainerCreate(context.Background(), &container.Config{Image: "unknown_image"}, nil, nil, nil, "unknown")
_, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{Image: "unknown_image"}, Name: "unknown"})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
}
@@ -65,7 +56,7 @@ func TestContainerCreateWithName(t *testing.T) {
)
assert.NilError(t, err)
r, err := client.ContainerCreate(context.Background(), &container.Config{}, nil, nil, nil, "container_name")
r, err := client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{Image: "test"}, Name: "container_name"})
assert.NilError(t, err)
assert.Check(t, is.Equal(r.ID, "container_id"))
}
@@ -87,7 +78,7 @@ func TestContainerCreateAutoRemove(t *testing.T) {
)
assert.NilError(t, err)
resp, err := client.ContainerCreate(context.Background(), &container.Config{}, &container.HostConfig{AutoRemove: true}, nil, nil, "")
resp, err := client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{Image: "test"}, HostConfig: &container.HostConfig{AutoRemove: true}})
assert.NilError(t, err)
assert.Check(t, is.Equal(resp.ID, "container_id"))
}
@@ -100,7 +91,7 @@ func TestContainerCreateConnectionError(t *testing.T) {
client, err := NewClientWithOpts(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid"))
assert.NilError(t, err)
_, err = client.ContainerCreate(context.Background(), &container.Config{}, nil, nil, nil, "")
_, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{Image: "test"}})
assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
}
@@ -142,6 +133,6 @@ func TestContainerCreateCapabilities(t *testing.T) {
)
assert.NilError(t, err)
_, err = client.ContainerCreate(context.Background(), &container.Config{}, &container.HostConfig{CapAdd: inputCaps, CapDrop: inputCaps}, nil, nil, "")
_, err = client.ContainerCreate(context.Background(), ContainerCreateOptions{Config: &container.Config{Image: "test"}, HostConfig: &container.HostConfig{CapAdd: inputCaps, CapDrop: inputCaps}})
assert.NilError(t, err)
}

View File

@@ -9,22 +9,22 @@ import (
)
// ContainerDiff shows differences in a container filesystem since it was started.
func (cli *Client) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) {
func (cli *Client) ContainerDiff(ctx context.Context, containerID string, options ContainerDiffOptions) (ContainerDiffResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
return nil, err
return ContainerDiffResult{}, err
}
resp, err := cli.get(ctx, "/containers/"+containerID+"/changes", url.Values{}, nil)
defer ensureReaderClosed(resp)
if err != nil {
return nil, err
return ContainerDiffResult{}, err
}
var changes []container.FilesystemChange
err = json.NewDecoder(resp.Body).Decode(&changes)
if err != nil {
return nil, err
return ContainerDiffResult{}, err
}
return changes, err
return ContainerDiffResult{Changes: changes}, err
}

View File

@@ -0,0 +1,13 @@
package client
import "github.com/moby/moby/api/types/container"
// ContainerDiffOptions holds parameters to show differences in a container filesystem.
type ContainerDiffOptions struct {
// Currently no options, but this allows for future extensibility
}
// ContainerDiffResult is the result from showing differences in a container filesystem.
type ContainerDiffResult struct {
Changes []container.FilesystemChange
}

View File

@@ -17,14 +17,14 @@ func TestContainerDiffError(t *testing.T) {
)
assert.NilError(t, err)
_, err = client.ContainerDiff(context.Background(), "nothing")
_, err = client.ContainerDiff(context.Background(), "nothing", ContainerDiffOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
_, err = client.ContainerDiff(context.Background(), "")
_, err = client.ContainerDiff(context.Background(), "", ContainerDiffOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
_, err = client.ContainerDiff(context.Background(), " ")
_, err = client.ContainerDiff(context.Background(), " ", ContainerDiffOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.Check(t, is.ErrorContains(err, "value is empty"))
}
@@ -57,7 +57,7 @@ func TestContainerDiff(t *testing.T) {
)
assert.NilError(t, err)
changes, err := client.ContainerDiff(context.Background(), "container_id")
result, err := client.ContainerDiff(context.Background(), "container_id", ContainerDiffOptions{})
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(changes, expected))
assert.Check(t, is.DeepEqual(result.Changes, expected))
}

View File

@@ -130,12 +130,12 @@ func (s *DockerAPISuite) TestContainerAPIGetChanges(c *testing.T) {
assert.NilError(c, err)
defer apiClient.Close()
changes, err := apiClient.ContainerDiff(testutil.GetContext(c), name)
result, err := apiClient.ContainerDiff(testutil.GetContext(c), name, client.ContainerDiffOptions{})
assert.NilError(c, err)
// Check the changelog for removal of /etc/passwd
success := false
for _, elem := range changes {
for _, elem := range result.Changes {
if elem.Path == "/etc/passwd" && elem.Kind == 2 {
success = true
}
@@ -517,7 +517,11 @@ func (s *DockerAPISuite) TestContainerAPIBadPort(c *testing.T) {
assert.NilError(c, err)
defer apiClient.Close()
_, err = apiClient.ContainerCreate(testutil.GetContext(c), &config, &hostConfig, &network.NetworkingConfig{}, nil, "")
_, err = apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &config,
HostConfig: &hostConfig,
NetworkingConfig: &network.NetworkingConfig{},
})
assert.ErrorContains(c, err, `invalid port specification: "aa80"`)
}
@@ -531,7 +535,11 @@ func (s *DockerAPISuite) TestContainerAPICreate(c *testing.T) {
assert.NilError(c, err)
defer apiClient.Close()
ctr, err := apiClient.ContainerCreate(testutil.GetContext(c), &config, &container.HostConfig{}, &network.NetworkingConfig{}, nil, "")
ctr, err := apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &config,
HostConfig: &container.HostConfig{},
NetworkingConfig: &network.NetworkingConfig{},
})
assert.NilError(c, err)
out := cli.DockerCmd(c, "start", "-a", ctr.ID).Stdout()
@@ -543,9 +551,13 @@ func (s *DockerAPISuite) TestContainerAPICreateEmptyConfig(c *testing.T) {
assert.NilError(c, err)
defer apiClient.Close()
_, err = apiClient.ContainerCreate(testutil.GetContext(c), &container.Config{}, &container.HostConfig{}, &network.NetworkingConfig{}, nil, "")
_, err = apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &container.Config{},
HostConfig: &container.HostConfig{},
NetworkingConfig: &network.NetworkingConfig{},
})
assert.ErrorContains(c, err, "no command specified")
assert.ErrorContains(c, err, "config.Image or Image is required")
}
func (s *DockerAPISuite) TestContainerAPICreateBridgeNetworkMode(c *testing.T) {
@@ -574,7 +586,11 @@ func UtilCreateNetworkMode(t *testing.T, networkMode container.NetworkMode) {
assert.NilError(t, err)
defer apiClient.Close()
ctr, err := apiClient.ContainerCreate(testutil.GetContext(t), &config, &hostConfig, &network.NetworkingConfig{}, nil, "")
ctr, err := apiClient.ContainerCreate(testutil.GetContext(t), client.ContainerCreateOptions{
Config: &config,
HostConfig: &hostConfig,
NetworkingConfig: &network.NetworkingConfig{},
})
assert.NilError(t, err)
containerJSON, err := apiClient.ContainerInspect(testutil.GetContext(t), ctr.ID)
@@ -601,7 +617,11 @@ func (s *DockerAPISuite) TestContainerAPICreateWithCpuSharesCpuset(c *testing.T)
assert.NilError(c, err)
defer apiClient.Close()
ctr, err := apiClient.ContainerCreate(testutil.GetContext(c), &config, &hostConfig, &network.NetworkingConfig{}, nil, "")
ctr, err := apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &config,
HostConfig: &hostConfig,
NetworkingConfig: &network.NetworkingConfig{},
})
assert.NilError(c, err)
containerJSON, err := apiClient.ContainerInspect(testutil.GetContext(c), ctr.ID)
@@ -746,7 +766,12 @@ func (s *DockerAPISuite) TestContainerAPIStart(c *testing.T) {
assert.NilError(c, err)
defer apiClient.Close()
_, err = apiClient.ContainerCreate(testutil.GetContext(c), &config, &container.HostConfig{}, &network.NetworkingConfig{}, nil, name)
_, err = apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &config,
HostConfig: &container.HostConfig{},
NetworkingConfig: &network.NetworkingConfig{},
Name: name,
})
assert.NilError(c, err)
err = apiClient.ContainerStart(testutil.GetContext(c), name, client.ContainerStartOptions{})
@@ -989,7 +1014,12 @@ func (s *DockerAPISuite) TestPostContainersCreateWithWrongCpusetValues(c *testin
}
const name = "wrong-cpuset-cpus"
_, err = apiClient.ContainerCreate(testutil.GetContext(c), &config, &hostConfig1, &network.NetworkingConfig{}, nil, name)
_, err = apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &config,
HostConfig: &hostConfig1,
NetworkingConfig: &network.NetworkingConfig{},
Name: name,
})
expected := "Invalid value 1-42,, for cpuset cpus"
assert.ErrorContains(c, err, expected)
@@ -999,7 +1029,12 @@ func (s *DockerAPISuite) TestPostContainersCreateWithWrongCpusetValues(c *testin
},
}
const name2 = "wrong-cpuset-mems"
_, err = apiClient.ContainerCreate(testutil.GetContext(c), &config, &hostConfig2, &network.NetworkingConfig{}, nil, name2)
_, err = apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &config,
HostConfig: &hostConfig2,
NetworkingConfig: &network.NetworkingConfig{},
Name: name2,
})
expected = "Invalid value 42-3,1-- for cpuset mems"
assert.ErrorContains(c, err, expected)
}
@@ -1015,7 +1050,11 @@ func (s *DockerAPISuite) TestPostContainersCreateMemorySwappinessHostConfigOmitt
assert.NilError(c, err)
defer apiClient.Close()
ctr, err := apiClient.ContainerCreate(testutil.GetContext(c), &config, &container.HostConfig{}, &network.NetworkingConfig{}, nil, "")
ctr, err := apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &config,
HostConfig: &container.HostConfig{},
NetworkingConfig: &network.NetworkingConfig{},
})
assert.NilError(c, err)
containerJSON, err := apiClient.ContainerInspect(testutil.GetContext(c), ctr.ID)
@@ -1042,7 +1081,12 @@ func (s *DockerAPISuite) TestPostContainersCreateWithOomScoreAdjInvalidRange(c *
defer apiClient.Close()
const name = "oomscoreadj-over"
_, err = apiClient.ContainerCreate(testutil.GetContext(c), &config, &hostConfig, &network.NetworkingConfig{}, nil, name)
_, err = apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &config,
HostConfig: &hostConfig,
NetworkingConfig: &network.NetworkingConfig{},
Name: name,
})
expected := "Invalid value 1001, range for oom score adj is [-1000, 1000]"
assert.ErrorContains(c, err, expected)
@@ -1052,7 +1096,12 @@ func (s *DockerAPISuite) TestPostContainersCreateWithOomScoreAdjInvalidRange(c *
}
const name2 = "oomscoreadj-low"
_, err = apiClient.ContainerCreate(testutil.GetContext(c), &config, &hostConfig, &network.NetworkingConfig{}, nil, name2)
_, err = apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &config,
HostConfig: &hostConfig,
NetworkingConfig: &network.NetworkingConfig{},
Name: name2,
})
expected = "Invalid value -1001, range for oom score adj is [-1000, 1000]"
assert.ErrorContains(c, err, expected)
@@ -1085,7 +1134,12 @@ func (s *DockerAPISuite) TestContainerAPIStatsWithNetworkDisabled(c *testing.T)
assert.NilError(c, err)
defer apiClient.Close()
_, err = apiClient.ContainerCreate(testutil.GetContext(c), &config, &container.HostConfig{}, &network.NetworkingConfig{}, nil, name)
_, err = apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &config,
HostConfig: &container.HostConfig{},
NetworkingConfig: &network.NetworkingConfig{},
Name: name,
})
assert.NilError(c, err)
err = apiClient.ContainerStart(testutil.GetContext(c), name, client.ContainerStartOptions{})
@@ -1421,7 +1475,11 @@ func (s *DockerAPISuite) TestContainersAPICreateMountsValidation(c *testing.T) {
// TODO add checks for statuscode returned by API
for i, tc := range tests {
c.Run(fmt.Sprintf("case %d", i), func(c *testing.T) {
_, err = apiClient.ContainerCreate(testutil.GetContext(c), &tc.config, &tc.hostConfig, &network.NetworkingConfig{}, nil, "")
_, err = apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &tc.config,
HostConfig: &tc.hostConfig,
NetworkingConfig: &network.NetworkingConfig{},
})
if tc.msg != "" {
assert.ErrorContains(c, err, tc.msg, "%v", tests[i].config)
} else {
@@ -1454,7 +1512,12 @@ func (s *DockerAPISuite) TestContainerAPICreateMountsBindRead(c *testing.T) {
assert.NilError(c, err)
defer apiClient.Close()
_, err = apiClient.ContainerCreate(testutil.GetContext(c), &config, &hostConfig, &network.NetworkingConfig{}, nil, "test")
_, err = apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &config,
HostConfig: &hostConfig,
NetworkingConfig: &network.NetworkingConfig{},
Name: "test",
})
assert.NilError(c, err)
out := cli.DockerCmd(c, "start", "-a", "test").Combined()
@@ -1590,11 +1653,11 @@ func (s *DockerAPISuite) TestContainersAPICreateMountsCreate(c *testing.T) {
c.Run(fmt.Sprintf("%d config: %v", i, tc.spec), func(c *testing.T) {
ctr, err := apiclient.ContainerCreate(
ctx,
&container.Config{Image: testImg},
&container.HostConfig{Mounts: []mount.Mount{tc.spec}},
&network.NetworkingConfig{},
nil,
"")
client.ContainerCreateOptions{
Config: &container.Config{Image: testImg},
HostConfig: &container.HostConfig{Mounts: []mount.Mount{tc.spec}},
NetworkingConfig: &network.NetworkingConfig{},
})
assert.NilError(c, err)
containerInspect, err := apiclient.ContainerInspect(ctx, ctr.ID)
@@ -1705,7 +1768,12 @@ func (s *DockerAPISuite) TestContainersAPICreateMountsTmpfs(c *testing.T) {
Mounts: []mount.Mount{x.cfg},
}
_, err = apiClient.ContainerCreate(testutil.GetContext(c), &config, &hostConfig, &network.NetworkingConfig{}, nil, cName)
_, err = apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &config,
HostConfig: &hostConfig,
NetworkingConfig: &network.NetworkingConfig{},
Name: cName,
})
assert.NilError(c, err)
out := cli.DockerCmd(c, "start", "-a", cName).Combined()
for _, option := range x.expectedOptions {

View File

@@ -12,6 +12,7 @@ import (
"github.com/Microsoft/go-winio"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/mount"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/client"
"github.com/moby/moby/v2/internal/testutil"
"github.com/pkg/errors"
@@ -51,19 +52,23 @@ func (s *DockerAPISuite) TestContainersAPICreateMountsBindNamedPipe(c *testing.T
ctx := testutil.GetContext(c)
apiClient := testEnv.APIClient()
_, err = apiClient.ContainerCreate(ctx,
&container.Config{
Image: testEnv.PlatformDefaults.BaseImage,
Cmd: []string{"cmd", "/c", cmd},
}, &container.HostConfig{
Mounts: []mount.Mount{
{
Type: "npipe",
Source: hostPipeName,
Target: containerPipeName,
},
client.ContainerCreateOptions{
Config: &container.Config{
Image: testEnv.PlatformDefaults.BaseImage,
Cmd: []string{"cmd", "/c", cmd},
},
HostConfig: &container.HostConfig{
Mounts: []mount.Mount{
{
Type: "npipe",
Source: hostPipeName,
Target: containerPipeName,
},
},
},
nil, nil, name)
NetworkingConfig: &network.NetworkingConfig{},
Name: name,
})
assert.NilError(c, err)
err = apiClient.ContainerStart(ctx, name, client.ContainerStartOptions{})

View File

@@ -596,7 +596,12 @@ func (s *DockerCLIVolumeSuite) TestDuplicateMountpointsForVolumesFromAndMounts(c
},
},
}
_, err = apiClient.ContainerCreate(testutil.GetContext(c), &config, &hostConfig, &network.NetworkingConfig{}, nil, "app")
_, err = apiClient.ContainerCreate(testutil.GetContext(c), client.ContainerCreateOptions{
Config: &config,
HostConfig: &hostConfig,
NetworkingConfig: &network.NetworkingConfig{},
Name: "app",
})
assert.NilError(c, err)

View File

@@ -41,17 +41,15 @@ func TestAttach(t *testing.T) {
t.Parallel()
ctx := testutil.StartSpan(ctx, t)
resp, err := apiClient.ContainerCreate(ctx,
&container.Config{
resp, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &container.Config{
Image: "busybox",
Cmd: []string{"echo", "hello"},
Tty: tc.tty,
},
&container.HostConfig{},
&network.NetworkingConfig{},
nil,
"",
)
HostConfig: &container.HostConfig{},
NetworkingConfig: &network.NetworkingConfig{},
})
assert.NilError(t, err)
attach, err := apiClient.ContainerAttach(ctx, resp.ID, client.ContainerAttachOptions{
Stdout: true,
@@ -81,16 +79,14 @@ func TestAttachDisconnectLeak(t *testing.T) {
apiClient := d.NewClientT(t)
resp, err := apiClient.ContainerCreate(ctx,
&container.Config{
resp, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &container.Config{
Image: "busybox",
Cmd: []string{"/bin/sh", "-c", "while true; usleep 100000; done"},
},
&container.HostConfig{},
&network.NetworkingConfig{},
nil,
"",
)
HostConfig: &container.HostConfig{},
NetworkingConfig: &network.NetworkingConfig{},
})
assert.NilError(t, err)
cID := resp.ID
defer apiClient.ContainerRemove(ctx, cID, client.ContainerRemoveOptions{

View File

@@ -59,13 +59,11 @@ func TestCreateFailsWhenIdentifierDoesNotExist(t *testing.T) {
t.Run(tc.doc, func(t *testing.T) {
t.Parallel()
ctx := testutil.StartSpan(ctx, t)
_, err := apiClient.ContainerCreate(ctx,
&container.Config{Image: tc.image},
&container.HostConfig{},
&network.NetworkingConfig{},
nil,
"",
)
_, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &container.Config{Image: tc.image},
HostConfig: &container.HostConfig{},
NetworkingConfig: &network.NetworkingConfig{},
})
assert.Check(t, is.ErrorContains(err, tc.expectedError))
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
})
@@ -125,15 +123,11 @@ func TestCreateByImageID(t *testing.T) {
t.Run(tc.doc, func(t *testing.T) {
t.Parallel()
ctx := testutil.StartSpan(ctx, t)
resp, err := apiClient.ContainerCreate(ctx,
&container.Config{Image: tc.image},
&container.HostConfig{},
&network.NetworkingConfig{},
nil,
"",
)
resp, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &container.Config{Image: tc.image},
})
if tc.expectedErr != "" {
assert.Check(t, is.DeepEqual(resp, container.CreateResponse{}))
assert.Check(t, is.DeepEqual(resp, client.ContainerCreateResult{}))
assert.Check(t, is.Error(err, tc.expectedErr))
assert.Check(t, is.ErrorType(err, tc.expectedErrType))
} else {
@@ -154,17 +148,14 @@ func TestCreateLinkToNonExistingContainer(t *testing.T) {
ctx := setupTest(t)
apiClient := testEnv.APIClient()
_, err := apiClient.ContainerCreate(ctx,
&container.Config{
_, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &container.Config{
Image: "busybox",
},
&container.HostConfig{
HostConfig: &container.HostConfig{
Links: []string{"no-such-container"},
},
&network.NetworkingConfig{},
nil,
"",
)
})
assert.Check(t, is.ErrorContains(err, "could not get container for no-such-container"))
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
}
@@ -195,16 +186,12 @@ func TestCreateWithInvalidEnv(t *testing.T) {
t.Run(strconv.Itoa(index), func(t *testing.T) {
t.Parallel()
ctx := testutil.StartSpan(ctx, t)
_, err := apiClient.ContainerCreate(ctx,
&container.Config{
_, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &container.Config{
Image: "busybox",
Env: []string{tc.env},
},
&container.HostConfig{},
&network.NetworkingConfig{},
nil,
"",
)
})
assert.Check(t, is.ErrorContains(err, tc.expectedError))
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
})
@@ -241,17 +228,14 @@ func TestCreateTmpfsMountsTarget(t *testing.T) {
}
for _, tc := range testCases {
_, err := apiClient.ContainerCreate(ctx,
&container.Config{
_, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &container.Config{
Image: "busybox",
},
&container.HostConfig{
HostConfig: &container.HostConfig{
Tmpfs: map[string]string{tc.target: ""},
},
&network.NetworkingConfig{},
nil,
"",
)
})
assert.Check(t, is.ErrorContains(err, tc.expectedError))
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
}
@@ -298,19 +282,17 @@ func TestCreateWithCustomMaskedPaths(t *testing.T) {
t.Parallel()
// Create the container.
ctr, err := apiClient.ContainerCreate(ctx,
&container.Config{
ctr, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &container.Config{
Image: "busybox",
Cmd: []string{"true"},
},
&container.HostConfig{
HostConfig: &container.HostConfig{
Privileged: tc.privileged,
MaskedPaths: tc.maskedPaths,
},
nil,
nil,
fmt.Sprintf("create-masked-paths-%d", i),
)
Name: fmt.Sprintf("create-masked-paths-%d", i),
})
assert.NilError(t, err)
ctrInspect, err := apiClient.ContainerInspect(ctx, ctr.ID)
@@ -371,19 +353,17 @@ func TestCreateWithCustomReadonlyPaths(t *testing.T) {
for i, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
t.Parallel()
ctr, err := apiClient.ContainerCreate(ctx,
&container.Config{
ctr, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &container.Config{
Image: "busybox",
Cmd: []string{"true"},
},
&container.HostConfig{
HostConfig: &container.HostConfig{
Privileged: tc.privileged,
ReadonlyPaths: tc.readonlyPaths,
},
nil,
nil,
fmt.Sprintf("create-readonly-paths-%d", i),
)
Name: fmt.Sprintf("create-readonly-paths-%d", i),
})
assert.NilError(t, err)
ctrInspect, err := apiClient.ContainerInspect(ctx, ctr.ID)
@@ -482,7 +462,9 @@ func TestCreateWithInvalidHealthcheckParams(t *testing.T) {
cfg.Healthcheck.StartPeriod = tc.startPeriod
}
resp, err := apiClient.ContainerCreate(ctx, &cfg, &container.HostConfig{}, nil, nil, "")
resp, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &cfg,
})
assert.Check(t, is.Equal(len(resp.Warnings), 0))
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
assert.ErrorContains(t, err, tc.expectedErr)
@@ -553,7 +535,10 @@ func TestCreateDifferentPlatform(t *testing.T) {
Architecture: img.Architecture,
Variant: img.Variant,
}
_, err := apiClient.ContainerCreate(ctx, &container.Config{Image: "busybox:latest"}, &container.HostConfig{}, nil, &p, "")
_, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &container.Config{Image: "busybox:latest"},
Platform: &p,
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
})
t.Run("different cpu arch", func(t *testing.T) {
@@ -563,7 +548,10 @@ func TestCreateDifferentPlatform(t *testing.T) {
Architecture: img.Architecture + "DifferentArch",
Variant: img.Variant,
}
_, err := apiClient.ContainerCreate(ctx, &container.Config{Image: "busybox:latest"}, &container.HostConfig{}, nil, &p, "")
_, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &container.Config{Image: "busybox:latest"},
Platform: &p,
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
})
}
@@ -574,12 +562,10 @@ func TestCreateVolumesFromNonExistingContainer(t *testing.T) {
_, err := apiClient.ContainerCreate(
ctx,
&container.Config{Image: "busybox"},
&container.HostConfig{VolumesFrom: []string{"nosuchcontainer"}},
nil,
nil,
"",
)
client.ContainerCreateOptions{
Config: &container.Config{Image: "busybox"},
HostConfig: &container.HostConfig{VolumesFrom: []string{"nosuchcontainer"}},
})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
}
@@ -594,12 +580,9 @@ func TestCreatePlatformSpecificImageNoPlatform(t *testing.T) {
_, err := apiClient.ContainerCreate(
ctx,
&container.Config{Image: "arm32v7/hello-world"},
&container.HostConfig{},
nil,
nil,
"",
)
client.ContainerCreateOptions{
Config: &container.Config{Image: "arm32v7/hello-world"},
})
assert.NilError(t, err)
}
@@ -653,7 +636,10 @@ func TestCreateInvalidHostConfig(t *testing.T) {
cfg := container.Config{
Image: "busybox",
}
resp, err := apiClient.ContainerCreate(ctx, &cfg, &tc.hc, nil, nil, "")
resp, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &cfg,
HostConfig: &tc.hc,
})
assert.Check(t, is.Equal(len(resp.Warnings), 0))
assert.Check(t, cerrdefs.IsInvalidArgument(err), "got: %T", err)
assert.Error(t, err, tc.expectedErr)
@@ -760,7 +746,10 @@ func TestCreateWithMultipleEndpointSettings(t *testing.T) {
"net3": {},
},
}
_, err = apiClient.ContainerCreate(ctx, &config, &container.HostConfig{}, &networkingConfig, nil, "")
_, err = apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &config,
NetworkingConfig: &networkingConfig,
})
if tc.expectedErr == "" {
assert.NilError(t, err)
} else {

View File

@@ -5,6 +5,7 @@ import (
"time"
containertypes "github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/moby/moby/v2/integration/internal/container"
"gotest.tools/v3/assert"
"gotest.tools/v3/poll"
@@ -24,9 +25,9 @@ func TestDiff(t *testing.T) {
}
poll.WaitOn(t, container.IsStopped(ctx, apiClient, cID))
items, err := apiClient.ContainerDiff(ctx, cID)
result, err := apiClient.ContainerDiff(ctx, cID, client.ContainerDiffOptions{})
assert.NilError(t, err)
assert.DeepEqual(t, expected, items)
assert.DeepEqual(t, expected, result.Changes)
}
func TestDiffStoppedContainer(t *testing.T) {
@@ -51,7 +52,7 @@ func TestDiffStoppedContainer(t *testing.T) {
}
}
items, err := apiClient.ContainerDiff(ctx, cID)
result, err := apiClient.ContainerDiff(ctx, cID, client.ContainerDiffOptions{})
assert.NilError(t, err)
assert.DeepEqual(t, expected, items)
assert.DeepEqual(t, expected, result.Changes)
}

View File

@@ -64,7 +64,10 @@ func testIpcNonePrivateShareable(t *testing.T, mode string, mustBeMounted bool,
}
apiClient := testEnv.APIClient()
resp, err := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
resp, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &cfg,
HostConfig: &hostCfg,
})
assert.NilError(t, err)
assert.Check(t, is.Equal(len(resp.Warnings), 0))
@@ -135,7 +138,10 @@ func testIpcContainer(t *testing.T, donorMode string, mustWork bool) {
apiClient := testEnv.APIClient()
// create and start the "donor" container
resp, err := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
resp, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &cfg,
HostConfig: &hostCfg,
})
assert.NilError(t, err)
assert.Check(t, is.Equal(len(resp.Warnings), 0))
name1 := resp.ID
@@ -145,7 +151,10 @@ func testIpcContainer(t *testing.T, donorMode string, mustWork bool) {
// create and start the second container
hostCfg.IpcMode = containertypes.IpcMode("container:" + name1)
resp, err = apiClient.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
resp, err = apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &cfg,
HostConfig: &hostCfg,
})
assert.NilError(t, err)
assert.Check(t, is.Equal(len(resp.Warnings), 0))
name2 := resp.ID
@@ -201,7 +210,10 @@ func TestAPIIpcModeHost(t *testing.T) {
ctx := testutil.StartSpan(baseContext, t)
apiClient := testEnv.APIClient()
resp, err := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, nil, nil, "")
resp, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &cfg,
HostConfig: &hostCfg,
})
assert.NilError(t, err)
assert.Check(t, is.Equal(len(resp.Warnings), 0))
name := resp.ID
@@ -237,7 +249,10 @@ func testDaemonIpcPrivateShareable(t *testing.T, mustBeShared bool, arg ...strin
Cmd: []string{"top"},
}
resp, err := c.ContainerCreate(ctx, &cfg, &containertypes.HostConfig{}, nil, nil, "")
resp, err := c.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &cfg,
HostConfig: &containertypes.HostConfig{},
})
assert.NilError(t, err)
assert.Check(t, is.Equal(len(resp.Warnings), 0))

View File

@@ -67,7 +67,11 @@ func TestContainerNetworkMountsNoChown(t *testing.T) {
assert.NilError(t, err)
defer cli.Close()
ctrCreate, err := cli.ContainerCreate(ctx, &config, &hostConfig, &network.NetworkingConfig{}, nil, "")
ctrCreate, err := cli.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &config,
HostConfig: &hostConfig,
NetworkingConfig: &network.NetworkingConfig{},
})
assert.NilError(t, err)
// container will exit immediately because of no tty, but we only need the start sequence to test the condition
err = cli.ContainerStart(ctx, ctrCreate.ID, client.ContainerStartOptions{})
@@ -179,10 +183,13 @@ func TestMountDaemonRoot(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
c, err := apiClient.ContainerCreate(ctx, &containertypes.Config{
Image: "busybox",
Cmd: []string{"true"},
}, hc, nil, nil, "")
c, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &containertypes.Config{
Image: "busybox",
Cmd: []string{"true"},
},
HostConfig: hc,
})
if err != nil {
if test.expected != "" {
t.Fatal(err)
@@ -430,7 +437,13 @@ func TestContainerVolumeAnonymous(t *testing.T) {
},
},
}))
_, err := apiClient.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Platform, config.Name)
_, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: config.Config,
HostConfig: config.HostConfig,
NetworkingConfig: config.NetworkingConfig,
Platform: config.Platform,
Name: config.Name,
})
// We use [testNonExistingPlugin] for this, which produces an error
// when used, which we use as indicator that the driver was passed
// through. We should have a cleaner way for this, but that would

View File

@@ -28,7 +28,7 @@ func TestNoOverlayfsWarningsAboutUndefinedBehaviors(t *testing.T) {
operation func(t *testing.T) error
}{
{name: "diff", operation: func(*testing.T) error {
_, err := apiClient.ContainerDiff(ctx, cID)
_, err := apiClient.ContainerDiff(ctx, cID, client.ContainerDiffOptions{})
return err
}},
{name: "export", operation: func(*testing.T) error {

View File

@@ -100,7 +100,10 @@ func TestDaemonRestartKillContainers(t *testing.T) {
Interval: 60 * time.Second,
}
}
resp, err := apiClient.ContainerCreate(ctx, &config, &hostConfig, nil, nil, "")
resp, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &config,
HostConfig: &hostConfig,
})
assert.NilError(t, err)
defer apiClient.ContainerRemove(ctx, resp.ID, client.ContainerRemoveOptions{Force: true})

View File

@@ -55,10 +55,13 @@ func TestGraphDriverPersistence(t *testing.T) {
assert.Check(t, info.DriverStatus[0][1] != "io.containerd.snapshotter.v1")
prevDriver := info.Driver
containerResp, err := c.ContainerCreate(ctx, &containertypes.Config{
Image: testImage,
Cmd: []string{"echo", "test"},
}, nil, nil, nil, "test-container")
containerResp, err := c.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &containertypes.Config{
Image: testImage,
Cmd: []string{"echo", "test"},
},
Name: "test-container",
})
assert.NilError(t, err, "Failed to create container")
containerID := containerResp.ID
@@ -141,7 +144,7 @@ func TestInspectGraphDriverAPIBC(t *testing.T) {
}
const testImage = "busybox:latest"
ctr, err := c.ContainerCreate(ctx, &containertypes.Config{Image: testImage}, nil, nil, nil, "test-container")
ctr, err := c.ContainerCreate(ctx, client.ContainerCreateOptions{Image: testImage, Name: "test-container"})
assert.NilError(t, err)
defer func() { _ = c.ContainerRemove(ctx, ctr.ID, client.ContainerRemoveOptions{Force: true}) }()

View File

@@ -55,7 +55,13 @@ func NewTestConfig(ops ...func(*TestContainerConfig)) *TestContainerConfig {
func Create(ctx context.Context, t *testing.T, apiClient client.APIClient, ops ...func(*TestContainerConfig)) string {
t.Helper()
config := NewTestConfig(ops...)
c, err := apiClient.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Platform, config.Name)
c, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: config.Config,
HostConfig: config.HostConfig,
NetworkingConfig: config.NetworkingConfig,
Platform: config.Platform,
Name: config.Name,
})
assert.NilError(t, err)
return c.ID
@@ -67,8 +73,14 @@ func Create(ctx context.Context, t *testing.T, apiClient client.APIClient, ops .
//
// ctr, err := container.CreateFromConfig(ctx, apiClient, container.NewTestConfig(container.WithAutoRemove))
// assert.Check(t, err)
func CreateFromConfig(ctx context.Context, apiClient client.APIClient, config *TestContainerConfig) (container.CreateResponse, error) {
return apiClient.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Platform, config.Name)
func CreateFromConfig(ctx context.Context, apiClient client.APIClient, config *TestContainerConfig) (client.ContainerCreateResult, error) {
return apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: config.Config,
HostConfig: config.HostConfig,
NetworkingConfig: config.NetworkingConfig,
Platform: config.Platform,
Name: config.Name,
})
}
// Run creates and start a container with the specified options
@@ -108,7 +120,7 @@ func RunAttach(ctx context.Context, t *testing.T, apiClient client.APIClient, op
err = apiClient.ContainerStart(ctx, id, client.ContainerStartOptions{})
assert.NilError(t, err)
s, err := demultiplexStreams(ctx, aresp)
s, err := demultiplexStreams(ctx, aresp.HijackedResponse)
if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) {
assert.NilError(t, err)
}

View File

@@ -963,7 +963,13 @@ func TestEmptyPortBindingsBC(t *testing.T) {
config := ctr.NewTestConfig(ctr.WithCmd("top"),
ctr.WithExposedPorts("80/tcp"),
ctr.WithPortMap(networktypes.PortMap{networktypes.MustParsePort("80/tcp"): pbs}))
c, err := apiClient.ContainerCreate(ctx, config.Config, config.HostConfig, config.NetworkingConfig, config.Platform, config.Name)
c, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: config.Config,
HostConfig: config.HostConfig,
NetworkingConfig: config.NetworkingConfig,
Platform: config.Platform,
Name: config.Name,
})
assert.NilError(t, err)
defer apiClient.ContainerRemove(ctx, c.ID, client.ContainerRemoveOptions{Force: true})

View File

@@ -54,13 +54,12 @@ func TestReadPluginNoRead(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
d.Start(t, append([]string{"--iptables=false", "--ip6tables=false"}, test.dOpts...)...)
defer d.Stop(t)
c, err := apiclient.ContainerCreate(ctx,
cfg,
&container.HostConfig{LogConfig: container.LogConfig{Type: "test"}},
nil,
nil,
"",
)
c, err := apiclient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: cfg,
HostConfig: &container.HostConfig{
LogConfig: container.LogConfig{Type: "test"},
},
})
assert.Assert(t, err)
defer apiclient.ContainerRemove(ctx, c.ID, client.ContainerRemoveOptions{Force: true})

View File

@@ -80,7 +80,12 @@ func TestRunMountVolumeSubdir(t *testing.T) {
}
ctrName := strings.ReplaceAll(t.Name(), "/", "_")
create, creatErr := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, &network.NetworkingConfig{}, nil, ctrName)
create, creatErr := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &cfg,
HostConfig: &hostCfg,
NetworkingConfig: &network.NetworkingConfig{},
Name: ctrName,
})
id := create.ID
if id != "" {
defer apiClient.ContainerRemove(ctx, id, client.ContainerRemoveOptions{Force: true})
@@ -175,7 +180,12 @@ func TestRunMountImage(t *testing.T) {
}
ctrName := strings.ReplaceAll(t.Name(), "/", "_")
create, creatErr := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, &network.NetworkingConfig{}, nil, ctrName)
create, creatErr := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
Config: &cfg,
HostConfig: &hostCfg,
NetworkingConfig: &network.NetworkingConfig{},
Name: ctrName,
})
id := create.ID
if id != "" {
defer container.Remove(ctx, t, apiClient, id, client.ContainerRemoveOptions{Force: true})

View File

@@ -154,10 +154,11 @@ COPY . /static`); err != nil {
assert.NilError(t, err)
// Start the container
b, err := c.ContainerCreate(context.Background(),
&containertypes.Config{Image: imgName},
&containertypes.HostConfig{PublishAllPorts: true},
nil, nil, ctrName)
b, err := c.ContainerCreate(context.Background(), client.ContainerCreateOptions{
Config: &containertypes.Config{Image: imgName},
HostConfig: &containertypes.HostConfig{PublishAllPorts: true},
Name: ctrName,
})
assert.NilError(t, err)
err = c.ContainerStart(context.Background(), b.ID, client.ContainerStartOptions{})
assert.NilError(t, err)

View File

@@ -12,7 +12,6 @@ import (
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/system"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// APIClient is an interface that clients that talk with a docker server must implement.
@@ -58,10 +57,10 @@ type HijackDialer interface {
// ContainerAPIClient defines API client methods for the containers
type ContainerAPIClient interface {
ContainerAttach(ctx context.Context, container string, options ContainerAttachOptions) (HijackedResponse, error)
ContainerCommit(ctx context.Context, container string, options ContainerCommitOptions) (container.CommitResponse, error)
ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error)
ContainerDiff(ctx context.Context, container string) ([]container.FilesystemChange, error)
ContainerAttach(ctx context.Context, container string, options ContainerAttachOptions) (ContainerAttachResult, error)
ContainerCommit(ctx context.Context, container string, options ContainerCommitOptions) (ContainerCommitResult, error)
ContainerCreate(ctx context.Context, options ContainerCreateOptions) (ContainerCreateResult, error)
ContainerDiff(ctx context.Context, container string, options ContainerDiffOptions) (ContainerDiffResult, error)
ExecAPIClient
ContainerExport(ctx context.Context, container string) (io.ReadCloser, error)
ContainerInspect(ctx context.Context, container string) (container.InspectResponse, error)

View File

@@ -16,6 +16,11 @@ type ContainerAttachOptions struct {
Logs bool
}
// ContainerAttachResult is the result from attaching to a container.
type ContainerAttachResult struct {
HijackedResponse
}
// ContainerAttach attaches a connection to a container in the server.
// It returns a [HijackedResponse] with the hijacked connection
// and a reader to get output. It's up to the called to close
@@ -44,10 +49,10 @@ type ContainerAttachOptions struct {
// [stdcopy.StdType]: https://pkg.go.dev/github.com/moby/moby/api/pkg/stdcopy#StdType
// [Stdout]: https://pkg.go.dev/github.com/moby/moby/api/pkg/stdcopy#Stdout
// [Stderr]: https://pkg.go.dev/github.com/moby/moby/api/pkg/stdcopy#Stderr
func (cli *Client) ContainerAttach(ctx context.Context, containerID string, options ContainerAttachOptions) (HijackedResponse, error) {
func (cli *Client) ContainerAttach(ctx context.Context, containerID string, options ContainerAttachOptions) (ContainerAttachResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
return HijackedResponse{}, err
return ContainerAttachResult{}, err
}
query := url.Values{}
@@ -70,7 +75,12 @@ func (cli *Client) ContainerAttach(ctx context.Context, containerID string, opti
query.Set("logs", "1")
}
return cli.postHijacked(ctx, "/containers/"+containerID+"/attach", query, nil, http.Header{
hijacked, err := cli.postHijacked(ctx, "/containers/"+containerID+"/attach", query, nil, http.Header{
"Content-Type": {"text/plain"},
})
if err != nil {
return ContainerAttachResult{}, err
}
return ContainerAttachResult{HijackedResponse: hijacked}, nil
}

View File

@@ -20,22 +20,27 @@ type ContainerCommitOptions struct {
Config *container.Config
}
// ContainerCommitResult is the result from committing a container.
type ContainerCommitResult struct {
ID string
}
// ContainerCommit applies changes to a container and creates a new tagged image.
func (cli *Client) ContainerCommit(ctx context.Context, containerID string, options ContainerCommitOptions) (container.CommitResponse, error) {
func (cli *Client) ContainerCommit(ctx context.Context, containerID string, options ContainerCommitOptions) (ContainerCommitResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
return container.CommitResponse{}, err
return ContainerCommitResult{}, err
}
var repository, tag string
if options.Reference != "" {
ref, err := reference.ParseNormalizedNamed(options.Reference)
if err != nil {
return container.CommitResponse{}, err
return ContainerCommitResult{}, err
}
if _, ok := ref.(reference.Digested); ok {
return container.CommitResponse{}, errors.New("refusing to create a tag with a digest reference")
return ContainerCommitResult{}, errors.New("refusing to create a tag with a digest reference")
}
ref = reference.TagNameOnly(ref)
@@ -62,9 +67,9 @@ func (cli *Client) ContainerCommit(ctx context.Context, containerID string, opti
resp, err := cli.post(ctx, "/commit", query, options.Config, nil)
defer ensureReaderClosed(resp)
if err != nil {
return response, err
return ContainerCommitResult{}, err
}
err = json.NewDecoder(resp.Body).Decode(&response)
return response, err
return ContainerCommitResult{ID: response.ID}, err
}

View File

@@ -10,49 +10,63 @@ import (
cerrdefs "github.com/containerd/errdefs"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// ContainerCreate creates a new container based on the given configuration.
// It can be associated with a name, but it's not mandatory.
func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) {
if config == nil {
return container.CreateResponse{}, cerrdefs.ErrInvalidArgument.WithMessage("config is nil")
func (cli *Client) ContainerCreate(ctx context.Context, options ContainerCreateOptions) (ContainerCreateResult, error) {
cfg := options.Config
if cfg == nil {
cfg = &container.Config{}
}
if options.Image != "" {
if cfg.Image != "" {
return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("either Image or config.Image should be set")
}
newCfg := *cfg
newCfg.Image = options.Image
cfg = &newCfg
}
if cfg.Image == "" {
return ContainerCreateResult{}, cerrdefs.ErrInvalidArgument.WithMessage("config.Image or Image is required")
}
var response container.CreateResponse
if hostConfig != nil {
hostConfig.CapAdd = normalizeCapabilities(hostConfig.CapAdd)
hostConfig.CapDrop = normalizeCapabilities(hostConfig.CapDrop)
if options.HostConfig != nil {
options.HostConfig.CapAdd = normalizeCapabilities(options.HostConfig.CapAdd)
options.HostConfig.CapDrop = normalizeCapabilities(options.HostConfig.CapDrop)
}
query := url.Values{}
if platform != nil {
if p := formatPlatform(*platform); p != "unknown" {
if options.Platform != nil {
if p := formatPlatform(*options.Platform); p != "unknown" {
query.Set("platform", p)
}
}
if containerName != "" {
query.Set("name", containerName)
if options.Name != "" {
query.Set("name", options.Name)
}
body := container.CreateRequest{
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
Config: cfg,
HostConfig: options.HostConfig,
NetworkingConfig: options.NetworkingConfig,
}
resp, err := cli.post(ctx, "/containers/create", query, body, nil)
defer ensureReaderClosed(resp)
if err != nil {
return response, err
return ContainerCreateResult{}, err
}
err = json.NewDecoder(resp.Body).Decode(&response)
return response, err
return ContainerCreateResult{ID: response.ID, Warnings: response.Warnings}, err
}
// formatPlatform returns a formatted string representing platform (e.g., "linux/arm/v7").

View File

@@ -0,0 +1,25 @@
package client
import (
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// ContainerCreateOptions holds parameters to create a container.
type ContainerCreateOptions struct {
Config *container.Config
HostConfig *container.HostConfig
NetworkingConfig *network.NetworkingConfig
Platform *ocispec.Platform
Name string
// Image is a shortcut for Config.Image - only one of Image or Config.Image should be set.
Image string
}
// ContainerCreateResult is the result from creating a container.
type ContainerCreateResult struct {
ID string
Warnings []string
}

View File

@@ -9,22 +9,22 @@ import (
)
// ContainerDiff shows differences in a container filesystem since it was started.
func (cli *Client) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) {
func (cli *Client) ContainerDiff(ctx context.Context, containerID string, options ContainerDiffOptions) (ContainerDiffResult, error) {
containerID, err := trimID("container", containerID)
if err != nil {
return nil, err
return ContainerDiffResult{}, err
}
resp, err := cli.get(ctx, "/containers/"+containerID+"/changes", url.Values{}, nil)
defer ensureReaderClosed(resp)
if err != nil {
return nil, err
return ContainerDiffResult{}, err
}
var changes []container.FilesystemChange
err = json.NewDecoder(resp.Body).Decode(&changes)
if err != nil {
return nil, err
return ContainerDiffResult{}, err
}
return changes, err
return ContainerDiffResult{Changes: changes}, err
}

View File

@@ -0,0 +1,13 @@
package client
import "github.com/moby/moby/api/types/container"
// ContainerDiffOptions holds parameters to show differences in a container filesystem.
type ContainerDiffOptions struct {
// Currently no options, but this allows for future extensibility
}
// ContainerDiffResult is the result from showing differences in a container filesystem.
type ContainerDiffResult struct {
Changes []container.FilesystemChange
}