mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
This patch creates a new `tlsconfig` package to handle creation of secure-enough TLS configurations for clients and servers. The package was created by refactoring TLS code in the client and the daemon. After this patch, it is expected that all code creating TLS configurations use this `tlsconfig` package for greater security, consistency and readability. On the server side, this fixes a bug where --tlsverify was not taken into account. Now, if specified, it will require the client to authenticate. Signed-off-by: Tibor Vass <tibor@docker.com>
1567 lines
43 KiB
Go
1567 lines
43 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.google.com/p/go.net/websocket"
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
"github.com/docker/docker/api"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/autogen/dockerversion"
|
|
"github.com/docker/docker/builder"
|
|
"github.com/docker/docker/cliconfig"
|
|
"github.com/docker/docker/daemon"
|
|
"github.com/docker/docker/graph"
|
|
"github.com/docker/docker/pkg/ioutils"
|
|
"github.com/docker/docker/pkg/jsonmessage"
|
|
"github.com/docker/docker/pkg/parsers"
|
|
"github.com/docker/docker/pkg/parsers/filters"
|
|
"github.com/docker/docker/pkg/parsers/kernel"
|
|
"github.com/docker/docker/pkg/signal"
|
|
"github.com/docker/docker/pkg/sockets"
|
|
"github.com/docker/docker/pkg/stdcopy"
|
|
"github.com/docker/docker/pkg/streamformatter"
|
|
"github.com/docker/docker/pkg/version"
|
|
"github.com/docker/docker/runconfig"
|
|
"github.com/docker/docker/utils"
|
|
)
|
|
|
|
type ServerConfig struct {
|
|
Logging bool
|
|
EnableCors bool
|
|
CorsHeaders string
|
|
Version string
|
|
SocketGroup string
|
|
TLSConfig *tls.Config
|
|
}
|
|
|
|
type Server struct {
|
|
daemon *daemon.Daemon
|
|
cfg *ServerConfig
|
|
router *mux.Router
|
|
start chan struct{}
|
|
servers []serverCloser
|
|
}
|
|
|
|
func New(cfg *ServerConfig) *Server {
|
|
srv := &Server{
|
|
cfg: cfg,
|
|
start: make(chan struct{}),
|
|
}
|
|
r := createRouter(srv)
|
|
srv.router = r
|
|
return srv
|
|
}
|
|
|
|
func (s *Server) Close() {
|
|
for _, srv := range s.servers {
|
|
if err := srv.Close(); err != nil {
|
|
logrus.Error(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
type serverCloser interface {
|
|
Serve() error
|
|
Close() error
|
|
}
|
|
|
|
// ServeApi loops through all of the protocols sent in to docker and spawns
|
|
// off a go routine to setup a serving http.Server for each.
|
|
func (s *Server) ServeApi(protoAddrs []string) error {
|
|
var chErrors = make(chan error, len(protoAddrs))
|
|
|
|
for _, protoAddr := range protoAddrs {
|
|
protoAddrParts := strings.SplitN(protoAddr, "://", 2)
|
|
if len(protoAddrParts) != 2 {
|
|
return fmt.Errorf("bad format, expected PROTO://ADDR")
|
|
}
|
|
srv, err := s.newServer(protoAddrParts[0], protoAddrParts[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.servers = append(s.servers, srv...)
|
|
|
|
for _, s := range srv {
|
|
logrus.Infof("Listening for HTTP on %s (%s)", protoAddrParts[0], protoAddrParts[1])
|
|
go func(s serverCloser) {
|
|
if err := s.Serve(); err != nil && strings.Contains(err.Error(), "use of closed network connection") {
|
|
err = nil
|
|
}
|
|
chErrors <- err
|
|
}(s)
|
|
}
|
|
}
|
|
|
|
for i := 0; i < len(protoAddrs); i++ {
|
|
err := <-chErrors
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type HttpServer struct {
|
|
srv *http.Server
|
|
l net.Listener
|
|
}
|
|
|
|
func (s *HttpServer) Serve() error {
|
|
return s.srv.Serve(s.l)
|
|
}
|
|
func (s *HttpServer) Close() error {
|
|
return s.l.Close()
|
|
}
|
|
|
|
type HttpApiFunc func(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error
|
|
|
|
func hijackServer(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) {
|
|
conn, _, err := w.(http.Hijacker).Hijack()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
// Flush the options to make sure the client sets the raw mode
|
|
conn.Write([]byte{})
|
|
return conn, conn, nil
|
|
}
|
|
|
|
func closeStreams(streams ...interface{}) {
|
|
for _, stream := range streams {
|
|
if tcpc, ok := stream.(interface {
|
|
CloseWrite() error
|
|
}); ok {
|
|
tcpc.CloseWrite()
|
|
} else if closer, ok := stream.(io.Closer); ok {
|
|
closer.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check to make sure request's Content-Type is application/json
|
|
func checkForJson(r *http.Request) error {
|
|
ct := r.Header.Get("Content-Type")
|
|
|
|
// No Content-Type header is ok as long as there's no Body
|
|
if ct == "" {
|
|
if r.Body == nil || r.ContentLength == 0 {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Otherwise it better be json
|
|
if api.MatchesContentType(ct, "application/json") {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Content-Type specified (%s) must be 'application/json'", ct)
|
|
}
|
|
|
|
//If we don't do this, POST method without Content-type (even with empty body) will fail
|
|
func parseForm(r *http.Request) error {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
if err := r.ParseForm(); err != nil && !strings.HasPrefix(err.Error(), "mime:") {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseMultipartForm(r *http.Request) error {
|
|
if err := r.ParseMultipartForm(4096); err != nil && !strings.HasPrefix(err.Error(), "mime:") {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func httpError(w http.ResponseWriter, err error) {
|
|
if err == nil || w == nil {
|
|
logrus.WithFields(logrus.Fields{"error": err, "writer": w}).Error("unexpected HTTP error handling")
|
|
return
|
|
}
|
|
statusCode := http.StatusInternalServerError
|
|
// FIXME: this is brittle and should not be necessary.
|
|
// If we need to differentiate between different possible error types, we should
|
|
// create appropriate error types with clearly defined meaning.
|
|
errStr := strings.ToLower(err.Error())
|
|
for keyword, status := range map[string]int{
|
|
"not found": http.StatusNotFound,
|
|
"no such": http.StatusNotFound,
|
|
"bad parameter": http.StatusBadRequest,
|
|
"conflict": http.StatusConflict,
|
|
"impossible": http.StatusNotAcceptable,
|
|
"wrong login/password": http.StatusUnauthorized,
|
|
"hasn't been activated": http.StatusForbidden,
|
|
} {
|
|
if strings.Contains(errStr, keyword) {
|
|
statusCode = status
|
|
break
|
|
}
|
|
}
|
|
|
|
logrus.WithFields(logrus.Fields{"statusCode": statusCode, "err": err}).Error("HTTP Error")
|
|
http.Error(w, err.Error(), statusCode)
|
|
}
|
|
|
|
// writeJSON writes the value v to the http response stream as json with standard
|
|
// json encoding.
|
|
func writeJSON(w http.ResponseWriter, code int, v interface{}) error {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
return json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func (s *Server) postAuth(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
var config *cliconfig.AuthConfig
|
|
err := json.NewDecoder(r.Body).Decode(&config)
|
|
r.Body.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
status, err := s.daemon.RegistryService.Auth(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeJSON(w, http.StatusOK, &types.AuthResponse{
|
|
Status: status,
|
|
})
|
|
}
|
|
|
|
func (s *Server) getVersion(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
v := &types.Version{
|
|
Version: dockerversion.VERSION,
|
|
ApiVersion: api.APIVERSION,
|
|
GitCommit: dockerversion.GITCOMMIT,
|
|
GoVersion: runtime.Version(),
|
|
Os: runtime.GOOS,
|
|
Arch: runtime.GOARCH,
|
|
}
|
|
|
|
if version.GreaterThanOrEqualTo("1.19") {
|
|
v.Experimental = utils.ExperimentalBuild()
|
|
}
|
|
|
|
if kernelVersion, err := kernel.GetKernelVersion(); err == nil {
|
|
v.KernelVersion = kernelVersion.String()
|
|
}
|
|
|
|
return writeJSON(w, http.StatusOK, v)
|
|
}
|
|
|
|
func (s *Server) postContainersKill(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
var sig uint64
|
|
name := vars["name"]
|
|
|
|
// If we have a signal, look at it. Otherwise, do nothing
|
|
if sigStr := r.Form.Get("signal"); sigStr != "" {
|
|
// Check if we passed the signal as a number:
|
|
// The largest legal signal is 31, so let's parse on 5 bits
|
|
sigN, err := strconv.ParseUint(sigStr, 10, 5)
|
|
if err != nil {
|
|
// The signal is not a number, treat it as a string (either like
|
|
// "KILL" or like "SIGKILL")
|
|
syscallSig, ok := signal.SignalMap[strings.TrimPrefix(sigStr, "SIG")]
|
|
if !ok {
|
|
return fmt.Errorf("Invalid signal: %s", sigStr)
|
|
}
|
|
sig = uint64(syscallSig)
|
|
} else {
|
|
sig = sigN
|
|
}
|
|
|
|
if sig == 0 {
|
|
return fmt.Errorf("Invalid signal: %s", sigStr)
|
|
}
|
|
}
|
|
|
|
if err := s.daemon.ContainerKill(name, sig); err != nil {
|
|
return err
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) postContainersPause(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.daemon.ContainerPause(vars["name"]); err != nil {
|
|
return err
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) postContainersUnpause(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.daemon.ContainerUnpause(vars["name"]); err != nil {
|
|
return err
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) getContainersExport(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
return s.daemon.ContainerExport(vars["name"], w)
|
|
}
|
|
|
|
func (s *Server) getImagesJSON(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
imagesConfig := graph.ImagesConfig{
|
|
Filters: r.Form.Get("filters"),
|
|
// FIXME this parameter could just be a match filter
|
|
Filter: r.Form.Get("filter"),
|
|
All: boolValue(r, "all"),
|
|
}
|
|
|
|
images, err := s.daemon.Repositories().Images(&imagesConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeJSON(w, http.StatusOK, images)
|
|
}
|
|
|
|
func (s *Server) getInfo(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
info, err := s.daemon.SystemInfo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeJSON(w, http.StatusOK, info)
|
|
}
|
|
|
|
func (s *Server) getEvents(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
var since int64 = -1
|
|
if r.Form.Get("since") != "" {
|
|
s, err := strconv.ParseInt(r.Form.Get("since"), 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
since = s
|
|
}
|
|
|
|
var until int64 = -1
|
|
if r.Form.Get("until") != "" {
|
|
u, err := strconv.ParseInt(r.Form.Get("until"), 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
until = u
|
|
}
|
|
timer := time.NewTimer(0)
|
|
timer.Stop()
|
|
if until > 0 {
|
|
dur := time.Unix(until, 0).Sub(time.Now())
|
|
timer = time.NewTimer(dur)
|
|
}
|
|
|
|
ef, err := filters.FromParam(r.Form.Get("filters"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
isFiltered := func(field string, filter []string) bool {
|
|
if len(filter) == 0 {
|
|
return false
|
|
}
|
|
for _, v := range filter {
|
|
if v == field {
|
|
return false
|
|
}
|
|
if strings.Contains(field, ":") {
|
|
image := strings.Split(field, ":")
|
|
if image[0] == v {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
d := s.daemon
|
|
es := d.EventsService
|
|
w.Header().Set("Content-Type", "application/json")
|
|
enc := json.NewEncoder(ioutils.NewWriteFlusher(w))
|
|
|
|
getContainerId := func(cn string) string {
|
|
c, err := d.Get(cn)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return c.ID
|
|
}
|
|
|
|
sendEvent := func(ev *jsonmessage.JSONMessage) error {
|
|
//incoming container filter can be name,id or partial id, convert and replace as a full container id
|
|
for i, cn := range ef["container"] {
|
|
ef["container"][i] = getContainerId(cn)
|
|
}
|
|
|
|
if isFiltered(ev.Status, ef["event"]) || isFiltered(ev.From, ef["image"]) ||
|
|
isFiltered(ev.ID, ef["container"]) {
|
|
return nil
|
|
}
|
|
|
|
return enc.Encode(ev)
|
|
}
|
|
|
|
current, l := es.Subscribe()
|
|
defer es.Evict(l)
|
|
for _, ev := range current {
|
|
if ev.Time < since {
|
|
continue
|
|
}
|
|
if err := sendEvent(ev); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var closeNotify <-chan bool
|
|
if closeNotifier, ok := w.(http.CloseNotifier); ok {
|
|
closeNotify = closeNotifier.CloseNotify()
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case ev := <-l:
|
|
jev, ok := ev.(*jsonmessage.JSONMessage)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if err := sendEvent(jev); err != nil {
|
|
return err
|
|
}
|
|
case <-timer.C:
|
|
return nil
|
|
case <-closeNotify:
|
|
logrus.Debug("Client disconnected, stop sending events")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) getImagesHistory(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
name := vars["name"]
|
|
history, err := s.daemon.Repositories().History(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeJSON(w, http.StatusOK, history)
|
|
}
|
|
|
|
func (s *Server) getContainersChanges(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
changes, err := s.daemon.ContainerChanges(vars["name"])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeJSON(w, http.StatusOK, changes)
|
|
}
|
|
|
|
func (s *Server) getContainersTop(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
procList, err := s.daemon.ContainerTop(vars["name"], r.Form.Get("ps_args"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeJSON(w, http.StatusOK, procList)
|
|
}
|
|
|
|
func (s *Server) getContainersJSON(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
config := &daemon.ContainersConfig{
|
|
All: boolValue(r, "all"),
|
|
Size: boolValue(r, "size"),
|
|
Since: r.Form.Get("since"),
|
|
Before: r.Form.Get("before"),
|
|
Filters: r.Form.Get("filters"),
|
|
}
|
|
|
|
if tmpLimit := r.Form.Get("limit"); tmpLimit != "" {
|
|
limit, err := strconv.Atoi(tmpLimit)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
config.Limit = limit
|
|
}
|
|
|
|
containers, err := s.daemon.Containers(config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeJSON(w, http.StatusOK, containers)
|
|
}
|
|
|
|
func (s *Server) getContainersStats(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
return s.daemon.ContainerStats(vars["name"], boolValueOrDefault(r, "stream", true), ioutils.NewWriteFlusher(w))
|
|
}
|
|
|
|
func (s *Server) getContainersLogs(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
// Validate args here, because we can't return not StatusOK after job.Run() call
|
|
stdout, stderr := boolValue(r, "stdout"), boolValue(r, "stderr")
|
|
if !(stdout || stderr) {
|
|
return fmt.Errorf("Bad parameters: you must choose at least one stream")
|
|
}
|
|
|
|
var since time.Time
|
|
if r.Form.Get("since") != "" {
|
|
s, err := strconv.ParseInt(r.Form.Get("since"), 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
since = time.Unix(s, 0)
|
|
}
|
|
|
|
logsConfig := &daemon.ContainerLogsConfig{
|
|
Follow: boolValue(r, "follow"),
|
|
Timestamps: boolValue(r, "timestamps"),
|
|
Since: since,
|
|
Tail: r.Form.Get("tail"),
|
|
UseStdout: stdout,
|
|
UseStderr: stderr,
|
|
OutStream: ioutils.NewWriteFlusher(w),
|
|
}
|
|
|
|
if err := s.daemon.ContainerLogs(vars["name"], logsConfig); err != nil {
|
|
fmt.Fprintf(w, "Error running logs job: %s\n", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) postImagesTag(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
repo := r.Form.Get("repo")
|
|
tag := r.Form.Get("tag")
|
|
force := boolValue(r, "force")
|
|
name := vars["name"]
|
|
if err := s.daemon.Repositories().Tag(repo, tag, name, force); err != nil {
|
|
return err
|
|
}
|
|
s.daemon.EventsService.Log("tag", utils.ImageReference(repo, tag), "")
|
|
w.WriteHeader(http.StatusCreated)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) postCommit(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := checkForJson(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
cont := r.Form.Get("container")
|
|
|
|
pause := boolValue(r, "pause")
|
|
if r.FormValue("pause") == "" && version.GreaterThanOrEqualTo("1.13") {
|
|
pause = true
|
|
}
|
|
|
|
c, _, err := runconfig.DecodeContainerConfig(r.Body)
|
|
if err != nil && err != io.EOF { //Do not fail if body is empty.
|
|
return err
|
|
}
|
|
|
|
if c == nil {
|
|
c = &runconfig.Config{}
|
|
}
|
|
|
|
containerCommitConfig := &daemon.ContainerCommitConfig{
|
|
Pause: pause,
|
|
Repo: r.Form.Get("repo"),
|
|
Tag: r.Form.Get("tag"),
|
|
Author: r.Form.Get("author"),
|
|
Comment: r.Form.Get("comment"),
|
|
Changes: r.Form["changes"],
|
|
Config: c,
|
|
}
|
|
|
|
imgID, err := builder.Commit(s.daemon, cont, containerCommitConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeJSON(w, http.StatusCreated, &types.ContainerCommitResponse{
|
|
ID: imgID,
|
|
})
|
|
}
|
|
|
|
// Creates an image from Pull or from Import
|
|
func (s *Server) postImagesCreate(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
var (
|
|
image = r.Form.Get("fromImage")
|
|
repo = r.Form.Get("repo")
|
|
tag = r.Form.Get("tag")
|
|
)
|
|
authEncoded := r.Header.Get("X-Registry-Auth")
|
|
authConfig := &cliconfig.AuthConfig{}
|
|
if authEncoded != "" {
|
|
authJson := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
|
|
if err := json.NewDecoder(authJson).Decode(authConfig); err != nil {
|
|
// for a pull it is not an error if no auth was given
|
|
// to increase compatibility with the existing api it is defaulting to be empty
|
|
authConfig = &cliconfig.AuthConfig{}
|
|
}
|
|
}
|
|
|
|
var (
|
|
err error
|
|
output = ioutils.NewWriteFlusher(w)
|
|
)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if image != "" { //pull
|
|
if tag == "" {
|
|
image, tag = parsers.ParseRepositoryTag(image)
|
|
}
|
|
metaHeaders := map[string][]string{}
|
|
for k, v := range r.Header {
|
|
if strings.HasPrefix(k, "X-Meta-") {
|
|
metaHeaders[k] = v
|
|
}
|
|
}
|
|
|
|
imagePullConfig := &graph.ImagePullConfig{
|
|
MetaHeaders: metaHeaders,
|
|
AuthConfig: authConfig,
|
|
OutStream: output,
|
|
}
|
|
|
|
err = s.daemon.Repositories().Pull(image, tag, imagePullConfig)
|
|
} else { //import
|
|
if tag == "" {
|
|
repo, tag = parsers.ParseRepositoryTag(repo)
|
|
}
|
|
|
|
src := r.Form.Get("fromSrc")
|
|
imageImportConfig := &graph.ImageImportConfig{
|
|
Changes: r.Form["changes"],
|
|
InConfig: r.Body,
|
|
OutStream: output,
|
|
}
|
|
|
|
// 'err' MUST NOT be defined within this block, we need any error
|
|
// generated from the download to be available to the output
|
|
// stream processing below
|
|
var newConfig *runconfig.Config
|
|
newConfig, err = builder.BuildFromConfig(s.daemon, &runconfig.Config{}, imageImportConfig.Changes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
imageImportConfig.ContainerConfig = newConfig
|
|
|
|
err = s.daemon.Repositories().Import(src, repo, tag, imageImportConfig)
|
|
}
|
|
if err != nil {
|
|
if !output.Flushed() {
|
|
return err
|
|
}
|
|
sf := streamformatter.NewJSONStreamFormatter()
|
|
output.Write(sf.FormatError(err))
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
func (s *Server) getImagesSearch(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
var (
|
|
config *cliconfig.AuthConfig
|
|
authEncoded = r.Header.Get("X-Registry-Auth")
|
|
headers = map[string][]string{}
|
|
)
|
|
|
|
if authEncoded != "" {
|
|
authJson := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
|
|
if err := json.NewDecoder(authJson).Decode(&config); err != nil {
|
|
// for a search it is not an error if no auth was given
|
|
// to increase compatibility with the existing api it is defaulting to be empty
|
|
config = &cliconfig.AuthConfig{}
|
|
}
|
|
}
|
|
for k, v := range r.Header {
|
|
if strings.HasPrefix(k, "X-Meta-") {
|
|
headers[k] = v
|
|
}
|
|
}
|
|
query, err := s.daemon.RegistryService.Search(r.Form.Get("term"), config, headers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.NewEncoder(w).Encode(query.Results)
|
|
}
|
|
|
|
func (s *Server) postImagesPush(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
metaHeaders := map[string][]string{}
|
|
for k, v := range r.Header {
|
|
if strings.HasPrefix(k, "X-Meta-") {
|
|
metaHeaders[k] = v
|
|
}
|
|
}
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
authConfig := &cliconfig.AuthConfig{}
|
|
|
|
authEncoded := r.Header.Get("X-Registry-Auth")
|
|
if authEncoded != "" {
|
|
// the new format is to handle the authConfig as a header
|
|
authJson := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
|
|
if err := json.NewDecoder(authJson).Decode(authConfig); err != nil {
|
|
// to increase compatibility to existing api it is defaulting to be empty
|
|
authConfig = &cliconfig.AuthConfig{}
|
|
}
|
|
} else {
|
|
// the old format is supported for compatibility if there was no authConfig header
|
|
if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
|
|
return fmt.Errorf("Bad parameters and missing X-Registry-Auth: %v", err)
|
|
}
|
|
}
|
|
|
|
name := vars["name"]
|
|
output := ioutils.NewWriteFlusher(w)
|
|
imagePushConfig := &graph.ImagePushConfig{
|
|
MetaHeaders: metaHeaders,
|
|
AuthConfig: authConfig,
|
|
Tag: r.Form.Get("tag"),
|
|
OutStream: output,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if err := s.daemon.Repositories().Push(name, imagePushConfig); err != nil {
|
|
if !output.Flushed() {
|
|
return err
|
|
}
|
|
sf := streamformatter.NewJSONStreamFormatter()
|
|
output.Write(sf.FormatError(err))
|
|
}
|
|
return nil
|
|
|
|
}
|
|
|
|
func (s *Server) getImagesGet(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/x-tar")
|
|
|
|
output := ioutils.NewWriteFlusher(w)
|
|
imageExportConfig := &graph.ImageExportConfig{Outstream: output}
|
|
if name, ok := vars["name"]; ok {
|
|
imageExportConfig.Names = []string{name}
|
|
} else {
|
|
imageExportConfig.Names = r.Form["names"]
|
|
}
|
|
|
|
if err := s.daemon.Repositories().ImageExport(imageExportConfig); err != nil {
|
|
if !output.Flushed() {
|
|
return err
|
|
}
|
|
sf := streamformatter.NewJSONStreamFormatter()
|
|
output.Write(sf.FormatError(err))
|
|
}
|
|
return nil
|
|
|
|
}
|
|
|
|
func (s *Server) postImagesLoad(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
return s.daemon.Repositories().Load(r.Body, w)
|
|
}
|
|
|
|
func (s *Server) postContainersCreate(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if err := checkForJson(r); err != nil {
|
|
return err
|
|
}
|
|
var (
|
|
warnings []string
|
|
name = r.Form.Get("name")
|
|
)
|
|
|
|
config, hostConfig, err := runconfig.DecodeContainerConfig(r.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
containerId, warnings, err := s.daemon.ContainerCreate(name, config, hostConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeJSON(w, http.StatusCreated, &types.ContainerCreateResponse{
|
|
ID: containerId,
|
|
Warnings: warnings,
|
|
})
|
|
}
|
|
|
|
func (s *Server) postContainersRestart(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
timeout, _ := strconv.Atoi(r.Form.Get("t"))
|
|
|
|
if err := s.daemon.ContainerRestart(vars["name"], timeout); err != nil {
|
|
return err
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) postContainerRename(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
name := vars["name"]
|
|
newName := r.Form.Get("name")
|
|
if err := s.daemon.ContainerRename(name, newName); err != nil {
|
|
return err
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) deleteContainers(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
name := vars["name"]
|
|
config := &daemon.ContainerRmConfig{
|
|
ForceRemove: boolValue(r, "force"),
|
|
RemoveVolume: boolValue(r, "v"),
|
|
RemoveLink: boolValue(r, "link"),
|
|
}
|
|
|
|
if err := s.daemon.ContainerRm(name, config); err != nil {
|
|
// Force a 404 for the empty string
|
|
if strings.Contains(strings.ToLower(err.Error()), "prefix can't be empty") {
|
|
return fmt.Errorf("no such id: \"\"")
|
|
}
|
|
return err
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) deleteImages(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
name := vars["name"]
|
|
force := boolValue(r, "force")
|
|
noprune := boolValue(r, "noprune")
|
|
|
|
list, err := s.daemon.ImageDelete(name, force, noprune)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeJSON(w, http.StatusOK, list)
|
|
}
|
|
|
|
func (s *Server) postContainersStart(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
// 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
|
|
var hostConfig *runconfig.HostConfig
|
|
if r.Body != nil && (r.ContentLength > 0 || r.ContentLength == -1) {
|
|
if err := checkForJson(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
c, err := runconfig.DecodeHostConfig(r.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hostConfig = c
|
|
}
|
|
|
|
if err := s.daemon.ContainerStart(vars["name"], hostConfig); err != nil {
|
|
if err.Error() == "Container already started" {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) postContainersStop(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
seconds, _ := strconv.Atoi(r.Form.Get("t"))
|
|
|
|
if err := s.daemon.ContainerStop(vars["name"], seconds); err != nil {
|
|
if err.Error() == "Container already stopped" {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) postContainersWait(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
status, err := s.daemon.ContainerWait(vars["name"], -1*time.Second)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeJSON(w, http.StatusOK, &types.ContainerWaitResponse{
|
|
StatusCode: status,
|
|
})
|
|
}
|
|
|
|
func (s *Server) postContainersResize(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
height, err := strconv.Atoi(r.Form.Get("h"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
width, err := strconv.Atoi(r.Form.Get("w"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.daemon.ContainerResize(vars["name"], height, width)
|
|
}
|
|
|
|
func (s *Server) postContainersAttach(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
inStream, outStream, err := hijackServer(w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closeStreams(inStream, outStream)
|
|
|
|
if _, ok := r.Header["Upgrade"]; ok {
|
|
fmt.Fprintf(outStream, "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(outStream, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n")
|
|
}
|
|
|
|
attachWithLogsConfig := &daemon.ContainerAttachWithLogsConfig{
|
|
InStream: inStream,
|
|
OutStream: outStream,
|
|
UseStdin: boolValue(r, "stdin"),
|
|
UseStdout: boolValue(r, "stdout"),
|
|
UseStderr: boolValue(r, "stderr"),
|
|
Logs: boolValue(r, "logs"),
|
|
Stream: boolValue(r, "stream"),
|
|
Multiplex: version.GreaterThanOrEqualTo("1.6"),
|
|
}
|
|
|
|
if err := s.daemon.ContainerAttachWithLogs(vars["name"], attachWithLogsConfig); err != nil {
|
|
fmt.Fprintf(outStream, "Error attaching: %s\n", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) wsContainersAttach(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
h := websocket.Handler(func(ws *websocket.Conn) {
|
|
defer ws.Close()
|
|
|
|
wsAttachWithLogsConfig := &daemon.ContainerWsAttachWithLogsConfig{
|
|
InStream: ws,
|
|
OutStream: ws,
|
|
ErrStream: ws,
|
|
Logs: boolValue(r, "logs"),
|
|
Stream: boolValue(r, "stream"),
|
|
}
|
|
|
|
if err := s.daemon.ContainerWsAttachWithLogs(vars["name"], wsAttachWithLogsConfig); err != nil {
|
|
logrus.Errorf("Error attaching websocket: %s", err)
|
|
}
|
|
})
|
|
h.ServeHTTP(w, r)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) getContainersByName(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
if version.LessThan("1.19") {
|
|
containerJSONRaw, err := s.daemon.ContainerInspectRaw(vars["name"])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeJSON(w, http.StatusOK, containerJSONRaw)
|
|
}
|
|
|
|
containerJSON, err := s.daemon.ContainerInspect(vars["name"])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return writeJSON(w, http.StatusOK, containerJSON)
|
|
}
|
|
|
|
func (s *Server) getExecByID(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter 'id'")
|
|
}
|
|
|
|
eConfig, err := s.daemon.ContainerExecInspect(vars["id"])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeJSON(w, http.StatusOK, eConfig)
|
|
}
|
|
|
|
func (s *Server) getImagesByName(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
imageInspect, err := s.daemon.Repositories().Lookup(vars["name"])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return writeJSON(w, http.StatusOK, imageInspect)
|
|
}
|
|
|
|
func (s *Server) postBuild(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
var (
|
|
authConfig = &cliconfig.AuthConfig{}
|
|
configFileEncoded = r.Header.Get("X-Registry-Config")
|
|
configFile = &cliconfig.ConfigFile{}
|
|
buildConfig = builder.NewBuildConfig()
|
|
)
|
|
|
|
if configFileEncoded != "" {
|
|
configFileJson := base64.NewDecoder(base64.URLEncoding, strings.NewReader(configFileEncoded))
|
|
if err := json.NewDecoder(configFileJson).Decode(configFile); err != nil {
|
|
// for a pull it is not an error if no auth was given
|
|
// to increase compatibility with the existing api it is defaulting to be empty
|
|
configFile = &cliconfig.ConfigFile{}
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
if boolValue(r, "forcerm") && version.GreaterThanOrEqualTo("1.12") {
|
|
buildConfig.Remove = true
|
|
} else if r.FormValue("rm") == "" && version.GreaterThanOrEqualTo("1.12") {
|
|
buildConfig.Remove = true
|
|
} else {
|
|
buildConfig.Remove = boolValue(r, "rm")
|
|
}
|
|
if boolValue(r, "pull") && version.GreaterThanOrEqualTo("1.16") {
|
|
buildConfig.Pull = true
|
|
}
|
|
|
|
output := ioutils.NewWriteFlusher(w)
|
|
buildConfig.Stdout = output
|
|
buildConfig.Context = r.Body
|
|
|
|
buildConfig.RemoteURL = r.FormValue("remote")
|
|
buildConfig.DockerfileName = r.FormValue("dockerfile")
|
|
buildConfig.RepoName = r.FormValue("t")
|
|
buildConfig.SuppressOutput = boolValue(r, "q")
|
|
buildConfig.NoCache = boolValue(r, "nocache")
|
|
buildConfig.ForceRemove = boolValue(r, "forcerm")
|
|
buildConfig.AuthConfig = authConfig
|
|
buildConfig.ConfigFile = configFile
|
|
buildConfig.MemorySwap = int64ValueOrZero(r, "memswap")
|
|
buildConfig.Memory = int64ValueOrZero(r, "memory")
|
|
buildConfig.CpuShares = int64ValueOrZero(r, "cpushares")
|
|
buildConfig.CpuPeriod = int64ValueOrZero(r, "cpuperiod")
|
|
buildConfig.CpuQuota = int64ValueOrZero(r, "cpuquota")
|
|
buildConfig.CpuSetCpus = r.FormValue("cpusetcpus")
|
|
buildConfig.CpuSetMems = r.FormValue("cpusetmems")
|
|
buildConfig.CgroupParent = r.FormValue("cgroupparent")
|
|
|
|
// Job cancellation. Note: not all job types support this.
|
|
if closeNotifier, ok := w.(http.CloseNotifier); ok {
|
|
finished := make(chan struct{})
|
|
defer close(finished)
|
|
go func() {
|
|
select {
|
|
case <-finished:
|
|
case <-closeNotifier.CloseNotify():
|
|
logrus.Infof("Client disconnected, cancelling job: build")
|
|
buildConfig.Cancel()
|
|
}
|
|
}()
|
|
}
|
|
|
|
if err := builder.Build(s.daemon, buildConfig); err != nil {
|
|
// Do not write the error in the http output if it's still empty.
|
|
// This prevents from writing a 200(OK) when there is an interal error.
|
|
if !output.Flushed() {
|
|
return err
|
|
}
|
|
sf := streamformatter.NewJSONStreamFormatter()
|
|
w.Write(sf.FormatError(err))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) postContainersCopy(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
if err := checkForJson(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg := types.CopyConfig{}
|
|
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
|
|
return err
|
|
}
|
|
|
|
if cfg.Resource == "" {
|
|
return fmt.Errorf("Path cannot be empty")
|
|
}
|
|
|
|
data, err := s.daemon.ContainerCopy(vars["name"], cfg.Resource)
|
|
if err != nil {
|
|
if strings.Contains(strings.ToLower(err.Error()), "no such id") {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return nil
|
|
}
|
|
if os.IsNotExist(err) {
|
|
return fmt.Errorf("Could not find the file %s in container %s", cfg.Resource, vars["name"])
|
|
}
|
|
return err
|
|
}
|
|
defer data.Close()
|
|
|
|
w.Header().Set("Content-Type", "application/x-tar")
|
|
if _, err := io.Copy(w, data); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) postContainerExecCreate(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
name := vars["name"]
|
|
|
|
execConfig := &runconfig.ExecConfig{}
|
|
if err := json.NewDecoder(r.Body).Decode(execConfig); err != nil {
|
|
return err
|
|
}
|
|
execConfig.Container = name
|
|
|
|
if len(execConfig.Cmd) == 0 {
|
|
return fmt.Errorf("No exec command specified")
|
|
}
|
|
|
|
// Register an instance of Exec in container.
|
|
id, err := s.daemon.ContainerExecCreate(execConfig)
|
|
if err != nil {
|
|
logrus.Errorf("Error setting up exec command in container %s: %s", name, err)
|
|
return err
|
|
}
|
|
|
|
return writeJSON(w, http.StatusCreated, &types.ContainerExecCreateResponse{
|
|
ID: id,
|
|
})
|
|
}
|
|
|
|
// TODO(vishh): Refactor the code to avoid having to specify stream config as part of both create and start.
|
|
func (s *Server) postContainerExecStart(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
var (
|
|
execName = vars["name"]
|
|
stdin io.ReadCloser
|
|
stdout io.Writer
|
|
stderr io.Writer
|
|
)
|
|
|
|
execStartCheck := &types.ExecStartCheck{}
|
|
if err := json.NewDecoder(r.Body).Decode(execStartCheck); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !execStartCheck.Detach {
|
|
// Setting up the streaming http interface.
|
|
inStream, outStream, err := hijackServer(w)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closeStreams(inStream, outStream)
|
|
|
|
var errStream io.Writer
|
|
|
|
if _, ok := r.Header["Upgrade"]; ok {
|
|
fmt.Fprintf(outStream, "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(outStream, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n")
|
|
}
|
|
|
|
if !execStartCheck.Tty {
|
|
errStream = stdcopy.NewStdWriter(outStream, stdcopy.Stderr)
|
|
outStream = stdcopy.NewStdWriter(outStream, stdcopy.Stdout)
|
|
}
|
|
|
|
stdin = inStream
|
|
stdout = outStream
|
|
stderr = errStream
|
|
}
|
|
// Now run the user process in container.
|
|
|
|
if err := s.daemon.ContainerExecStart(execName, stdin, stdout, stderr); err != nil {
|
|
logrus.Errorf("Error starting exec command in container %s: %s", execName, err)
|
|
return err
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) postContainerExecResize(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
if err := parseForm(r); err != nil {
|
|
return err
|
|
}
|
|
if vars == nil {
|
|
return fmt.Errorf("Missing parameter")
|
|
}
|
|
|
|
height, err := strconv.Atoi(r.Form.Get("h"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
width, err := strconv.Atoi(r.Form.Get("w"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.daemon.ContainerExecResize(vars["name"], height, width)
|
|
}
|
|
|
|
func (s *Server) optionsHandler(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
w.WriteHeader(http.StatusOK)
|
|
return nil
|
|
}
|
|
func writeCorsHeaders(w http.ResponseWriter, r *http.Request, corsHeaders string) {
|
|
logrus.Debugf("CORS header is enabled and set to: %s", corsHeaders)
|
|
w.Header().Add("Access-Control-Allow-Origin", corsHeaders)
|
|
w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, X-Registry-Auth")
|
|
w.Header().Add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS")
|
|
}
|
|
|
|
func (s *Server) ping(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
|
_, err := w.Write([]byte{'O', 'K'})
|
|
return err
|
|
}
|
|
|
|
func (s *Server) initTcpSocket(addr string) (l net.Listener, err error) {
|
|
if s.cfg.TLSConfig == nil || s.cfg.TLSConfig.ClientAuth != tls.RequireAndVerifyClientCert {
|
|
logrus.Warn("/!\\ DON'T BIND ON ANY IP ADDRESS WITHOUT setting -tlsverify IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\")
|
|
}
|
|
if l, err = sockets.NewTcpSocket(addr, s.cfg.TLSConfig, s.start); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := allocateDaemonPort(addr); err != nil {
|
|
return nil, err
|
|
}
|
|
return
|
|
}
|
|
|
|
func makeHttpHandler(logging bool, localMethod string, localRoute string, handlerFunc HttpApiFunc, corsHeaders string, dockerVersion version.Version) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// log the request
|
|
logrus.Debugf("Calling %s %s", localMethod, localRoute)
|
|
|
|
if logging {
|
|
logrus.Infof("%s %s", r.Method, r.RequestURI)
|
|
}
|
|
|
|
if strings.Contains(r.Header.Get("User-Agent"), "Docker-Client/") {
|
|
userAgent := strings.Split(r.Header.Get("User-Agent"), "/")
|
|
if len(userAgent) == 2 && !dockerVersion.Equal(version.Version(userAgent[1])) {
|
|
logrus.Debugf("Warning: client and server don't have the same version (client: %s, server: %s)", userAgent[1], dockerVersion)
|
|
}
|
|
}
|
|
version := version.Version(mux.Vars(r)["version"])
|
|
if version == "" {
|
|
version = api.APIVERSION
|
|
}
|
|
if corsHeaders != "" {
|
|
writeCorsHeaders(w, r, corsHeaders)
|
|
}
|
|
|
|
if version.GreaterThan(api.APIVERSION) {
|
|
http.Error(w, fmt.Errorf("client is newer than server (client API version: %s, server API version: %s)", version, api.APIVERSION).Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := handlerFunc(version, w, r, mux.Vars(r)); err != nil {
|
|
logrus.Errorf("Handler for %s %s returned error: %s", localMethod, localRoute, err)
|
|
httpError(w, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// we keep enableCors just for legacy usage, need to be removed in the future
|
|
func createRouter(s *Server) *mux.Router {
|
|
r := mux.NewRouter()
|
|
if os.Getenv("DEBUG") != "" {
|
|
ProfilerSetup(r, "/debug/")
|
|
}
|
|
m := map[string]map[string]HttpApiFunc{
|
|
"GET": {
|
|
"/_ping": s.ping,
|
|
"/events": s.getEvents,
|
|
"/info": s.getInfo,
|
|
"/version": s.getVersion,
|
|
"/images/json": s.getImagesJSON,
|
|
"/images/search": s.getImagesSearch,
|
|
"/images/get": s.getImagesGet,
|
|
"/images/{name:.*}/get": s.getImagesGet,
|
|
"/images/{name:.*}/history": s.getImagesHistory,
|
|
"/images/{name:.*}/json": s.getImagesByName,
|
|
"/containers/ps": s.getContainersJSON,
|
|
"/containers/json": s.getContainersJSON,
|
|
"/containers/{name:.*}/export": s.getContainersExport,
|
|
"/containers/{name:.*}/changes": s.getContainersChanges,
|
|
"/containers/{name:.*}/json": s.getContainersByName,
|
|
"/containers/{name:.*}/top": s.getContainersTop,
|
|
"/containers/{name:.*}/logs": s.getContainersLogs,
|
|
"/containers/{name:.*}/stats": s.getContainersStats,
|
|
"/containers/{name:.*}/attach/ws": s.wsContainersAttach,
|
|
"/exec/{id:.*}/json": s.getExecByID,
|
|
},
|
|
"POST": {
|
|
"/auth": s.postAuth,
|
|
"/commit": s.postCommit,
|
|
"/build": s.postBuild,
|
|
"/images/create": s.postImagesCreate,
|
|
"/images/load": s.postImagesLoad,
|
|
"/images/{name:.*}/push": s.postImagesPush,
|
|
"/images/{name:.*}/tag": s.postImagesTag,
|
|
"/containers/create": s.postContainersCreate,
|
|
"/containers/{name:.*}/kill": s.postContainersKill,
|
|
"/containers/{name:.*}/pause": s.postContainersPause,
|
|
"/containers/{name:.*}/unpause": s.postContainersUnpause,
|
|
"/containers/{name:.*}/restart": s.postContainersRestart,
|
|
"/containers/{name:.*}/start": s.postContainersStart,
|
|
"/containers/{name:.*}/stop": s.postContainersStop,
|
|
"/containers/{name:.*}/wait": s.postContainersWait,
|
|
"/containers/{name:.*}/resize": s.postContainersResize,
|
|
"/containers/{name:.*}/attach": s.postContainersAttach,
|
|
"/containers/{name:.*}/copy": s.postContainersCopy,
|
|
"/containers/{name:.*}/exec": s.postContainerExecCreate,
|
|
"/exec/{name:.*}/start": s.postContainerExecStart,
|
|
"/exec/{name:.*}/resize": s.postContainerExecResize,
|
|
"/containers/{name:.*}/rename": s.postContainerRename,
|
|
},
|
|
"DELETE": {
|
|
"/containers/{name:.*}": s.deleteContainers,
|
|
"/images/{name:.*}": s.deleteImages,
|
|
},
|
|
"OPTIONS": {
|
|
"": s.optionsHandler,
|
|
},
|
|
}
|
|
|
|
// If "api-cors-header" is not given, but "api-enable-cors" is true, we set cors to "*"
|
|
// otherwise, all head values will be passed to HTTP handler
|
|
corsHeaders := s.cfg.CorsHeaders
|
|
if corsHeaders == "" && s.cfg.EnableCors {
|
|
corsHeaders = "*"
|
|
}
|
|
|
|
for method, routes := range m {
|
|
for route, fct := range routes {
|
|
logrus.Debugf("Registering %s, %s", method, route)
|
|
// NOTE: scope issue, make sure the variables are local and won't be changed
|
|
localRoute := route
|
|
localFct := fct
|
|
localMethod := method
|
|
|
|
// build the handler function
|
|
f := makeHttpHandler(s.cfg.Logging, localMethod, localRoute, localFct, corsHeaders, version.Version(s.cfg.Version))
|
|
|
|
// add the new route
|
|
if localRoute == "" {
|
|
r.Methods(localMethod).HandlerFunc(f)
|
|
} else {
|
|
r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f)
|
|
r.Path(localRoute).Methods(localMethod).HandlerFunc(f)
|
|
}
|
|
}
|
|
}
|
|
|
|
return r
|
|
}
|