Add integration tests for resizing exec's.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
| ... | ... |
@@ -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) |