diff --git a/CHANGELOG.md b/CHANGELOG.md index 72bf381fa8..93c3997608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog -## 0.2.1 (2012-05-01) +## 0.2.2 (2013-05-03) + + Support for data volumes ('docker run -v=PATH') + + Share data volumes between containers ('docker run -volumes-from') + + Improved documentation + * Upgrade to Go 1.0.3 + * Various upgrades to the dev environment for contributors + +## 0.2.1 (2013-05-01) + 'docker commit -run' bundles a layer with default runtime options: command, ports etc. * Improve install process on Vagrant + New Dockerfile operation: "maintainer" @@ -10,7 +17,7 @@ + 'docker -d -r': restart crashed containers at daemon startup * Runtime: improve test coverage -## 0.2.0 (2012-04-23) +## 0.2.0 (2013-04-23) - Runtime: ghost containers can be killed and waited for * Documentation: update install intructions - Packaging: fix Vagrantfile diff --git a/Vagrantfile b/Vagrantfile index 319fbc7530..06e8b47a4c 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,8 +1,8 @@ # -*- mode: ruby -*- # vi: set ft=ruby : -BOX_NAME = "ubuntu" -BOX_URI = "http://files.vagrantup.com/precise64.box" +BOX_NAME = ENV['BOX_NAME'] || "ubuntu" +BOX_URI = ENV['BOX_URI'] || "http://files.vagrantup.com/precise64.box" PPA_KEY = "E61D797F63561DC6" Vagrant::Config.run do |config| @@ -11,7 +11,7 @@ Vagrant::Config.run do |config| config.vm.box_url = BOX_URI # Add docker PPA key to the local repository and install docker pkg_cmd = "apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys #{PPA_KEY}; " - pkg_cmd << "echo 'deb http://ppa.launchpad.net/dotcloud/lxc-docker/ubuntu precise main' >>/etc/apt/sources.list; " + pkg_cmd << "echo 'deb http://ppa.launchpad.net/dotcloud/lxc-docker/ubuntu precise main' >/etc/apt/sources.list.d/lxc-docker.list; " pkg_cmd << "apt-get update -qq; apt-get install -q -y lxc-docker" if ARGV.include?("--provider=aws".downcase) # Add AUFS dependency to amazon's VM diff --git a/api.go b/api.go index decd273e27..738d8d5633 100644 --- a/api.go +++ b/api.go @@ -320,9 +320,16 @@ func ListenAndServe(addr string, srv *Server) error { r.Path("/containers/{name:.*}").Methods("DELETE").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println(r.Method, r.RequestURI) + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } vars := mux.Vars(r) name := vars["name"] - if err := srv.ContainerDestroy(name); err != nil { + var v bool + if r.Form.Get("v") == "1" { + v = true + } + if err := srv.ContainerDestroy(name, v); err != nil { httpError(w, err) } else { w.WriteHeader(http.StatusOK) diff --git a/commands.go b/commands.go index 87c6dcc55e..0305c115af 100644 --- a/commands.go +++ b/commands.go @@ -13,12 +13,13 @@ import ( "net/http/httputil" "net/url" "os" + "path/filepath" "strconv" "text/tabwriter" "time" ) -const VERSION = "0.2.1" +const VERSION = "0.2.2" var ( GIT_COMMIT string @@ -434,7 +435,7 @@ func CmdRmi(args ...string) error { return nil } - for _, name := range args { + for _, name := range cmd.Args() { _, _, err := call("DELETE", "/images/"+name, nil) if err != nil { fmt.Printf("%s", err) @@ -476,7 +477,8 @@ func CmdHistory(args ...string) error { } func CmdRm(args ...string) error { - cmd := Subcmd("rm", "CONTAINER [CONTAINER...]", "Remove a container") + cmd := Subcmd("rm", "[OPTIONS] CONTAINER [CONTAINER...]", "Remove a container") + v := cmd.Bool("v", false, "Remove the volumes associated to the container") if err := cmd.Parse(args); err != nil { return nil } @@ -484,9 +486,12 @@ func CmdRm(args ...string) error { cmd.Usage() return nil } - - for _, name := range args { - _, _, err := call("DELETE", "/containers/"+name, nil) + val := url.Values{} + if *v { + val.Set("v", "1") + } + for _, name := range cmd.Args() { + _, _, err := call("DELETE", "/containers/"+name+"?"+val.Encode(), nil) if err != nil { fmt.Printf("%s", err) } else { @@ -930,6 +935,25 @@ func (opts AttachOpts) Get(val string) bool { return false } +// PathOpts stores a unique set of absolute paths +type PathOpts map[string]struct{} + +func NewPathOpts() PathOpts { + return make(PathOpts) +} + +func (opts PathOpts) String() string { + return fmt.Sprintf("%v", map[string]struct{}(opts)) +} + +func (opts PathOpts) Set(val string) error { + if !filepath.IsAbs(val) { + return fmt.Errorf("%s is not an absolute path", val) + } + opts[filepath.Clean(val)] = struct{}{} + return nil +} + func CmdTag(args ...string) error { cmd := Subcmd("tag", "[OPTIONS] IMAGE REPOSITORY [TAG]", "Tag an image into a repository") force := cmd.Bool("f", false, "Force") diff --git a/container.go b/container.go index aa65737f16..a852020450 100644 --- a/container.go +++ b/container.go @@ -48,6 +48,7 @@ type Container struct { runtime *Runtime waitLock chan struct{} + Volumes map[string]string } type Config struct { @@ -66,6 +67,8 @@ type Config struct { Cmd []string Dns []string Image string // Name of the image as it was passed by the operator (eg. could be symbolic) + Volumes map[string]struct{} + VolumesFrom string } func ParseRun(args []string) (*Config, *flag.FlagSet, error) { @@ -92,6 +95,11 @@ func ParseRun(args []string) (*Config, *flag.FlagSet, error) { var flDns ListOpts cmd.Var(&flDns, "dns", "Set custom dns servers") + flVolumes := NewPathOpts() + cmd.Var(flVolumes, "v", "Attach a data volume") + + flVolumesFrom := cmd.String("volumes-from", "", "Mount volumes from the specified container") + if err := cmd.Parse(args); err != nil { return nil, cmd, err } @@ -131,6 +139,8 @@ func ParseRun(args []string) (*Config, *flag.FlagSet, error) { Cmd: runCmd, Dns: flDns, Image: image, + Volumes: flVolumes, + VolumesFrom: *flVolumesFrom, } // When allocating stdin in attached mode, close stdin at client disconnect @@ -384,10 +394,40 @@ func (container *Container) Start() error { log.Printf("WARNING: Your kernel does not support swap limit capabilities. Limitation discarded.\n") container.Config.MemorySwap = -1 } + container.Volumes = make(map[string]string) + + // Create the requested volumes volumes + for volPath := range container.Config.Volumes { + if c, err := container.runtime.volumes.Create(nil, container, "", "", nil); err != nil { + return err + } else { + if err := os.MkdirAll(path.Join(container.RootfsPath(), volPath), 0755); err != nil { + return nil + } + container.Volumes[volPath] = c.Id + } + } + + if container.Config.VolumesFrom != "" { + c := container.runtime.Get(container.Config.VolumesFrom) + if c == nil { + return fmt.Errorf("Container %s not found. Impossible to mount its volumes", container.Id) + } + for volPath, id := range c.Volumes { + if _, exists := container.Volumes[volPath]; exists { + return fmt.Errorf("The requested volume %s overlap one of the volume of the container %s", volPath, c.Id) + } + if err := os.MkdirAll(path.Join(container.RootfsPath(), volPath), 0755); err != nil { + return nil + } + container.Volumes[volPath] = id + } + } if err := container.generateLXCConfig(); err != nil { return err } + params := []string{ "-n", container.Id, "-f", container.lxcConfigPath(), @@ -446,6 +486,7 @@ func (container *Container) Start() error { // Init the lock container.waitLock = make(chan struct{}) + container.ToDisk() go container.monitor() return nil @@ -777,6 +818,22 @@ func (container *Container) RootfsPath() string { return path.Join(container.root, "rootfs") } +func (container *Container) GetVolumes() (map[string]string, error) { + ret := make(map[string]string) + for volPath, id := range container.Volumes { + volume, err := container.runtime.volumes.Get(id) + if err != nil { + return nil, err + } + root, err := volume.root() + if err != nil { + return nil, err + } + ret[volPath] = path.Join(root, "layer") + } + return ret, nil +} + func (container *Container) rwPath() string { return path.Join(container.root, "rw") } diff --git a/docs/sources/commandline/command/run.rst b/docs/sources/commandline/command/run.rst index c2096b3bd9..d5e571b41b 100644 --- a/docs/sources/commandline/command/run.rst +++ b/docs/sources/commandline/command/run.rst @@ -17,3 +17,6 @@ -p=[]: Map a network port to the container -t=false: Allocate a pseudo-tty -u="": Username or UID + -d=[]: Set custom dns servers for the container + -v=[]: Creates a new volumes and mount it at the specified path. + -volumes-from="": Mount all volumes from the given container. diff --git a/docs/sources/concepts/buildingblocks.rst b/docs/sources/concepts/buildingblocks.rst index d422e6eef3..154ef00f45 100644 --- a/docs/sources/concepts/buildingblocks.rst +++ b/docs/sources/concepts/buildingblocks.rst @@ -12,7 +12,7 @@ Images ------ An original container image. These are stored on disk and are comparable with what you normally expect from a stopped virtual machine image. Images are stored (and retrieved from) repository -Images are stored on your local file system under /var/lib/docker/images +Images are stored on your local file system under /var/lib/docker/graph .. _containers: diff --git a/docs/sources/examples/couchdb_data_volumes.rst b/docs/sources/examples/couchdb_data_volumes.rst new file mode 100644 index 0000000000..5b50c73e38 --- /dev/null +++ b/docs/sources/examples/couchdb_data_volumes.rst @@ -0,0 +1,53 @@ +:title: Sharing data between 2 couchdb databases +:description: Sharing data between 2 couchdb databases +:keywords: docker, example, package installation, networking, couchdb, data volumes + +.. _running_redis_service: + +Create a redis service +====================== + +.. include:: example_header.inc + +Here's an example of using data volumes to share the same data between 2 couchdb containers. +This could be used for hot upgrades, testing different versions of couchdb on the same data, etc. + +Create first database +--------------------- + +Note that we're marking /var/lib/couchdb as a data volume. + +.. code-block:: bash + + COUCH1=$(docker run -d -v /var/lib/couchdb shykes/couchdb:2013-05-03) + +Add data to the first database +------------------------------ + +We're assuming your docker host is reachable at `localhost`. If not, replace `localhost` with the public IP of your docker host. + +.. code-block:: bash + + HOST=localhost + URL="http://$HOST:$(docker port $COUCH1 5984)/_utils/" + echo "Navigate to $URL in your browser, and use the couch interface to add data" + +Create second database +---------------------- + +This time, we're requesting shared access to $COUCH1's volumes. + +.. code-block:: bash + + COUCH2=$(docker run -d -volumes-from $COUCH1) shykes/couchdb:2013-05-03) + +Browse data on the second database +---------------------------------- + +.. code-block:: bash + + HOST=localhost + URL="http://$HOST:$(docker port $COUCH2 5984)/_utils/" + echo "Navigate to $URL in your browser. You should see the same data as in the first database!" + +Congratulations, you are running 2 Couchdb containers, completely isolated from each other *except* for their data. diff --git a/docs/sources/examples/index.rst b/docs/sources/examples/index.rst index 6a616ec8ff..7eb2ecbe94 100644 --- a/docs/sources/examples/index.rst +++ b/docs/sources/examples/index.rst @@ -18,3 +18,4 @@ Contents: python_web_app running_redis_service running_ssh_service + couchdb_data_volumes diff --git a/lxc_template.go b/lxc_template.go index 5ac62f52af..e2be3f21cd 100644 --- a/lxc_template.go +++ b/lxc_template.go @@ -79,7 +79,11 @@ lxc.mount.entry = {{.SysInitPath}} {{$ROOTFS}}/sbin/init none bind,ro 0 0 # In order to get a working DNS environment, mount bind (ro) the host's /etc/resolv.conf into the container lxc.mount.entry = {{.ResolvConfPath}} {{$ROOTFS}}/etc/resolv.conf none bind,ro 0 0 - +{{if .Volumes}} +{{range $virtualPath, $realPath := .GetVolumes}} +lxc.mount.entry = {{$realPath}} {{$ROOTFS}}/{{$virtualPath}} none bind,rw 0 0 +{{end}} +{{end}} # drop linux capabilities (apply mainly to the user root in the container) lxc.cap.drop = audit_control audit_write mac_admin mac_override mknod setfcap setpcap sys_admin sys_boot sys_module sys_nice sys_pacct sys_rawio sys_resource sys_time sys_tty_config diff --git a/packaging/ubuntu/changelog b/packaging/ubuntu/changelog index 88f6c5021e..49eabfbb32 100644 --- a/packaging/ubuntu/changelog +++ b/packaging/ubuntu/changelog @@ -1,3 +1,12 @@ +lxc-docker (0.2.2-1) precise; urgency=low + - Support for data volumes ('docker run -v=PATH') + - Share data volumes between containers ('docker run -volumes-from') + - Improved documentation + - Upgrade to Go 1.0.3 + - Various upgrades to the dev environment for contributors + + -- dotCloud Fri, 3 May 2013 00:00:00 -0700 + lxc-docker (0.2.1-1) precise; urgency=low diff --git a/runtime.go b/runtime.go index 6e03226b36..79a7170c7d 100644 --- a/runtime.go +++ b/runtime.go @@ -32,6 +32,7 @@ type Runtime struct { capabilities *Capabilities kernelVersion *KernelVersionInfo autoRestart bool + volumes *Graph } var sysInitPath string @@ -79,10 +80,10 @@ func (runtime *Runtime) containerRoot(id string) string { } func (runtime *Runtime) mergeConfig(userConf, imageConf *Config) { - if userConf.Hostname != "" { + if userConf.Hostname == "" { userConf.Hostname = imageConf.Hostname } - if userConf.User != "" { + if userConf.User == "" { userConf.User = imageConf.User } if userConf.Memory == 0 { @@ -126,7 +127,7 @@ func (runtime *Runtime) Create(config *Config) (*Container, error) { runtime.mergeConfig(config, img.Config) } - if config.Cmd == nil { + if config.Cmd == nil || len(config.Cmd) == 0 { return nil, fmt.Errorf("No command specified") } @@ -405,6 +406,10 @@ func NewRuntimeFromDirectory(root string, autoRestart bool) (*Runtime, error) { if err != nil { return nil, err } + volumes, err := NewGraph(path.Join(root, "volumes")) + if err != nil { + return nil, err + } repositories, err := NewTagStore(path.Join(root, "repositories"), g) if err != nil { return nil, fmt.Errorf("Couldn't create Tag store: %s", err) @@ -432,6 +437,7 @@ func NewRuntimeFromDirectory(root string, autoRestart bool) (*Runtime, error) { idIndex: NewTruncIndex(), capabilities: &Capabilities{}, autoRestart: autoRestart, + volumes: volumes, } if err := runtime.restore(); err != nil { diff --git a/server.go b/server.go index ddf1f849e7..4d6feb3f71 100644 --- a/server.go +++ b/server.go @@ -294,11 +294,38 @@ func (srv *Server) ContainerRestart(name string, t int) error { return nil } -func (srv *Server) ContainerDestroy(name string) error { +func (srv *Server) ContainerDestroy(name string, v bool) error { + if container := srv.runtime.Get(name); container != nil { + volumes := make(map[string]struct{}) + // Store all the deleted containers volumes + for _, volumeId := range container.Volumes { + volumes[volumeId] = struct{}{} + } if err := srv.runtime.Destroy(container); err != nil { return fmt.Errorf("Error destroying container %s: %s", name, err.Error()) } + + if v { + // Retrieve all volumes from all remaining containers + usedVolumes := make(map[string]*Container) + for _, container := range srv.runtime.List() { + for _, containerVolumeId := range container.Volumes { + usedVolumes[containerVolumeId] = container + } + } + + for volumeId := range volumes { + // If the requested volu + if c, exists := usedVolumes[volumeId]; exists { + log.Printf("The volume %s is used by the container %s. Impossible to remove it. Skipping.\n", volumeId, c.Id) + continue + } + if err := srv.runtime.volumes.Delete(volumeId); err != nil { + return err + } + } + } } else { return fmt.Errorf("No such container: %s", name) } diff --git a/utils.go b/utils.go index 364bd73251..40f67f7193 100644 --- a/utils.go +++ b/utils.go @@ -466,7 +466,7 @@ func FindCgroupMountpoint(cgroupType string) (string, error) { // cgroup /sys/fs/cgroup/devices cgroup rw,relatime,devices 0 0 for _, line := range strings.Split(string(output), "\n") { parts := strings.Split(line, " ") - if parts[2] == "cgroup" { + if len(parts) == 6 && parts[2] == "cgroup" { for _, opt := range strings.Split(parts[3], ",") { if opt == cgroupType { return parts[1], nil