From 687d27ab575778196ea646b6d3fa61b6c8e512b0 Mon Sep 17 00:00:00 2001 From: Marco Hennings Date: Thu, 15 Aug 2013 20:38:17 +0200 Subject: [PATCH] Add an option to set the working directory. This makes it possible to simply wrap a command inside a container. This makes it easier to use a container as an unified build environment. Examples: ~/workspace/docker $ docker run -v `pwd`:`pwd` -w `pwd` -i -t ubuntu ls AUTHORS Makefile archive.go changes.go docker [...] docker run -v `pwd`:`pwd` -w `pwd` -i -t ubuntu pwd /home/marco/workspace/docker --- commands_test.go | 63 +++++++++++++++++++++ container.go | 23 ++++++++ docs/sources/api/docker_remote_api_v1.4.rst | 11 +++- docs/sources/commandline/command/run.rst | 20 +++++++ sysinit.go | 11 ++++ 5 files changed, 125 insertions(+), 3 deletions(-) diff --git a/commands_test.go b/commands_test.go index ac30cc73f9..db344d7043 100644 --- a/commands_test.go +++ b/commands_test.go @@ -90,6 +90,69 @@ func TestRunHostname(t *testing.T) { } +// TestRunWorkdir checks that 'docker run -w' correctly sets a custom working directory +func TestRunWorkdir(t *testing.T) { + stdout, stdoutPipe := io.Pipe() + + cli := NewDockerCli(nil, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr) + defer cleanup(globalRuntime) + + c := make(chan struct{}) + go func() { + defer close(c) + if err := cli.CmdRun("-w", "/foo/bar", unitTestImageID, "pwd"); err != nil { + t.Fatal(err) + } + }() + + setTimeout(t, "Reading command output time out", 2*time.Second, func() { + cmdOutput, err := bufio.NewReader(stdout).ReadString('\n') + if err != nil { + t.Fatal(err) + } + if cmdOutput != "/foo/bar\n" { + t.Fatalf("'pwd' should display '%s', not '%s'", "/foo/bar\n", cmdOutput) + } + }) + + setTimeout(t, "CmdRun timed out", 5*time.Second, func() { + <-c + }) + +} + +// TestRunWorkdirExists checks that 'docker run -w' correctly sets a custom working directory, even if it exists +func TestRunWorkdirExists(t *testing.T) { + stdout, stdoutPipe := io.Pipe() + + cli := NewDockerCli(nil, stdoutPipe, ioutil.Discard, testDaemonProto, testDaemonAddr) + defer cleanup(globalRuntime) + + c := make(chan struct{}) + go func() { + defer close(c) + if err := cli.CmdRun("-w", "/proc", unitTestImageID, "pwd"); err != nil { + t.Fatal(err) + } + }() + + setTimeout(t, "Reading command output time out", 2*time.Second, func() { + cmdOutput, err := bufio.NewReader(stdout).ReadString('\n') + if err != nil { + t.Fatal(err) + } + if cmdOutput != "/proc\n" { + t.Fatalf("'pwd' should display '%s', not '%s'", "/proc\n", cmdOutput) + } + }) + + setTimeout(t, "CmdRun timed out", 5*time.Second, func() { + <-c + }) + +} + + func TestRunExit(t *testing.T) { stdin, stdinPipe := io.Pipe() stdout, stdoutPipe := io.Pipe() diff --git a/container.go b/container.go index 4d38d49fab..472ae3990d 100644 --- a/container.go +++ b/container.go @@ -2,6 +2,7 @@ package docker import ( "encoding/json" + "errors" "flag" "fmt" "github.com/dotcloud/docker/term" @@ -76,6 +77,7 @@ type Config struct { Image string // Name of the image as it was passed by the operator (eg. could be symbolic) Volumes map[string]struct{} VolumesFrom string + WorkingDir string Entrypoint []string NetworkDisabled bool Privileged bool @@ -92,6 +94,10 @@ type BindMap struct { Mode string } +var ( + ErrInvaidWorikingDirectory = errors.New("The working directory is invalid. It needs to be an absolute path.") +) + func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, *flag.FlagSet, error) { cmd := Subcmd("run", "[OPTIONS] IMAGE [COMMAND] [ARG...]", "Run a command in a new container") if len(args) > 0 && args[0] != "--help" { @@ -100,6 +106,7 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, } flHostname := cmd.String("h", "", "Container host name") + flWorkingDir := cmd.String("w", "", "Working directory inside the container") flUser := cmd.String("u", "", "Username or UID") flDetach := cmd.Bool("d", false, "Detached mode: Run container in the background, print new container id") flAttach := NewAttachOpts() @@ -139,6 +146,9 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, if *flDetach && len(flAttach) > 0 { return nil, nil, cmd, fmt.Errorf("Conflicting options: -a and -d") } + if *flWorkingDir != "" && !path.IsAbs(*flWorkingDir) { + return nil, nil, cmd, ErrInvaidWorikingDirectory + } // If neither -d or -a are set, attach to everything by default if len(flAttach) == 0 && !*flDetach { if !*flDetach { @@ -197,6 +207,7 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig, VolumesFrom: *flVolumesFrom, Entrypoint: entrypoint, Privileged: *flPrivileged, + WorkingDir: *flWorkingDir, } hostConfig := &HostConfig{ Binds: binds, @@ -666,6 +677,18 @@ func (container *Container) Start(hostConfig *HostConfig) error { "-e", "container=lxc", "-e", "HOSTNAME="+container.Config.Hostname, ) + if container.Config.WorkingDir != "" { + workingDir := path.Clean(container.Config.WorkingDir) + utils.Debugf("[working dir] working dir is %s", workingDir) + + if err := os.MkdirAll(path.Join(container.RootfsPath(), workingDir), 0755); err != nil { + return nil + } + + params = append(params, + "-w", workingDir, + ) + } for _, elem := range container.Config.Env { params = append(params, "-e", elem) diff --git a/docs/sources/api/docker_remote_api_v1.4.rst b/docs/sources/api/docker_remote_api_v1.4.rst index adb0c1decb..14dbfd7c3f 100644 --- a/docs/sources/api/docker_remote_api_v1.4.rst +++ b/docs/sources/api/docker_remote_api_v1.4.rst @@ -129,7 +129,9 @@ Create a container "Dns":null, "Image":"base", "Volumes":{}, - "VolumesFrom":"" + "VolumesFrom":"", + "WorkingDir":"" + } **Example response**: @@ -195,7 +197,9 @@ Inspect a container "Dns": null, "Image": "base", "Volumes": {}, - "VolumesFrom": "" + "VolumesFrom": "", + "WorkingDir":"" + }, "State": { "Running": false, @@ -746,7 +750,8 @@ Inspect an image ,"Dns":null, "Image":"base", "Volumes":null, - "VolumesFrom":"" + "VolumesFrom":"", + "WorkingDir":"" }, "Size": 6824592 } diff --git a/docs/sources/commandline/command/run.rst b/docs/sources/commandline/command/run.rst index 46b206a20b..bd78ea473f 100644 --- a/docs/sources/commandline/command/run.rst +++ b/docs/sources/commandline/command/run.rst @@ -29,6 +29,7 @@ -v=[]: Create a bind mount with: [host-dir]:[container-dir]:[rw|ro]. If "host-dir" is missing, then docker creates a new volume. -volumes-from="": Mount all volumes from the given container. -entrypoint="": Overwrite the default entrypoint set by the image. + -w="": Working directory inside the container Examples @@ -62,3 +63,22 @@ cgroup controller. In other words, the container can then do almost everything that the host can do. This flag exists to allow special use-cases, like running Docker within Docker. +.. code-block:: bash + + docker run -w /path/to/dir/ -i -t ubuntu pwd + +The ``-w`` lets the command beeing executed inside directory given, +here /path/to/dir/. If the path does not exists it is created inside the +container. + +.. code-block:: bash + + docker run -v `pwd`:`pwd` -w `pwd` -i -t ubuntu pwd + +The ``-v`` flag mounts the current working directory into the container. +The ``-w`` lets the command beeing executed inside the current +working directory, by changeing into the directory to the value +returned by ``pwd``. So this combination executes the command +using the container, but inside the current working directory. + + diff --git a/sysinit.go b/sysinit.go index fb36cd2543..aa5d2b2a17 100644 --- a/sysinit.go +++ b/sysinit.go @@ -22,6 +22,15 @@ func setupNetworking(gw string) { } } +// Setup working directory +func setupWorkingDirectory(workdir string) { + if workdir == "" { + return + } + syscall.Chdir(workdir) +} + + // Takes care of dropping privileges to the desired user func changeUser(u string) { if u == "" { @@ -83,6 +92,7 @@ func SysInit() { } var u = flag.String("u", "", "username or uid") var gw = flag.String("g", "", "gateway address") + var workdir = flag.String("w", "", "workdir") var flEnv ListOpts flag.Var(&flEnv, "e", "Set environment variables") @@ -91,6 +101,7 @@ func SysInit() { cleanupEnv(flEnv) setupNetworking(*gw) + setupWorkingDirectory(*workdir) changeUser(*u) executeProgram(flag.Arg(0), flag.Args()) }