Skip to content

Commit

Permalink
Separate API router from server.
Browse files Browse the repository at this point in the history
Implement basic interfaces to write custom routers that can be plugged
to the server. Remove server coupling with the daemon.

Signed-off-by: David Calavera <david.calavera@gmail.com>
  • Loading branch information
calavera committed Sep 29, 2015
1 parent 144abf2 commit da982cf
Show file tree
Hide file tree
Showing 32 changed files with 809 additions and 591 deletions.
57 changes: 0 additions & 57 deletions api/server/form.go

This file was deleted.

63 changes: 63 additions & 0 deletions api/server/httputils/form.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package httputils

import (
"fmt"
"net/http"
"path/filepath"
"strconv"
"strings"
)

// BoolValue transforms a form value in different formats into a boolean type.
func BoolValue(r *http.Request, k string) bool {
s := strings.ToLower(strings.TrimSpace(r.FormValue(k)))
return !(s == "" || s == "0" || s == "no" || s == "false" || s == "none")
}

// BoolValueOrDefault returns the default bool passed if the query param is
// missing, otherwise it's just a proxy to boolValue above
func BoolValueOrDefault(r *http.Request, k string, d bool) bool {
if _, ok := r.Form[k]; !ok {
return d
}
return BoolValue(r, k)
}

// Int64ValueOrZero parses a form value into a int64 type.
// It returns 0 if the parsing fails.
func Int64ValueOrZero(r *http.Request, k string) int64 {
val, err := strconv.ParseInt(r.FormValue(k), 10, 64)
if err != nil {
return 0
}
return val
}

// ArchiveOptions stores archive information for different operations.
type ArchiveOptions struct {
Name string
Path string
}

// ArchiveFormValues parses form values and turns them into ArchiveOptions.
// It fails if the archive name and path are not in the request.
func ArchiveFormValues(r *http.Request, vars map[string]string) (ArchiveOptions, error) {
if vars == nil {
return ArchiveOptions{}, fmt.Errorf("Missing parameter")
}
if err := ParseForm(r); err != nil {
return ArchiveOptions{}, err
}

name := vars["name"]
path := filepath.FromSlash(r.Form.Get("path"))

switch {
case name == "":
return ArchiveOptions{}, fmt.Errorf("bad parameter: 'name' cannot be empty")
case path == "":
return ArchiveOptions{}, fmt.Errorf("bad parameter: 'path' cannot be empty")
}

return ArchiveOptions{name, path}, nil
}
10 changes: 5 additions & 5 deletions api/server/form_test.go → api/server/httputils/form_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package server
package httputils

import (
"net/http"
Expand Down Expand Up @@ -26,7 +26,7 @@ func TestBoolValue(t *testing.T) {
r, _ := http.NewRequest("POST", "", nil)
r.Form = v

a := boolValue(r, "test")
a := BoolValue(r, "test")
if a != e {
t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a)
}
Expand All @@ -35,15 +35,15 @@ func TestBoolValue(t *testing.T) {

func TestBoolValueOrDefault(t *testing.T) {
r, _ := http.NewRequest("GET", "", nil)
if !boolValueOrDefault(r, "queryparam", true) {
if !BoolValueOrDefault(r, "queryparam", true) {
t.Fatal("Expected to get true default value, got false")
}

v := url.Values{}
v.Set("param", "")
r, _ = http.NewRequest("GET", "", nil)
r.Form = v
if boolValueOrDefault(r, "param", true) {
if BoolValueOrDefault(r, "param", true) {
t.Fatal("Expected not to get true")
}
}
Expand All @@ -62,7 +62,7 @@ func TestInt64ValueOrZero(t *testing.T) {
r, _ := http.NewRequest("POST", "", nil)
r.Form = v

a := int64ValueOrZero(r, "test")
a := Int64ValueOrZero(r, "test")
if a != e {
t.Fatalf("Value: %s, expected: %v, actual: %v", c, e, a)
}
Expand Down
180 changes: 180 additions & 0 deletions api/server/httputils/httputils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package httputils

import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"

"golang.org/x/net/context"

"github.com/Sirupsen/logrus"
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/docker/api"
"github.com/docker/docker/pkg/version"
"github.com/docker/docker/utils"
)

// APIVersionKey is the client's requested API version.
const APIVersionKey = "api-version"

// APIFunc 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 APIFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error

// HijackConnection interrupts the http response writer to get the
// underlying connection and operate with it.
func HijackConnection(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
}

// CloseStreams ensures that a list for http streams are properly closed.
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)
}

// ParseForm ensures the request form is parsed even with invalid content types.
// 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
}

// ParseMultipartForm ensure the request form is parsed, even with invalid content types.
func ParseMultipartForm(r *http.Request) error {
if err := r.ParseMultipartForm(4096); err != nil && !strings.HasPrefix(err.Error(), "mime:") {
return err
}
return nil
}

// WriteError decodes a specific docker error and sends it in the response.
func WriteError(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)
}

// VersionFromContext returns an API version from the context using APIVersionKey.
// It panics if the context value does not have version.Version type.
func VersionFromContext(ctx context.Context) (ver version.Version) {
if ctx == nil {
return
}
val := ctx.Value(APIVersionKey)
if val == nil {
return
}
return val.(version.Version)
}
Loading

0 comments on commit da982cf

Please sign in to comment.