client: refactor Events, Info, RegistryLogin

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Austin Vazquez
2025-10-20 15:11:53 -05:00
committed by Sebastiaan van Stijn
parent c438b3fbbf
commit e46058cbae
32 changed files with 181 additions and 77 deletions

View File

@@ -7,9 +7,7 @@ import (
"github.com/moby/moby/api/types" "github.com/moby/moby/api/types"
"github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/events"
"github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/system" "github.com/moby/moby/api/types/system"
) )
@@ -176,9 +174,9 @@ type SwarmAPIClient interface {
// SystemAPIClient defines API client methods for the system // SystemAPIClient defines API client methods for the system
type SystemAPIClient interface { type SystemAPIClient interface {
Events(ctx context.Context, options EventsListOptions) (<-chan events.Message, <-chan error) Events(ctx context.Context, options EventsListOptions) EventsResult
Info(ctx context.Context) (system.Info, error) Info(ctx context.Context, options InfoOptions) (SystemInfoResult, error)
RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error) RegistryLogin(ctx context.Context, auth RegistryLoginOptions) (RegistryLoginResult, error)
DiskUsage(ctx context.Context, options DiskUsageOptions) (system.DiskUsage, error) DiskUsage(ctx context.Context, options DiskUsageOptions) (system.DiskUsage, error)
Ping(ctx context.Context, options PingOptions) (PingResult, error) Ping(ctx context.Context, options PingOptions) (PingResult, error)
} }

View File

@@ -408,13 +408,13 @@ func TestNegotiateAPIVersionAutomatic(t *testing.T) {
// First request should trigger negotiation // First request should trigger negotiation
pingVersion = "1.50" pingVersion = "1.50"
expected = "1.50" expected = "1.50"
_, _ = client.Info(ctx) _, _ = client.Info(ctx, InfoOptions{})
assert.Check(t, is.Equal(client.ClientVersion(), expected)) assert.Check(t, is.Equal(client.ClientVersion(), expected))
// Once successfully negotiated, subsequent requests should not re-negotiate // Once successfully negotiated, subsequent requests should not re-negotiate
pingVersion = "1.49" pingVersion = "1.49"
expected = "1.50" expected = "1.50"
_, _ = client.Info(ctx) _, _ = client.Info(ctx, InfoOptions{})
assert.Check(t, is.Equal(client.ClientVersion(), expected)) assert.Check(t, is.Equal(client.ClientVersion(), expected))
} }

View File

@@ -8,17 +8,38 @@ import (
"github.com/moby/moby/api/types/registry" "github.com/moby/moby/api/types/registry"
) )
type RegistryLoginOptions struct {
Username string
Password string
ServerAddress string
IdentityToken string
RegistryToken string
}
// RegistryLoginResult holds the result of a RegistryLogin query.
type RegistryLoginResult struct {
Auth registry.AuthenticateOKBody
}
// RegistryLogin authenticates the docker server with a given docker registry. // RegistryLogin authenticates the docker server with a given docker registry.
// It returns unauthorizedError when the authentication fails. // It returns unauthorizedError when the authentication fails.
func (cli *Client) RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error) { func (cli *Client) RegistryLogin(ctx context.Context, options RegistryLoginOptions) (RegistryLoginResult, error) {
auth := registry.AuthConfig{
Username: options.Username,
Password: options.Password,
ServerAddress: options.ServerAddress,
IdentityToken: options.IdentityToken,
RegistryToken: options.RegistryToken,
}
resp, err := cli.post(ctx, "/auth", url.Values{}, auth, nil) resp, err := cli.post(ctx, "/auth", url.Values{}, auth, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return registry.AuthenticateOKBody{}, err return RegistryLoginResult{}, err
} }
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 RegistryLoginResult{Auth: response}, err
} }

View File

