mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
ce1ceeb257
Metrics collectors generally don't need the daemon to prime the stats with something to compare since they already have something to compare with. Before this change, the API does 2 collection cycles (which takes roughly 2s) in order to provide comparison for CPU usage over 1s. This was primarily added so that `docker stats --no-stream` had something to compare against. Really the CLI should have just made a 2nd call and done the comparison itself rather than forcing it on all API consumers. That ship has long sailed, though. With this change, clients can set an option to just pull a single stat, which is *at least* a full second faster: Old: ``` time curl --unix-socket /go/src/github.com/docker/docker/bundles/test-integration-shell/docker.sock http://./containers/test/stats?stream=false\&one-shot=false > /dev/null 2>&1 real0m1.864s user0m0.005s sys0m0.007s time curl --unix-socket /go/src/github.com/docker/docker/bundles/test-integration-shell/docker.sock http://./containers/test/stats?stream=false\&one-shot=false > /dev/null 2>&1 real0m1.173s user0m0.010s sys0m0.006s ``` New: ``` time curl --unix-socket /go/src/github.com/docker/docker/bundles/test-integration-shell/docker.sock http://./containers/test/stats?stream=false\&one-shot=true > /dev/null 2>&1 real0m0.680s user0m0.008s sys0m0.004s time curl --unix-socket /go/src/github.com/docker/docker/bundles/test-integration-shell/docker.sock http://./containers/test/stats?stream=false\&one-shot=true > /dev/null 2>&1 real0m0.156s user0m0.007s sys0m0.007s ``` This fixes issues with downstreams ability to use the stats API to collect metrics. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
706 lines
20 KiB
Go
706 lines
20 KiB
Go
package container // import "github.com/docker/docker/api/server/router/container"
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"syscall"
|
|
|
|
"github.com/docker/docker/api/server/httputils"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/backend"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/api/types/versions"
|
|
containerpkg "github.com/docker/docker/container"
|
|
"github.com/docker/docker/errdefs"
|
|
"github.com/docker/docker/pkg/ioutils"
|
|
"github.com/docker/docker/pkg/signal"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
"golang.org/x/net/websocket"
|
|
)
|
|
|
|
func (s *containerRouter) postCommit(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := httputils.CheckForJSON(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: remove pause arg, and always pause in backend
|
|
pause := httputils.BoolValue(r, "pause")
|
|
version := httputils.VersionFromContext(ctx)
|
|
if r.FormValue("pause") == "" && versions.GreaterThanOrEqualTo(version, "1.13") {
|
|
pause = true
|
|
}
|
|
|
|
config, _, _, err := s.decoder.DecodeConfig(r.Body)
|
|
if err != nil && err != io.EOF { // Do not fail if body is empty.
|
|
return err
|
|
}
|
|
|
|
commitCfg := &backend.CreateImageConfig{
|
|
Pause: pause,
|
|
Repo: r.Form.Get("repo"),
|
|
Tag: r.Form.Get("tag"),
|
|
Author: r.Form.Get("author"),
|
|
Comment: r.Form.Get("comment"),
|
|
Config: config,
|
|
Changes: r.Form["changes"],
|
|
}
|
|
|
|
imgID, err := s.backend.CreateImageFromContainer(r.Form.Get("container"), commitCfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return httputils.WriteJSON(w, http.StatusCreated, &types.IDResponse{ID: imgID})
|
|
}
|
|
|
|
func (s *containerRouter) getContainersJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
filter, err := filters.FromJSON(r.Form.Get("filters"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
config := &types.ContainerListOptions{
|
|
All: httputils.BoolValue(r, "all"),
|
|
Size: httputils.BoolValue(r, "size"),
|
|
Since: r.Form.Get("since"),
|
|
Before: r.Form.Get("before"),
|
|
Filters: filter,
|
|
}
|
|
|
|
if tmpLimit := r.Form.Get("limit"); tmpLimit != "" {
|
|
limit, err := strconv.Atoi(tmpLimit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
config.Limit = limit
|
|
}
|
|
|
|
containers, err := s.backend.Containers(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return httputils.WriteJSON(w, http.StatusOK, containers)
|
|
}
|
|
|
|
func (s *containerRouter) getContainersStats(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
stream := httputils.BoolValueOrDefault(r, "stream", true)
|
|
if !stream {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
}
|
|
var oneShot bool
|
|
if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.41") {
|
|
oneShot = httputils.BoolValueOrDefault(r, "one-shot", false)
|
|
}
|
|
|
|
config := &backend.ContainerStatsConfig{
|
|
Stream: stream,
|
|
OneShot: oneShot,
|
|
OutStream: w,
|
|
Version: httputils.VersionFromContext(ctx),
|
|
}
|
|
|
|
return s.backend.ContainerStats(ctx, vars["name"], config)
|
|
}
|
|
|
|
func (s *containerRouter) getContainersLogs(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Args are validated before the stream starts because when it starts we're
|
|
// sending HTTP 200 by writing an empty chunk of data to tell the client that
|
|
// daemon is going to stream. By sending this initial HTTP 200 we can't report
|
|
// any error after the stream starts (i.e. container not found, wrong parameters)
|
|
// with the appropriate status code.
|
|
stdout, stderr := httputils.BoolValue(r, "stdout"), httputils.BoolValue(r, "stderr")
|
|
if !(stdout || stderr) {
|
|
return errdefs.InvalidParameter(errors.New("Bad parameters: you must choose at least one stream"))
|
|
}
|
|
|
|
containerName := vars["name"]
|
|
logsConfig := &types.ContainerLogsOptions{
|
|
Follow: httputils.BoolValue(r, "follow"),
|
|
Timestamps: httputils.BoolValue(r, "timestamps"),
|
|
Since: r.Form.Get("since"),
|
|
Until: r.Form.Get("until"),
|
|
Tail: r.Form.Get("tail"),
|
|
ShowStdout: stdout,
|
|
ShowStderr: stderr,
|
|
Details: httputils.BoolValue(r, "details"),
|
|
}
|
|
|
|
msgs, tty, err := s.backend.ContainerLogs(ctx, containerName, logsConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// if has a tty, we're not muxing streams. if it doesn't, we are. simple.
|
|
// this is the point of no return for writing a response. once we call
|
|
// WriteLogStream, the response has been started and errors will be
|
|
// returned in band by WriteLogStream
|
|
httputils.WriteLogStream(ctx, w, msgs, logsConfig, !tty)
|
|
return nil
|
|
}
|
|
|
|
func (s *containerRouter) getContainersExport(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
return s.backend.ContainerExport(vars["name"], w)
|
|
}
|
|
|
|
type bodyOnStartError struct{}
|
|
|
|
func (bodyOnStartError) Error() string {
|
|
return "starting container with non-empty request body was deprecated since API v1.22 and removed in v1.24"
|
|
}
|
|
|
|
func (bodyOnStartError) InvalidParameter() {}
|
|
|
|
func (s *containerRouter) postContainersStart(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
// If contentLength is -1, we can assumed chunked encoding
|
|
// or more technically that the length is unknown
|
|
// https://golang.org/src/pkg/net/http/request.go#L139
|
|
// net/http otherwise seems to swallow any headers related to chunked encoding
|
|
// including r.TransferEncoding
|
|
// allow a nil body for backwards compatibility
|
|
|
|
version := httputils.VersionFromContext(ctx)
|
|
var hostConfig *container.HostConfig
|
|
// A non-nil json object is at least 7 characters.
|
|
if r.ContentLength > 7 || r.ContentLength == -1 {
|
|
if versions.GreaterThanOrEqualTo(version, "1.24") {
|
|
return bodyOnStartError{}
|
|
}
|
|
|
|
if err := httputils.CheckForJSON(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
c, err := s.decoder.DecodeHostConfig(r.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hostConfig = c
|
|
}
|
|
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
checkpoint := r.Form.Get("checkpoint")
|
|
checkpointDir := r.Form.Get("checkpoint-dir")
|
|
if err := s.backend.ContainerStart(vars["name"], hostConfig, checkpoint, checkpointDir); err != nil {
|
|
return err
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return nil
|
|
}
|
|
|
|
func (s *containerRouter) postContainersStop(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
var seconds *int
|
|
if tmpSeconds := r.Form.Get("t"); tmpSeconds != "" {
|
|
valSeconds, err := strconv.Atoi(tmpSeconds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
seconds = &valSeconds
|
|
}
|
|
|
|
if err := s.backend.ContainerStop(vars["name"], seconds); err != nil {
|
|
return err
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *containerRouter) postContainersKill(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
var sig syscall.Signal
|
|
name := vars["name"]
|
|
|
|
// If we have a signal, look at it. Otherwise, do nothing
|
|
if sigStr := r.Form.Get("signal"); sigStr != "" {
|
|
var err error
|
|
if sig, err = signal.ParseSignal(sigStr); err != nil {
|
|
return errdefs.InvalidParameter(err)
|
|
}
|
|
}
|
|
|
|
if err := s.backend.ContainerKill(name, uint64(sig)); err != nil {
|
|
var isStopped bool
|
|
if errdefs.IsConflict(err) {
|
|
isStopped = true
|
|
}
|
|
|
|
// Return error that's not caused because the container is stopped.
|
|
// Return error if the container is not running and the api is >= 1.20
|
|
// to keep backwards compatibility.
|
|
version := httputils.VersionFromContext(ctx)
|
|
if versions.GreaterThanOrEqualTo(version, "1.20") || !isStopped {
|
|
return errors.Wrapf(err, "Cannot kill container: %s", name)
|
|
}
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return nil
|
|
}
|
|
|
|
func (s *containerRouter) postContainersRestart(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
var seconds *int
|
|
if tmpSeconds := r.Form.Get("t"); tmpSeconds != "" {
|
|
valSeconds, err := strconv.Atoi(tmpSeconds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
seconds = &valSeconds
|
|
}
|
|
|
|
if err := s.backend.ContainerRestart(vars["name"], seconds); err != nil {
|
|
return err
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *containerRouter) postContainersPause(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.backend.ContainerPause(vars["name"]); err != nil {
|
|
return err
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *containerRouter) postContainersUnpause(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.backend.ContainerUnpause(vars["name"]); err != nil {
|
|
return err
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *containerRouter) postContainersWait(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
// Behavior changed in version 1.30 to handle wait condition and to
|
|
// return headers immediately.
|
|
version := httputils.VersionFromContext(ctx)
|
|
legacyBehaviorPre130 := versions.LessThan(version, "1.30")
|
|
legacyRemovalWaitPre134 := false
|
|
|
|
// The wait condition defaults to "not-running".
|
|
waitCondition := containerpkg.WaitConditionNotRunning
|
|
if !legacyBehaviorPre130 {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
switch container.WaitCondition(r.Form.Get("condition")) {
|
|
case container.WaitConditionNextExit:
|
|
waitCondition = containerpkg.WaitConditionNextExit
|
|
case container.WaitConditionRemoved:
|
|
waitCondition = containerpkg.WaitConditionRemoved
|
|
legacyRemovalWaitPre134 = versions.LessThan(version, "1.34")
|
|
}
|
|
}
|
|
|
|
waitC, err := s.backend.ContainerWait(ctx, vars["name"], waitCondition)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if !legacyBehaviorPre130 {
|
|
// Write response header immediately.
|
|
w.WriteHeader(http.StatusOK)
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
|
|
// Block on the result of the wait operation.
|
|
status := <-waitC
|
|
|
|
// With API < 1.34, wait on WaitConditionRemoved did not return
|
|
// in case container removal failed. The only way to report an
|
|
// error back to the client is to not write anything (i.e. send
|
|
// an empty response which will be treated as an error).
|
|
if legacyRemovalWaitPre134 && status.Err() != nil {
|
|
return nil
|
|
}
|
|
|
|
var waitError *container.ContainerWaitOKBodyError
|
|
if status.Err() != nil {
|
|
waitError = &container.ContainerWaitOKBodyError{Message: status.Err().Error()}
|
|
}
|
|
|
|
return json.NewEncoder(w).Encode(&container.ContainerWaitOKBody{
|
|
StatusCode: int64(status.ExitCode()),
|
|
Error: waitError,
|
|
})
|
|
}
|
|
|
|
func (s *containerRouter) getContainersChanges(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
changes, err := s.backend.ContainerChanges(vars["name"])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return httputils.WriteJSON(w, http.StatusOK, changes)
|
|
}
|
|
|
|
func (s *containerRouter) getContainersTop(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
procList, err := s.backend.ContainerTop(vars["name"], r.Form.Get("ps_args"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return httputils.WriteJSON(w, http.StatusOK, procList)
|
|
}
|
|
|
|
func (s *containerRouter) postContainerRename(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
name := vars["name"]
|
|
newName := r.Form.Get("name")
|
|
if err := s.backend.ContainerRename(name, newName); err != nil {
|
|
return err
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return nil
|
|
}
|
|
|
|
func (s *containerRouter) postContainerUpdate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if err := httputils.CheckForJSON(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
var updateConfig container.UpdateConfig
|
|
|
|
decoder := json.NewDecoder(r.Body)
|
|
if err := decoder.Decode(&updateConfig); err != nil {
|
|
return err
|
|
}
|
|
if versions.LessThan(httputils.VersionFromContext(ctx), "1.40") {
|
|
updateConfig.PidsLimit = nil
|
|
}
|
|
if updateConfig.PidsLimit != nil && *updateConfig.PidsLimit <= 0 {
|
|
// Both `0` and `-1` are accepted to set "unlimited" when updating.
|
|
// Historically, any negative value was accepted, so treat them as
|
|
// "unlimited" as well.
|
|
var unlimited int64
|
|
updateConfig.PidsLimit = &unlimited
|
|
}
|
|
|
|
hostConfig := &container.HostConfig{
|
|
Resources: updateConfig.Resources,
|
|
RestartPolicy: updateConfig.RestartPolicy,
|
|
}
|
|
|
|
name := vars["name"]
|
|
resp, err := s.backend.ContainerUpdate(name, hostConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return httputils.WriteJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (s *containerRouter) postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if err := httputils.CheckForJSON(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
name := r.Form.Get("name")
|
|
|
|
config, hostConfig, networkingConfig, err := s.decoder.DecodeConfig(r.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
version := httputils.VersionFromContext(ctx)
|
|
adjustCPUShares := versions.LessThan(version, "1.19")
|
|
|
|
// When using API 1.24 and under, the client is responsible for removing the container
|
|
if hostConfig != nil && versions.LessThan(version, "1.25") {
|
|
hostConfig.AutoRemove = false
|
|
}
|
|
|
|
if hostConfig != nil && versions.LessThan(version, "1.40") {
|
|
// Ignore BindOptions.NonRecursive because it was added in API 1.40.
|
|
for _, m := range hostConfig.Mounts {
|
|
if bo := m.BindOptions; bo != nil {
|
|
bo.NonRecursive = false
|
|
}
|
|
}
|
|
// Ignore KernelMemoryTCP because it was added in API 1.40.
|
|
hostConfig.KernelMemoryTCP = 0
|
|
|
|
// Ignore Capabilities because it was added in API 1.40.
|
|
hostConfig.Capabilities = nil
|
|
|
|
// Older clients (API < 1.40) expects the default to be shareable, make them happy
|
|
if hostConfig.IpcMode.IsEmpty() {
|
|
hostConfig.IpcMode = container.IpcMode("shareable")
|
|
}
|
|
}
|
|
if hostConfig != nil && versions.LessThan(version, "1.41") {
|
|
// Older clients expect the default to be "host"
|
|
if hostConfig.CgroupnsMode.IsEmpty() {
|
|
hostConfig.CgroupnsMode = container.CgroupnsMode("host")
|
|
}
|
|
}
|
|
|
|
if hostConfig != nil && hostConfig.PidsLimit != nil && *hostConfig.PidsLimit <= 0 {
|
|
// Don't set a limit if either no limit was specified, or "unlimited" was
|
|
// explicitly set.
|
|
// Both `0` and `-1` are accepted as "unlimited", and historically any
|
|
// negative value was accepted, so treat those as "unlimited" as well.
|
|
hostConfig.PidsLimit = nil
|
|
}
|
|
|
|
ccr, err := s.backend.ContainerCreate(types.ContainerCreateConfig{
|
|
Name: name,
|
|
Config: config,
|
|
HostConfig: hostConfig,
|
|
NetworkingConfig: networkingConfig,
|
|
AdjustCPUShares: adjustCPUShares,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return httputils.WriteJSON(w, http.StatusCreated, ccr)
|
|
}
|
|
|
|
func (s *containerRouter) deleteContainers(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
name := vars["name"]
|
|
config := &types.ContainerRmConfig{
|
|
ForceRemove: httputils.BoolValue(r, "force"),
|
|
RemoveVolume: httputils.BoolValue(r, "v"),
|
|
RemoveLink: httputils.BoolValue(r, "link"),
|
|
}
|
|
|
|
if err := s.backend.ContainerRm(name, config); err != nil {
|
|
return err
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *containerRouter) postContainersResize(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
height, err := strconv.Atoi(r.Form.Get("h"))
|
|
if err != nil {
|
|
return errdefs.InvalidParameter(err)
|
|
}
|
|
width, err := strconv.Atoi(r.Form.Get("w"))
|
|
if err != nil {
|
|
return errdefs.InvalidParameter(err)
|
|
}
|
|
|
|
return s.backend.ContainerResize(vars["name"], height, width)
|
|
}
|
|
|
|
func (s *containerRouter) postContainersAttach(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
err := httputils.ParseForm(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
containerName := vars["name"]
|
|
|
|
_, upgrade := r.Header["Upgrade"]
|
|
detachKeys := r.FormValue("detachKeys")
|
|
|
|
hijacker, ok := w.(http.Hijacker)
|
|
if !ok {
|
|
return errdefs.InvalidParameter(errors.Errorf("error attaching to container %s, hijack connection missing", containerName))
|
|
}
|
|
|
|
setupStreams := func() (io.ReadCloser, io.Writer, io.Writer, error) {
|
|
conn, _, err := hijacker.Hijack()
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
// set raw mode
|
|
conn.Write([]byte{})
|
|
|
|
if upgrade {
|
|
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")
|
|
} else {
|
|
fmt.Fprintf(conn, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n")
|
|
}
|
|
|
|
closer := func() error {
|
|
httputils.CloseStreams(conn)
|
|
return nil
|
|
}
|
|
return ioutils.NewReadCloserWrapper(conn, closer), conn, conn, nil
|
|
}
|
|
|
|
attachConfig := &backend.ContainerAttachConfig{
|
|
GetStreams: setupStreams,
|
|
UseStdin: httputils.BoolValue(r, "stdin"),
|
|
UseStdout: httputils.BoolValue(r, "stdout"),
|
|
UseStderr: httputils.BoolValue(r, "stderr"),
|
|
Logs: httputils.BoolValue(r, "logs"),
|
|
Stream: httputils.BoolValue(r, "stream"),
|
|
DetachKeys: detachKeys,
|
|
MuxStreams: true,
|
|
}
|
|
|
|
if err = s.backend.ContainerAttach(containerName, attachConfig); err != nil {
|
|
logrus.Errorf("Handler for %s %s returned error: %v", r.Method, r.URL.Path, err)
|
|
// Remember to close stream if error happens
|
|
conn, _, errHijack := hijacker.Hijack()
|
|
if errHijack == nil {
|
|
statusCode := errdefs.GetHTTPErrorStatusCode(err)
|
|
statusText := http.StatusText(statusCode)
|
|
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())
|
|
httputils.CloseStreams(conn)
|
|
} else {
|
|
logrus.Errorf("Error Hijacking: %v", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *containerRouter) wsContainersAttach(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
containerName := vars["name"]
|
|
|
|
var err error
|
|
detachKeys := r.FormValue("detachKeys")
|
|
|
|
done := make(chan struct{})
|
|
started := make(chan struct{})
|
|
|
|
version := httputils.VersionFromContext(ctx)
|
|
|
|
setupStreams := func() (io.ReadCloser, io.Writer, io.Writer, error) {
|
|
wsChan := make(chan *websocket.Conn)
|
|
h := func(conn *websocket.Conn) {
|
|
wsChan <- conn
|
|
<-done
|
|
}
|
|
|
|
srv := websocket.Server{Handler: h, Handshake: nil}
|
|
go func() {
|
|
close(started)
|
|
srv.ServeHTTP(w, r)
|
|
}()
|
|
|
|
conn := <-wsChan
|
|
// In case version 1.28 and above, a binary frame will be sent.
|
|
// See 28176 for details.
|
|
if versions.GreaterThanOrEqualTo(version, "1.28") {
|
|
conn.PayloadType = websocket.BinaryFrame
|
|
}
|
|
return conn, conn, conn, nil
|
|
}
|
|
|
|
attachConfig := &backend.ContainerAttachConfig{
|
|
GetStreams: setupStreams,
|
|
Logs: httputils.BoolValue(r, "logs"),
|
|
Stream: httputils.BoolValue(r, "stream"),
|
|
DetachKeys: detachKeys,
|
|
UseStdin: true,
|
|
UseStdout: true,
|
|
UseStderr: true,
|
|
MuxStreams: false, // TODO: this should be true since it's a single stream for both stdout and stderr
|
|
}
|
|
|
|
err = s.backend.ContainerAttach(containerName, attachConfig)
|
|
close(done)
|
|
select {
|
|
case <-started:
|
|
if err != nil {
|
|
logrus.Errorf("Error attaching websocket: %s", err)
|
|
} else {
|
|
logrus.Debug("websocket connection was closed by client")
|
|
}
|
|
return nil
|
|
default:
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *containerRouter) postContainersPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := httputils.ParseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
pruneFilters, err := filters.FromJSON(r.Form.Get("filters"))
|
|
if err != nil {
|
|
return errdefs.InvalidParameter(err)
|
|
}
|
|
|
|
pruneReport, err := s.backend.ContainersPrune(ctx, pruneFilters)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return httputils.WriteJSON(w, http.StatusOK, pruneReport)
|
|
}
|