Browse code

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>

Austin Vazquez authored on 2025/10/21 05:11:53
Showing 32 changed files
... ...
@@ -7,9 +7,7 @@ import (
7 7
 
8 8
 	"github.com/moby/moby/api/types"
9 9
 	"github.com/moby/moby/api/types/container"
10
-	"github.com/moby/moby/api/types/events"
11 10
 	"github.com/moby/moby/api/types/network"
12
-	"github.com/moby/moby/api/types/registry"
13 11
 	"github.com/moby/moby/api/types/system"
14 12
 )
15 13
 
... ...
@@ -176,9 +174,9 @@ type SwarmAPIClient interface {
176 176
 
177 177
 // SystemAPIClient defines API client methods for the system
178 178
 type SystemAPIClient interface {
179
-	Events(ctx context.Context, options EventsListOptions) (<-chan events.Message, <-chan error)
180
-	Info(ctx context.Context) (system.Info, error)
181
-	RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error)
179
+	Events(ctx context.Context, options EventsListOptions) EventsResult
180
+	Info(ctx context.Context, options InfoOptions) (SystemInfoResult, error)
181
+	RegistryLogin(ctx context.Context, auth RegistryLoginOptions) (RegistryLoginResult, error)
182 182
 	DiskUsage(ctx context.Context, options DiskUsageOptions) (system.DiskUsage, error)
183 183
 	Ping(ctx context.Context, options PingOptions) (PingResult, error)
184 184
 }
... ...
@@ -408,13 +408,13 @@ func TestNegotiateAPIVersionAutomatic(t *testing.T) {
408 408
 	// First request should trigger negotiation
409 409
 	pingVersion = "1.50"
410 410
 	expected = "1.50"
411
-	_, _ = client.Info(ctx)
411
+	_, _ = client.Info(ctx, InfoOptions{})
412 412
 	assert.Check(t, is.Equal(client.ClientVersion(), expected))
413 413
 
414 414
 	// Once successfully negotiated, subsequent requests should not re-negotiate
415 415
 	pingVersion = "1.49"
416 416
 	expected = "1.50"
417
-	_, _ = client.Info(ctx)
417
+	_, _ = client.Info(ctx, InfoOptions{})
418 418
 	assert.Check(t, is.Equal(client.ClientVersion(), expected))
419 419
 }
420 420
 
... ...
@@ -8,17 +8,38 @@ import (
8 8
 	"github.com/moby/moby/api/types/registry"
9 9
 )
10 10
 
11
+type RegistryLoginOptions struct {
12
+	Username      string
13
+	Password      string
14
+	ServerAddress string
15
+	IdentityToken string
16
+	RegistryToken string
17
+}
18
+
19
+// RegistryLoginResult holds the result of a RegistryLogin query.
20
+type RegistryLoginResult struct {
21
+	Auth registry.AuthenticateOKBody
22
+}
23
+
11 24
 // RegistryLogin authenticates the docker server with a given docker registry.
12 25
 // It returns unauthorizedError when the authentication fails.
