From 610b8d19d03bf193f6d0ef817decb99734291fbf Mon Sep 17 00:00:00 2001 From: Jeff Lindsay Date: Thu, 9 Apr 2015 20:03:44 -0500 Subject: [PATCH 1/3] setup for local development --- Gunfile | 34 ++++++++++++++++++++++++++++++++++ VERSION | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 Gunfile diff --git a/Gunfile b/Gunfile new file mode 100644 index 0000000000000..5f8961c162c1a --- /dev/null +++ b/Gunfile @@ -0,0 +1,34 @@ + +# temporary Gunfile for plugins development on OS X. +# curious to use it yourself? +# https://github.com/gliderlabs/glidergun + +init() { + cmd-export build + cmd-export push + cmd-export restore + cmd-export backup +} + +build() { + make BINDDIR=. +} + +push() { + cat bundles/1.5.0-plugins/binary/docker-1.5.0-plugins \ + | boot2docker ssh "cat > docker" + boot2docker ssh " + chmod +x ./docker + mv ./docker /usr/local/bin/docker + sudo /etc/init.d/docker restart + " +} + +backup() { + boot2docker ssh "cp /usr/local/bin/docker /usr/local/bin/docker.backup" +} + +restore() { + boot2docker ssh "cp /usr/local/bin/docker.backup /usr/local/bin/docker" + boot2docker ssh "sudo /etc/init.d/docker restart" +} diff --git a/VERSION b/VERSION index 59b9db0c75150..af92eb97db5f4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.5.0-dev +1.5.0-plugins From 1986936102fc94fd18644e83678b52579a3ad465 Mon Sep 17 00:00:00 2001 From: Jeff Lindsay Date: Thu, 9 Apr 2015 20:05:17 -0500 Subject: [PATCH 2/3] extracted plugin and volume extension point specific code from original plugin_mode branch --- daemon/container.go | 50 ++++++++++++++++++++++++++++++++ daemon/daemon.go | 1 + daemon/volumes.go | 20 +++++++++++-- plugins/client.go | 64 +++++++++++++++++++++++++++++++++++++++++ plugins/plugin.go | 36 +++++++++++++++++++++++ plugins/repository.go | 61 +++++++++++++++++++++++++++++++++++++++ runconfig/hostconfig.go | 2 ++ runconfig/parse.go | 2 ++ volumes/repository.go | 43 ++++++++++++++++++++++++++- 9 files changed, 275 insertions(+), 4 deletions(-) create mode 100644 plugins/client.go create mode 100644 plugins/plugin.go create mode 100644 plugins/repository.go diff --git a/daemon/container.go b/daemon/container.go index 46defe9683298..34716e82e1617 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "io/ioutil" + "net" "os" "path" "path/filepath" @@ -40,6 +41,7 @@ import ( "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/symlink" "github.com/docker/docker/pkg/ulimit" + "github.com/docker/docker/plugins" "github.com/docker/docker/runconfig" "github.com/docker/docker/utils" ) @@ -1454,9 +1456,53 @@ func (container *Container) waitForStart() error { return err } + if container.hostConfig.Plugin { + if err := container.waitForPluginSock(); err != nil { + return err + } + } + return nil } +func (container *Container) waitForPluginSock() error { + pluginSock, err := container.getPluginSocketPath() + if err != nil { + return err + } + + chConn := make(chan net.Conn) + chStop := make(chan struct{}) + go func() { + logrus.Debugf("waiting for plugin socket at: %s", pluginSock) + for { + conn, err := net.DialTimeout("unix", pluginSock, 100*time.Millisecond) + // If the file doesn't exist yet, that's ok, maybe plugin hasn't created it yet + if err != nil { + select { + case <-chStop: + return + default: + continue + } + } + logrus.Debugf("got plugin socket") + chConn <- conn + return + } + }() + + select { + case conn := <-chConn: + // We can close this net.Conn since the plugin system will establish it's own connection + conn.Close() + return plugins.Repo.RegisterPlugin(pluginSock) + case <-time.After(30 * time.Second): + chStop <- struct{}{} + return fmt.Errorf("connection to plugin sock timed out") + } +} + func (container *Container) allocatePort(eng *engine.Engine, port nat.Port, bindings nat.PortMap) error { binding := bindings[port] if container.hostConfig.PublishAllPorts && len(binding) == 0 { @@ -1534,3 +1580,7 @@ func (c *Container) LogDriverType() string { } return c.hostConfig.LogConfig.Type } + +func (container *Container) getPluginSocketPath() (string, error) { + return container.getRootResourcePath(filepath.Join("p", "plugin.sock")) +} diff --git a/daemon/daemon.go b/daemon/daemon.go index 40d1ecb418ad5..eac68a14be58e 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -322,6 +322,7 @@ func (daemon *Daemon) restore() error { return err } + // TODO: sort out plugins from normal containers and load plugins first for _, v := range dir { id := v.Name() container, err := daemon.load(id) diff --git a/daemon/volumes.go b/daemon/volumes.go index f40fdd3e49df1..f64286b086ecc 100644 --- a/daemon/volumes.go +++ b/daemon/volumes.go @@ -131,7 +131,7 @@ func (container *Container) registerVolumes() { if rw, exists := container.VolumesRW[path]; exists { writable = rw } - v, err := container.daemon.volumes.FindOrCreateVolume(path, writable) + v, err := container.daemon.volumes.FindOrCreateVolume(path, container.ID, writable) if err != nil { logrus.Debugf("error registering volume %s: %v", path, err) continue @@ -164,7 +164,7 @@ func (container *Container) parseVolumeMountConfig() (map[string]*Mount, error) return nil, fmt.Errorf("Duplicate volume %q: %q already in use, mounted from %q", path, mountToPath, m.volume.Path) } // Check if a volume already exists for this and use it - vol, err := container.daemon.volumes.FindOrCreateVolume(path, writable) + vol, err := container.daemon.volumes.FindOrCreateVolume(path, container.ID, writable) if err != nil { return nil, err } @@ -199,7 +199,7 @@ func (container *Container) parseVolumeMountConfig() (map[string]*Mount, error) } } - vol, err := container.daemon.volumes.FindOrCreateVolume("", true) + vol, err := container.daemon.volumes.FindOrCreateVolume("", container.ID, true) if err != nil { return nil, err } @@ -323,6 +323,20 @@ func validMountMode(mode string) bool { func (container *Container) setupMounts() error { mounts := []execdriver.Mount{} + if container.hostConfig.Plugin { + // We are going to create this socket then close/unlink it so it can be + // bind-mounted into the container and used by the plugin process. + socketPath, err := container.getPluginSocketPath() + if err != nil { + return err + } + pluginDir := filepath.Dir(socketPath) + if err := os.MkdirAll(pluginDir, 0700); err != nil { + return err + } + mounts = append(mounts, execdriver.Mount{Source: pluginDir, Destination: "/var/run/docker-plugin", Writable: true, Private: true}) + } + // Mount user specified volumes // Note, these are not private because you may want propagation of (un)mounts from host // volumes. For instance if you use -v /usr:/usr and the host later mounts /usr/share you diff --git a/plugins/client.go b/plugins/client.go new file mode 100644 index 0000000000000..a2a070cbdb33f --- /dev/null +++ b/plugins/client.go @@ -0,0 +1,64 @@ +package plugins + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/http/httputil" + "time" + + log "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/ioutils" +) + +const pluginApiVersion = "v1" + +func connect(addr string) (*httputil.ClientConn, error) { + c, err := net.DialTimeout("unix", addr, 30*time.Second) + if err != nil { + return nil, err + } + return httputil.NewClientConn(c, nil), nil +} + +func call(addr, method, path string, data interface{}) (io.ReadCloser, error) { + client, err := connect(addr) + if err != nil { + return nil, err + } + + reqBody, err := json.Marshal(data) + if err != nil { + return nil, err + } + + log.Debugf("sending request for extension:\n%s", string(reqBody)) + path = "/" + pluginApiVersion + "/" + path + req, err := http.NewRequest(method, path, bytes.NewBuffer(reqBody)) + if err != nil { + client.Close() + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + client.Close() + return nil, err + } + + // FIXME: this should be better defined + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("got bad status: %s", resp.Status) + } + + return ioutils.NewReadCloserWrapper(resp.Body, func() error { + if err := resp.Body.Close(); err != nil { + return err + } + return client.Close() + }), nil +} diff --git a/plugins/plugin.go b/plugins/plugin.go new file mode 100644 index 0000000000000..c0fffa6ed6ff5 --- /dev/null +++ b/plugins/plugin.go @@ -0,0 +1,36 @@ +package plugins + +import ( + "encoding/json" + "io" +) + +type Plugin struct { + addr string + kind string +} + +type handshakeResp struct { + InterestedIn []string + Name string + Author string + Org string + Website string +} + +func (p *Plugin) Call(method, path string, data interface{}) (io.ReadCloser, error) { + path = p.kind + "/" + path + return call(p.addr, method, path, data) +} + +func (p *Plugin) handshake() (*handshakeResp, error) { + // Don't use the local `call` because this shouldn't be namespaced + respBody, err := call(p.addr, "POST", "handshake", nil) + if err != nil { + return nil, err + } + defer respBody.Close() + + var data handshakeResp + return &data, json.NewDecoder(respBody).Decode(&data) +} diff --git a/plugins/repository.go b/plugins/repository.go new file mode 100644 index 0000000000000..7e00e1ab4a86d --- /dev/null +++ b/plugins/repository.go @@ -0,0 +1,61 @@ +package plugins + +import ( + "errors" + "fmt" +) + +// Temporary singleton +var Repo = NewRepository() + +var ErrNotRegistered = errors.New("plugin type is not registered") + +type Repository struct { + plugins map[string]Plugins +} + +type Plugins []*Plugin + +func (repository *Repository) GetPlugins(kind string) (Plugins, error) { + plugins, exists := repository.plugins[kind] + // TODO: check whether 'kind' is a supportedPluginType + if !exists { + // If no plugins have been registered for this kind yet, that's + // OK. Just set and return an empty list. + repository.plugins[kind] = make([]*Plugin, 0) + return repository.plugins[kind], nil + } + return plugins, nil +} + +var supportedPluginTypes = map[string]struct{}{ + "volume": {}, +} + +func NewRepository() *Repository { + return &Repository{ + plugins: make(map[string]Plugins), + } +} + +func (repository *Repository) RegisterPlugin(addr string) error { + plugin := &Plugin{addr: addr} + resp, err := plugin.handshake() + if err != nil { + return fmt.Errorf("error in plugin handshake: %v", err) + } + + for _, interest := range resp.InterestedIn { + if _, exists := supportedPluginTypes[interest]; !exists { + return fmt.Errorf("plugin type %s is not supported", interest) + } + + if _, exists := repository.plugins[interest]; !exists { + repository.plugins[interest] = []*Plugin{} + } + plugin.kind = interest + repository.plugins[interest] = append(repository.plugins[interest], plugin) + } + + return nil +} diff --git a/runconfig/hostconfig.go b/runconfig/hostconfig.go index 84d636b5c4beb..dbc5cdd3d633a 100644 --- a/runconfig/hostconfig.go +++ b/runconfig/hostconfig.go @@ -132,6 +132,7 @@ type HostConfig struct { Ulimits []*ulimit.Ulimit LogConfig LogConfig CgroupParent string // Parent cgroup. + Plugin bool } // This is used by the create command when you want to set both the @@ -184,6 +185,7 @@ func ContainerHostConfigFromJob(job *engine.Job) *HostConfig { PidMode: PidMode(job.Getenv("PidMode")), ReadonlyRootfs: job.GetenvBool("ReadonlyRootfs"), CgroupParent: job.Getenv("CgroupParent"), + Plugin: job.GetenvBool("Plugin"), } // FIXME: This is for backward compatibility, if people use `Cpuset` diff --git a/runconfig/parse.go b/runconfig/parse.go index 1fb36e4ace539..d237bb473572c 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -72,6 +72,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flReadonlyRootfs = cmd.Bool([]string{"-read-only"}, false, "Mount the container's root filesystem as read only") flLoggingDriver = cmd.String([]string{"-log-driver"}, "", "Logging driver for container") flCgroupParent = cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container") + flPlugin = cmd.Bool([]string{"-plugin"}, false, "Enable plugin mode!") ) cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR") @@ -334,6 +335,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe Ulimits: flUlimits.GetList(), LogConfig: LogConfig{Type: *flLoggingDriver}, CgroupParent: *flCgroupParent, + Plugin: *flPlugin, } // When allocating stdin in attached mode, close stdin at client disconnect diff --git a/volumes/repository.go b/volumes/repository.go index 08c5849818295..32efe3e49f59a 100644 --- a/volumes/repository.go +++ b/volumes/repository.go @@ -1,6 +1,7 @@ package volumes import ( + "encoding/json" "fmt" "io/ioutil" "os" @@ -10,8 +11,18 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/daemon/graphdriver" "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/plugins" ) +type VolumeExtensionReq struct { + HostPath string + ContainerID string +} + +type VolumeExtensionResp struct { + ModifiedHostPath string +} + type Repository struct { configPath string driver graphdriver.Driver @@ -179,10 +190,40 @@ func (r *Repository) createNewVolumePath(id string) (string, error) { return path, nil } -func (r *Repository) FindOrCreateVolume(path string, writable bool) (*Volume, error) { +func (r *Repository) FindOrCreateVolume(path, containerId string, writable bool) (*Volume, error) { r.lock.Lock() defer r.lock.Unlock() + plugins, err := plugins.Repo.GetPlugins("volume") + if err != nil { + return nil, err + } + + for _, plugin := range plugins { + data := VolumeExtensionReq{ + HostPath: path, + ContainerID: containerId, + } + + resp, err := plugin.Call("POST", "volumes", data) + if err != nil { + return nil, fmt.Errorf("got error calling volume extension: %v", err) + } + defer resp.Close() + + var extResp VolumeExtensionResp + logrus.Debugf("decoding volume extension response") + if err := json.NewDecoder(resp).Decode(&extResp); err != nil { + return nil, err + } + + // Use the path provided by the extension instead of creating one + if extResp.ModifiedHostPath != "" { + logrus.Debugf("using modified host path for volume extension") + path = extResp.ModifiedHostPath + } + } + if path == "" { return r.newVolume(path, writable) } From fd3256cade2b4fe70e7923d3560cf1a2bd079f96 Mon Sep 17 00:00:00 2001 From: Jeff Lindsay Date: Thu, 9 Apr 2015 22:25:26 -0500 Subject: [PATCH 3/3] dropped DialTimeout in favor of regular DialUnix for better error handling. had to rename plugin socket file to get around UNIX_PATH_MAX kernel setting. kind of a big issue... --- Gunfile | 8 +++++++- daemon/container.go | 12 +++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Gunfile b/Gunfile index 5f8961c162c1a..0d38adbf9b732 100644 --- a/Gunfile +++ b/Gunfile @@ -8,18 +8,24 @@ init() { cmd-export push cmd-export restore cmd-export backup + cmd-export install } build() { make BINDDIR=. } +install() { + cp bundles/1.5.0-plugins/cross/darwin/amd64/docker-1.5.0-plugins \ + ~/Projects/.bin/docker +} + push() { cat bundles/1.5.0-plugins/binary/docker-1.5.0-plugins \ | boot2docker ssh "cat > docker" boot2docker ssh " chmod +x ./docker - mv ./docker /usr/local/bin/docker + sudo mv ./docker /usr/local/bin/docker sudo /etc/init.d/docker restart " } diff --git a/daemon/container.go b/daemon/container.go index 34716e82e1617..90a61214dd17b 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -1476,13 +1476,19 @@ func (container *Container) waitForPluginSock() error { go func() { logrus.Debugf("waiting for plugin socket at: %s", pluginSock) for { - conn, err := net.DialTimeout("unix", pluginSock, 100*time.Millisecond) + addr, err := net.ResolveUnixAddr("unix", pluginSock) + if err != nil { + logrus.Debugf("bad addr: %s", err) + } + conn, err := net.DialUnix("unix", nil, addr) // If the file doesn't exist yet, that's ok, maybe plugin hasn't created it yet if err != nil { select { case <-chStop: return default: + logrus.Debugf("retrying. got: %s", err) + time.Sleep(time.Second) continue } } @@ -1497,7 +1503,7 @@ func (container *Container) waitForPluginSock() error { // We can close this net.Conn since the plugin system will establish it's own connection conn.Close() return plugins.Repo.RegisterPlugin(pluginSock) - case <-time.After(30 * time.Second): + case <-time.After(5 * time.Second): chStop <- struct{}{} return fmt.Errorf("connection to plugin sock timed out") } @@ -1582,5 +1588,5 @@ func (c *Container) LogDriverType() string { } func (container *Container) getPluginSocketPath() (string, error) { - return container.getRootResourcePath(filepath.Join("p", "plugin.sock")) + return container.getRootResourcePath(filepath.Join("p", "p.s")) }