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