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