@@ -19,11 +19,17 @@ type EventsListOptions struct {
Filters Filters Filters Filters
} }
// EventsResult holds the result of an Events query.
type EventsResult struct {
Messages <-chan events.Message
Err <-chan error
}
// Events returns a stream of events in the daemon. It's up to the caller to close the stream // Events returns a stream of events in the daemon. It's up to the caller to close the stream
// by cancelling the context. Once the stream has been completely read an [io.EOF] error is // by cancelling the context. Once the stream has been completely read an [io.EOF] error is
// sent over the error channel. If an error is sent, all processing is stopped. It's up // sent over the error channel. If an error is sent, all processing is stopped. It's up
// to the caller to reopen the stream in the event of an error by reinvoking this method. // to the caller to reopen the stream in the event of an error by reinvoking this method.
func (cli *Client) Events(ctx context.Context, options EventsListOptions) (<-chan events.Message, <-chan error) { func (cli *Client) Events(ctx context.Context, options EventsListOptions) EventsResult {
messages := make(chan events.Message) messages := make(chan events.Message)
errs := make(chan error, 1) errs := make(chan error, 1)
@@ -76,7 +82,10 @@ func (cli *Client) Events(ctx context.Context, options EventsListOptions) (<-cha
}() }()
<-started <-started
return messages, errs return EventsResult{
Messages: messages,
Err: errs,
}
} }
func buildEventsQueryParams(options EventsListOptions) (url.Values, error) { func buildEventsQueryParams(options EventsListOptions) (url.Values, error) {

View File

@@ -37,8 +37,8 @@ func TestEventsErrorInOptions(t *testing.T) {
for _, tc := range errorCases { for _, tc := range errorCases {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err) assert.NilError(t, err)
_, errs := client.Events(context.Background(), tc.options) events := client.Events(context.Background(), tc.options)
err = <-errs err = <-events.Err
assert.Check(t, is.ErrorContains(err, tc.expectedError)) assert.Check(t, is.ErrorContains(err, tc.expectedError))
} }
} }
@@ -46,8 +46,8 @@ func TestEventsErrorInOptions(t *testing.T) {
func TestEventsErrorFromServer(t *testing.T) { func TestEventsErrorFromServer(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err) assert.NilError(t, err)
_, errs := client.Events(context.Background(), EventsListOptions{}) events := client.Events(context.Background(), EventsListOptions{})
err = <-errs err = <-events.Err
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
} }
@@ -133,18 +133,18 @@ func TestEvents(t *testing.T) {
})) }))
assert.NilError(t, err) assert.NilError(t, err)
messages, errs := client.Events(context.Background(), eventsCase.options) events := client.Events(context.Background(), eventsCase.options)
loop: loop:
for { for {
select { select {
case err := <-errs: case err := <-events.Err:
if err != nil && !errors.Is(err, io.EOF) { if err != nil && !errors.Is(err, io.EOF) {
t.Fatal(err) t.Fatal(err)
} }
break loop break loop
case e := <-messages: case e := <-events.Messages:
_, ok := eventsCase.expectedEvents[e.Actor.ID] _, ok := eventsCase.expectedEvents[e.Actor.ID]
assert.Check(t, ok, "event received not expected with action %s & id %s", e.Action, e.Actor.ID) assert.Check(t, ok, "event received not expected with action %s & id %s", e.Action, e.Actor.ID)
} }

View File

@@ -9,18 +9,26 @@ import (
"github.com/moby/moby/api/types/system" "github.com/moby/moby/api/types/system"
) )
type InfoOptions struct {
// No options currently; placeholder for future use
}
type SystemInfoResult struct {
Info system.Info
}
// 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, options InfoOptions) (SystemInfoResult, error) {
var info system.Info
resp, err := cli.get(ctx, "/info", url.Values{}, nil) resp, err := cli.get(ctx, "/info", url.Values{}, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return info, err return SystemInfoResult{}, err
} }
var info system.Info
if err := json.NewDecoder(resp.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 SystemInfoResult{}, fmt.Errorf("Error reading remote info: %v", err)
} }
return info, nil return SystemInfoResult{Info: info}, nil
} }

View File

