Browse code

client: ExecCreateOptions: change ConsoleSize to a ConsoleSize type

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

Sebastiaan van Stijn authored on 2025/11/05 22:39:13
Showing 4 changed files
... ...
@@ -12,17 +12,17 @@ import (
12 12
 // ExecCreateOptions is a small subset of the Config struct that holds the configuration
13 13
 // for the exec feature of docker.
14 14
 type ExecCreateOptions struct {
15
-	User         string   // User that will run the command
16
-	Privileged   bool     // Is the container in privileged mode
17
-	TTY          bool     // Attach standard streams to a tty.
18
-	ConsoleSize  *[2]uint `json:",omitempty"` // Initial console size [height, width]
19
-	AttachStdin  bool     // Attach the standard input, makes possible user interaction
20
-	AttachStderr bool     // Attach the standard error
21
-	AttachStdout bool     // Attach the standard output
22
-	DetachKeys   string   // Escape keys for detach
23
-	Env          []string // Environment variables
24
-	WorkingDir   string   // Working directory
25
-	Cmd          []string // Execution commands and args
15
+	User         string      // User that will run the command
16
+	Privileged   bool        // Is the container in privileged mode
17
+	TTY          bool        // Attach standard streams to a tty.
18
+	ConsoleSize  ConsoleSize // Initial terminal size [height, width], unused if TTY == false
19
+	AttachStdin  bool        // Attach the standard input, makes possible user interaction
20
+	AttachStderr bool        // Attach the standard error
21
+	AttachStdout bool        // Attach the standard output
22
+	DetachKeys   string      // Escape keys for detach
23
+	Env          []string    // Environment variables
24
+	WorkingDir   string      // Working directory
25
+	Cmd          []string    // Execution commands and args
26 26
 }
27 27
 
28 28
 // ExecCreateResult holds the result of creating a container exec.
... ...
@@ -37,11 +37,16 @@ func (cli *Client) ExecCreate(ctx context.Context, containerID string, options E
37 37
 		return ExecCreateResult{}, err
38 38
 	}
39 39
 
40
+	consoleSize, err := getConsoleSize(options.TTY, options.ConsoleSize)
41
+	if err != nil {
42
+		return ExecCreateResult{}, err
43
+	}
44
+
40 45
 	req := container.ExecCreateRequest{
41 46
 		User:         options.User,
42 47
 		Privileged:   options.Privileged,
43 48
 		Tty:          options.TTY,
44
-		ConsoleSize:  options.ConsoleSize,
49
+		ConsoleSize:  consoleSize,
45 50
 		AttachStdin:  options.AttachStdin,
46 51
 		AttachStderr: options.AttachStderr,
47 52
 		AttachStdout: options.AttachStdout,
... ...
@@ -73,7 +78,7 @@ type ExecStartOptions struct {
73 73
 	// Check if there's a tty
74 74
 	TTY bool
75 75
 	// Terminal size [height, width], unused if TTY == false
76
-	ConsoleSize ConsoleSize `json:",omitzero"`
76
+	ConsoleSize ConsoleSize
77 77
 }
78 78
 
79 79
 // ExecStartResult holds the result of starting a container exec.
... ...
@@ -82,13 +87,16 @@ type ExecStartResult struct {
82 82
 
83 83
 // ExecStart starts an exec process already created in the docker host.
84 84
 func (cli *Client) ExecStart(ctx context.Context, execID string, options ExecStartOptions) (ExecStartResult, error) {
85
-	req := container.ExecStartRequest{
86
-		Detach: options.Detach,
87
-		Tty:    options.TTY,
88
-	}
89
-	if err := applyConsoleSize(&req, &options.ConsoleSize); err != nil {
85
+	consoleSize, err := getConsoleSize(options.TTY, options.ConsoleSize)
86
+	if err != nil {
90 87
 		return ExecStartResult{}, err
91 88
 	}
89
+
90
+	req := container.ExecStartRequest{
91
+		Detach:      options.Detach,
92
+		Tty:         options.TTY,
93
+		ConsoleSize: consoleSize,
94
+	}
92 95
 	resp, err := cli.post(ctx, "/exec/"+execID+"/start", nil, req, nil)
93 96
 	defer ensureReaderClosed(resp)
94 97
 	return ExecStartResult{}, err
... ...
@@ -126,27 +134,29 @@ type ExecAttachResult struct {
126 126
 //
127 127
 // [stdcopy.StdCopy]: https://pkg.go.dev/github.com/moby/moby/api/pkg/stdcopy#StdCopy
128 128
 func (cli *Client) ExecAttach(ctx context.Context, execID string, options ExecAttachOptions) (ExecAttachResult, error) {
129
-	req := container.ExecStartRequest{
130
-		Detach: false,
131
-		Tty:    options.TTY,
132
-	}
133
-	if err := applyConsoleSize(&req, &options.ConsoleSize); err != nil {
129
+	consoleSize, err := getConsoleSize(options.TTY, options.ConsoleSize)
130
+	if err != nil {
134 131
 		return ExecAttachResult{}, err
135 132
 	}
133
+	req := container.ExecStartRequest{
134
+		Detach:      false,
135
+		Tty:         options.TTY,
136
+		ConsoleSize: consoleSize,
137
+	}
136 138
 	response, err := cli.postHijacked(ctx, "/exec/"+execID+"/start", nil, req, http.Header{
137 139
 		"Content-Type": {"application/json"},
138 140
 	})
139 141
 	return ExecAttachResult{HijackedResponse: response}, err
140 142
 }
141 143
 
142
-func applyConsoleSize(req *container.ExecStartRequest, consoleSize *ConsoleSize) error {
144
+func getConsoleSize(hasTTY bool, consoleSize ConsoleSize) (*[2]uint, error) {
143 145
 	if consoleSize.Height != 0 || consoleSize.Width != 0 {
144
-		if !req.Tty {
145
-			return errdefs.ErrInvalidArgument.WithMessage("console size is only supported when TTY is enabled")
146
+		if !hasTTY {
147
+			return nil, errdefs.ErrInvalidArgument.WithMessage("console size is only supported when TTY is enabled")
146 148
 		}
147
-		req.ConsoleSize = &[2]uint{consoleSize.Height, consoleSize.Width}
149
+		return &[2]uint{consoleSize.Height, consoleSize.Width}, nil
148 150
 	}
149
-	return nil
151
+	return nil, nil
150 152
 }
151 153
 
152 154
 // ExecInspectOptions holds options for inspecting a container exec.
... ...
@@ -1,7 +1,6 @@
1 1
 package client
2 2
 
3 3
 import (
4
-	"context"
5 4
 	"encoding/json"
6 5
 	"fmt"
7 6
 	"net/http"
... ...
@@ -20,14 +19,14 @@ func TestExecCreateError(t *testing.T) {
20 20
 	)
21 21
 	assert.NilError(t, err)
22 22
 
23
-	_, err = client.ExecCreate(context.Background(), "container_id", ExecCreateOptions{})
23
+	_, err = client.ExecCreate(t.Context(), "container_id", ExecCreateOptions{})
24 24
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
25 25
 
26
-	_, err = client.ExecCreate(context.Background(), "", ExecCreateOptions{})
26
+	_, err = client.ExecCreate(t.Context(), "", ExecCreateOptions{})
27 27
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
28 28
 	assert.Check(t, is.ErrorContains(err, "value is empty"))
29 29
 
30
-	_, err = client.ExecCreate(context.Background(), "    ", ExecCreateOptions{})
30
+	_, err = client.ExecCreate(t.Context(), "    ", ExecCreateOptions{})
31 31
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInvalidArgument))
32 32
 	assert.Check(t, is.ErrorContains(err, "value is empty"))
33 33
 }
... ...
@@ -40,7 +39,7 @@ func TestExecCreateConnectionError(t *testing.T) {
40 40
 	client, err := New(WithAPIVersionNegotiation(), WithHost("tcp://no-such-host.invalid"))
41 41
 	assert.NilError(t, err)
42 42
 
43
-	_, err = client.ExecCreate(context.Background(), "container_id", ExecCreateOptions{})
43
+	_, err = client.ExecCreate(t.Context(), "container_id", ExecCreateOptions{})
44 44
 	assert.Check(t, is.ErrorType(err, IsErrConnectionFailed))
45 45
 }
46 46
 
... ...
@@ -69,7 +68,7 @@ func TestExecCreate(t *testing.T) {
69 69
 	)
70 70
 	assert.NilError(t, err)
71 71
 
72
-	res, err := client.ExecCreate(context.Background(), "container_id", ExecCreateOptions{
72
+	res, err := client.ExecCreate(t.Context(), "container_id", ExecCreateOptions{
73 73
 		User: "user",
74 74
 	})
75 75
 	assert.NilError(t, err)
... ...
@@ -82,7 +81,7 @@ func TestExecStartError(t *testing.T) {
82 82
 	)
83 83
 	assert.NilError(t, err)
84 84
 
85
-	_, err = client.ExecStart(context.Background(), "nothing", ExecStartOptions{})
85
+	_, err = client.ExecStart(t.Context(), "nothing", ExecStartOptions{})
86 86
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
87 87
 }
88 88
 
... ...
@@ -108,7 +107,7 @@ func TestExecStart(t *testing.T) {
108 108
 	)
109 109
 	assert.NilError(t, err)
110 110
 
111
-	_, err = client.ExecStart(context.Background(), "exec_id", ExecStartOptions{
111
+	_, err = client.ExecStart(t.Context(), "exec_id", ExecStartOptions{
112 112
 		Detach: true,
113 113
 		TTY:    false,
114 114
 	})
... ...
@@ -116,20 +115,63 @@ func TestExecStart(t *testing.T) {
116 116
 }
117 117
 
118 118
 func TestExecStartConsoleSize(t *testing.T) {
119
-	client, err := New(
120
-		WithMockClient(func(req *http.Request) (*http.Response, error) {
121
-			return nil, nil
122
-		}),
123
-	)
124
-	assert.NilError(t, err)
125
-
126
-	_, err = client.ExecStart(context.Background(), "exec_id", ExecStartOptions{
127
-		Detach:      true,
128
-		TTY:         false,
129
-		ConsoleSize: ConsoleSize{Height: 100, Width: 100},
130
-	})
131
-	assert.Check(t, is.ErrorType(err, errdefs.IsInvalidArgument))
132
-	assert.Check(t, is.ErrorContains(err, "console size is only supported when TTY is enabled"))
119
+	tests := []struct {
120
+		doc     string
121
+		options ExecStartOptions
122
+		expErr  string
123
+		expReq  container.ExecStartRequest
124
+	}{
125
+		{
126
+			doc: "without TTY",
127
+			options: ExecStartOptions{
128
+				Detach:      true,
129
+				TTY:         false,
130
+				ConsoleSize: ConsoleSize{Height: 100, Width: 200},
131
+			},
132
+			expErr: "console size is only supported when TTY is enabled",
133
+		},
134
+		{
135
+			doc: "with TTY",
136
+			options: ExecStartOptions{
137
+				Detach:      true,
138
+				TTY:         true,
139
+				ConsoleSize: ConsoleSize{Height: 100, Width: 200},
140
+			},
141
+			expReq: container.ExecStartRequest{
142
+				Detach:      true,
143
+				Tty:         true,
144
+				ConsoleSize: &[2]uint{100, 200},
145
+			},
146
+		},
147
+	}
148
+	for _, tc := range tests {
149
+		t.Run(tc.doc, func(t *testing.T) {
150
+			var actualReq container.ExecStartRequest
151
+			client, err := New(
152
+				WithMockClient(func(req *http.Request) (*http.Response, error) {
153
+					if tc.expErr != "" {
154
+						return nil, fmt.Errorf("should not have made API request")
155
+					}
156
+					if err := json.NewDecoder(req.Body).Decode(&actualReq); err != nil {
157
+						return nil, err
158
+					}
159
+
160
+					return mockJSONResponse(http.StatusOK, nil, ExecStartResult{})(req)
161
+				}),
162
+			)
163
+			assert.NilError(t, err)
164
+
165
+			_, err = client.ExecStart(t.Context(), "exec_id", tc.options)
166
+			if tc.expErr != "" {
167
+				assert.Check(t, is.ErrorType(err, errdefs.IsInvalidArgument))
168
+				assert.Check(t, is.ErrorContains(err, tc.expErr))
169
+				assert.Check(t, is.DeepEqual(actualReq, tc.expReq))
170
+			} else {
171
+				assert.NilError(t, err)
172
+				assert.Check(t, is.DeepEqual(actualReq, tc.expReq))
173
+			}
174
+		})
175
+	}
133 176
 }
134 177
 
135 178
 func TestExecInspectError(t *testing.T) {
... ...
@@ -138,7 +180,7 @@ func TestExecInspectError(t *testing.T) {
138 138
 	)
139 139
 	assert.NilError(t, err)
140 140
 
141
-	_, err = client.ExecInspect(context.Background(), "nothing", ExecInspectOptions{})
141
+	_, err = client.ExecInspect(t.Context(), "nothing", ExecInspectOptions{})
142 142
 	assert.Check(t, is.ErrorType(err, cerrdefs.IsInternal))
143 143
 }
144 144
 
... ...
@@ -157,7 +199,7 @@ func TestExecInspect(t *testing.T) {
157 157
 	)
158 158
 	assert.NilError(t, err)
159 159
 
160
-	inspect, err := client.ExecInspect(context.Background(), "exec_id", ExecInspectOptions{})
160
+	inspect, err := client.ExecInspect(t.Context(), "exec_id", ExecInspectOptions{})
161 161
 	assert.NilError(t, err)
162 162
 	assert.Check(t, is.Equal(inspect.ID, "exec_id"))
163 163
 	assert.Check(t, is.Equal(inspect.ContainerID, "container_id"))
... ...
@@ -21,7 +21,10 @@ func TestExecConsoleSize(t *testing.T) {
21 21
 	result, err := container.Exec(ctx, apiClient, cID, []string{"stty", "size"},
22 22
 		func(ec *client.ExecCreateOptions) {
23 23
 			ec.TTY = true
24
-			ec.ConsoleSize = &[2]uint{57, 123}
24
+			ec.ConsoleSize = client.ConsoleSize{
25
+				Height: 57,
26
+				Width:  123,
27
+			}
25 28
 		},
26 29
 	)
27 30
 
... ...
@@ -12,17 +12,17 @@ import (
12 12
 // ExecCreateOptions is a small subset of the Config struct that holds the configuration
13 13
 // for the exec feature of docker.
14 14
 type ExecCreateOptions struct {
15
-	User         string   // User that will run the command
16
-	Privileged   bool     // Is the container in privileged mode
17
-	TTY          bool     // Attach standard streams to a tty.
18
-	ConsoleSize  *[2]uint `json:",omitempty"` // Initial console size [height, width]
19
-	AttachStdin  bool     // Attach the standard input, makes possible user interaction
20
-	AttachStderr bool     // Attach the standard error
21
-	AttachStdout bool     // Attach the standard output
22
-	DetachKeys   string   // Escape keys for detach
23
-	Env          []string // Environment variables
24
-	WorkingDir   string   // Working directory
25
-	Cmd          []string // Execution commands and args
15
+	User         string      // User that will run the command
16
+	Privileged   bool        // Is the container in privileged mode
17
+	TTY          bool        // Attach standard streams to a tty.
18
+	ConsoleSize  ConsoleSize // Initial terminal size [height, width], unused if TTY == false
19
+	AttachStdin  bool        // Attach the standard input, makes possible user interaction
20
+	AttachStderr bool        // Attach the standard error
21
+	AttachStdout bool        // Attach the standard output
22
+	DetachKeys   string      // Escape keys for detach
23
+	Env          []string    // Environment variables
24
+	WorkingDir   string      // Working directory
25
+	Cmd          []string    // Execution commands and args
26 26
 }
27 27
 
28 28
 // ExecCreateResult holds the result of creating a container exec.
... ...
@@ -37,11 +37,16 @@ func (cli *Client) ExecCreate(ctx context.Context, containerID string, options E
37 37
 		return ExecCreateResult{}, err
38 38
 	}
39 39
 
40
+	consoleSize, err := getConsoleSize(options.TTY, options.ConsoleSize)
41
+	if err != nil {
42
+		return ExecCreateResult{}, err
43
+	}
44
+
40 45
 	req := container.ExecCreateRequest{
41 46
 		User:         options.User,
42 47
 		Privileged:   options.Privileged,
43 48
 		Tty:          options.TTY,
44
-		ConsoleSize:  options.ConsoleSize,
49
+		ConsoleSize:  consoleSize,
45 50
 		AttachStdin:  options.AttachStdin,
46 51
 		AttachStderr: options.AttachStderr,
47 52
 		AttachStdout: options.AttachStdout,
... ...
@@ -73,7 +78,7 @@ type ExecStartOptions struct {
73 73
 	// Check if there's a tty
74 74
 	TTY bool
75 75
 	// Terminal size [height, width], unused if TTY == false
76
-	ConsoleSize ConsoleSize `json:",omitzero"`
76
+	ConsoleSize ConsoleSize
77 77
 }
78 78
 
79 79
 // ExecStartResult holds the result of starting a container exec.
... ...
@@ -82,13 +87,16 @@ type ExecStartResult struct {
82 82
 
83 83
 // ExecStart starts an exec process already created in the docker host.
84 84
 func (cli *Client) ExecStart(ctx context.Context, execID string, options ExecStartOptions) (ExecStartResult, error) {
85
-	req := container.ExecStartRequest{
86
-		Detach: options.Detach,
87
-		Tty:    options.TTY,
88
-	}
89
-	if err := applyConsoleSize(&req, &options.ConsoleSize); err != nil {
85
+	consoleSize, err := getConsoleSize(options.TTY, options.ConsoleSize)
86
+	if err != nil {
90 87
 		return ExecStartResult{}, err
91 88
 	}
89
+
90
+	req := container.ExecStartRequest{
91
+		Detach:      options.Detach,
92
+		Tty:         options.TTY,
93
+		ConsoleSize: consoleSize,
94
+	}
92 95
 	resp, err := cli.post(ctx, "/exec/"+execID+"/start", nil, req, nil)
93 96
 	defer ensureReaderClosed(resp)
94 97
 	return ExecStartResult{}, err
... ...
@@ -126,27 +134,29 @@ type ExecAttachResult struct {
126 126
 //
127 127
 // [stdcopy.StdCopy]: https://pkg.go.dev/github.com/moby/moby/api/pkg/stdcopy#StdCopy
128 128
 func (cli *Client) ExecAttach(ctx context.Context, execID string, options ExecAttachOptions) (ExecAttachResult, error) {
129
-	req := container.ExecStartRequest{
130
-		Detach: false,
131
-		Tty:    options.TTY,
132
-	}
133
-	if err := applyConsoleSize(&req, &options.ConsoleSize); err != nil {
129
+	consoleSize, err := getConsoleSize(options.TTY, options.ConsoleSize)
130
+	if err != nil {
134 131
 		return ExecAttachResult{}, err
135 132
 	}
133
+	req := container.ExecStartRequest{
134
+		Detach:      false,
135
+		Tty:         options.TTY,
136
+		ConsoleSize: consoleSize,
137
+	}
136 138
 	response, err := cli.postHijacked(ctx, "/exec/"+execID+"/start", nil, req, http.Header{
137 139
 		"Content-Type": {"application/json"},
138 140
 	})
139 141
 	return ExecAttachResult{HijackedResponse: response}, err
140 142
 }
141 143
 
142
-func applyConsoleSize(req *container.ExecStartRequest, consoleSize *ConsoleSize) error {
144
+func getConsoleSize(hasTTY bool, consoleSize ConsoleSize) (*[2]uint, error) {
143 145
 	if consoleSize.Height != 0 || consoleSize.Width != 0 {
144
-		if !req.Tty {
145
-			return errdefs.ErrInvalidArgument.WithMessage("console size is only supported when TTY is enabled")
146
+		if !hasTTY {
147
+			return nil, errdefs.ErrInvalidArgument.WithMessage("console size is only supported when TTY is enabled")
146 148
 		}
147
-		req.ConsoleSize = &[2]uint{consoleSize.Height, consoleSize.Width}
149
+		return &[2]uint{consoleSize.Height, consoleSize.Width}, nil
148 150
 	}
149
-	return nil
151
+	return nil, nil
150 152
 }
151 153
 
152 154
 // ExecInspectOptions holds options for inspecting a container exec.