13
-func (cli *Client) RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error) {
26
+func (cli *Client) RegistryLogin(ctx context.Context, options RegistryLoginOptions) (RegistryLoginResult, error) {
27
+	auth := registry.AuthConfig{
28
+		Username:      options.Username,
29
+		Password:      options.Password,
30
+		ServerAddress: options.ServerAddress,
31
+		IdentityToken: options.IdentityToken,
32
+		RegistryToken: options.RegistryToken,
33
+	}
34
+
14 35
 	resp, err := cli.post(ctx, "/auth", url.Values{}, auth, nil)
15 36
 	defer ensureReaderClosed(resp)
16 37
 
17 38
 	if err != nil {
18
-		return registry.AuthenticateOKBody{}, err
39
+		return RegistryLoginResult{}, err
19 40
 	}
20 41
 
21 42
 	var response registry.AuthenticateOKBody
22 43
 	err = json.NewDecoder(resp.Body).Decode(&response)
23
-	return response, err
44
+	return RegistryLoginResult{Auth: response}, err
24 45
 }
... ...
@@ -19,11 +19,17 @@ type EventsListOptions struct {
19 19
 	Filters Filters
20 20
 }
21 21
 
22
+// EventsResult holds the result of an Events query.
23
+type EventsResult struct {
24
+	Messages <-chan events.Message
25
+	Err      <-chan error
26
+}
27
+
22 28
 // Events returns a stream of events in the daemon. It's up to the caller to close the stream
23 29
 // by cancelling the context. Once the stream has been completely read an [io.EOF] error is
24 30
 // sent over the error channel. If an error is sent, all processing is stopped. It's up
25 31
 // to the caller to reopen the stream in the event of an error by reinvoking this method.
26
-func (cli *Client) Events(ctx context.Context, options EventsListOptions) (<-chan events.Message, <-chan error) {
32
+func (cli *Client) Events(ctx context.Context, options EventsListOptions) EventsResult {
27 33
 	messages := make(chan events.Message)
28 34
 	errs := make(chan error, 1)
29 35
 
... ...
@@ -76,7 +82,10 @@ func (cli *Client) Events(ctx context.Context, options EventsListOptions) (<-cha
76 76
 	}()
77 77
 	<-started
78 78
 
79
-	return messages, errs
79
+	return EventsResult{
80
+		Messages: messages,
81
+		Err:      errs,
82
+	}
80 83
 }
81 84
 
82 85
 func buildEventsQueryParams(options EventsListOptions) (url.Values, error) {
... ...
@@ -37,8 +37,8 @@ func TestEventsErrorInOptions(t *testing.T) {
37 37
 	for _, tc := range errorCases {
38 38
 		client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
39 39
 		assert.NilError(t, err)
40
-		_, errs := client.Events(context.Background(), tc.options)
41
-		err = <-errs
40
+		events := client.Events(context.Background(), tc.options)
41
+		err = <-events.Err
42 42
 		assert.Check(t, is.ErrorContains(err, tc.expectedError))
43 43
 	}
44 44
 }
... ...
@@ -46,8 +46,8 @@ func TestEventsErrorInOptions(t *testing.T) {
46 46
 func TestEventsErrorFromServer(t *testing.T) {
47 47
 	client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
48 48
 	assert.NilError(t, err)
49
-	_, errs := client.Events(context.Background(), EventsListOptions{})
50
-	err = <-errs
49
+	events := client.Events(context.Background(), EventsListOptions{})
50
+	err = <-events.Err
51 51
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
52 52
 }
53 53
 
... ...
@@ -133,18 +133,18 @@ func TestEvents(t *testing.T) {
133 133
 		}))
134 134
 		assert.NilError(t, err)
135 135
 
136
-		messages, errs := client.Events(context.Background(), eventsCase.options)
136
+		events := client.Events(context.Background(), eventsCase.options)
137 137
 
138 138
 	loop:
139 139
 		for {
140 140
 			select {
141
-			case err := <-errs:
141
+			case err := <-events.Err:
142 142
 				if err != nil && !errors.Is(err, io.EOF) {
143 143
 					t.Fatal(err)
144 144
 				}
145 145
 
146 146
 				break loop
147
-			case e := <-messages:
147
+			case e := <-events.Messages:
148 148
 				_, ok := eventsCase.expectedEvents[e.Actor.ID]
149 149
 				assert.Check(t, ok, "event received not expected with action %s & id %s", e.Action, e.Actor.ID)
150 150
 			}
... ...
@@ -9,18 +9,26 @@ import (
9 9
 	"github.com/moby/moby/api/types/system"
10 10
 )
11 11
 
12
+type InfoOptions struct {
13
+	// No options currently; placeholder for future use
14
+}
15
+
16
+type SystemInfoResult struct {
17
+	Info system.Info
18
+}
19
+
12 20
 // Info returns information about the docker server.
13
-func (cli *Client) Info(ctx context.Context) (system.Info, error) {
14
-	var info system.Info
21
+func (cli *Client) Info(ctx context.Context, options InfoOptions) (SystemInfoResult, error) {
15 22
 	resp, err := cli.get(ctx, "/info", url.Values{}, nil)
16 23
 	defer ensureReaderClosed(resp)
17 24
 	if err != nil {
18
-		return info, err
25
+		return SystemInfoResult{}, err
19 26
 	}
20 27
 
28
+	var info system.Info
21 29
 	if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
22
-		return info, fmt.Errorf("Error reading remote info: %v", err)
30
+		return SystemInfoResult{}, fmt.Errorf("Error reading remote info: %v", err)
23 31
 	}
24 32
 
25
-	return info, nil
33
+	return SystemInfoResult{Info: info}, nil
26 34
 }
... ...
@@ -14,14 +14,14 @@ import (
14 14
 func TestInfoServerError(t *testing.T) {
15 15
 	client, err := NewClientWithOpts(WithMockClient(errorMock(http.StatusInternalServerError, "Server error")))
16 16
 	assert.NilError(t, err)
17
-	_, err = client.Info(context.Background())
17
+	_, err = client.Info(context.Background(), InfoOptions{})
18 18
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
19 19
 }
20 20
 
21 21
 func TestInfoInvalidResponseJSONError(t *testing.T) {
22 22
 	client, err := NewClientWithOpts(WithMockClient(mockResponse(http.StatusOK, nil, "invalid json")))
23 23
 	assert.NilError(t, err)
24
-	_, err = client.Info(context.Background())
24
+	_, err = client.Info(context.Background(), InfoOptions{})
25 25
 	assert.Check(t, is.ErrorContains(err, "invalid character"))
26 26
 }
27 27
 
... ...
@@ -38,8 +38,9 @@ func TestInfo(t *testing.T) {
38 38
 	}))
39 39
 	assert.NilError(t, err)
40 40
 
41
-	info, err := client.Info(context.Background())
41
+	result, err := client.Info(context.Background(), InfoOptions{})
42 42
 	assert.NilError(t, err)
43
+	info := result.Info
43 44
 
44 45
 	assert.Check(t, is.Equal(info.ID, "daemonID"))
45 46
 	assert.Check(t, is.Equal(info.Containers, 3))
... ...
@@ -68,8 +69,9 @@ func TestInfoWithDiscoveredDevices(t *testing.T) {
68 68
 	}))
