github.com/pwn-term/docker@v0.0.0-20210616085119-6e977cce2565/moby/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 // Older clients (API < 1.40) expects the default to be shareable, make them happy 493 if hostConfig.IpcMode.IsEmpty() { 494 hostConfig.IpcMode = container.IpcMode("shareable") 495 } 496 } 497 if hostConfig != nil && versions.LessThan(version, "1.41") && !s.cgroup2 { 498 // Older clients expect the default to be "host" on cgroup v1 hosts 499 if hostConfig.CgroupnsMode.IsEmpty() { 500 hostConfig.CgroupnsMode = container.CgroupnsMode("host") 501 } 502 } 503 504 var platform *specs.Platform 505 if versions.GreaterThanOrEqualTo(version, "1.41") { 506 if v := r.Form.Get("platform"); v != "" { 507 p, err := platforms.Parse(v) 508 if err != nil { 509 return errdefs.InvalidParameter(err) 510 } 511 platform = &p 512 } 513 } 514 515 if hostConfig != nil && hostConfig.PidsLimit != nil && *hostConfig.PidsLimit <= 0 { 516 // Don't set a limit if either no limit was specified, or "unlimited" was 517 // explicitly set. 518 // Both `0` and `-1` are accepted as "unlimited", and historically any 519 // negative value was accepted, so treat those as "unlimited" as well. 520 hostConfig.PidsLimit = nil 521 } 522 523 ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{ 524 Name: name, 525 Config: config, 526 HostConfig: hostConfig, 527 NetworkingConfig: networkingConfig, 528 AdjustCPUShares: adjustCPUShares, 529 Platform: platform, 530 }) 531 if err != nil { 532 return err 533 } 534 535 return httputils.WriteJSON(w, http.StatusCreated, ccr) 536 } 537 538 func (s *containerRouter) deleteContainers(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 539 if err := httputils.ParseForm(r); err != nil { 540 return err 541 } 542 543 name := vars["name"] 544 config := &types.ContainerRmConfig{ 545 ForceRemove: httputils.BoolValue(r, "force"), 546 RemoveVolume: httputils.BoolValue(r, "v"), 547 RemoveLink: httputils.BoolValue(r, "link"), 548 } 549 550 if err := s.backend.ContainerRm(name, config); err != nil { 551 return err 552 } 553 554 w.WriteHeader(http.StatusNoContent) 555 556 return nil 557 } 558 559 func (s *containerRouter) postContainersResize(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 560 if err := httputils.ParseForm(r); err != nil { 561 return err 562 } 563 564 height, err := strconv.Atoi(r.Form.Get("h")) 565 if err != nil { 566 return errdefs.InvalidParameter(err) 567 } 568 width, err := strconv.Atoi(r.Form.Get("w")) 569 if err != nil { 570 return errdefs.InvalidParameter(err) 571 } 572 573 return s.backend.ContainerResize(vars["name"], height, width) 574 } 575 576 func (s *containerRouter) postContainersAttach(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 577 err := httputils.ParseForm(r) 578 if err != nil { 579 return err 580 } 581 containerName := vars["name"] 582 583 _, upgrade := r.Header["Upgrade"] 584 detachKeys := r.FormValue("detachKeys") 585 586 hijacker, ok := w.(http.Hijacker) 587 if !ok { 588 return errdefs.InvalidParameter(errors.Errorf("error attaching to container %s, hijack connection missing", containerName)) 589 } 590 591 setupStreams := func() (io.ReadCloser, io.Writer, io.Writer, error) { 592 conn, _, err := hijacker.Hijack() 593 if err != nil { 594 return nil, nil, nil, err 595 } 596 597 // set raw mode 598 conn.Write([]byte{}) 599 600 if upgrade { 601 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") 602 } else { 603 fmt.Fprintf(conn, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") 604 } 605 606 closer := func() error { 607 httputils.CloseStreams(conn) 608 return nil 609 } 610 return ioutils.NewReadCloserWrapper(conn, closer), conn, conn, nil 611 } 612 613 attachConfig := &backend.ContainerAttachConfig{ 614 GetStreams: setupStreams, 615 UseStdin: httputils.BoolValue(r, "stdin"), 616 UseStdout: httputils.BoolValue(r, "stdout"), 617 UseStderr: httputils.BoolValue(r, "stderr"), 618 Logs: httputils.BoolValue(r, "logs"), 619 Stream: httputils.BoolValue(r, "stream"), 620 DetachKeys: detachKeys, 621 MuxStreams: true, 622 } 623 624 if err = s.backend.ContainerAttach(containerName, attachConfig); err != nil { 625 logrus.Errorf("Handler for %s %s returned error: %v", r.Method, r.URL.Path, err) 626 // Remember to close stream if error happens 627 conn, _, errHijack := hijacker.Hijack() 628 if errHijack == nil { 629 statusCode := errdefs.GetHTTPErrorStatusCode(err) 630 statusText := http.StatusText(statusCode) 631 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()) 632 httputils.CloseStreams(conn) 633 } else { 634 logrus.Errorf("Error Hijacking: %v", err) 635 } 636 } 637 return nil 638 } 639 640 func (s *containerRouter) wsContainersAttach(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 641 if err := httputils.ParseForm(r); err != nil { 642 return err 643 } 644 containerName := vars["name"] 645 646 var err error 647 detachKeys := r.FormValue("detachKeys") 648 649 done := make(chan struct{}) 650 started := make(chan struct{}) 651 652 version := httputils.VersionFromContext(ctx) 653 654 setupStreams := func() (io.ReadCloser, io.Writer, io.Writer, error) { 655 wsChan := make(chan *websocket.Conn) 656 h := func(conn *websocket.Conn) { 657 wsChan <- conn 658 <-done 659 } 660 661 srv := websocket.Server{Handler: h, Handshake: nil} 662 go func() { 663 close(started) 664 srv.ServeHTTP(w, r) 665 }() 666 667 conn := <-wsChan 668 // In case version 1.28 and above, a binary frame will be sent. 669 // See 28176 for details. 670 if versions.GreaterThanOrEqualTo(version, "1.28") { 671 conn.PayloadType = websocket.BinaryFrame 672 } 673 return conn, conn, conn, nil 674 } 675 676 attachConfig := &backend.ContainerAttachConfig{ 677 GetStreams: setupStreams, 678 Logs: httputils.BoolValue(r, "logs"), 679 Stream: httputils.BoolValue(r, "stream"), 680 DetachKeys: detachKeys, 681 UseStdin: true, 682 UseStdout: true, 683 UseStderr: true, 684 MuxStreams: false, // TODO: this should be true since it's a single stream for both stdout and stderr 685 } 686 687 err = s.backend.ContainerAttach(containerName, attachConfig) 688 close(done) 689 select { 690 case <-started: 691 if err != nil { 692 logrus.Errorf("Error attaching websocket: %s", err) 693 } else { 694 logrus.Debug("websocket connection was closed by client") 695 } 696 return nil 697 default: 698 } 699 return err 700 } 701 702 func (s *containerRouter) postContainersPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 703 if err := httputils.ParseForm(r); err != nil { 704 return err 705 } 706 707 pruneFilters, err := filters.FromJSON(r.Form.Get("filters")) 708 if err != nil { 709 return errdefs.InvalidParameter(err) 710 } 711 712 pruneReport, err := s.backend.ContainersPrune(ctx, pruneFilters) 713 if err != nil { 714 return err 715 } 716 return httputils.WriteJSON(w, http.StatusOK, pruneReport) 717 }