Browse Source

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
Marco Hennings 12 years ago
parent
commit
687d27ab57

+ 63 - 0
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) {
 func TestRunExit(t *testing.T) {
 	stdin, stdinPipe := io.Pipe()
 	stdin, stdinPipe := io.Pipe()
 	stdout, stdoutPipe := io.Pipe()
 	stdout, stdoutPipe := io.Pipe()

+ 23 - 0
container.go

@@ -2,6 +2,7 @@ package docker
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
+	"errors"
 	"flag"
 	"flag"
 	"fmt"
 	"fmt"
 	"github.com/dotcloud/docker/term"
 	"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)
 	Image           string // Name of the image as it was passed by the operator (eg. could be symbolic)
 	Volumes         map[string]struct{}
 	Volumes         map[string]struct{}
 	VolumesFrom     string
 	VolumesFrom     string
+	WorkingDir      string
 	Entrypoint      []string
 	Entrypoint      []string
 	NetworkDisabled bool
 	NetworkDisabled bool
 	Privileged      bool
 	Privileged      bool
@@ -92,6 +94,10 @@ type BindMap struct {
 	Mode    string
 	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) {
 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")
 	cmd := Subcmd("run", "[OPTIONS] IMAGE [COMMAND] [ARG...]", "Run a command in a new container")
 	if len(args) > 0 && args[0] != "--help" {
 	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")
 	flHostname := cmd.String("h", "", "Container host name")
+	flWorkingDir := cmd.String("w", "", "Working directory inside the container")
 	flUser := cmd.String("u", "", "Username or UID")
 	flUser := cmd.String("u", "", "Username or UID")
 	flDetach := cmd.Bool("d", false, "Detached mode: Run container in the background, print new container id")
 	flDetach := cmd.Bool("d", false, "Detached mode: Run container in the background, print new container id")
 	flAttach := NewAttachOpts()
 	flAttach := NewAttachOpts()
@@ -139,6 +146,9 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig,
 	if *flDetach && len(flAttach) > 0 {
 	if *flDetach && len(flAttach) > 0 {
 		return nil, nil, cmd, fmt.Errorf("Conflicting options: -a and -d")
 		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 neither -d or -a are set, attach to everything by default
 	if len(flAttach) == 0 && !*flDetach {
 	if len(flAttach) == 0 && !*flDetach {
 		if !*flDetach {
 		if !*flDetach {
@@ -197,6 +207,7 @@ func ParseRun(args []string, capabilities *Capabilities) (*Config, *HostConfig,
 		VolumesFrom:     *flVolumesFrom,
 		VolumesFrom:     *flVolumesFrom,
 		Entrypoint:      entrypoint,
 		Entrypoint:      entrypoint,
 		Privileged:      *flPrivileged,
 		Privileged:      *flPrivileged,
+		WorkingDir:      *flWorkingDir,
 	}
 	}
 	hostConfig := &HostConfig{
 	hostConfig := &HostConfig{
 		Binds:           binds,
 		Binds:           binds,
@@ -666,6 +677,18 @@ func (container *Container) Start(hostConfig *HostConfig) error {
 		"-e", "container=lxc",
 		"-e", "container=lxc",
 		"-e", "HOSTNAME="+container.Config.Hostname,
 		"-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 {
 	for _, elem := range container.Config.Env {
 		params = append(params, "-e", elem)
 		params = append(params, "-e", elem)

+ 8 - 3
docs/sources/api/docker_remote_api_v1.4.rst

@@ -129,7 +129,9 @@ Create a container
 		"Dns":null,
 		"Dns":null,
 		"Image":"base",
 		"Image":"base",
 		"Volumes":{},
 		"Volumes":{},
-		"VolumesFrom":""
+		"VolumesFrom":"",
+		"WorkingDir":""
+
 	   }
 	   }
 	   
 	   
 	**Example response**:
 	**Example response**:
@@ -195,7 +197,9 @@ Inspect a container
 				"Dns": null,
 				"Dns": null,
 				"Image": "base",
 				"Image": "base",
 				"Volumes": {},
 				"Volumes": {},
-				"VolumesFrom": ""
+				"VolumesFrom": "",
+				"WorkingDir":""
+
 			},
 			},
 			"State": {
 			"State": {
 				"Running": false,
 				"Running": false,
@@ -746,7 +750,8 @@ Inspect an image
 				,"Dns":null,
 				,"Dns":null,
 				"Image":"base",
 				"Image":"base",
 				"Volumes":null,
 				"Volumes":null,
-				"VolumesFrom":""
+				"VolumesFrom":"",
+				"WorkingDir":""
 			},
 			},
 		"Size": 6824592
 		"Size": 6824592
 	   }
 	   }

+ 20 - 0
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.
       -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.
       -volumes-from="": Mount all volumes from the given container.
       -entrypoint="": Overwrite the default entrypoint set by the image.
       -entrypoint="": Overwrite the default entrypoint set by the image.
+      -w="": Working directory inside the container
 
 
 
 
 Examples
 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
 everything that the host can do. This flag exists to allow special
 use-cases, like running Docker within Docker.
 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.
+
+

+ 11 - 0
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
 // Takes care of dropping privileges to the desired user
 func changeUser(u string) {
 func changeUser(u string) {
 	if u == "" {
 	if u == "" {
@@ -83,6 +92,7 @@ func SysInit() {
 	}
 	}
 	var u = flag.String("u", "", "username or uid")
 	var u = flag.String("u", "", "username or uid")
 	var gw = flag.String("g", "", "gateway address")
 	var gw = flag.String("g", "", "gateway address")
+	var workdir = flag.String("w", "", "workdir")
 
 
 	var flEnv ListOpts
 	var flEnv ListOpts
 	flag.Var(&flEnv, "e", "Set environment variables")
 	flag.Var(&flEnv, "e", "Set environment variables")
@@ -91,6 +101,7 @@ func SysInit() {
 
 
 	cleanupEnv(flEnv)
 	cleanupEnv(flEnv)
 	setupNetworking(*gw)
 	setupNetworking(*gw)
+	setupWorkingDirectory(*workdir)
 	changeUser(*u)
 	changeUser(*u)
 	executeProgram(flag.Arg(0), flag.Args())
 	executeProgram(flag.Arg(0), flag.Args())
 }
 }