69 69
 	assert.NilError(t, err)
70 70
 
71
-	info, err := client.Info(context.Background())
71
+	result, err := client.Info(context.Background(), InfoOptions{})
72 72
 	assert.NilError(t, err)
73
+	info := result.Info
73 74
 
74 75
 	assert.Check(t, is.Equal(info.ID, "daemonID"))
75 76
 	assert.Check(t, is.Equal(info.Containers, 3))
... ...
@@ -756,9 +756,9 @@ func checkClusterHealth(t *testing.T, cl []*daemon.Daemon, managerCount, workerC
756 756
 
757 757
 		// check info in a poll.WaitOn(), because if the cluster doesn't have a leader, `info` will return an error
758 758
 		checkInfo := func(t *testing.T) (any, string) {
759
-			client := d.NewClientT(t)
760
-			daemonInfo, err := client.Info(ctx)
761
-			info = daemonInfo.Swarm
759
+			apiClient := d.NewClientT(t)
760
+			result, err := apiClient.Info(ctx, client.InfoOptions{})
761
+			info = result.Info.Swarm
762 762
 			return err, "cluster not ready in time"
763 763
 		}
764 764
 		poll.WaitOn(t, pollCheck(t, checkInfo, checker.IsNil()), poll.WithTimeout(defaultReconciliationTimeout))
... ...
@@ -21,8 +21,9 @@ func (s *DockerCLIInfoSuite) TestInfoSecurityOptions(c *testing.T) {
21 21
 	apiClient, err := client.NewClientWithOpts(client.FromEnv)
22 22
 	assert.NilError(c, err)
23 23
 	defer apiClient.Close()
24
-	info, err := apiClient.Info(testutil.GetContext(c))
24
+	result, err := apiClient.Info(testutil.GetContext(c), client.InfoOptions{})
25 25
 	assert.NilError(c, err)
26
+	info := result.Info
26 27
 
27 28
 	if Apparmor() {
28 29
 		assert.Check(c, is.Contains(info.SecurityOptions, "name=apparmor"))
... ...
@@ -54,8 +54,9 @@ func (s *DockerCLIPluginLogDriverSuite) TestPluginLogDriverInfoList(c *testing.T
54 54
 	assert.NilError(c, err)
55 55
 	defer apiClient.Close()
56 56
 
57
-	info, err := apiClient.Info(testutil.GetContext(c))
57
+	result, err := apiClient.Info(testutil.GetContext(c), client.InfoOptions{})
58 58
 	assert.NilError(c, err)
59
+	info := result.Info
59 60
 
60 61
 	drivers := strings.Join(info.Plugins.Log, " ")
61 62
 	assert.Assert(c, is.Contains(drivers, "json-file"))
... ...
@@ -200,8 +200,9 @@ func daemonTime(t *testing.T) time.Time {
200 200
 	assert.NilError(t, err)
201 201
 	defer apiClient.Close()
202 202
 
203
-	info, err := apiClient.Info(testutil.GetContext(t))
203
+	result, err := apiClient.Info(testutil.GetContext(t), client.InfoOptions{})
204 204
 	assert.NilError(t, err)
205
+	info := result.Info
205 206
 
206 207
 	dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
207 208
 	assert.Assert(t, err == nil, "invalid time format in GET /info response")
... ...
@@ -286,11 +287,11 @@ func minimalBaseImage() string {
286 286
 }
287 287
 
288 288
 func getGoroutineNumber(ctx context.Context, apiClient client.APIClient) (int, error) {
289
-	info, err := apiClient.Info(ctx)
289
+	result, err := apiClient.Info(ctx, client.InfoOptions{})
290 290
 	if err != nil {
291 291
 		return 0, err
292 292
 	}
293
-	return info.NGoroutines, nil
293
+	return result.Info.NGoroutines, nil
294 294
 }
295 295
 
296 296
 func waitForStableGoroutineCount(ctx context.Context, t poll.TestingT, apiClient client.APIClient) int {
... ...
@@ -740,10 +740,12 @@ func TestBuildEmitsImageCreateEvent(t *testing.T) {
740 740
 			assert.NilError(t, err)
741 741
 			buildLogs := out.String()
742 742
 
743
-			eventsChan, errs := apiClient.Events(ctx, client.EventsListOptions{
743
+			result := apiClient.Events(ctx, client.EventsListOptions{
744 744
 				Since: since.Format(time.RFC3339Nano),
745 745
 				Until: time.Now().Format(time.RFC3339Nano),
746 746
 			})
747
+			eventsChan := result.Messages
748
+			errs := result.Err
747 749
 
748 750
 			var eventsReceived []string
749 751
 			imageCreateEvts := 0
... ...
@@ -188,8 +188,9 @@ func TestCDIInfoDiscoveredDevices(t *testing.T) {
188 188
 	defer d.Stop(t)
189 189
 
190 190
 	c := d.NewClientT(t)
191
-	info, err := c.Info(ctx)
191
+	result, err := c.Info(ctx, client.InfoOptions{})
192 192
 	assert.NilError(t, err)
193
+	info := result.Info
193 194
 
194 195
 	assert.Check(t, is.Len(info.CDISpecDirs, 1))
195 196
 	assert.Check(t, is.Equal(info.CDISpecDirs[0], cdiDir))
... ...
@@ -807,9 +807,10 @@ func TestContainerdContainerImageInfo(t *testing.T) {
807 807
 	apiClient := testEnv.APIClient()
808 808
 	defer apiClient.Close()
809 809
 
810
-	info, err := apiClient.Info(ctx)
810
+	result, err := apiClient.Info(ctx, client.InfoOptions{})
811 811
 	assert.NilError(t, err)
812 812
 
813
+	info := result.Info
813 814
 	skip.If(t, info.Containerd == nil, "requires containerd")
814 815
 
815 816
 	// Currently a containerd container is only created when the container is started.
... ...
@@ -98,10 +98,11 @@ func TestMountDaemonRoot(t *testing.T) {
98 98
 
99 99
 	ctx := setupTest(t)
100 100
 	apiClient := testEnv.APIClient()
101
-	info, err := apiClient.Info(ctx)
101
+	result, err := apiClient.Info(ctx, client.InfoOptions{})
102 102
 	if err != nil {
103 103
 		t.Fatal(err)
104 104
 	}
105
+	info := result.Info
105 106
 
106 107
 	for _, test := range []struct {
107 108
 		desc        string
... ...
@@ -39,11 +39,13 @@ func TestPause(t *testing.T) {
39 39
 
40 40
 	until := request.DaemonUnixTime(ctx, t, apiClient, testEnv)
41 41
 
42
-	messages, errs := apiClient.Events(ctx, client.EventsListOptions{
42
+	result := apiClient.Events(ctx, client.EventsListOptions{
43 43
 		Since:   since,
44 44
 		Until:   until,
45 45
 		Filters: make(client.Filters).Add(string(events.ContainerEventType), cID),
46 46
 	})
47
+	messages := result.Messages
48
+	errs := result.Err
47 49
 	assert.Check(t, is.DeepEqual([]events.Action{events.ActionPause, events.ActionUnPause}, getEventActions(t, messages, errs)))
48 50
 }
49 51
 
... ...
@@ -247,9 +247,11 @@ func TestContainerRestartWithCancelledRequest(t *testing.T) {
247 247
 	}()
248 248
 
249 249
 	// Start listening for events.
250
-	messages, errs := apiClient.Events(ctx, client.EventsListOptions{
250
+	result := apiClient.Events(ctx, client.EventsListOptions{
251 251
 		Filters: make(client.Filters).Add("container", cID).Add("event", string(events.ActionRestart)),
252 252
 	})
253
+	messages := result.Messages
254
+	errs := result.Err
253 255
 
254 256
 	// Make restart request, but cancel the request before the container
255 257
 	// is (forcibly) killed.
... ...
@@ -21,9 +21,10 @@ func TestStats(t *testing.T) {
21 21
 	ctx := setupTest(t)
22 22
 	apiClient := testEnv.APIClient()
23 23
 
24
-	info, err := apiClient.Info(ctx)
24
+	result, err := apiClient.Info(ctx, client.InfoOptions{})
25 25
 	assert.NilError(t, err)
26 26
 
27
+	info := result.Info
27 28
 	cID := container.Run(ctx, t, apiClient)
28 29
 	t.Run("no-stream", func(t *testing.T) {
29 30
 		resp, err := apiClient.ContainerStats(ctx, cID, client.ContainerStatsOptions{
... ...
@@ -70,9 +70,9 @@ func CheckGoroutineCount(ctx context.Context, apiClient client.SystemAPIClient,
70 70
 }
71 71
 
72 72
 func getGoroutineNumber(ctx context.Context, apiClient client.SystemAPIClient) (int, error) {
73
-	info, err := apiClient.Info(ctx)
73
+	result, err := apiClient.Info(ctx, client.InfoOptions{})
74 74
 	if err != nil {
75 75
 		return 0, err
76 76
 	}
77
-	return info.NGoroutines, nil
77
+	return result.Info.NGoroutines, nil
78 78
 }
... ...
@@ -31,8 +31,9 @@ func TestInfoFirewallBackend(t *testing.T) {
31 31
 	if !testEnv.IsRootless() && networking.FirewalldRunning() {
32 32
 		expDriver += "+firewalld"
33 33
 	}
34
-	info, err := c.Info(ctx)
34
+	result, err := c.Info(ctx, client.InfoOptions{})
35 35
 	assert.NilError(t, err)
36
+	info := result.Info
36 37
 	assert.Assert(t, info.FirewallBackend != nil, "expected firewall backend in info response")
37 38
 	t.Log("FirewallBackend: Driver:", info.FirewallBackend.Driver)
38 39
 	for _, kv := range info.FirewallBackend.Info {
... ...
@@ -43,8 +44,9 @@ func TestInfoFirewallBackend(t *testing.T) {
43 43
 	// Check FirewallBackend is omitted for API <= 1.48.
44 44
 	t.Run("api 1.48", func(t *testing.T) {
45 45
 		c148 := request.NewAPIClient(t, client.WithVersion("1.48"))
46
-		info148, err := c148.Info(ctx)
46
+		result, err := c148.Info(ctx, client.InfoOptions{})
47 47
 		assert.NilError(t, err)
48
+		info148 := result.Info
48 49
 		assert.Check(t, is.Nil(info148.FirewallBackend))
49 50
 	})
50 51
 }
... ...
@@ -276,9 +276,10 @@ func systemTime(ctx context.Context, t *testing.T, apiClient client.APIClient, t
276 276
 		return time.Now()
277 277
 	}
278 278
 
279
-	info, err := apiClient.Info(ctx)
279
+	result, err := apiClient.Info(ctx, client.InfoOptions{})
280 280
 	assert.NilError(t, err)
281 281
 
282
+	info := result.Info
282 283
 	dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
283 284
 	assert.NilError(t, err, "invalid time format in GET /info response")
284 285
 	return dt
... ...
@@ -289,9 +290,9 @@ func systemEventsSince(ctx context.Context, apiClient client.APIClient, since st
289 289
 		Since: since,
290 290
 	}
291 291
 	ctx, cancel := context.WithCancel(ctx)
292
-	events, errs := apiClient.Events(ctx, eventOptions)
292
+	result := apiClient.Events(ctx, eventOptions)
293 293
 
294
-	return events, errs, cancel
294
+	return result.Messages, result.Err, cancel
295 295
 }
296 296
 
297 297
 func TestAuthZPluginErrorResponse(t *testing.T) {
... ...
@@ -30,9 +30,11 @@ func TestEventsExecDie(t *testing.T) {
30 30
 	})
31 31
 	assert.NilError(t, err)
32 32
 
33
-	msg, errs := apiClient.Events(ctx, client.EventsListOptions{
33
+	result := apiClient.Events(ctx, client.EventsListOptions{
34 34
 		Filters: make(client.Filters).Add("container", cID).Add("event", string(events.ActionExecDie)),
35 35
 	})
36
+	msg := result.Messages
37
+	errs := result.Err
36 38
 
37 39
 	_, err = apiClient.ExecStart(ctx, res.ID, client.ExecStartOptions{
38 40
 		Detach: true,
... ...
@@ -107,11 +109,13 @@ func TestEventsVolumeCreate(t *testing.T) {
107 107
 		Add("type", "volume").
108 108
 		Add("event", "create").
109 109
 		Add("volume", volName)
110
-	messages, errs := apiClient.Events(ctx, client.EventsListOptions{
110
+	result := apiClient.Events(ctx, client.EventsListOptions{
111 111
 		Since:   since,
112 112
 		Until:   request.DaemonUnixTime(ctx, t, apiClient, testEnv),
113 113
 		Filters: filter,
114 114
 	})
115
+	messages := result.Messages
116
+	errs := result.Err
115 117
 
116 118
 	volEvents, err := getEvents(messages, errs)
117 119
 	assert.NilError(t, err)
... ...
@@ -123,11 +127,13 @@ func TestEventsVolumeCreate(t *testing.T) {
123 123
 		Target: "/tmp/foo",
124 124
 	}))
125 125
 
126
-	messages, errs = apiClient.Events(ctx, client.EventsListOptions{
126
+	result = apiClient.Events(ctx, client.EventsListOptions{
127 127
 		Since:   since,
128 128
 		Until:   request.DaemonUnixTime(ctx, t, apiClient, testEnv),
129 129
 		Filters: filter,
130 130
 	})
131
+	messages = result.Messages
132
+	errs = result.Err
131 133
 
132 134
 	volEvents, err = getEvents(messages, errs)
133 135
 	assert.NilError(t, err)
... ...
@@ -6,6 +6,7 @@ import (
6 6
 	"testing"
7 7
 
8 8
 	"github.com/moby/moby/api/types/registry"
9
+	"github.com/moby/moby/client"
9 10
 	"github.com/moby/moby/v2/internal/testutil"
10 11
 	"github.com/moby/moby/v2/internal/testutil/daemon"
11 12
 	"gotest.tools/v3/assert"
... ...
@@ -17,9 +18,10 @@ func TestInfoAPI(t *testing.T) {
17 17
 	ctx := setupTest(t)
18 18
 	apiClient := testEnv.APIClient()
19 19
 
20
-	info, err := apiClient.Info(ctx)
20
+	result, err := apiClient.Info(ctx, client.InfoOptions{})
21 21
 	assert.NilError(t, err)
22 22
 
23
+	info := result.Info
23 24
 	// TODO(thaJeztah): make sure we have other tests that run a local daemon and check other fields based on known state.
24 25
 	assert.Check(t, info.ID != "")
25 26
 	assert.Check(t, is.Equal(info.Containers, info.ContainersRunning+info.ContainersPaused+info.ContainersStopped))
... ...
@@ -51,9 +53,11 @@ func TestInfoAPIWarnings(t *testing.T) {
51 51
 	d.Start(t, "-H=0.0.0.0:23756", "-H="+d.Sock())
52 52
 	defer d.Stop(t)
53 53
 
54
-	info, err := c.Info(ctx)
54
+	result, err := c.Info(ctx, client.InfoOptions{})
55 55
 	assert.NilError(t, err)
56 56
 
57
+	info := result.Info
58
+
57 59
 	stringsToCheck := []string{
58 60
 		"Access to the remote API is equivalent to root access",
59 61
 		"http://0.0.0.0:23756",
... ...
@@ -4,7 +4,7 @@ import (
4 4
 	"fmt"
5 5
 	"testing"
6 6
 
7
-	"github.com/moby/moby/api/types/registry"
7
+	"github.com/moby/moby/client"
8 8
 	registrypkg "github.com/moby/moby/v2/daemon/pkg/registry"
9 9
 	"github.com/moby/moby/v2/integration/internal/requirement"
10 10
 	"gotest.tools/v3/assert"
... ...
@@ -19,7 +19,7 @@ func TestLoginFailsWithBadCredentials(t *testing.T) {
19 19
 	ctx := setupTest(t)
20 20
 	apiClient := testEnv.APIClient()
21 21
 
22
-	_, err := apiClient.RegistryLogin(ctx, registry.AuthConfig{
22
+	_, err := apiClient.RegistryLogin(ctx, client.RegistryLoginOptions{
23 23
 		Username: "no-user",
24 24
 		Password: "no-password",
25 25
 	})
... ...
@@ -985,8 +985,9 @@ func (d *Daemon) queryRootDir() (string, error) {
985 985
 func (d *Daemon) Info(t testing.TB) system.Info {
986 986
 	t.Helper()
987 987
 	c := d.NewClientT(t)
988
-	info, err := c.Info(context.Background())
988
+	result, err := c.Info(context.Background(), client.InfoOptions{})
989 989
 	assert.NilError(t, err)
990
+	info := result.Info
990 991
 	assert.NilError(t, c.Close())
991 992
 	return info
992 993
 }
... ...
@@ -162,8 +162,9 @@ func (d *Daemon) SwarmLeave(ctx context.Context, t testing.TB, force bool) error
162 162
 func (d *Daemon) SwarmInfo(ctx context.Context, t testing.TB) swarm.Info {
163 163
 	t.Helper()
164 164
 	cli := d.NewClientT(t)
165
-	info, err := cli.Info(ctx)
165
+	result, err := cli.Info(ctx, client.InfoOptions{})
166 166
 	assert.NilError(t, err, "get swarm info")
167
+	info := result.Info
167 168
 	return info.Swarm
168 169
 }
169 170
 
... ...
@@ -46,10 +46,11 @@ func New(ctx context.Context) (*Execution, error) {
46 46
 
47 47
 // FromClient creates a new Execution environment from the passed in client
48 48
 func FromClient(ctx context.Context, c *client.Client) (*Execution, error) {
49
-	info, err := c.Info(ctx)
49
+	result, err := c.Info(ctx, client.InfoOptions{})
50 50
 	if err != nil {
51 51
 		return nil, errors.Wrapf(err, "failed to get info from daemon")
52 52
 	}
53
+	info := result.Info
53 54
 	v, err := c.ServerVersion(context.Background())
54 55
 	if err != nil {
55 56
 		return nil, errors.Wrapf(err, "failed to get version info from daemon")
... ...
@@ -33,14 +33,15 @@ func NewAPIClient(t testing.TB, ops ...client.Opt) client.APIClient {
33 33
 }
34 34
 
35 35
 // DaemonTime provides the current time on the daemon host
36
-func DaemonTime(ctx context.Context, t testing.TB, client client.APIClient, testEnv *environment.Execution) time.Time {
36
+func DaemonTime(ctx context.Context, t testing.TB, apiClient client.APIClient, testEnv *environment.Execution) time.Time {
37 37
 	t.Helper()
38 38
 	if testEnv.IsLocalDaemon() {
39 39
 		return time.Now()
40 40
 	}
41 41
 
42
-	info, err := client.Info(ctx)
42
+	result, err := apiClient.Info(ctx, client.InfoOptions{})
43 43
 	assert.NilError(t, err)
44
+	info := result.Info
44 45
 
45 46
 	dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
46 47
 	assert.NilError(t, err, "invalid time format in GET /info response")
... ...
@@ -7,9 +7,7 @@ import (
7 7
 
8 8
 	"github.com/moby/moby/api/types"
9 9
 	"github.com/moby/moby/api/types/container"
10
-	"github.com/moby/moby/api/types/events"
11 10
 	"github.com/moby/moby/api/types/network"
12
-	"github.com/moby/moby/api/types/registry"
13 11
 	"github.com/moby/moby/api/types/system"
14 12
 )
15 13
 
... ...
@@ -176,9 +174,9 @@ type SwarmAPIClient interface {
176 176
 
177 177
 // SystemAPIClient defines API client methods for the system
178 178
 type SystemAPIClient interface {
179
-	Events(ctx context.Context, options EventsListOptions) (<-chan events.Message, <-chan error)
180
-	Info(ctx context.Context) (system.Info, error)
181
-	RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error)
179
+	Events(ctx context.Context, options EventsListOptions) EventsResult
180
+	Info(ctx context.Context, options InfoOptions) (SystemInfoResult, error)
181
+	RegistryLogin(ctx context.Context, auth RegistryLoginOptions) (RegistryLoginResult, error)
182 182
 	DiskUsage(ctx context.Context, options DiskUsageOptions) (system.DiskUsage, error)
183 183
 	Ping(ctx context.Context, options PingOptions) (PingResult, error)
184 184
 }
... ...
@@ -8,17 +8,38 @@ import (
8 8
 	"github.com/moby/moby/api/types/registry"
9 9
 )
10 10
 
11
+type RegistryLoginOptions struct {
12
+	Username      string
13
+	Password      string
14
+	ServerAddress string
15
+	IdentityToken string
16
+	RegistryToken string
17
+}
18
+
19
+// RegistryLoginResult holds the result of a RegistryLogin query.
20
+type RegistryLoginResult struct {
21
+	Auth registry.AuthenticateOKBody
22
+}
23
+
11 24
 // RegistryLogin authenticates the docker server with a given docker registry.
12 25
 // It returns unauthorizedError when the authentication fails.
13
-func (cli *Client) RegistryLogin(ctx context.Context, auth registry.AuthConfig) (registry.AuthenticateOKBody, error) {
26
+func (cli *Client) RegistryLogin(ctx context.Context, options RegistryLoginOptions) (RegistryLoginResult, error) {
27
+	auth := registry.AuthConfig{
28
+		Username:      options.Username,
29
+		Password:      options.Password,
30
+		ServerAddress: options.ServerAddress,
31
+		IdentityToken: options.IdentityToken,
32
+		RegistryToken: options.RegistryToken,
33
+	}
34
+
14 35
 	resp, err := cli.post(ctx, "/auth", url.Values{}, auth, nil)
15 36
 	defer ensureReaderClosed(resp)
16 37
 
17 38
 	if err != nil {
18
-		return registry.AuthenticateOKBody{}, err
39
+		return RegistryLoginResult{}, err
19 40
 	}
20 41
 
21 42
 	var response registry.AuthenticateOKBody
22 43
 	err = json.NewDecoder(resp.Body).Decode(&response)
23
-	return response, err
44
+	return RegistryLoginResult{Auth: response}, err
24 45
 }
... ...
@@ -19,11 +19,17 @@ type EventsListOptions struct {
19 19
 	Filters Filters
20 20
 }
21 21
 
22
+// EventsResult holds the result of an Events query.
23
+type EventsResult struct {
24
+	Messages <-chan events.Message
25
+	Err      <-chan error
26
+}
27
+
22 28
 // Events returns a stream of events in the daemon. It's up to the caller to close the stream
23 29
 // by cancelling the context. Once the stream has been completely read an [io.EOF] error is
24 30
 // sent over the error channel. If an error is sent, all processing is stopped. It's up
25 31
 // to the caller to reopen the stream in the event of an error by reinvoking this method.
26
-func (cli *Client) Events(ctx context.Context, options EventsListOptions) (<-chan events.Message, <-chan error) {
32
+func (cli *Client) Events(ctx context.Context, options EventsListOptions) EventsResult {
27 33
 	messages := make(chan events.Message)
28 34
 	errs := make(chan error, 1)
29 35
 
... ...
@@ -76,7 +82,10 @@ func (cli *Client) Events(ctx context.Context, options EventsListOptions) (<-cha
76 76
 	}()
77 77
 	<-started
78 78
 
79
-	return messages, errs
79
+	return EventsResult{
80
+		Messages: messages,
81
+		Err:      errs,
82
+	}
80 83
 }
81 84
 
82 85
 func buildEventsQueryParams(options EventsListOptions) (url.Values, error) {
... ...
@@ -9,18 +9,26 @@ import (
9 9
 	"github.com/moby/moby/api/types/system"
10 10
 )
11 11
 
12
+type InfoOptions struct {
13
+	// No options currently; placeholder for future use
14
+}
15
+
16
+type SystemInfoResult struct {
17
+	Info system.Info
18
+}
19
+
12 20
 // Info returns information about the docker server.
13
-func (cli *Client) Info(ctx context.Context) (system.Info, error) {
14
-	var info system.Info
21
+func (cli *Client) Info(ctx context.Context, options InfoOptions) (SystemInfoResult, error) {
15 22
 	resp, err := cli.get(ctx, "/info", url.Values{}, nil)
16 23
 	defer ensureReaderClosed(resp)
17 24
 	if err != nil {
18
-		return info, err
25
+		return SystemInfoResult{}, err
19 26
 	}
20 27
 
28
+	var info system.Info
21 29
 	if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
22
-		return info, fmt.Errorf("Error reading remote info: %v", err)
30
+		return SystemInfoResult{}, fmt.Errorf("Error reading remote info: %v", err)
23 31
 	}
24 32
 
25
-	return info, nil
33
+	return SystemInfoResult{Info: info}, nil
26 34
 }