This PR adds a "request ID" to each event generated, the 'docker events' stream now looks like this: ``` 2015-09-10T15:02:50.000000000-07:00 [reqid: c01e3534ddca] de7c5d4ca927253cf4e978ee9c4545161e406e9b5a14617efb52c658b249174a: (from ubuntu) create ``` Note the `[reqID: c01e3534ddca]` part, that's new. Each HTTP request will generate its own unique ID. So, if you do a `docker build` you'll see a series of events all with the same reqID. This allow for log processing tools to determine which events are all related to the same http request. I didn't propigate the context to all possible funcs in the daemon, I decided to just do the ones that needed it in order to get the reqID into the events. I'd like to have people review this direction first, and if we're ok with it then I'll make sure we're consistent about when we pass around the context - IOW, make sure that all funcs at the same level have a context passed in even if they don't call the log funcs - this will ensure we're consistent w/o passing it around for all calls unnecessarily. ping @icecrime @calavera @crosbymichael Signed-off-by: Doug Davis <dug@us.ibm.com> Upstream-commit: 26b1064967d9fcefd4c35f60e96bf6d7c9a3b5f8 Component: engine
413 lines
13 KiB
Go
413 lines
13 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
"github.com/docker/distribution/registry/api/errcode"
|
|
"github.com/docker/docker/api"
|
|
"github.com/docker/docker/context"
|
|
"github.com/docker/docker/daemon"
|
|
"github.com/docker/docker/pkg/sockets"
|
|
"github.com/docker/docker/pkg/stringid"
|
|
"github.com/docker/docker/utils"
|
|
)
|
|
|
|
// Config provides the configuration for the API server
|
|
type Config struct {
|
|
Logging bool
|
|
EnableCors bool
|
|
CorsHeaders string
|
|
Version string
|
|
SocketGroup string
|
|
TLSConfig *tls.Config
|
|
}
|
|
|
|
// Server contains instance details for the server
|
|
type Server struct {
|
|
daemon *daemon.Daemon
|
|
cfg *Config
|
|
router *mux.Router
|
|
start chan struct{}
|
|
servers []serverCloser
|
|
}
|
|
|
|
// New returns a new instance of the server based on the specified configuration.
|
|
func New(ctx context.Context, cfg *Config) *Server {
|
|
srv := &Server{
|
|
cfg: cfg,
|
|
start: make(chan struct{}),
|
|
}
|
|
srv.router = createRouter(ctx, srv)
|
|
return srv
|
|
}
|
|
|
|
// Close closes servers and thus stop receiving requests
|
|
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
|
|
}
|
|
|
|
// HTTPServer contains an instance of http server and the listener.
|
|
// srv *http.Server, contains configuration to create a http server and a mux router with all api end points.
|
|
// l net.Listener, is a TCP or Socket listener that dispatches incoming request to the router.
|
|
type HTTPServer struct {
|
|
srv *http.Server
|
|
l net.Listener
|
|
}
|
|
|
|
// Serve starts listening for inbound requests.
|
|
func (s *HTTPServer) Serve() error {
|
|
return s.srv.Serve(s.l)
|
|
}
|
|
|
|
// Close closes the HTTPServer from listening for the inbound requests.
|
|
func (s *HTTPServer) Close() error {
|
|
return s.l.Close()
|
|
}
|
|
|
|
// HTTPAPIFunc is an adapter to allow the use of ordinary functions as Docker API endpoints.
|
|
// Any function that has the appropriate signature can be register as a API endpoint (e.g. getVersion).
|
|
type HTTPAPIFunc func(ctx context.Context, 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()
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkForJSON makes sure that the 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
|
|
errMsg := err.Error()
|
|
|
|
// Based on the type of error we get we need to process things
|
|
// slightly differently to extract the error message.
|
|
// In the 'errcode.*' cases there are two different type of
|
|
// error that could be returned. errocode.ErrorCode is the base
|
|
// type of error object - it is just an 'int' that can then be
|
|
// used as the look-up key to find the message. errorcode.Error
|
|
// extends errorcode.Error by adding error-instance specific
|
|
// data, like 'details' or variable strings to be inserted into
|
|
// the message.
|
|
//
|
|
// Ideally, we should just be able to call err.Error() for all
|
|
// cases but the errcode package doesn't support that yet.
|
|
//
|
|
// Additionally, in both errcode cases, there might be an http
|
|
// status code associated with it, and if so use it.
|
|
switch err.(type) {
|
|
case errcode.ErrorCode:
|
|
daError, _ := err.(errcode.ErrorCode)
|
|
statusCode = daError.Descriptor().HTTPStatusCode
|
|
errMsg = daError.Message()
|
|
|
|
case errcode.Error:
|
|
// For reference, if you're looking for a particular error
|
|
// then you can do something like :
|
|
// import ( derr "github.com/docker/docker/errors" )
|
|
// if daError.ErrorCode() == derr.ErrorCodeNoSuchContainer { ... }
|
|
|
|
daError, _ := err.(errcode.Error)
|
|
statusCode = daError.ErrorCode().Descriptor().HTTPStatusCode
|
|
errMsg = daError.Message
|
|
|
|
default:
|
|
// This part of will be removed once we've
|
|
// converted everything over to use the errcode package
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
|
|
if statusCode == 0 {
|
|
statusCode = http.StatusInternalServerError
|
|
}
|
|
|
|
logrus.WithFields(logrus.Fields{"statusCode": statusCode, "err": utils.GetErrorMessage(err)}).Error("HTTP Error")
|
|
http.Error(w, errMsg, 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) optionsHandler(ctx context.Context, 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", "HEAD, GET, POST, DELETE, PUT, OPTIONS")
|
|
}
|
|
|
|
func (s *Server) ping(ctx context.Context, 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 (s *Server) makeHTTPHandler(ctx context.Context, localMethod string, localRoute string, localHandler HTTPAPIFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// log the handler generation
|
|
logrus.Debugf("Calling %s %s", localMethod, localRoute)
|
|
|
|
// Define the context that we'll pass around to share info
|
|
// like the docker-request-id.
|
|
//
|
|
// The 'context' will be used for global data that should
|
|
// apply to all requests. Data that is specific to the
|
|
// immediate function being called should still be passed
|
|
// as 'args' on the function call.
|
|
reqID := stringid.TruncateID(stringid.GenerateNonCryptoID())
|
|
ctx = context.WithValue(ctx, context.RequestID, reqID)
|
|
handlerFunc := s.handleWithGlobalMiddlewares(localHandler)
|
|
|
|
if err := handlerFunc(ctx, w, r, mux.Vars(r)); err != nil {
|
|
logrus.Errorf("Handler for %s %s returned error: %s", localMethod, localRoute, utils.GetErrorMessage(err))
|
|
httpError(w, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// createRouter initializes the main router the server uses.
|
|
// we keep enableCors just for legacy usage, need to be removed in the future
|
|
func createRouter(ctx context.Context, s *Server) *mux.Router {
|
|
r := mux.NewRouter()
|
|
if os.Getenv("DEBUG") != "" {
|
|
profilerSetup(r, "/debug/")
|
|
}
|
|
m := map[string]map[string]HTTPAPIFunc{
|
|
"HEAD": {
|
|
"/containers/{name:.*}/archive": s.headContainersArchive,
|
|
},
|
|
"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/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,
|
|
"/containers/{name:.*}/archive": s.getContainersArchive,
|
|
"/volumes": s.getVolumesList,
|
|
"/volumes/{name:.*}": s.getVolumeByName,
|
|
},
|
|
"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,
|
|
"/volumes": s.postVolumesCreate,
|
|
},
|
|
"PUT": {
|
|
"/containers/{name:.*}/archive": s.putContainersArchive,
|
|
},
|
|
"DELETE": {
|
|
"/containers/{name:.*}": s.deleteContainers,
|
|
"/images/{name:.*}": s.deleteImages,
|
|
"/volumes/{name:.*}": s.deleteVolumes,
|
|
},
|
|
"OPTIONS": {
|
|
"": s.optionsHandler,
|
|
},
|
|
}
|
|
|
|
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 := s.makeHTTPHandler(ctx, localMethod, localRoute, localFct)
|
|
|
|
// 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
|
|
}
|