diff --git a/README.md b/README.md index d00f26c..0eaec3e 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,27 @@ A collection of helper packages to extend Docker Engine in Go Graph (experimental) | [Link](https://github.com/docker/docker/blob/master/experimental/plugins_graphdriver.md) | Extend image and container fs storage See the [understand Docker plugins documentation section](https://docs.docker.com/engine/extend/plugins/). + +# Serving multiple drivers on a single socket + +An abridged example of serving identically named handlers over the same socket. + +```go + +import ( + "github.com/docker/go-plugins-helpers/sdk" + "github.com/docker/go-plugins-helpers/ipam" + "github.com/docker/go-plugins-helpers/network" +) + +handler := sdk.NewHandler() + +driver := &NetworkDriver{} +ipamDriver := &IpamDriver{} + +network.InitMux(handler, driver) +ipam.InitMux(handler, ipamDriver) + +handler.ServeUnix("/run/docker/plugins/my_combined_driver.sock", 0) + +``` diff --git a/authorization/api.go b/authorization/api.go index 0c63f42..8b9fe32 100644 --- a/authorization/api.go +++ b/authorization/api.go @@ -9,9 +9,9 @@ import ( ) const ( - manifest = `{"Implements": ["` + authorization.AuthZApiImplements + `"]}` - reqPath = "/" + authorization.AuthZApiRequest - resPath = "/" + authorization.AuthZApiResponse + pluginType = authorization.AuthZApiImplements + reqPath = "/" + authorization.AuthZApiRequest + resPath = "/" + authorization.AuthZApiResponse ) // Request is the structure that docker's requests are deserialized to. @@ -34,24 +34,28 @@ type Handler struct { // NewHandler initializes the request handler with a plugin implementation. func NewHandler(plugin Plugin) *Handler { - h := &Handler{plugin, sdk.NewHandler(manifest)} - h.initMux() + h := &Handler{plugin, sdk.NewHandler()} + InitMux(h, plugin) return h } -func (h *Handler) initMux() { - h.handle(reqPath, func(req Request) Response { - return h.plugin.AuthZReq(req) +// InitMux initializes a compatible HTTP mux with routes for the specified driver. Can be used +// to combine multiple drivers into a single plugin mux. +func InitMux(h sdk.Mux, plugin Plugin) { + handle(h, reqPath, func(req Request) Response { + return plugin.AuthZReq(req) }) - h.handle(resPath, func(req Request) Response { - return h.plugin.AuthZRes(req) + handle(h, resPath, func(req Request) Response { + return plugin.AuthZRes(req) }) + + h.AddImplementation(pluginType) } type actionHandler func(Request) Response -func (h *Handler) handle(name string, actionCall actionHandler) { +func handle(h sdk.Mux, name string, actionCall actionHandler) { h.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) { var ( req Request diff --git a/authorization/api_test.go b/authorization/api_test.go index 1f1ac2f..7295e3d 100644 --- a/authorization/api_test.go +++ b/authorization/api_test.go @@ -40,14 +40,15 @@ func TestActivate(t *testing.T) { defer response.Body.Close() - body, err := ioutil.ReadAll(response.Body) + manifest := &sdk.PluginManifest{} + json.NewDecoder(response.Body).Decode(manifest) - if err != nil { - t.Fatal(err) + if len(manifest.Implements) != 1 { + t.Fatalf("Expected %v, got %v", 1, len(manifest.Implements)) } - if string(body) != manifest+"\n" { - t.Fatalf("Expected %s, got %s\n", manifest+"\n", string(body)) + if manifest.Implements[0] != pluginType { + t.Fatalf("Expected %s, got %s\n", pluginType, manifest.Implements[0]) } } diff --git a/graphdriver/api.go b/graphdriver/api.go index 673c3ca..1a22f2b 100644 --- a/graphdriver/api.go +++ b/graphdriver/api.go @@ -16,7 +16,7 @@ const ( // DefaultDockerRootDirectory is the default directory where graph drivers will be created. DefaultDockerRootDirectory = "/var/lib/docker/graph" - manifest = `{"Implements": ["GraphDriver"]}` + pluginType = "GraphDriver" initPath = "/GraphDriver.Init" createPath = "/GraphDriver.Create" createRWPath = "/GraphDriver.CreateReadWrite" @@ -246,19 +246,21 @@ type Handler struct { // NewHandler initializes the request handler with a driver implementation. func NewHandler(driver Driver) *Handler { - h := &Handler{driver, sdk.NewHandler(manifest)} - h.initMux() + h := &Handler{driver, sdk.NewHandler()} + InitMux(h, driver) return h } -func (h *Handler) initMux() { +// InitMux initializes a compatible HTTP mux with routes for the specified driver. Can be used +// to combine multiple drivers into a single plugin mux. +func InitMux(h sdk.Mux, driver Driver) { h.HandleFunc(initPath, func(w http.ResponseWriter, r *http.Request) { req := InitRequest{} err := sdk.DecodeRequest(w, r, &req) if err != nil { return } - err = h.driver.Init(req.Home, req.Options, req.UIDMaps, req.GIDMaps) + err = driver.Init(req.Home, req.Options, req.UIDMaps, req.GIDMaps) msg := "" if err != nil { msg = err.Error() @@ -271,7 +273,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.driver.Create(req.ID, req.Parent, req.MountLabel, req.StorageOpt) + err = driver.Create(req.ID, req.Parent, req.MountLabel, req.StorageOpt) msg := "" if err != nil { msg = err.Error() @@ -284,7 +286,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.driver.CreateReadWrite(req.ID, req.Parent, req.MountLabel, req.StorageOpt) + err = driver.CreateReadWrite(req.ID, req.Parent, req.MountLabel, req.StorageOpt) msg := "" if err != nil { msg = err.Error() @@ -297,7 +299,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.driver.Remove(req.ID) + err = driver.Remove(req.ID) msg := "" if err != nil { msg = err.Error() @@ -311,7 +313,7 @@ func (h *Handler) initMux() { if err != nil { return } - dir, err := h.driver.Get(req.ID, req.MountLabel) + dir, err := driver.Get(req.ID, req.MountLabel) msg := "" if err != nil { msg = err.Error() @@ -324,7 +326,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.driver.Put(req.ID) + err = driver.Put(req.ID) msg := "" if err != nil { msg = err.Error() @@ -337,11 +339,11 @@ func (h *Handler) initMux() { if err != nil { return } - exists := h.driver.Exists(req.ID) + exists := driver.Exists(req.ID) sdk.EncodeResponse(w, &ExistsResponse{Exists: exists}, "") }) h.HandleFunc(statusPath, func(w http.ResponseWriter, r *http.Request) { - status := h.driver.Status() + status := driver.Status() sdk.EncodeResponse(w, &StatusResponse{Status: status}, "") }) h.HandleFunc(getMetadataPath, func(w http.ResponseWriter, r *http.Request) { @@ -350,7 +352,7 @@ func (h *Handler) initMux() { if err != nil { return } - metadata, err := h.driver.GetMetadata(req.ID) + metadata, err := driver.GetMetadata(req.ID) msg := "" if err != nil { msg = err.Error() @@ -358,7 +360,7 @@ func (h *Handler) initMux() { sdk.EncodeResponse(w, &GetMetadataResponse{Err: msg, Metadata: metadata}, msg) }) h.HandleFunc(cleanupPath, func(w http.ResponseWriter, r *http.Request) { - err := h.driver.Cleanup() + err := driver.Cleanup() msg := "" if err != nil { msg = err.Error() @@ -371,7 +373,7 @@ func (h *Handler) initMux() { if err != nil { return } - stream := h.driver.Diff(req.ID, req.Parent) + stream := driver.Diff(req.ID, req.Parent) sdk.StreamResponse(w, stream) }) h.HandleFunc(changesPath, func(w http.ResponseWriter, r *http.Request) { @@ -380,7 +382,7 @@ func (h *Handler) initMux() { if err != nil { return } - changes, err := h.driver.Changes(req.ID, req.Parent) + changes, err := driver.Changes(req.ID, req.Parent) msg := "" if err != nil { msg = err.Error() @@ -391,7 +393,7 @@ func (h *Handler) initMux() { params := r.URL.Query() id := params.Get("id") parent := params.Get("parent") - size, err := h.driver.ApplyDiff(id, parent, r.Body) + size, err := driver.ApplyDiff(id, parent, r.Body) msg := "" if err != nil { msg = err.Error() @@ -404,13 +406,14 @@ func (h *Handler) initMux() { if err != nil { return } - size, err := h.driver.DiffSize(req.ID, req.Parent) + size, err := driver.DiffSize(req.ID, req.Parent) msg := "" if err != nil { msg = err.Error() } sdk.EncodeResponse(w, &DiffSizeResponse{Err: msg, Size: size}, msg) }) + h.AddImplementation(pluginType) } // CallInit is the raw call to the Graphdriver.Init method diff --git a/ipam/api.go b/ipam/api.go index 284bc0d..bb5eda6 100644 --- a/ipam/api.go +++ b/ipam/api.go @@ -7,7 +7,7 @@ import ( ) const ( - manifest = `{"Implements": ["IpamDriver"]}` + pluginType = "IpamDriver" capabilitiesPath = "/IpamDriver.GetCapabilities" addressSpacesPath = "/IpamDriver.GetDefaultAddressSpaces" @@ -96,14 +96,16 @@ type Handler struct { // NewHandler initializes the request handler with a driver implementation. func NewHandler(ipam Ipam) *Handler { - h := &Handler{ipam, sdk.NewHandler(manifest)} - h.initMux() + h := &Handler{ipam, sdk.NewHandler()} + InitMux(h, ipam) return h } -func (h *Handler) initMux() { +// InitMux initializes a compatible HTTP mux with routes for the specified driver. Can be used +// to combine multiple drivers into a single plugin mux. +func InitMux(h sdk.Mux, ipam Ipam) { h.HandleFunc(capabilitiesPath, func(w http.ResponseWriter, r *http.Request) { - res, err := h.ipam.GetCapabilities() + res, err := ipam.GetCapabilities() if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -112,7 +114,7 @@ func (h *Handler) initMux() { sdk.EncodeResponse(w, res, "") }) h.HandleFunc(addressSpacesPath, func(w http.ResponseWriter, r *http.Request) { - res, err := h.ipam.GetDefaultAddressSpaces() + res, err := ipam.GetDefaultAddressSpaces() if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -126,7 +128,7 @@ func (h *Handler) initMux() { if err != nil { return } - res, err := h.ipam.RequestPool(req) + res, err := ipam.RequestPool(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -140,7 +142,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.ipam.ReleasePool(req) + err = ipam.ReleasePool(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -154,7 +156,7 @@ func (h *Handler) initMux() { if err != nil { return } - res, err := h.ipam.RequestAddress(req) + res, err := ipam.RequestAddress(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -168,11 +170,12 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.ipam.ReleaseAddress(req) + err = ipam.ReleaseAddress(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) } sdk.EncodeResponse(w, make(map[string]string), "") }) + h.AddImplementation(pluginType) } diff --git a/network/api.go b/network/api.go index 111b376..b464797 100644 --- a/network/api.go +++ b/network/api.go @@ -8,7 +8,7 @@ import ( ) const ( - manifest = `{"Implements": ["NetworkDriver"]}` + pluginType = "NetworkDriver" // LocalScope is the correct scope response for a local scope driver LocalScope = `local` // GlobalScope is the correct scope response for a global scope driver @@ -213,14 +213,16 @@ type Handler struct { // NewHandler initializes the request handler with a driver implementation. func NewHandler(driver Driver) *Handler { - h := &Handler{driver, sdk.NewHandler(manifest)} - h.initMux() + h := &Handler{driver, sdk.NewHandler()} + InitMux(h, driver) return h } -func (h *Handler) initMux() { +// InitMux initializes a compatible HTTP mux with routes for the specified driver. Can be used +// to combine multiple drivers into a single plugin mux. +func InitMux(h sdk.Mux, driver Driver) { h.HandleFunc(capabilitiesPath, func(w http.ResponseWriter, r *http.Request) { - res, err := h.driver.GetCapabilities() + res, err := driver.GetCapabilities() if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -240,7 +242,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.driver.CreateNetwork(req) + err = driver.CreateNetwork(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -255,7 +257,7 @@ func (h *Handler) initMux() { if err != nil { return } - res, err := h.driver.AllocateNetwork(req) + res, err := driver.AllocateNetwork(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -269,7 +271,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.driver.DeleteNetwork(req) + err = driver.DeleteNetwork(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -283,7 +285,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.driver.FreeNetwork(req) + err = driver.FreeNetwork(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -297,7 +299,7 @@ func (h *Handler) initMux() { if err != nil { return } - res, err := h.driver.CreateEndpoint(req) + res, err := driver.CreateEndpoint(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -311,7 +313,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.driver.DeleteEndpoint(req) + err = driver.DeleteEndpoint(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -325,7 +327,7 @@ func (h *Handler) initMux() { if err != nil { return } - res, err := h.driver.EndpointInfo(req) + res, err := driver.EndpointInfo(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -338,7 +340,7 @@ func (h *Handler) initMux() { if err != nil { return } - res, err := h.driver.Join(req) + res, err := driver.Join(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -351,7 +353,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.driver.Leave(req) + err = driver.Leave(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -365,7 +367,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.driver.DiscoverNew(req) + err = driver.DiscoverNew(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -379,7 +381,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.driver.DiscoverDelete(req) + err = driver.DiscoverDelete(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -393,7 +395,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.driver.ProgramExternalConnectivity(req) + err = driver.ProgramExternalConnectivity(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -407,7 +409,7 @@ func (h *Handler) initMux() { if err != nil { return } - err = h.driver.RevokeExternalConnectivity(req) + err = driver.RevokeExternalConnectivity(req) if err != nil { msg := err.Error() sdk.EncodeResponse(w, NewErrorResponse(msg), msg) @@ -415,4 +417,6 @@ func (h *Handler) initMux() { } sdk.EncodeResponse(w, make(map[string]string), "") }) + + h.AddImplementation(pluginType) } diff --git a/network/api_test.go b/network/api_test.go index d7befc2..2f88f45 100644 --- a/network/api_test.go +++ b/network/api_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "encoding/json" "github.com/docker/go-plugins-helpers/sdk" ) @@ -113,10 +114,16 @@ func TestActivate(t *testing.T) { t.Fatal(err) } defer response.Body.Close() - body, err := ioutil.ReadAll(response.Body) - if string(body) != manifest+"\n" { - t.Fatalf("Expected %s, got %s\n", manifest+"\n", string(body)) + manifest := &sdk.PluginManifest{} + json.NewDecoder(response.Body).Decode(manifest) + + if len(manifest.Implements) != 1 { + t.Fatalf("Expected %v, got %v", 1, len(manifest.Implements)) + } + + if manifest.Implements[0] != pluginType { + t.Fatalf("Expected %s, got %s\n", pluginType, manifest.Implements[0]) } } diff --git a/sdk/handler.go b/sdk/handler.go index 2dda5fa..611412b 100644 --- a/sdk/handler.go +++ b/sdk/handler.go @@ -2,7 +2,6 @@ package sdk import ( "crypto/tls" - "fmt" "net" "net/http" "os" @@ -10,22 +9,36 @@ import ( const activatePath = "/Plugin.Activate" +// Mux exposes the minimal methods needed to set handler functions. +type Mux interface { + HandleFunc(path string, fn func(http.ResponseWriter, *http.Request)) + // Add an implementation to Mux's route for Plugin.Activate + AddImplementation(implementation string) + http.Handler +} + +// PluginManifest implements the Plugin.Activate JSON response. +type PluginManifest struct { + Implements []string +} + // Handler is the base to create plugin handlers. // It initializes connections and sockets to listen to. type Handler struct { - mux *http.ServeMux + mux *http.ServeMux + manifest *PluginManifest } // NewHandler creates a new Handler with an http mux. -func NewHandler(manifest string) Handler { +func NewHandler() Handler { mux := http.NewServeMux() + internalManifest := &PluginManifest{} mux.HandleFunc(activatePath, func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", DefaultContentTypeV1_1) - fmt.Fprintln(w, manifest) + EncodeResponse(w, internalManifest, "") }) - return Handler{mux: mux} + return Handler{mux: mux, manifest: internalManifest} } // Serve sets up the handler to serve requests on the passed in listener @@ -63,7 +76,25 @@ func (h Handler) ServeUnix(addr string, gid int) error { return h.Serve(l) } +// ServeHTTP implements http.Handler and passes the request through to the contained mux. +// This method allows plugin handlers to be used with other Go HTTP frameworks. +func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + h.mux.ServeHTTP(w, r) +} + // HandleFunc registers a function to handle a request path with. func (h Handler) HandleFunc(path string, fn func(w http.ResponseWriter, r *http.Request)) { h.mux.HandleFunc(path, fn) } + +// AddImplementation adds the given implementation string to the manifest of the plugin handler. +// Unique implmentation names are only added once. +func (h Handler) AddImplementation(implementation string) { + // Check the impl + for _, v := range h.manifest.Implements { + if v == implementation { + return + } + } + h.manifest.Implements = append(h.manifest.Implements, implementation) +} diff --git a/volume/api.go b/volume/api.go index 283d1b2..1552a01 100644 --- a/volume/api.go +++ b/volume/api.go @@ -10,7 +10,7 @@ const ( // DefaultDockerRootDirectory is the default directory where volumes will be created. DefaultDockerRootDirectory = "/var/lib/docker-volumes" - manifest = `{"Implements": ["VolumeDriver"]}` + pluginType = "VolumeDriver" createPath = "/VolumeDriver.Create" getPath = "/VolumeDriver.Get" listPath = "/VolumeDriver.List" @@ -84,45 +84,49 @@ type unmountActionHandler func(UnmountRequest) Response // NewHandler initializes the request handler with a driver implementation. func NewHandler(driver Driver) *Handler { - h := &Handler{driver, sdk.NewHandler(manifest)} - h.initMux() + h := &Handler{driver, sdk.NewHandler()} + InitMux(h, driver) return h } -func (h *Handler) initMux() { - h.handle(createPath, func(req Request) Response { - return h.driver.Create(req) +// InitMux initializes a compatible HTTP mux with routes for the specified driver. Can be used +// to combine multiple drivers into a single plugin mux. +func InitMux(h sdk.Mux, driver Driver) { + handle(h, createPath, func(req Request) Response { + return driver.Create(req) }) - h.handle(getPath, func(req Request) Response { - return h.driver.Get(req) + handle(h, getPath, func(req Request) Response { + return driver.Get(req) }) - h.handle(listPath, func(req Request) Response { - return h.driver.List(req) + handle(h, listPath, func(req Request) Response { + return driver.List(req) }) - h.handle(removePath, func(req Request) Response { - return h.driver.Remove(req) + handle(h, removePath, func(req Request) Response { + return driver.Remove(req) }) - h.handle(hostVirtualPath, func(req Request) Response { - return h.driver.Path(req) + handle(h, hostVirtualPath, func(req Request) Response { + return driver.Path(req) }) - h.handleMount(mountPath, func(req MountRequest) Response { - return h.driver.Mount(req) + handleMount(h, mountPath, func(req MountRequest) Response { + return driver.Mount(req) }) - h.handleUnmount(unmountPath, func(req UnmountRequest) Response { - return h.driver.Unmount(req) + handleUnmount(h, unmountPath, func(req UnmountRequest) Response { + return driver.Unmount(req) }) - h.handle(capabilitiesPath, func(req Request) Response { - return h.driver.Capabilities(req) + handle(h, capabilitiesPath, func(req Request) Response { + return driver.Capabilities(req) }) + + h.AddImplementation(pluginType) } -func (h *Handler) handle(name string, actionCall actionHandler) { +func handle(h sdk.Mux, name string, actionCall actionHandler) { h.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) { var req Request if err := sdk.DecodeRequest(w, r, &req); err != nil { @@ -135,7 +139,7 @@ func (h *Handler) handle(name string, actionCall actionHandler) { }) } -func (h *Handler) handleMount(name string, actionCall mountActionHandler) { +func handleMount(h sdk.Mux, name string, actionCall mountActionHandler) { h.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) { var req MountRequest if err := sdk.DecodeRequest(w, r, &req); err != nil { @@ -147,7 +151,7 @@ func (h *Handler) handleMount(name string, actionCall mountActionHandler) { }) } -func (h *Handler) handleUnmount(name string, actionCall unmountActionHandler) { +func handleUnmount(h sdk.Mux, name string, actionCall unmountActionHandler) { h.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) { var req UnmountRequest if err := sdk.DecodeRequest(w, r, &req); err != nil {