@@ -14,14 +14,14 @@ import (
func TestInfoServerError(t *testing.T) { func TestInfoServerError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error"))) client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
assert.NilError(t, err) assert.NilError(t, err)
_, err = client.Info(context.Background()) _, err = client.Info(context.Background(), InfoOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal)) assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
} }
func TestInfoInvalidResponseJSONError(t *testing.T) { func TestInfoInvalidResponseJSONError(t *testing.T) {
client, err := NewClientWithOpts(WithMockClient(mockResponse(http.StatusOK, nil, "invalid json"))) client, err := NewClientWithOpts(WithMockClient(mockResponse(http.StatusOK, nil, "invalid json")))
assert.NilError(t, err) assert.NilError(t, err)
_, err = client.Info(context.Background()) _, err = client.Info(context.Background(), InfoOptions{})
assert.Check(t, is.ErrorContains(err, "invalid character")) assert.Check(t, is.ErrorContains(err, "invalid character"))
} }
@@ -38,8 +38,9 @@ func TestInfo(t *testing.T) {
})) }))
assert.NilError(t, err) assert.NilError(t, err)
info, err := client.Info(context.Background()) result, err := client.Info(context.Background(), InfoOptions{})
assert.NilError(t, err) assert.NilError(t, err)
info := result.Info
assert.Check(t, is.Equal(info.ID, "daemonID")) assert.Check(t, is.Equal(info.ID, "daemonID"))
assert.Check(t, is.Equal(info.Containers, 3)) assert.Check(t, is.Equal(info.Containers, 3))
@@ -68,8 +69,9 @@ func TestInfoWithDiscoveredDevices(t *testing.T) {
})) }))
assert.NilError(t, err) assert.NilError(t, err)
info, err := client.Info(context.Background()) result, err := client.Info(context.Background(), InfoOptions{})
assert.NilError(t, err) assert.NilError(t, err)
info := result.Info
assert.Check(t, is.Equal(info.ID, "daemonID")) assert.Check(t, is.Equal(info.ID, "daemonID"))
assert.Check(t, is.Equal(info.Containers, 3)) assert.Check(t, is.Equal(info.Containers, 3))

View File

