diff --git a/plugin/docker/container.go b/plugin/docker/container.go index 4992afd1e..351afbe45 100644 --- a/plugin/docker/container.go +++ b/plugin/docker/container.go @@ -105,7 +105,7 @@ func (c *container) Exec(ctx context.Context, cmd string, args []string, opts pl command := append([]string{cmd}, args...) activity.Record(ctx, "Exec %v on %v", command, c.Name()) - cfg := types.ExecConfig{Cmd: command, AttachStdout: true, AttachStderr: true, Tty: opts.Tty} + cfg := types.ExecConfig{Cmd: command, AttachStdout: true, AttachStderr: true, Tty: opts.Tty, WorkingDir: opts.WorkingDir} if opts.Stdin != nil || opts.Tty { cfg.AttachStdin = true } diff --git a/plugin/external/pluginEntry_test.go b/plugin/external/pluginEntry_test.go index 0828aff48..3089a4bf6 100644 --- a/plugin/external/pluginEntry_test.go +++ b/plugin/external/pluginEntry_test.go @@ -954,7 +954,7 @@ func (suite *ExternalPluginEntryTestSuite) TestExec() { ctx := context.Background() mockRunAndWait := func(cmd []string, startErr, waitErr error, exitCode int) { mockInv := &mockedInvocation{Command: NewCommand(ctx, "")} - args := []interface{}{ctx, "exec", entry, append([]string{`{"tty":false,"elevate":false,"stdin":false}`}, cmd...)} + args := []interface{}{ctx, "exec", entry, append([]string{`{"tty":false,"elevate":false,"WorkingDir":"","stdin":false}`}, cmd...)} mockScript.On("NewInvocation", args...).Return(mockInv).Once() mockInv.MockExec(startErr, waitErr, exitCode) } diff --git a/plugin/types.go b/plugin/types.go index 859fd51a4..b698b9611 100644 --- a/plugin/types.go +++ b/plugin/types.go @@ -141,6 +141,9 @@ type ExecOptions struct { // Elevate execution to run as a privileged user if not already running as a privileged user. Elevate bool `json:"elevate"` + + // WorkingDir is the directory in which to execute the command. + WorkingDir string } // ExecPacketType identifies the packet type. diff --git a/volume/core.go b/volume/core.go index 4659e1c52..d9cc697a5 100644 --- a/volume/core.go +++ b/volume/core.go @@ -53,7 +53,7 @@ type dirMap struct { // ChildSchemas returns a volume's child schema func ChildSchemas() []*plugin.EntrySchema { return []*plugin.EntrySchema{ - (&dir{}).Schema(), + (&execableDir{}).Schema(), (&file{}).Schema(), } } @@ -66,6 +66,9 @@ const RootPath = "" // Requests are cached against the supplied Interface using the VolumeListCB op. func List(ctx context.Context, impl Interface) ([]plugin.Entry, error) { // Start with the implementation as the cache key so we re-use data we get from it for subdirectory queries. + if exImpl, ok := impl.(execableInterface); ok { + return newExecDir("dummy", plugin.EntryAttributes{}, exImpl, RootPath).List(ctx) + } return newDir("dummy", plugin.EntryAttributes{}, impl, RootPath).List(ctx) } diff --git a/volume/execable_dir.go b/volume/execable_dir.go new file mode 100644 index 000000000..90e0e5676 --- /dev/null +++ b/volume/execable_dir.go @@ -0,0 +1,63 @@ +package volume + +import ( + "context" + "fmt" + + "github.com/puppetlabs/wash/plugin" +) + +type execableInterface interface { + Interface + VolumeExec(ctx context.Context, path string, cmd string, args []string, opts plugin.ExecOptions) (plugin.ExecCommand, error) +} + +// execableDir adds the exec action to dir. +type execableDir struct { + dir + impl execableInterface +} + +func newExecDir(name string, attr plugin.EntryAttributes, impl execableInterface, path string) *execableDir { + e := new(execableDir) + e.dir = *newDir(name, attr, impl, path) + e.impl = impl + return e +} + +func (v *execableDir) Exec(ctx context.Context, cmd string, args []string, opts plugin.ExecOptions) (plugin.ExecCommand, error) { + return v.impl.VolumeExec(ctx, v.path, cmd, args, opts) +} + +func (v *execableDir) generateChildren(dirmap *dirMap) []plugin.Entry { + entries := v.dir.generateChildren(dirmap) + + for i, entry := range entries { + dir, ok := entry.(*dir) + if ok { + fmt.Println(dir) + entries[i] = &execableDir{dir: *dir, impl: v.impl} + } + } + return entries +} + +// List lists the children of the directory. +func (v *execableDir) List(ctx context.Context) ([]plugin.Entry, error) { + if v.dirmap != nil { + // Children have been pre-populated by a source parent. + return v.generateChildren(v.dirmap), nil + } + + // Generate child hierarchy. Don't store it on this entry, but populate new dirs from it. + dirmap, err := v.impl.VolumeList(ctx, v.path) + if err != nil { + return nil, err + } + + return v.generateChildren(&dirMap{mp: dirmap}), nil +} + +func (v *execableDir) Schema() *plugin.EntrySchema { + return plugin.NewEntrySchema(v, "execDir").SetDescription(dirDescription) +} diff --git a/volume/fs.go b/volume/fs.go index 16064586c..555139222 100644 --- a/volume/fs.go +++ b/volume/fs.go @@ -243,6 +243,12 @@ func (d *FS) loginShell() plugin.Shell { return plugin.POSIXShell } +// VolumeExec executes cmd in the directory at path. +func (d *FS) VolumeExec(ctx context.Context, path string, cmd string, args []string, opts plugin.ExecOptions) (plugin.ExecCommand, error) { + opts.WorkingDir = path + return d.executor.Exec(ctx, cmd, args, opts) +} + const fsDescription = ` This represents the root directory of a container/VM. It lets you navigate and interact with that container/VM's filesystem as if you were logged into