github.com/docker/docker@v299999999.0.0-20200612211812-aaf470eca7b5+incompatible/api/server/router/container/container_routes.go (about) 1 package container // import "github.com/docker/docker/api/server/router/container" 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "strconv" 10 "syscall" 11 12 "github.com/containerd/containerd/platforms" 13 "github.com/docker/docker/api/server/httputils" 14 "github.com/docker/docker/api/types" 15 "github.com/docker/docker/api/types/backend" 16 "github.com/docker/docker/api/types/container" 17 "github.com/docker/docker/api/types/filters" 18 "github.com/docker/docker/api/types/versions" 19 containerpkg "github.com/docker/docker/container" 20 "github.com/docker/docker/errdefs" 21 "github.com/docker/docker/pkg/ioutils" 22 "github.com/docker/docker/pkg/signal" 23 specs "github.com/opencontainers/image-spec/specs-go/v1" 24 "github.com/pkg/errors" 25 "github.com/sirupsen/logrus" 26 "golang.org/x/net/websocket" 27 ) 28 29 func (s *containerRouter) postCommit(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 30 if err := httputils.ParseForm(r); err != nil { 31 return err 32 } 33 34 if err := httputils.CheckForJSON(r); err != nil { 35 return err 36 } 37 38 // TODO: remove pause arg, and always pause in backend 39 pause := httputils.BoolValue(r, "pause") 40 version := httputils.VersionFromContext(ctx) 41 if r.FormValue("pause") == "" && versions.GreaterThanOrEqualTo(version, "1.13") { 42 pause = true 43 } 44 45 config, _, _, err := s.decoder.DecodeConfig(r.Body) 46 if err != nil && err != io.EOF { // Do not fail if body is empty. 47 return err 48 } 49 50 commitCfg := &backend.CreateImageConfig{ 51 Pause: pause, 52 Repo: r.Form.Get("repo"), 53 Tag: r.Form.Get("tag"), 54 Author: r.Form.Get("author"), 55 Comment: r.Form.Get("comment"), 56 Config: config, 57 Changes: r.Form["changes"], 58 } 59 60 imgID, err := s.backend.CreateImageFromContainer(r.Form.Get("container"), commitCfg) 61 if err != nil { 62 return err 63 } 64 65 return httputils.WriteJSON(w, http.StatusCreated, &types.IDResponse{ID: imgID}) 66 } 67 68 func (s *containerRouter) getContainersJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 69 if err := httputils.ParseForm(r); err != nil { 70 return err 71 } 72 filter, err := filters.FromJSON(r.Form.Get("filters")) 73 if err != nil { 74 return err 75 } 76 77 config := &types.ContainerListOptions{ 78 All: httputils.BoolValue(r, "all"), 79 Size: httputils.BoolValue(r, "size"), 80 Since: r.Form.Get("since"), 81 Before: r.Form.Get("before"), 82 Filters: filter, 83 } 84 85 if tmpLimit := r.Form.Get("limit"); tmpLimit != "" { 86 limit, err := strconv.Atoi(tmpLimit) 87 if err != nil { 88 return err 89 } 90 config.Limit = limit 91 } 92 93 containers, err := s.backend.Containers(config) 94 if err != nil { 95 return err 96 } 97 98 return httputils.WriteJSON(w, http.StatusOK, containers) 99 } 100 101 func (s *containerRouter) getContainersStats(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 102 if err := httputils.ParseForm(r); err != nil { 103 return err 104 } 105 106 stream := httputils.BoolValueOrDefault(r, "stream", true) 107 if !stream { 108 w.Header().Set("Content-Type", "application/json") 109 } 110 var oneShot bool 111 if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.41") { 112 oneShot = httputils.BoolValueOrDefault(r, "one-shot", false) 113 } 114 115 config := &backend.ContainerStatsConfig{ 116 Stream: stream, 117 OneShot: oneShot, 118 OutStream: w, 119 Version: httputils.VersionFromContext(ctx), 120 } 121 122 return s.backend.ContainerStats(ctx, vars["name"], config) 123 } 124 125 func (s *containerRouter) getContainersLogs(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 126 if err := httputils.ParseForm(r); err != nil { 127 return err 128 } 129 130 // Args are validated before the stream starts because when it starts we're 131 // sending HTTP 200 by writing an empty chunk of data to tell the client that 132 // daemon is going to stream. By sending this initial HTTP 200 we can't report 133 // any error after the stream starts (i.e. container not found, wrong parameters) 134 // with the appropriate status code. 135 stdout, stderr := httputils.BoolValue(r, "stdout"), httputils.BoolValue(r, "stderr") 136 if !(stdout || stderr) { 137 return errdefs.InvalidParameter(errors.New("Bad parameters: you must choose at least one stream")) 138 } 139 140 containerName := vars["name"] 141 logsConfig := &types.ContainerLogsOptions{ 142 Follow: httputils.BoolValue(r, "follow"), 143 Timestamps: httputils.BoolValue(r, "timestamps"), 144 Since: r.Form.Get("since"), 145 Until: r.Form.Get("until"), 146 Tail: r.Form.Get("tail"), 147 ShowStdout: stdout, 148 ShowStderr: stderr, 149 Details: httputils.BoolValue(r, "details"), 150 } 151 152 msgs, tty, err := s.backend.ContainerLogs(ctx, containerName, logsConfig) 153 if err != nil { 154 return err 155 } 156 157 // if has a tty, we're not muxing streams. if it doesn't, we are. simple. 158 // this is the point of no return for writing a response. once we call 159 // WriteLogStream, the response has been started and errors will be 160 // returned in band by WriteLogStream 161 httputils.WriteLogStream(ctx, w, msgs, logsConfig, !tty) 162 return nil 163 } 164 165 func (s *containerRouter) getContainersExport(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 166 return s.backend.ContainerExport(vars["name"], w) 167 } 168 169 type bodyOnStartError struct{} 170 171 func (bodyOnStartError) Error() string { 172 return "starting container with non-empty request body was deprecated since API v1.22 and removed in v1.24" 173 } 174 175 func (bodyOnStartError) InvalidParameter() {} 176 177 func (s *containerRouter) postContainersStart(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 178 // If contentLength is -1, we can assumed chunked encoding 179 // or more technically that the length is unknown 180 // https://golang.org/src/pkg/net/http/request.go#L139 181 // net/http otherwise seems to swallow any headers related to chunked encoding 182 // including r.TransferEncoding 183 // allow a nil body for backwards compatibility 184 185 version := httputils.VersionFromContext(ctx) 186 var hostConfig *container.HostConfig 187 // A non-nil json object is at least 7 characters. 188 if r.ContentLength > 7 || r.ContentLength == -1 { 189 if versions.GreaterThanOrEqualTo(version, "1.24") { 190 return bodyOnStartError{} 191 } 192 193 if err := httputils.CheckForJSON(r); err != nil { 194 return err 195 } 196 197 c, err := s.decoder.DecodeHostConfig(r.Body) 198 if err != nil { 199 return err 200 } 201 hostConfig = c 202 } 203 204 if err := httputils.ParseForm(r); err != nil { 205 return err 206 } 207 208 checkpoint := r.Form.Get("checkpoint") 209 checkpointDir := r.Form.Get("checkpoint-dir") 210 if err := s.backend.ContainerStart(vars["name"], hostConfig, checkpoint, checkpointDir); err != nil { 211 return err 212 } 213 214 w.WriteHeader(http.StatusNoContent) 215 return nil 216 } 217 218 func (s *containerRouter) postContainersStop(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 219 if err := httputils.ParseForm(r); err != nil { 220 return err 221 } 222 223 var seconds *int 224 if tmpSeconds := r.Form.Get("t"); tmpSeconds != "" { 225 valSeconds, err := strconv.Atoi(tmpSeconds) 226 if err != nil { 227 return err 228 } 229 seconds = &valSeconds 230 } 231 232 if err := s.backend.ContainerStop(vars["name"], seconds); err != nil { 233 return err 234 } 235 w.WriteHeader(http.StatusNoContent) 236 237 return nil 238 } 239 240 func (s *containerRouter) postContainersKill(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 241 if err := httputils.ParseForm(r); err != nil { 242 return err 243 } 244 245 var sig syscall.Signal 246 name := vars["name"] 247 248 // If we have a signal, look at it. Otherwise, do nothing 249 if sigStr := r.Form.Get("signal"); sigStr != "" { 250 var err error 251 if sig, err = signal.ParseSignal(sigStr); err != nil { 252 return errdefs.InvalidParameter(err) 253 } 254 } 255 256 if err := s.backend.ContainerKill(name, uint64(sig)); err != nil { 257 var isStopped bool 258 if errdefs.IsConflict(err) { 259 isStopped = true 260 } 261 262 // Return error that's not caused because the container is stopped. 263 // Return error if the container is not running and the api is >= 1.20 264 // to keep backwards compatibility. 265 version := httputils.VersionFromContext(ctx) 266 if versions.GreaterThanOrEqualTo(version, "1.20") || !isStopped { 267 return errors.Wrapf(err, "Cannot kill container: %s", name) 268 } 269 } 270 271 w.WriteHeader(http.StatusNoContent) 272 return nil 273 } 274 275 func (s *containerRouter) postContainersRestart(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 276 if err := httputils.ParseForm(r); err != nil { 277 return err 278 } 279 280 var seconds *int 281 if tmpSeconds := r.Form.Get("t"); tmpSeconds != "" { 282 valSeconds, err := strconv.Atoi(tmpSeconds) 283 if err != nil { 284 return err 285 } 286 seconds = &valSeconds 287 } 288 289 if err := s.backend.ContainerRestart(vars["name"], seconds); err != nil { 290 return err 291 } 292 293 w.WriteHeader(http.StatusNoContent) 294 295 return nil 296 } 297 298 func (s *containerRouter) postContainersPause(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 299 if err := httputils.ParseForm(r); err != nil { 300 return err 301 } 302 303 if err := s.backend.ContainerPause(vars["name"]); err != nil { 304 return err 305 } 306 307 w.WriteHeader(http.StatusNoContent) 308 309 return nil 310 } 311 312 func (s *containerRouter) postContainersUnpause(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 313 if err := httputils.ParseForm(r); err != nil { 314 return err 315 } 316 317 if err := s.backend.ContainerUnpause(vars["name"]); err != nil { 318 return err 319 } 320 321 w.WriteHeader(http.StatusNoContent) 322 323 return nil 324 } 325 326 func (s *containerRouter) postContainersWait(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 327 // Behavior changed in version 1.30 to handle wait condition and to 328 // return headers immediately. 329 version := httputils.VersionFromContext(ctx) 330 legacyBehaviorPre130 := versions.LessThan(version, "1.30") 331 legacyRemovalWaitPre134 := false 332 333 // The wait condition defaults to "not-running". 334 waitCondition := containerpkg.WaitConditionNotRunning 335 if !legacyBehaviorPre130 { 336 if err := httputils.ParseForm(r); err != nil { 337 return err 338 } 339 switch container.WaitCondition(r.Form.Get("condition")) { 340 case container.WaitConditionNextExit: 341 waitCondition = containerpkg.WaitConditionNextExit 342 case container.WaitConditionRemoved: 343 waitCondition = containerpkg.WaitConditionRemoved 344 legacyRemovalWaitPre134 = versions.LessThan(version, "1.34") 345 } 346 } 347 348 waitC, err := s.backend.ContainerWait(ctx, vars["name"], waitCondition) 349 if err != nil { 350 return err 351 } 352 353 w.Header().Set("Content-Type", "application/json") 354 355 if !legacyBehaviorPre130 { 356 // Write response header immediately. 357 w.WriteHeader(http.StatusOK) 358 if flusher, ok := w.(http.Flusher); ok { 359 flusher.Flush() 360 } 361 } 362 363 // Block on the result of the wait operation. 364 status := <-waitC 365 366 // With API < 1.34, wait on WaitConditionRemoved did not return 367 // in case container removal failed. The only way to report an 368 // error back to the client is to not write anything (i.e. send 369 // an empty response which will be treated as an error). 370 if legacyRemovalWaitPre134 && status.Err() != nil { 371 return nil 372 } 373 374 var waitError *container.ContainerWaitOKBodyError 375 if status.Err() != nil { 376 waitError = &container.ContainerWaitOKBodyError{Message: status.Err().Error()} 377 } 378 379 return json.NewEncoder(w).Encode(&container.ContainerWaitOKBody{ 380 StatusCode: int64(status.ExitCode()), 381 Error: waitError, 382 }) 383 } 384 385 func (s *containerRouter) getContainersChanges(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 386 changes, err := s.backend.ContainerChanges(vars["name"]) 387 if err != nil { 388 return err 389 } 390 391 return httputils.WriteJSON(w, http.StatusOK, changes) 392 } 393 394 func (s *containerRouter) getContainersTop(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 395 if err := httputils.ParseForm(r); err != nil { 396 return err 397 } 398 399 procList, err := s.backend.ContainerTop(vars["name"], r.Form.Get("ps_args")) 400 if err != nil { 401 return err 402 } 403 404 return httputils.WriteJSON(w, http.StatusOK, procList) 405 } 406 407 func (s *containerRouter) postContainerRename(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 408 if err := httputils.ParseForm(r); err != nil { 409 return err 410 } 411 412 name := vars["name"] 413 newName := r.Form.Get("name") 414 if err := s.backend.ContainerRename(name, newName); err != nil { 415 return err 416 } 417 w.WriteHeader(http.StatusNoContent) 418 return nil 419 } 420 421 func (s *containerRouter) postContainerUpdate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 422 if err := httputils.ParseForm(r); err != nil { 423 return err 424 } 425 if err := httputils.CheckForJSON(r); err != nil { 426 return err 427 } 428 429 var updateConfig container.UpdateConfig 430 431 decoder := json.NewDecoder(r.Body) 432 if err := decoder.Decode(&updateConfig); err != nil { 433 return err 434 } 435 if versions.LessThan(httputils.VersionFromContext(ctx), "1.40") { 436 updateConfig.PidsLimit = nil 437 } 438 if updateConfig.PidsLimit != nil && *updateConfig.PidsLimit <= 0 { 439 // Both `0` and `-1` are accepted to set "unlimited" when updating. 440 // Historically, any negative value was accepted, so treat them as 441 // "unlimited" as well. 442 var unlimited int64 443 updateConfig.PidsLimit = &unlimited 444 } 445 446 hostConfig := &container.HostConfig{ 447 Resources: updateConfig.Resources, 448 RestartPolicy: updateConfig.RestartPolicy, 449 } 450 451 name := vars["name"] 452 resp, err := s.backend.ContainerUpdate(name, hostConfig) 453 if err != nil { 454 return err 455 } 456 457 return httputils.WriteJSON(w, http.StatusOK, resp) 458 } 459 460 func (s *containerRouter) postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 461 if err := httputils.ParseForm(r); err != nil { 462 return err 463 } 464 if err := httputils.CheckForJSON(r); err != nil { 465 return err 466 } 467 468 name := r.Form.Get("name") 469 470 config, hostConfig, networkingConfig, err := s.decoder.DecodeConfig(r.Body) 471 if err != nil { 472 return err 473 } 474 version := httputils.VersionFromContext(ctx) 475 adjustCPUShares := versions.LessThan(version, "1.19") 476 477 // When using API 1.24 and under, the client is responsible for removing the container 478 if hostConfig != nil && versions.LessThan(version, "1.25") { 479 hostConfig.AutoRemove = false 480 } 481 482 if hostConfig != nil && versions.LessThan(version, "1.40") { 483 // Ignore BindOptions.NonRecursive because it was added in API 1.40. 484 for _, m := range hostConfig.Mounts { 485 if bo := m.BindOptions; bo != nil { 486 bo.NonRecursive = false 487 } 488 } 489 // Ignore KernelMemoryTCP because it was added in API 1.40. 490 hostConfig.KernelMemoryTCP = 0 491 492 // Ignore Capabilities because it was added in API 1.40. 493 hostConfig.Capabilities = nil 494 495 // Older clients (API < 1.40) expects the default to be shareable, make them happy 496 if hostConfig.IpcMode.IsEmpty() { 497 hostConfig.IpcMode = container.IpcMode("shareable") 498 } 499 } 500 if hostConfig != nil && versions.LessThan(version, "1.41") { 501 // Older clients expect the default to be "host" 502 if hostConfig.CgroupnsMode.IsEmpty() { 503 hostConfig.CgroupnsMode = container.CgroupnsMode("host") 504 } 505 } 506 507 var platform *specs.Platform 508 if versions.GreaterThanOrEqualTo(version, "1.41") { 509 if v := r.Form.Get("platform"); v != "" { 510 p, err := platforms.Parse(v) 511 if err != nil { 512 return errdefs.InvalidParameter(err) 513 } 514 platform = &p 515 } 516 defaultPlatform := platforms.DefaultSpec() 517 if platform == nil { 518 platform = &defaultPlatform 519 } 520 if platform.OS == "" { 521 platform.OS = defaultPlatform.OS 522 } 523 if platform.Architecture == "" { 524 platform.Architecture = defaultPlatform.Architecture 525 platform.Variant = defaultPlatform.Variant 526 } 527 } 528 529 if hostConfig != nil && hostConfig.PidsLimit != nil && *hostConfig.PidsLimit <= 0 { 530 // Don't set a limit if either no limit was specified, or "unlimited" was 531 // explicitly set. 532 // Both `0` and `-1` are accepted as "unlimited", and historically any 533 // negative value was accepted, so treat those as "unlimited" as well. 534 hostConfig.PidsLimit = nil 535 } 536 537 ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{ 538 Name: name, 539 Config: config, 540 HostConfig: hostConfig, 541 NetworkingConfig: networkingConfig, 542 AdjustCPUShares: adjustCPUShares, 543 Platform: platform, 544 }) 545 if err != nil { 546 return err 547 } 548 549 return httputils.WriteJSON(w, http.StatusCreated, ccr) 550 } 551 552 func (s *containerRouter) deleteContainers(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 553 if err := httputils.ParseForm(r); err != nil { 554 return err 555 } 556 557 name := vars["name"] 558 config := &types.ContainerRmConfig{ 559 ForceRemove: httputils.BoolValue(r, "force"), 560 RemoveVolume: httputils.BoolValue(r, "v"), 561 RemoveLink: httputils.BoolValue(r, "link"), 562 } 563 564 if err := s.backend.ContainerRm(name, config); err != nil { 565 return err 566 } 567 568 w.WriteHeader(http.StatusNoContent) 569 570 return nil 571 } 572 573 func (s *containerRouter) postContainersResize(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 574 if err := httputils.ParseForm(r); err != nil { 575 return err 576 } 577 578 height, err := strconv.Atoi(r.Form.Get("h")) 579 if err != nil { 580 return errdefs.InvalidParameter(err) 581 } 582 width, err := strconv.Atoi(r.Form.Get("w")) 583 if err != nil { 584 return errdefs.InvalidParameter(err) 585 } 586 587 return s.backend.ContainerResize(vars["name"], height, width) 588 } 589 590 func (s *containerRouter) postContainersAttach(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 591 err := httputils.ParseForm(r) 592 if err != nil { 593 return err 594 } 595 containerName := vars["name"] 596 597 _, upgrade := r.Header["Upgrade"] 598 detachKeys := r.FormValue("detachKeys") 599 600 hijacker, ok := w.(http.Hijacker) 601 if !ok { 602 return errdefs.InvalidParameter(errors.Errorf("error attaching to container %s, hijack connection missing", containerName)) 603 } 604 605 setupStreams := func() (io.ReadCloser, io.Writer, io.Writer, error) { 606 conn, _, err := hijacker.Hijack() 607 if err != nil { 608 return nil, nil, nil, err 609 } 610 611 // set raw mode 612 conn.Write([]byte{}) 613 614 if upgrade { 615 fmt.Fprintf(conn, "HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: tcp\r\n\r\n") 616 } else { 617 fmt.Fprintf(conn, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") 618 } 619 620 closer := func() error { 621 httputils.CloseStreams(conn) 622 return nil 623 } 624 return ioutils.NewReadCloserWrapper(conn, closer), conn, conn, nil 625 } 626 627 attachConfig := &backend.ContainerAttachConfig{ 628 GetStreams: setupStreams, 629 UseStdin: httputils.BoolValue(r, "stdin"), 630 UseStdout: httputils.BoolValue(r, "stdout"), 631 UseStderr: httputils.BoolValue(r, "stderr"), 632 Logs: httputils.BoolValue(r, "logs"), 633 Stream: httputils.BoolValue(r, "stream"), 634 DetachKeys: detachKeys, 635 MuxStreams: true, 636 } 637 638 if err = s.backend.ContainerAttach(containerName, attachConfig); err != nil { 639 logrus.Errorf("Handler for %s %s returned error: %v", r.Method, r.URL.Path, err) 640 // Remember to close stream if error happens 641 conn, _, errHijack := hijacker.Hijack() 642 if errHijack == nil { 643 statusCode := errdefs.GetHTTPErrorStatusCode(err) 644 statusText := http.StatusText(statusCode) 645 fmt.Fprintf(conn, "HTTP/1.1 %d %s\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n%s\r\n", statusCode, statusText, err.Error()) 646 httputils.CloseStreams(conn) 647 } else { 648 logrus.Errorf("Error Hijacking: %v", err) 649 } 650 } 651 return nil 652 } 653 654 func (s *containerRouter) wsContainersAttach(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 655 if err := httputils.ParseForm(r); err != nil { 656 return err 657 } 658 containerName := vars["name"] 659 660 var err error 661 detachKeys := r.FormValue("detachKeys") 662 663 done := make(chan struct{}) 664 started := make(chan struct{}) 665 666 version := httputils.VersionFromContext(ctx) 667 668 setupStreams := func() (io.ReadCloser, io.Writer, io.Writer, error) { 669 wsChan := make(chan *websocket.Conn) 670 h := func(conn *websocket.Conn) { 671 wsChan <- conn 672 <-done 673 } 674 675 srv := websocket.Server{Handler: h, Handshake: nil} 676 go func() { 677 close(started) 678 srv.ServeHTTP(w, r) 679 }() 680 681 conn := <-wsChan 682 // In case version 1.28 and above, a binary frame will be sent. 683 // See 28176 for details. 684 if versions.GreaterThanOrEqualTo(version, "1.28") { 685 conn.PayloadType = websocket.BinaryFrame 686 } 687 return conn, conn, conn, nil 688 } 689 690 attachConfig := &backend.ContainerAttachConfig{ 691 GetStreams: setupStreams, 692 Logs: httputils.BoolValue(r, "logs"), 693 Stream: httputils.BoolValue(r, "stream"), 694 DetachKeys: detachKeys, 695 UseStdin: true, 696 UseStdout: true, 697 UseStderr: true, 698 MuxStreams: false, // TODO: this should be true since it's a single stream for both stdout and stderr 699 } 700 701 err = s.backend.ContainerAttach(containerName, attachConfig) 702 close(done) 703 select { 704 case <-started: 705 if err != nil { 706 logrus.Errorf("Error attaching websocket: %s", err) 707 } else { 708 logrus.Debug("websocket connection was closed by client") 709 } 710 return nil 711 default: 712 } 713 return err 714 } 715 716 func (s *containerRouter) postContainersPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 717 if err := httputils.ParseForm(r); err != nil { 718 return err 719 } 720 721 pruneFilters, err := filters.FromJSON(r.Form.Get("filters")) 722 if err != nil { 723 return errdefs.InvalidParameter(err) 724 } 725 726 pruneReport, err := s.backend.ContainersPrune(ctx, pruneFilters) 727 if err != nil { 728 return err 729 } 730 return httputils.WriteJSON(w, http.StatusOK, pruneReport) 731 }