@@ -756,9 +756,9 @@ func checkClusterHealth(t *testing.T, cl []*daemon.Daemon, managerCount, workerC
// check info in a poll.WaitOn(), because if the cluster doesn't have a leader, `info` will return an error // check info in a poll.WaitOn(), because if the cluster doesn't have a leader, `info` will return an error
checkInfo := func(t *testing.T) (any, string) { checkInfo := func(t *testing.T) (any, string) {
client := d.NewClientT(t) apiClient := d.NewClientT(t)
daemonInfo, err := client.Info(ctx) result, err := apiClient.Info(ctx, client.InfoOptions{})
info = daemonInfo.Swarm info = result.Info.Swarm
return err, "cluster not ready in time" return err, "cluster not ready in time"
} }
poll.WaitOn(t, pollCheck(t, checkInfo, checker.IsNil()), poll.WithTimeout(defaultReconciliationTimeout)) poll.WaitOn(t, pollCheck(t, checkInfo, checker.IsNil()), poll.WithTimeout(defaultReconciliationTimeout))

View File

@@ -21,8 +21,9 @@ func (s *DockerCLIInfoSuite) TestInfoSecurityOptions(c *testing.T) {
apiClient, err := client.NewClientWithOpts(client.FromEnv) apiClient, err := client.NewClientWithOpts(client.FromEnv)
assert.NilError(c, err) assert.NilError(c, err)
defer apiClient.Close() defer apiClient.Close()
info, err := apiClient.Info(testutil.GetContext(c)) result, err := apiClient.Info(testutil.GetContext(c), client.InfoOptions{})
assert.NilError(c, err) assert.NilError(c, err)
info := result.Info
if Apparmor() { if Apparmor() {
assert.Check(c, is.Contains(info.SecurityOptions, "name=apparmor")) assert.Check(c, is.Contains(info.SecurityOptions, "name=apparmor"))

View File

@@ -54,8 +54,9 @@ func (s *DockerCLIPluginLogDriverSuite) TestPluginLogDriverInfoList(c *testing.T
assert.NilError(c, err) assert.NilError(c, err)
defer apiClient.Close() defer apiClient.Close()
info, err := apiClient.Info(testutil.GetContext(c)) result, err := apiClient.Info(testutil.GetContext(c), client.InfoOptions{})
assert.NilError(c, err) assert.NilError(c, err)
info := result.Info
drivers := strings.Join(info.Plugins.Log, " ") drivers := strings.Join(info.Plugins.Log, " ")
assert.Assert(c, is.Contains(drivers, "json-file")) assert.Assert(c, is.Contains(drivers, "json-file"))

View File

@@ -200,8 +200,9 @@ func daemonTime(t *testing.T) time.Time {
assert.NilError(t, err) assert.NilError(t, err)
defer apiClient.Close() defer apiClient.Close()
info, err := apiClient.Info(testutil.GetContext(t)) result, err := apiClient.Info(testutil.GetContext(t), client.InfoOptions{})
assert.NilError(t, err) assert.NilError(t, err)
info := result.Info
dt, err := time.Parse(time.RFC3339Nano, info.SystemTime) dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
assert.Assert(t, err == nil, "invalid time format in GET /info response") assert.Assert(t, err == nil, "invalid time format in GET /info response")
@@ -286,11 +287,11 @@ func minimalBaseImage() string {
} }
func getGoroutineNumber(ctx context.Context, apiClient client.APIClient) (int, error) { func getGoroutineNumber(ctx context.Context, apiClient client.APIClient) (int, error) {
info, err := apiClient.Info(ctx) result, err := apiClient.Info(ctx, client.InfoOptions{})
if err != nil { if err != nil {
return 0, err return 0, err
} }
return info.NGoroutines, nil return result.Info.NGoroutines, nil
} }
func waitForStableGoroutineCount(ctx context.Context, t poll.TestingT, apiClient client.APIClient) int { func waitForStableGoroutineCount(ctx context.Context, t poll.TestingT, apiClient client.APIClient) int {

View File

@@ -740,10 +740,12 @@ func TestBuildEmitsImageCreateEvent(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
buildLogs := out.String() buildLogs := out.String()
eventsChan, errs := apiClient.Events(ctx, client.EventsListOptions{ result := apiClient.Events(ctx, client.EventsListOptions{
Since: since.Format(time.RFC3339Nano), Since: since.Format(time.RFC3339Nano),
Until: time.Now().Format(time.RFC3339Nano), Until: time.Now().Format(time.RFC3339Nano),
}) })
eventsChan := result.Messages
errs := result.Err
var eventsReceived []string var eventsReceived []string
imageCreateEvts := 0 imageCreateEvts := 0

View File

@@ -188,8 +188,9 @@ func TestCDIInfoDiscoveredDevices(t *testing.T) {
defer d.Stop(t) defer d.Stop(t)
c := d.NewClientT(t) c := d.NewClientT(t)
info, err := c.Info(ctx) result, err := c.Info(ctx, client.InfoOptions{})
assert.NilError(t, err) assert.NilError(t, err)
info := result.Info
assert.Check(t, is.Len(info.CDISpecDirs, 1)) assert.Check(t, is.Len(info.CDISpecDirs, 1))
assert.Check(t, is.Equal(info.CDISpecDirs[0], cdiDir)) assert.Check(t, is.Equal(info.CDISpecDirs[0], cdiDir))

View File

@@ -807,9 +807,10 @@ func TestContainerdContainerImageInfo(t *testing.T) {
apiClient := testEnv.APIClient() apiClient := testEnv.APIClient()
defer apiClient.Close() defer apiClient.Close()
info, err := apiClient.Info(ctx) result, err := apiClient.Info(ctx, client.InfoOptions{})
assert.NilError(t, err) assert.NilError(t, err)
info := result.Info
skip.If(t, info.Containerd == nil, "requires containerd") skip.If(t, info.Containerd == nil, "requires containerd")
// Currently a containerd container is only created when the container is started. // Currently a containerd container is only created when the container is started.

View File

@@ -98,10 +98,11 @@ func TestMountDaemonRoot(t *testing.T) {
ctx := setupTest(t) ctx := setupTest(t)
apiClient := testEnv.APIClient() apiClient := testEnv.APIClient()
info, err := apiClient.Info(ctx) result, err := apiClient.Info(ctx, client.InfoOptions{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
info := result.Info
for _, test := range []struct { for _, test := range []struct {
desc string desc string

View File

@@ -39,11 +39,13 @@ func TestPause(t *testing.T) {
until := request.DaemonUnixTime(ctx, t, apiClient, testEnv) until := request.DaemonUnixTime(ctx, t, apiClient, testEnv)
messages, errs := apiClient.Events(ctx, client.EventsListOptions{ result := apiClient.Events(ctx, client.EventsListOptions{
Since: since, Since: since,
Until: until, Until: until,
Filters: make(client.Filters).Add(string(events.ContainerEventType), cID), Filters: make(client.Filters).Add(string(events.ContainerEventType), cID),
}) })
messages := result.Messages
errs := result.Err
assert.Check(t, is.DeepEqual([]events.Action{events.ActionPause, events.ActionUnPause}, getEventActions(t, messages, errs))) assert.Check(t, is.DeepEqual([]events.Action{events.ActionPause, events.ActionUnPause}, getEventActions(t, messages, errs)))
} }

View File

@@ -247,9 +247,11 @@ func TestContainerRestartWithCancelledRequest(t *testing.T) {
}() }()
// Start listening for events. // Start listening for events.
messages, errs := apiClient.Events(ctx, client.EventsListOptions{ result := apiClient.Events(ctx, client.EventsListOptions{
Filters: make(client.Filters).Add("container", cID).Add("event", string(events.ActionRestart)), Filters: make(client.Filters).Add("container", cID).Add("event", string(events.ActionRestart)),
}) })
messages := result.Messages
errs := result.Err
// Make restart request, but cancel the request before the container // Make restart request, but cancel the request before the container
// is (forcibly) killed. // is (forcibly) killed.

View File

@@ -21,9 +21,10 @@ func TestStats(t *testing.T) {
ctx := setupTest(t) ctx := setupTest(t)
apiClient := testEnv.APIClient() apiClient := testEnv.APIClient()
info, err := apiClient.Info(ctx) result, err := apiClient.Info(ctx, client.InfoOptions{})
assert.NilError(t, err) assert.NilError(t, err)
info := result.Info
cID := container.Run(ctx, t, apiClient) cID := container.Run(ctx, t, apiClient)
t.Run("no-stream", func(t *testing.T) { t.Run("no-stream", func(t *testing.T) {
resp, err := apiClient.ContainerStats(ctx, cID, client.ContainerStatsOptions{ resp, err := apiClient.ContainerStats(ctx, cID, client.ContainerStatsOptions{

View File

@@ -70,9 +70,9 @@ func CheckGoroutineCount(ctx context.Context, apiClient client.SystemAPIClient,
} }
func getGoroutineNumber(ctx context.Context, apiClient client.SystemAPIClient) (int, error) { func getGoroutineNumber(ctx context.Context, apiClient client.SystemAPIClient) (int, error) {
info, err := apiClient.Info(ctx) result, err := apiClient.Info(ctx, client.InfoOptions{})
if err != nil { if err != nil {
return 0, err return 0, err
} }
return info.NGoroutines, nil return result.Info.NGoroutines, nil
} }

View File

@@ -31,8 +31,9 @@ func TestInfoFirewallBackend(t *testing.T) {
if !testEnv.IsRootless() && networking.FirewalldRunning() { if !testEnv.IsRootless() && networking.FirewalldRunning() {
expDriver += "+firewalld" expDriver += "+firewalld"
} }
info, err := c.Info(ctx) result, err := c.Info(ctx, client.InfoOptions{})
assert.NilError(t, err) assert.NilError(t, err)
info := result.Info
assert.Assert(t, info.FirewallBackend != nil, "expected firewall backend in info response") assert.Assert(t, info.FirewallBackend != nil, "expected firewall backend in info response")
t.Log("FirewallBackend: Driver:", info.FirewallBackend.Driver) t.Log("FirewallBackend: Driver:", info.FirewallBackend.Driver)
for _, kv := range info.FirewallBackend.Info { for _, kv := range info.FirewallBackend.Info {
@@ -43,8 +44,9 @@ func TestInfoFirewallBackend(t *testing.T) {
// Check FirewallBackend is omitted for API <= 1.48. // Check FirewallBackend is omitted for API <= 1.48.
t.Run("api 1.48", func(t *testing.T) { t.Run("api 1.48", func(t *testing.T) {
c148 := request.NewAPIClient(t, client.WithVersion("1.48")) c148 := request.NewAPIClient(t, client.WithVersion("1.48"))
info148, err := c148.Info(ctx) result, err := c148.Info(ctx, client.InfoOptions{})
assert.NilError(t, err) assert.NilError(t, err)
info148 := result.Info
assert.Check(t, is.Nil(info148.FirewallBackend)) assert.Check(t, is.Nil(info148.FirewallBackend))
}) })
} }

View File

@@ -276,9 +276,10 @@ func systemTime(ctx context.Context, t *testing.T, apiClient client.APIClient, t
return time.Now() return time.Now()
} }
info, err := apiClient.Info(ctx) result, err := apiClient.Info(ctx, client.InfoOptions{})
assert.NilError(t, err) assert.NilError(t, err)
info := result.Info
dt, err := time.Parse(time.RFC3339Nano, info.SystemTime) dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
assert.NilError(t, err, "invalid time format in GET /info response") assert.NilError(t, err, "invalid time format in GET /info response")
return dt return dt
@@ -289,9 +290,9 @@ func systemEventsSince(ctx context.Context, apiClient client.APIClient, since st
Since: since, Since: since,
} }
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
events, errs := apiClient.Events(ctx, eventOptions) result := apiClient.Events(ctx, eventOptions)
return events, errs, cancel return result.Messages, result.Err, cancel
} }
func TestAuthZPluginErrorResponse(t *testing.T) { func TestAuthZPluginErrorResponse(t *testing.T) {

View File

@@ -30,9 +30,11 @@ func TestEventsExecDie(t *testing.T) {
}) })
assert.NilError(t, err) assert.NilError(t, err)
msg, errs := apiClient.Events(ctx, client.EventsListOptions{ result := apiClient.Events(ctx, client.EventsListOptions{
Filters: make(client.Filters).Add("container", cID).Add("event", string(events.ActionExecDie)), Filters: make(client.Filters).Add("container", cID).Add("event", string(events.ActionExecDie)),
}) })
msg := result.Messages
errs := result.Err
_, err = apiClient.ExecStart(ctx, res.ID, client.ExecStartOptions{ _, err = apiClient.ExecStart(ctx, res.ID, client.ExecStartOptions{
Detach: true, Detach: true,
@@ -107,11 +109,13 @@ func TestEventsVolumeCreate(t *testing.T) {
Add("type", "volume"). Add("type", "volume").
Add("event", "create"). Add("event", "create").
Add("volume", volName) Add("volume", volName)
messages, errs := apiClient.Events(ctx, client.EventsListOptions{ result := apiClient.Events(ctx, client.EventsListOptions{
Since: since, Since: since,
Until: request.DaemonUnixTime(ctx, t, apiClient, testEnv), Until: request.DaemonUnixTime(ctx, t, apiClient, testEnv),
Filters: filter, Filters: filter,
}) })
messages := result.Messages
errs := result.Err
volEvents, err := getEvents(messages, errs) volEvents, err := getEvents(messages, errs)
assert.NilError(t, err) assert.NilError(t, err)
@@ -123,11 +127,13 @@ func TestEventsVolumeCreate(t *testing.T) {
Target: "/tmp/foo", Target: "/tmp/foo",
})) }))
messages, errs = apiClient.Events(ctx, client.EventsListOptions{ result = apiClient.Events(ctx, client.EventsListOptions{
Since: since, Since: since,
Until: request.DaemonUnixTime(ctx, t, apiClient, testEnv), Until: request.DaemonUnixTime(ctx, t, apiClient, testEnv),
Filters: filter, Filters: filter,
}) })
messages = result.Messages
errs = result.Err
volEvents, err = getEvents(messages, errs) volEvents, err = getEvents(messages, errs)
assert.NilError(t, err) assert.NilError(t, err)

View File

@@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/moby/moby/api/types/registry" "github.com/moby/moby/api/types/registry"
"github.com/moby/moby/client"
"github.com/moby/moby/v2/internal/testutil" "github.com/moby/moby/v2/internal/testutil"
"github.com/moby/moby/v2/internal/testutil/daemon" "github.com/moby/moby/v2/internal/testutil/daemon"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
@@ -17,9 +18,10 @@ func TestInfoAPI(t *testing.T) {
ctx := setupTest(t) ctx := setupTest(t)
apiClient := testEnv.APIClient() apiClient := testEnv.APIClient()
info, err := apiClient.Info(ctx) result, err := apiClient.Info(ctx, client.InfoOptions{})
assert.NilError(t, err) assert.NilError(t, err)
info := result.Info
// TODO(thaJeztah): make sure we have other tests that run a local daemon and check other fields based on known state. // TODO(thaJeztah): make sure we have other tests that run a local daemon and check other fields based on known state.
assert.Check(t, info.ID != "") assert.Check(t, info.ID != "")
assert.Check(t, is.Equal(info.Containers, info.ContainersRunning+info.ContainersPaused+info.ContainersStopped)) assert.Check(t, is.Equal(info.Containers, info.ContainersRunning+info.ContainersPaused+info.ContainersStopped))
@@ -51,9 +53,11 @@ func TestInfoAPIWarnings(t *testing.T) {
d.Start(t, "-H=0.0.0.0:23756", "-H="+d.Sock()) d.Start(t, "-H=0.0.0.0:23756", "-H="+d.Sock())
defer d.Stop(t) defer d.Stop(t)
info, err := c.Info(ctx) result, err := c.Info(ctx, client.InfoOptions{})
assert.NilError(t, err) assert.NilError(t, err)
info := result.Info
stringsToCheck := []string{ stringsToCheck := []string{
"Access to the remote API is equivalent to root access", "Access to the remote API is equivalent to root access",
"http://0.0.0.0:23756", "http://0.0.0.0:23756",

View File

@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/moby/moby/api/types/registry" "github.com/moby/moby/client"
registrypkg "github.com/moby/moby/v2/daemon/pkg/registry" registrypkg "github.com/moby/moby/v2/daemon/pkg/registry"
"github.com/moby/moby/v2/integration/internal/requirement" "github.com/moby/moby/v2/integration/internal/requirement"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
@@ -19,7 +19,7 @@ func TestLoginFailsWithBadCredentials(t *testing.T) {
ctx := setupTest(t) ctx := setupTest(t)
apiClient := testEnv.APIClient() apiClient := testEnv.APIClient()
_, err := apiClient.RegistryLogin(ctx, registry.AuthConfig{ _, err := apiClient.RegistryLogin(ctx, client.RegistryLoginOptions{
Username: "no-user", Username: "no-user",
Password: "no-password", Password: "no-password",
}) })

View File

@@ -985,8 +985,9 @@ func (d *Daemon) queryRootDir() (string, error) {
func (d *Daemon) Info(t testing.TB) system.Info { func (d *Daemon) Info(t testing.TB) system.Info {
t.Helper() t.Helper()
c := d.NewClientT(t) c := d.NewClientT(t)
info, err := c.Info(context.Background()) result, err := c.Info(context.Background(), client.InfoOptions{})
assert.NilError(t, err) assert.NilError(t, err)
info := result.Info
assert.NilError(t, c.Close()) assert.NilError(t, c.Close())
return info return info
} }

View File

@@ -162,8 +162,9 @@ func (d *Daemon) SwarmLeave(ctx context.Context, t testing.TB, force bool) error
func (d *Daemon) SwarmInfo(ctx context.Context, t testing.TB) swarm.Info { func (d *Daemon) SwarmInfo(ctx context.Context, t testing.TB) swarm.Info {
t.Helper() t.Helper()
cli := d.NewClientT(t) cli := d.NewClientT(t)
info, err := cli.Info(ctx) result, err := cli.Info(ctx, client.InfoOptions{})
assert.NilError(t, err, "get swarm info") assert.NilError(t, err, "get swarm info")
info := result.Info
return info.Swarm return info.Swarm
} }

View File

@@ -46,10 +46,11 @@ func New(ctx context.Context) (*Execution, error) {
// FromClient creates a new Execution environment from the passed in client // FromClient creates a new Execution environment from the passed in client
func FromClient(ctx context.Context, c *client.Client) (*Execution, error) { func FromClient(ctx context.Context, c *client.Client) (*Execution, error) {
info, err := c.Info(ctx) result, err := c.Info(ctx, client.InfoOptions{})
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to get info from daemon") return nil, errors.Wrapf(err, "failed to get info from daemon")
} }
info := result.Info
v, err := c.ServerVersion(context.Background()) v, err := c.ServerVersion(context.Background())
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to get version info from daemon") return nil, errors.Wrapf(err, "failed to get version info from daemon")

View File

@@ -33,14 +33,15 @@ func NewAPIClient(t testing.TB, ops ...client.Opt) client.APIClient {
} }
// DaemonTime provides the current time on the daemon host // DaemonTime provides the current time on the daemon host
func DaemonTime(ctx context.Context, t testing.TB, client client.APIClient, testEnv *environment.Execution) time.Time { func DaemonTime(ctx context.Context, t testing.TB, apiClient client.APIClient, testEnv *environment.Execution) time.Time {
t.Helper() t.Helper()
if testEnv.IsLocalDaemon() { if testEnv.IsLocalDaemon() {
return time.Now() return time.Now()
} }
info, err := client.Info(ctx) result, err := apiClient.Info(ctx, client.InfoOptions{})
assert.NilError(t, err) assert.NilError(t, err)
info := result.Info
dt, err := time.Parse(time.RFC3339Nano, info.SystemTime) dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
assert.NilError(t, err, "invalid time format in GET /info response") assert.NilError(t, err, "invalid time format in GET /info response")

View File

@@ -7,9 +7,7 @@ import (
"github.com/moby/moby/api/types" "github.com/moby/moby/api/types"
"github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/events"
"github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/system" "github.com/moby/moby/api/types/system"
) )
@@ -176,9 +174,9 @@ type SwarmAPIClient interface {
// SystemAPIClient defines API client methods for the system // SystemAPIClient defines API client methods for the system
type SystemAPIClient interface { type SystemAPIClient interface {
Events(ctx context.Context, options EventsListOptions) (<-chan events.Message, <-chan error) Events(ctx context.Context, options EventsListOptions) EventsResult
Info(ctx context.Context) (system.Info, error) Info(ctx context.Context, options InfoOptions) (SystemInfoResult, error)
RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error) RegistryLogin(ctx context.Context, auth RegistryLoginOptions) (RegistryLoginResult, error)
DiskUsage(ctx context.Context, options DiskUsageOptions) (system.DiskUsage, error) DiskUsage(ctx context.Context, options DiskUsageOptions) (system.DiskUsage, error)
Ping(ctx context.Context, options PingOptions) (PingResult, error) Ping(ctx context.Context, options PingOptions) (PingResult, error)
} }

View File

@@ -8,17 +8,38 @@ import (
"github.com/moby/moby/api/types/registry" "github.com/moby/moby/api/types/registry"
) )
type RegistryLoginOptions struct {
Username string
Password string
ServerAddress string
IdentityToken string
RegistryToken string
}
// RegistryLoginResult holds the result of a RegistryLogin query.
type RegistryLoginResult struct {
Auth registry.AuthenticateOKBody
}
// RegistryLogin authenticates the docker server with a given docker registry. // RegistryLogin authenticates the docker server with a given docker registry.
// It returns unauthorizedError when the authentication fails. // It returns unauthorizedError when the authentication fails.
func (cli *Client) RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error) { func (cli *Client) RegistryLogin(ctx context.Context, options RegistryLoginOptions) (RegistryLoginResult, error) {
auth := registry.AuthConfig{
Username: options.Username,
Password: options.Password,
ServerAddress: options.ServerAddress,
IdentityToken: options.IdentityToken,
RegistryToken: options.RegistryToken,
}
resp, err := cli.post(ctx, "/auth", url.Values{}, auth, nil) resp, err := cli.post(ctx, "/auth", url.Values{}, auth, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return registry.AuthenticateOKBody{}, err return RegistryLoginResult{}, err
} }
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 RegistryLoginResult{Auth: response}, err
} }

View File

@@ -19,11 +19,17 @@ type EventsListOptions struct {
Filters Filters Filters Filters
} }
// EventsResult holds the result of an Events query.
type EventsResult struct {
Messages <-chan events.Message
Err <-chan error
}
// Events returns a stream of events in the daemon. It's up to the caller to close the stream // Events returns a stream of events in the daemon. It's up to the caller to close the stream
// by cancelling the context. Once the stream has been completely read an [io.EOF] error is // by cancelling the context. Once the stream has been completely read an [io.EOF] error is
// sent over the error channel. If an error is sent, all processing is stopped. It's up // sent over the error channel. If an error is sent, all processing is stopped. It's up
// to the caller to reopen the stream in the event of an error by reinvoking this method. // to the caller to reopen the stream in the event of an error by reinvoking this method.
func (cli *Client) Events(ctx context.Context, options EventsListOptions) (<-chan events.Message, <-chan error) { func (cli *Client) Events(ctx context.Context, options EventsListOptions) EventsResult {
messages := make(chan events.Message) messages := make(chan events.Message)
errs := make(chan error, 1) errs := make(chan error, 1)
@@ -76,7 +82,10 @@ func (cli *Client) Events(ctx context.Context, options EventsListOptions) (<-cha
}() }()
<-started <-started
return messages, errs return EventsResult{
Messages: messages,
Err: errs,
}
} }
func buildEventsQueryParams(options EventsListOptions) (url.Values, error) { func buildEventsQueryParams(options EventsListOptions) (url.Values, error) {

View File

@@ -9,18 +9,26 @@ import (
"github.com/moby/moby/api/types/system" "github.com/moby/moby/api/types/system"
) )
type InfoOptions struct {
// No options currently; placeholder for future use
}
type SystemInfoResult struct {
Info system.Info
}
// 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, options InfoOptions) (SystemInfoResult, error) {
var info system.Info
resp, err := cli.get(ctx, "/info", url.Values{}, nil) resp, err := cli.get(ctx, "/info", url.Values{}, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {
return info, err return SystemInfoResult{}, err
} }
var info system.Info
if err := json.NewDecoder(resp.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 SystemInfoResult{}, fmt.Errorf("Error reading remote info: %v", err)
} }
return info, nil return SystemInfoResult{Info: info}, nil
} }