Browse code

integration/container: add TestExecResize

Add integration tests for resizing exec's.

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

Sebastiaan van Stijn authored on 2024/10/15 06:09:10
Showing 1 changed files
... ...
@@ -1,13 +1,20 @@
1 1
 package container // import "github.com/docker/docker/integration/container"
2 2
 
3 3
 import (
4
+	"encoding/json"
4 5
 	"io"
6
+	"net/http"
7
+	"net/url"
8
+	"runtime"
5 9
 	"strings"
6 10
 	"testing"
7 11
 	"time"
8 12
 
13
+	"github.com/docker/docker/api/types"
9 14
 	containertypes "github.com/docker/docker/api/types/container"
15
+	"github.com/docker/docker/errdefs"
10 16
 	"github.com/docker/docker/integration/internal/container"
17
+	req "github.com/docker/docker/testutil/request"
11 18
 	"gotest.tools/v3/assert"
12 19
 	is "gotest.tools/v3/assert/cmp"
13 20
 	"gotest.tools/v3/skip"
... ...
@@ -106,6 +113,145 @@ func TestExec(t *testing.T) {
106 106
 	assert.Check(t, is.Contains(out, "FOO=BAR"), "exec command not running with expected environment variable FOO")
107 107
 }
108 108
 
109
+func TestExecResize(t *testing.T) {
110
+	ctx := setupTest(t)
111
+	apiClient := testEnv.APIClient()
112
+
113
+	cID := container.Run(ctx, t, apiClient, container.WithTty(true))
114
+	defer container.Remove(ctx, t, apiClient, cID, containertypes.RemoveOptions{Force: true})
115
+
116
+	cmd := []string{"top"}
117
+	if runtime.GOOS == "windows" {
118
+		cmd = []string{"sleep", "240"}
119
+	}
120
+	resp, err := apiClient.ContainerExecCreate(ctx, cID, containertypes.ExecOptions{
121
+		Tty:    true, // Windows requires a TTY for the resize to work, otherwise fails with "is not a tty: failed precondition", see https://github.com/moby/moby/pull/48665#issuecomment-2412530345
122
+		Detach: true,
123
+		Cmd:    cmd,
124
+	})
125
+	assert.NilError(t, err)
126
+	execID := resp.ID
127
+	assert.NilError(t, err)
128
+	err = apiClient.ContainerExecStart(ctx, execID, containertypes.ExecStartOptions{Detach: true})
129
+	assert.NilError(t, err)
130
+
131
+	t.Run("success", func(t *testing.T) {
132
+		err := apiClient.ContainerExecResize(ctx, execID, containertypes.ResizeOptions{
133
+			Height: 40,
134
+			Width:  40,
135
+		})
136
+		assert.NilError(t, err)
137
+		// TODO(thaJeztah): also check if the resize happened
138
+		//
139
+		// Note: container inspect shows the initial size that was
140
+		// set when creating the container. Actual resize happens in
141
+		// containerd, and currently does not update the container's
142
+		// config after running (but does send a "resize" event).
143
+	})
144
+
145
+	t.Run("invalid size", func(t *testing.T) {
146
+		const valueNotSet = "unset"
147
+
148
+		sizes := []struct {
149
+			doc, height, width, expErr string
150
+		}{
151
+			{
152
+				doc:    "unset height",
153
+				height: valueNotSet,
154
+				width:  "100",
155
+				expErr: `strconv.Atoi: parsing "": invalid syntax`,
156
+			},
157
+			{
158
+				doc:    "unset width",
159
+				height: "100",
160
+				width:  valueNotSet,
161
+				expErr: `strconv.Atoi: parsing "": invalid syntax`,
162
+			},
163
+			{
164
+				doc:    "empty height",
165
+				width:  "100",
166
+				expErr: `strconv.Atoi: parsing "": invalid syntax`,
167
+			},
168
+			{
169
+				doc:    "empty width",
170
+				height: "100",
171
+				expErr: `strconv.Atoi: parsing "": invalid syntax`,
172
+			},
173
+			{
174
+				doc:    "non-numeric height",
175
+				height: "not-a-number",
176
+				width:  "100",
177
+				expErr: `strconv.Atoi: parsing "not-a-number": invalid syntax`,
178
+			},
179
+			{
180
+				doc:    "non-numeric width",
181
+				height: "100",
182
+				width:  "not-a-number",
183
+				expErr: `strconv.Atoi: parsing "not-a-number": invalid syntax`,
184
+			},
185
+		}
186
+		for _, tc := range sizes {
187
+			tc := tc
188
+			t.Run(tc.doc, func(t *testing.T) {
189
+				// Manually creating a request here, as the APIClient would invalidate
190
+				// these values before they're sent.
191
+				vals := url.Values{}
192
+				if tc.height != valueNotSet {
193
+					vals.Add("h", tc.height)
194
+				}
195
+				if tc.width != valueNotSet {
196
+					vals.Add("w", tc.width)
197
+				}
198
+				res, _, err := req.Post(ctx, "/exec/"+execID+"/resize?"+vals.Encode())
199
+				assert.NilError(t, err)
200
+				assert.Check(t, is.Equal(http.StatusBadRequest, res.StatusCode))
201
+
202
+				var errorResponse types.ErrorResponse
203
+				err = json.NewDecoder(res.Body).Decode(&errorResponse)
204
+				assert.NilError(t, err)
205
+				assert.Check(t, is.ErrorContains(errorResponse, tc.expErr))
206
+			})
207
+		}
208
+	})
209
+
210
+	t.Run("unknown execID", func(t *testing.T) {
211
+		err = apiClient.ContainerExecResize(ctx, "no-such-exec-id", containertypes.ResizeOptions{
212
+			Height: 40,
213
+			Width:  40,
214
+		})
215
+		assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
216
+		assert.Check(t, is.ErrorContains(err, "No such exec instance: no-such-exec-id"))
217
+	})
218
+
219
+	t.Run("invalid state", func(t *testing.T) {
220
+		// FIXME(thaJeztah): Windows + builtin returns a NotFound instead of a Conflict error
221
+		//
222
+		// When using the builtin runtime, stopping the container causes
223
+		// the exec-resize to return a "NotFound" error, whereas with containerd
224
+		// as runtime, it returns the expected "Conflict" error. This could be
225
+		// either a limitation of the "builtin" runtime, or there's a bug to
226
+		// be fixed.
227
+		//
228
+		// See https://github.com/moby/moby/pull/48665#issuecomment-2412579701
229
+		//
230
+		//  === RUN   TestExecResize/invalid_state
231
+		//      exec_test.go:234: assertion failed: error is Error response from daemon: No such exec instance: cc728a332d3f594249fb7ee9adb3bb12a59a5d1776f8f6dedc56355364361711 (errdefs.errNotFound), not errdefs.IsConflict
232
+		//      exec_test.go:235: assertion failed: expected error to contain "is not running", got "Error response from daemon: No such exec instance: cc728a332d3f594249fb7ee9adb3bb12a59a5d1776f8f6dedc56355364361711"
233
+		//          Error response from daemon: No such exec instance: cc728a332d3f594249fb7ee9adb3bb12a59a5d1776f8f6dedc56355364361711
234
+		skip.If(t, testEnv.DaemonInfo.OSType == "windows" && !testEnv.RuntimeIsWindowsContainerd(), "FIXME. Windows + builtin returns a NotFound instead of a Conflict error")
235
+
236
+		err := apiClient.ContainerKill(ctx, cID, "SIGKILL")
237
+		assert.NilError(t, err)
238
+
239
+		err = apiClient.ContainerExecResize(ctx, execID, containertypes.ResizeOptions{
240
+			Height: 40,
241
+			Width:  40,
242
+		})
243
+		assert.Check(t, is.ErrorType(err, errdefs.IsConflict))
244
+		assert.Check(t, is.ErrorContains(err, "is not running"))
245
+	})
246
+}
247
+
109 248
 func TestExecUser(t *testing.T) {
110 249
 	skip.If(t, testEnv.DaemonInfo.OSType == "windows", "FIXME. Probably needs to wait for container to be in running state.")
111 250
 	ctx := setupTest(t)