| package server | |
| import ( | |
| "context" | |
| "net/http" | |
| "github.com/containerd/log" | |
| "github.com/gorilla/mux" | |
| "github.com/moby/moby/api/types/common" | |
| "github.com/moby/moby/v2/daemon/internal/otelutil" | |
| "github.com/moby/moby/v2/daemon/internal/versions" | |
| "github.com/moby/moby/v2/daemon/server/httpstatus" | |
| "github.com/moby/moby/v2/daemon/server/httputils" | |
| "github.com/moby/moby/v2/daemon/server/middleware" | |
| "github.com/moby/moby/v2/daemon/server/router" | |
| "github.com/moby/moby/v2/dockerversion" | |
| "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" | |
| "go.opentelemetry.io/otel/baggage" | |
| ) | |
| // versionMatcher defines a variable matcher to be parsed by the router | |
| // when a request is about to be served. | |
| const versionMatcher = "/v{version:[0-9.]+}" | |
| // statusClientClosedRequest (HTTP 499 Client Closed Request) is a non-standard | |
| // HTTP status code used by NGINX to indicate that the client closed the connection | |
| // before the server was able to send a response. | |
| // | |
| // It is not part of the IANA HTTP status code registry and is primarily used | |
| // for logging and telemetry. The client will typically not observe this status, | |
| // as the connection is already closed. | |
| // | |
| // See: | |
| // - https://developers.cloudflare.com/support/troubleshooting/http-status-codes/4xx-client-error/error-499/ | |
| // - https://nginx.org/en/docs/http/ngx_http_log_module.html | |
| const statusClientClosedRequest = 499 | |
| // Server contains instance details for the server | |
| type Server struct { | |
| middlewares []middleware.Middleware | |
| } | |
| // UseMiddleware appends a new middleware to the request chain. | |
| // This needs to be called before the API routes are configured. | |
| func (s *Server) UseMiddleware(m middleware.Middleware) { | |
| s.middlewares = append(s.middlewares, m) | |
| } | |
| func (s *Server) makeHTTPHandler(route router.Route) http.HandlerFunc { | |
| handler := route.Handler() | |
| operation := route.Method() + " " + route.Path() | |
| return otelhttp.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| // 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. | |
| // use intermediate variable to prevent "should not use basic type | |
| // string as key in context.WithValue" golint errors | |
| ua := r.Header.Get("User-Agent") | |
| ctx := baggage.ContextWithBaggage(context.WithValue(r.Context(), dockerversion.UAStringKey{}, ua), otelutil.MustNewBaggage( | |
| otelutil.MustNewMemberRaw(otelutil.TriggerKey, "api"), | |
| )) | |
| r = r.WithContext(ctx) | |
| handlerFunc := s.handlerWithGlobalMiddlewares(handler) | |
| vars := mux.Vars(r) | |
| if vars == nil { | |
| vars = make(map[string]string) | |
| } | |
| if err := handlerFunc(ctx, w, r, vars); err != nil { | |
| if r.Context().Err() != nil { | |
| // Request is canceled, and client likely went away. Don't attempt | |
| // to write JSON body, but log for debugging. Log the status as | |
| // "499 Client Closed Request", which is non-standard, but aligns | |
| // with NGINX and CloudFlare. | |
| w.WriteHeader(statusClientClosedRequest) // for OTEL/metrics | |
| log.G(ctx).WithFields(log.Fields{ | |
| "module": "api", | |
| "method": route.Method(), | |
| "request-url": r.RequestURI, | |
| "vars": vars, | |
| "error": err, | |
| "status": statusClientClosedRequest, | |
| }).Info("request cancelled by client") | |
| return | |
| } | |
| statusCode := httpstatus.FromError(err) | |
| // While we no longer support API versions older than 1.24 [config.DefaultMinAPIVersion], | |
| // a client may try to connect using an older version and expect a plain-text error | |
| // instead of a JSON error. This would result in an "API version too old" error | |
| // formatted in JSON being printed as-is. | |
| // | |
| // Let's be nice, and return errors in plain-text to provide a more readable error | |
| // to help the user understand the API version they're using is no longer supported. | |
| if v := vars["version"]; v != "" && versions.LessThan(v, "1.24") { | |
| http.Error(w, err.Error(), statusCode) | |
| } else { | |
| _ = httputils.WriteJSON(w, statusCode, &common.ErrorResponse{ | |
| Message: err.Error(), | |
| }) | |
| } | |
| if statusCode >= http.StatusInternalServerError { | |
| log.G(ctx).WithFields(log.Fields{ | |
| "module": "api", | |
| "method": route.Method(), | |
| "request-url": r.RequestURI, | |
| "vars": vars, | |
| "error-response": err, | |
| "status": statusCode, | |
| }).Errorf("Handler for %s %s returned error", route.Method(), route.Path()) | |
| } | |
| } | |
| }), operation).ServeHTTP | |
| } | |
| // CreateMux returns a new mux with all the routers registered. | |
| func (s *Server) CreateMux(ctx context.Context, routers ...router.Router) *mux.Router { | |
| log.G(ctx).Debug("Registering routers") | |
| m := mux.NewRouter() | |
| for _, apiRouter := range routers { | |
| for _, r := range apiRouter.Routes() { | |
| if ctx.Err() != nil { | |
| return m | |
| } | |
| log.G(ctx).WithFields(log.Fields{"method": r.Method(), "path": r.Path()}).Debug("Registering route") | |
| f := s.makeHTTPHandler(r) | |
| m.Path(versionMatcher + r.Path()).Methods(r.Method()).Handler(f) | |
| m.Path(r.Path()).Methods(r.Method()).Handler(f) | |
| } | |
| } | |
| // Setup handlers for undefined paths and methods | |
| notFoundHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| _ = httputils.WriteJSON(w, http.StatusNotFound, &common.ErrorResponse{ | |
| Message: "page not found", | |
| }) | |
| }) | |
| m.HandleFunc(versionMatcher+"/{path:.*}", notFoundHandler) | |
| m.NotFoundHandler = notFoundHandler | |
| m.MethodNotAllowedHandler = notFoundHandler | |
| return m | |
| } |