From e74004739a752f3f39a774aac96c29ec5fab9e3f Mon Sep 17 00:00:00 2001 From: Will Rouesnel Date: Fri, 13 Jan 2017 15:08:18 +1100 Subject: [PATCH 1/2] Refactor plugin handlers to allow composable plugins. This patch refactors the plugin handlers so they implement http.Handler, and exposes the individual initMux methods as public methods. This allows an advanced user to combine multiple drivers and plugin handlers under a common mux - enabling the likely use-case of a network driver and IPAM driver which should be launched together. This patch is mostly backwards compatible with the existing API with the exception of sdk.Handler.NewHandler() which by necessity is adapted to provide for step-wise addition of implementations. --- authorization/api.go | 26 +++++++++++--------- authorization/api_test.go | 11 +++++---- graphdriver/api.go | 39 ++++++++++++++++-------------- ipam/api.go | 23 ++++++++++-------- network/api.go | 40 +++++++++++++++++-------------- network/api_test.go | 13 +++++++--- sdk/handler.go | 43 ++++++++++++++++++++++++++++----- volume/api.go | 50 +++++++++++++++++++++------------------ 8 files changed, 151 insertions(+), 94 deletions(-) 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 { From d68e06690b3f92f8c8c66b1dd91b2e73aeaee831 Mon Sep 17 00:00:00 2001 From: Will Rouesnel Date: Wed, 8 Feb 2017 08:15:24 +1100 Subject: [PATCH 2/2] Add a multi-serving example to README.md. --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) 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) + +```