diff --git a/README.md b/README.md index 0f0f47fb01..3979c1e8d4 100644 --- a/README.md +++ b/README.md @@ -78,3 +78,85 @@ Client installation 4. Download the latest version of the [docker binaries](https://dl.dropbox.com/u/20637798/docker.tar.gz) (`wget https://dl.dropbox.com/u/20637798/docker.tar.gz`) 5. Extract the contents of the tar file `tar -xf docker.tar.gz` 6. You can now use the docker client binary `./docker`. Consider adding it to your `PATH` for simplicity. + +Vagrant Usage +------------- + +1. Install Vagrant from http://vagrantup.com +2. Run `vagrant up`. This will take a few minutes as it does the following: + - Download Quantal64 base box + - Kick off Puppet to do: + - Download & untar most recent docker binary tarball to vagrant homedir. + - Debootstrap to /var/lib/docker/images/ubuntu. + - Install & run dockerd as service. + - Put docker in /usr/local/bin. + - Put latest Go toolchain in /usr/local/go. + +Sample run output: + +```bash +$ vagrant up +[default] Importing base box 'quantal64'... +[default] Matching MAC address for NAT networking... +[default] Clearing any previously set forwarded ports... +[default] Forwarding ports... +[default] -- 22 => 2222 (adapter 1) +[default] Creating shared folders metadata... +[default] Clearing any previously set network interfaces... +[default] Booting VM... +[default] Waiting for VM to boot. This can take a few minutes. +[default] VM booted and ready for use! +[default] Mounting shared folders... +[default] -- v-root: /vagrant +[default] -- manifests: /tmp/vagrant-puppet/manifests +[default] -- v-pp-m0: /tmp/vagrant-puppet/modules-0 +[default] Running provisioner: Vagrant::Provisioners::Puppet... +[default] Running Puppet with /tmp/vagrant-puppet/manifests/quantal64.pp... +stdin: is not a tty +notice: /Stage[main]//Node[default]/Exec[apt_update]/returns: executed successfully + +notice: /Stage[main]/Docker/Exec[fetch-docker]/returns: executed successfully +notice: /Stage[main]/Docker/Package[lxc]/ensure: ensure changed 'purged' to 'present' +notice: /Stage[main]/Docker/Exec[fetch-go]/returns: executed successfully + +notice: /Stage[main]/Docker/Exec[copy-docker-bin]/returns: executed successfully +notice: /Stage[main]/Docker/Exec[debootstrap]/returns: executed successfully +notice: /Stage[main]/Docker/File[/etc/init/dockerd.conf]/ensure: defined content as '{md5}78a593d38dd9919af14d8f0545ac95e9' + +notice: /Stage[main]/Docker/Service[dockerd]/ensure: ensure changed 'stopped' to 'running' + +notice: Finished catalog run in 329.74 seconds +``` + +When this has successfully completed, you should be able to get into your new system with `vagrant ssh` and use `docker`: + +```bash +$ vagrant ssh +Welcome to Ubuntu 12.10 (GNU/Linux 3.5.0-17-generic x86_64) + + * Documentation: https://help.ubuntu.com/ + +Last login: Sun Feb 3 19:37:37 2013 +vagrant@vagrant-ubuntu-12:~$ DOCKER=localhost:4242 docker help +Usage: docker COMMAND [arg...] + +A self-sufficient runtime for linux containers. + +Commands: + run Run a command in a container + ps Display a list of containers + pull Download a tarball and create a container from it + put Upload a tarball and create a container from it + rm Remove containers + wait Wait for the state of a container to change + stop Stop a running container + logs Fetch the logs of a container + diff Inspect changes on a container's filesystem + commit Save the state of a container + attach Attach to the standard inputs and outputs of a running container + info Display system-wide information + tar Stream the contents of a container as a tar archive + web Generate a web UI + attach Attach to a running container +``` + diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000000..dcc3e50bdb --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,100 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant::Config.run do |config| + # All Vagrant configuration is done here. The most common configuration + # options are documented and commented below. For a complete reference, + # please see the online documentation at vagrantup.com. + + # Every Vagrant virtual environment requires a box to build off of. + config.vm.box = "quantal64" + + # The url from where the 'config.vm.box' box will be fetched if it + # doesn't already exist on the user's system. + config.vm.box_url = "http://unworkable.org/~niallo/quantal64.box" + + # Boot with a GUI so you can see the screen. (Default is headless) + # config.vm.boot_mode = :gui + + # Assign this VM to a host-only network IP, allowing you to access it + # via the IP. Host-only networks can talk to the host machine as well as + # any other machines on the same network, but cannot be accessed (through this + # network interface) by any external networks. + # config.vm.network :hostonly, "192.168.33.10" + + # Assign this VM to a bridged network, allowing you to connect directly to a + # network using the host's network device. This makes the VM appear as another + # physical device on your network. + # config.vm.network :bridged + + # Forward a port from the guest to the host, which allows for outside + # computers to access the VM, whereas host only networking does not. + # config.vm.forward_port 80, 8080 + + # Share an additional folder to the guest VM. The first argument is + # an identifier, the second is the path on the guest to mount the + # folder, and the third is the path on the host to the actual folder. + # config.vm.share_folder "v-data", "/vagrant_data", "../data" + + # Enable provisioning with Puppet stand alone. Puppet manifests + # are contained in a directory path relative to this Vagrantfile. + # You will need to create the manifests directory and a manifest in + # the file quantal64.pp in the manifests_path directory. + # + # An example Puppet manifest to provision the message of the day: + # + # # group { "puppet": + # # ensure => "present", + # # } + # # + # # File { owner => 0, group => 0, mode => 0644 } + # # + # # file { '/etc/motd': + # # content => "Welcome to your Vagrant-built virtual machine! + # # Managed by Puppet.\n" + # # } + # + config.vm.provision :puppet do |puppet| + puppet.manifests_path = "puppet/manifests" + puppet.manifest_file = "quantal64.pp" + puppet.module_path = "puppet/modules" + end + + # Enable provisioning with chef solo, specifying a cookbooks path, roles + # path, and data_bags path (all relative to this Vagrantfile), and adding + # some recipes and/or roles. + # + # config.vm.provision :chef_solo do |chef| + # chef.cookbooks_path = "../my-recipes/cookbooks" + # chef.roles_path = "../my-recipes/roles" + # chef.data_bags_path = "../my-recipes/data_bags" + # chef.add_recipe "mysql" + # chef.add_role "web" + # + # # You may also specify custom JSON attributes: + # chef.json = { :mysql_password => "foo" } + # end + + # Enable provisioning with chef server, specifying the chef server URL, + # and the path to the validation key (relative to this Vagrantfile). + # + # The Opscode Platform uses HTTPS. Substitute your organization for + # ORGNAME in the URL and validation key. + # + # If you have your own Chef Server, use the appropriate URL, which may be + # HTTP instead of HTTPS depending on your configuration. Also change the + # validation key to validation.pem. + # + # config.vm.provision :chef_client do |chef| + # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME" + # chef.validation_key_path = "ORGNAME-validator.pem" + # end + # + # If you're using the Opscode platform, your validator client is + # ORGNAME-validator, replacing ORGNAME with your organization name. + # + # IF you have your own Chef Server, the default validation client name is + # chef-validator, unless you changed the configuration. + # + # chef.validation_client_name = "ORGNAME-validator" +end diff --git a/docker/docker.go b/docker/docker.go index e919ec14c7..973c2623ea 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -4,11 +4,15 @@ import ( "github.com/dotcloud/docker/rcli" "github.com/dotcloud/docker/future" "io" + "io/ioutil" "log" "os" + "os/exec" "syscall" "unsafe" - "fmt" + "path" + "path/filepath" + "flag" ) @@ -160,11 +164,24 @@ func Fatal(err error) { func main() { - var err error - if os.Getenv("DOCKER") == "" { - fmt.Printf("Can't connect. Please set environment variable DOCKER to ip:port, eg. 'localhost:4242'.\n") - os.Exit(1) + if cmd := path.Base(os.Args[0]); cmd == "docker" { + fl_shell := flag.Bool("i", false, "Interactive mode") + flag.Parse() + if *fl_shell { + if err := InteractiveMode(flag.Args()...); err != nil { + log.Fatal(err) + } + } else { + SimpleMode(os.Args[1:]) + } + } else { + SimpleMode(append([]string{cmd}, os.Args[1:]...)) } +} + +// Run docker in "simple mode": run a single command and return. +func SimpleMode(args []string) { + var err error if IsTerminal(0) && os.Getenv("NORAW") == "" { oldState, err = MakeRaw(0) if err != nil { @@ -172,7 +189,11 @@ func main() { } defer Restore(0, oldState) } - conn, err := rcli.CallTCP(os.Getenv("DOCKER"), os.Args[1:]...) + // FIXME: we want to use unix sockets here, but net.UnixConn doesn't expose + // CloseWrite(), which we need to cleanly signal that stdin is closed without + // closing the connection. + // See http://code.google.com/p/go/issues/detail?id=3345 + conn, err := rcli.Call("tcp", "127.0.0.1:4242", args...) if err != nil { Fatal(err) } @@ -199,3 +220,69 @@ func main() { } } } + +// Run docker in "interactive mode": run a bash-compatible shell capable of running docker commands. +func InteractiveMode(scripts ...string) error { + // Determine path of current docker binary + dockerPath, err := exec.LookPath(os.Args[0]) + if err != nil { + return err + } + dockerPath, err = filepath.Abs(dockerPath) + if err != nil { + return err + } + + // Create a temp directory + tmp, err := ioutil.TempDir("", "docker-shell") + if err != nil { + return err + } + defer os.RemoveAll(tmp) + + // For each command, create an alias in temp directory + // FIXME: generate this list dynamically with introspection of some sort + // It might make sense to merge docker and dockerd to keep that introspection + // within a single binary. + for _, cmd := range []string{ + "help", + "run", + "ps", + "pull", + "put", + "rm", + "kill", + "wait", + "stop", + "logs", + "diff", + "commit", + "attach", + "info", + "tar", + "web", + "images", + "docker", + } { + if err := os.Symlink(dockerPath, path.Join(tmp, cmd)); err != nil { + return err + } + } + + // Run $SHELL with PATH set to temp directory + rcfile, err := ioutil.TempFile("", "docker-shell-rc") + if err != nil { + return err + } + io.WriteString(rcfile, "enable -n help\n") + os.Setenv("PATH", tmp) + os.Setenv("PS1", "\\h docker> ") + shell := exec.Command("/bin/bash", append([]string{"--rcfile", rcfile.Name()}, scripts...)...) + shell.Stdin = os.Stdin + shell.Stdout = os.Stdout + shell.Stderr = os.Stderr + if err := shell.Run(); err != nil { + return err + } + return nil +} diff --git a/dockerd/dockerd.go b/dockerd/dockerd.go index 2ee5bd4b18..41a81f19d5 100644 --- a/dockerd/dockerd.go +++ b/dockerd/dockerd.go @@ -14,7 +14,9 @@ import ( "io" "log" "net/http" + "net/url" "os" + "path" "strings" "sync" "text/tabwriter" @@ -35,6 +37,7 @@ func (srv *Server) Help() string { {"pull", "Download a tarball and create a container from it"}, {"put", "Upload a tarball and create a container from it"}, {"rm", "Remove containers"}, + {"kill", "Kill a running container"}, {"wait", "Wait for the state of a container to change"}, {"stop", "Stop a running container"}, {"logs", "Fetch the logs of a container"}, @@ -44,7 +47,7 @@ func (srv *Server) Help() string { {"info", "Display system-wide information"}, {"tar", "Stream the contents of a container as a tar archive"}, {"web", "Generate a web UI"}, - {"attach", "Attach to a running container"}, + {"images", "List images"}, } { help += fmt.Sprintf(" %-10.10s%s\n", cmd...) } @@ -254,11 +257,11 @@ func (srv *Server) CmdRmi(stdin io.ReadCloser, stdout io.Writer, args ...string) } func (srv *Server) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := rcli.Subcmd(stdout, "rm", "[OPTIONS] CONTAINER", "Remove a container") - if err := flags.Parse(args); err != nil { + cmd := rcli.Subcmd(stdout, "rm", "[OPTIONS] CONTAINER", "Remove a container") + if err := cmd.Parse(args); err != nil { return nil } - for _, name := range flags.Args() { + for _, name := range cmd.Args() { container := srv.containers.Get(name) if container == nil { return errors.New("No such container: " + name) @@ -270,15 +273,59 @@ func (srv *Server) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...string) return nil } +// 'docker kill NAME' kills a running container +func (srv *Server) CmdKill(stdin io.ReadCloser, stdout io.Writer, args ...string) error { + cmd := rcli.Subcmd(stdout, "kill", "[OPTIONS] CONTAINER [CONTAINER...]", "Kill a running container") + if err := cmd.Parse(args); err != nil { + return nil + } + for _, name := range cmd.Args() { + container := srv.containers.Get(name) + if container == nil { + return errors.New("No such container: " + name) + } + if err := container.Kill(); err != nil { + fmt.Fprintln(stdout, "Error killing container "+name+": "+err.Error()) + } + } + return nil +} + func (srv *Server) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - if len(args) < 1 { + cmd := rcli.Subcmd(stdout, "pull", "[OPTIONS] NAME", "Download a new image from a remote location") + fl_bzip2 := cmd.Bool("j", false, "Bzip2 compression") + fl_gzip := cmd.Bool("z", false, "Gzip compression") + if err := cmd.Parse(args); err != nil { + return nil + } + var compression image.Compression + if *fl_bzip2 { + compression = image.Bzip2 + } else if *fl_gzip { + compression = image.Gzip + } + name := cmd.Arg(0) + if name == "" { return errors.New("Not enough arguments") } - resp, err := http.Get(args[0]) + u, err := url.Parse(name) if err != nil { return err } - img, err := srv.images.Import(args[0], resp.Body, stdout, nil) + if u.Scheme == "" { + u.Scheme = "http" + } + // FIXME: hardcode a mirror URL that does not depend on a single provider. + if u.Host == "" { + u.Host = "s3.amazonaws.com" + u.Path = path.Join("/docker.io/images", u.Path) + } + fmt.Fprintf(stdout, "Downloading %s from %s...\n", name, u.String()) + resp, err := http.Get(u.String()) + if err != nil { + return err + } + img, err := srv.images.Import(name, resp.Body, stdout, nil, compression) if err != nil { return err } @@ -287,10 +334,23 @@ func (srv *Server) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string } func (srv *Server) CmdPut(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - if len(args) < 1 { + cmd := rcli.Subcmd(stdout, "put", "[OPTIONS] NAME", "Import a new image from a local archive.") + fl_bzip2 := cmd.Bool("j", false, "Bzip2 compression") + fl_gzip := cmd.Bool("z", false, "Gzip compression") + if err := cmd.Parse(args); err != nil { + return nil + } + var compression image.Compression + if *fl_bzip2 { + compression = image.Bzip2 + } else if *fl_gzip { + compression = image.Gzip + } + name := cmd.Arg(0) + if name == "" { return errors.New("Not enough arguments") } - img, err := srv.images.Import(args[0], stdin, stdout, nil) + img, err := srv.images.Import(name, stdin, stdout, nil, compression) if err != nil { return err } @@ -299,17 +359,17 @@ func (srv *Server) CmdPut(stdin io.ReadCloser, stdout io.Writer, args ...string) } func (srv *Server) CmdImages(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := rcli.Subcmd(stdout, "images", "[OPTIONS] [NAME]", "List images") - limit := flags.Int("l", 0, "Only show the N most recent versions of each image") - quiet := flags.Bool("q", false, "only show numeric IDs") - flags.Parse(args) - if flags.NArg() > 1 { - flags.Usage() + cmd := rcli.Subcmd(stdout, "images", "[OPTIONS] [NAME]", "List images") + limit := cmd.Int("l", 0, "Only show the N most recent versions of each image") + quiet := cmd.Bool("q", false, "only show numeric IDs") + cmd.Parse(args) + if cmd.NArg() > 1 { + cmd.Usage() return nil } var nameFilter string - if flags.NArg() == 1 { - nameFilter = flags.Arg(0) + if cmd.NArg() == 1 { + nameFilter = cmd.Arg(0) } w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0) if !*quiet { @@ -402,10 +462,10 @@ func (srv *Server) CmdPs(stdin io.ReadCloser, stdout io.Writer, args ...string) } func (srv *Server) CmdLayers(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := rcli.Subcmd(stdout, + cmd := rcli.Subcmd(stdout, "layers", "[OPTIONS]", "List filesystem layers (debug only)") - if err := flags.Parse(args); err != nil { + if err := cmd.Parse(args); err != nil { return nil } for _, layer := range srv.images.Layers.List() { @@ -415,13 +475,13 @@ func (srv *Server) CmdLayers(stdin io.ReadCloser, stdout io.Writer, args ...stri } func (srv *Server) CmdCp(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := rcli.Subcmd(stdout, + cmd := rcli.Subcmd(stdout, "cp", "[OPTIONS] IMAGE NAME", "Create a copy of IMAGE and call it NAME") - if err := flags.Parse(args); err != nil { + if err := cmd.Parse(args); err != nil { return nil } - if newImage, err := srv.images.Copy(flags.Arg(0), flags.Arg(1)); err != nil { + if newImage, err := srv.images.Copy(cmd.Arg(0), cmd.Arg(1)); err != nil { return err } else { fmt.Fprintln(stdout, newImage.Id) @@ -430,15 +490,15 @@ func (srv *Server) CmdCp(stdin io.ReadCloser, stdout io.Writer, args ...string) } func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := rcli.Subcmd(stdout, + cmd := rcli.Subcmd(stdout, "commit", "[OPTIONS] CONTAINER [DEST]", "Create a new image from a container's changes") - if err := flags.Parse(args); err != nil { + if err := cmd.Parse(args); err != nil { return nil } - containerName, imgName := flags.Arg(0), flags.Arg(1) + containerName, imgName := cmd.Arg(0), cmd.Arg(1) if containerName == "" || imgName == "" { - flags.Usage() + cmd.Usage() return nil } if container := srv.containers.Get(containerName); container != nil { @@ -449,7 +509,7 @@ func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...stri } // Create a new image from the container's base layers + a new layer from container changes parentImg := srv.images.Find(container.GetUserData("image")) - img, err := srv.images.Import(imgName, rwTar, stdout, parentImg) + img, err := srv.images.Import(imgName, rwTar, stdout, parentImg, image.Uncompressed) if err != nil { return err } @@ -460,17 +520,17 @@ func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...stri } func (srv *Server) CmdTar(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := rcli.Subcmd(stdout, + cmd := rcli.Subcmd(stdout, "tar", "CONTAINER", "Stream the contents of a container as a tar archive") - fl_sparse := flags.Bool("s", false, "Generate a sparse tar stream (top layer + reference to bottom layers)") - if err := flags.Parse(args); err != nil { + fl_sparse := cmd.Bool("s", false, "Generate a sparse tar stream (top layer + reference to bottom layers)") + if err := cmd.Parse(args); err != nil { return nil } if *fl_sparse { return errors.New("Sparse mode not yet implemented") // FIXME } - name := flags.Arg(0) + name := cmd.Arg(0) if container := srv.containers.Get(name); container != nil { data, err := container.Filesystem.Tar() if err != nil { @@ -486,16 +546,16 @@ func (srv *Server) CmdTar(stdin io.ReadCloser, stdout io.Writer, args ...string) } func (srv *Server) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := rcli.Subcmd(stdout, + cmd := rcli.Subcmd(stdout, "diff", "CONTAINER [OPTIONS]", "Inspect changes on a container's filesystem") - if err := flags.Parse(args); err != nil { + if err := cmd.Parse(args); err != nil { return nil } - if flags.NArg() < 1 { + if cmd.NArg() < 1 { return errors.New("Not enough arguments") } - if container := srv.containers.Get(flags.Arg(0)); container == nil { + if container := srv.containers.Get(cmd.Arg(0)); container == nil { return errors.New("No such container") } else { changes, err := container.Filesystem.Changes() @@ -510,16 +570,16 @@ func (srv *Server) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string } func (srv *Server) CmdReset(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := rcli.Subcmd(stdout, + cmd := rcli.Subcmd(stdout, "reset", "CONTAINER [OPTIONS]", "Reset changes to a container's filesystem") - if err := flags.Parse(args); err != nil { + if err := cmd.Parse(args); err != nil { return nil } - if flags.NArg() < 1 { + if cmd.NArg() < 1 { return errors.New("Not enough arguments") } - for _, name := range flags.Args() { + for _, name := range cmd.Args() { if container := srv.containers.Get(name); container != nil { if err := container.Filesystem.Reset(); err != nil { return errors.New("Reset " + container.Id + ": " + err.Error()) @@ -530,15 +590,15 @@ func (srv *Server) CmdReset(stdin io.ReadCloser, stdout io.Writer, args ...strin } func (srv *Server) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := rcli.Subcmd(stdout, "logs", "[OPTIONS] CONTAINER", "Fetch the logs of a container") - if err := flags.Parse(args); err != nil { + cmd := rcli.Subcmd(stdout, "logs", "[OPTIONS] CONTAINER", "Fetch the logs of a container") + if err := cmd.Parse(args); err != nil { return nil } - if flags.NArg() != 1 { - flags.Usage() + if cmd.NArg() != 1 { + cmd.Usage() return nil } - name := flags.Arg(0) + name := cmd.Arg(0) if container := srv.containers.Get(name); container != nil { if _, err := io.Copy(stdout, container.StdoutLog()); err != nil { return err @@ -548,7 +608,7 @@ func (srv *Server) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string } return nil } - return errors.New("No such container: " + flags.Arg(0)) + return errors.New("No such container: " + cmd.Arg(0)) } func (srv *Server) CreateContainer(img *image.Image, tty bool, openStdin bool, comment string, cmd string, args ...string) (*docker.Container, error) { @@ -616,29 +676,29 @@ func (srv *Server) CmdAttach(stdin io.ReadCloser, stdout io.Writer, args ...stri } func (srv *Server) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := rcli.Subcmd(stdout, "run", "[OPTIONS] IMAGE COMMAND [ARG...]", "Run a command in a new container") - fl_attach := flags.Bool("a", false, "Attach stdin and stdout") - fl_stdin := flags.Bool("i", false, "Keep stdin open even if not attached") - fl_tty := flags.Bool("t", false, "Allocate a pseudo-tty") - fl_comment := flags.String("c", "", "Comment") - if err := flags.Parse(args); err != nil { + cmd := rcli.Subcmd(stdout, "run", "[OPTIONS] IMAGE COMMAND [ARG...]", "Run a command in a new container") + fl_attach := cmd.Bool("a", false, "Attach stdin and stdout") + fl_stdin := cmd.Bool("i", false, "Keep stdin open even if not attached") + fl_tty := cmd.Bool("t", false, "Allocate a pseudo-tty") + fl_comment := cmd.String("c", "", "Comment") + if err := cmd.Parse(args); err != nil { return nil } - name := flags.Arg(0) - var cmd []string - if len(flags.Args()) >= 2 { - cmd = flags.Args()[1:] + name := cmd.Arg(0) + var cmdline []string + if len(cmd.Args()) >= 2 { + cmdline = cmd.Args()[1:] } // Choose a default image if needed if name == "" { name = "base" } // Choose a default command if needed - if len(cmd) == 0 { + if len(cmdline) == 0 { *fl_stdin = true *fl_tty = true *fl_attach = true - cmd = []string{"/bin/bash", "-i"} + cmdline = []string{"/bin/bash", "-i"} } // Find the image img := srv.images.Find(name) @@ -646,7 +706,7 @@ func (srv *Server) CmdRun(stdin io.ReadCloser, stdout io.Writer, args ...string) return errors.New("No such image: " + name) } // Create new container - container, err := srv.CreateContainer(img, *fl_tty, *fl_stdin, *fl_comment, cmd[0], cmd[1:]...) + container, err := srv.CreateContainer(img, *fl_tty, *fl_stdin, *fl_comment, cmdline[0], cmdline[1:]...) if err != nil { return errors.New("Error creating container: " + err.Error()) } @@ -717,11 +777,15 @@ func main() { log.Fatal(err) } go func() { - if err := rcli.ListenAndServeHTTP(":8080", d); err != nil { + if err := rcli.ListenAndServeHTTP("127.0.0.1:8080", d); err != nil { log.Fatal(err) } }() - if err := rcli.ListenAndServeTCP(":4242", d); err != nil { + // FIXME: we want to use unix sockets here, but net.UnixConn doesn't expose + // CloseWrite(), which we need to cleanly signal that stdin is closed without + // closing the connection. + // See http://code.google.com/p/go/issues/detail?id=3345 + if err := rcli.ListenAndServe("tcp", "127.0.0.1:4242", d); err != nil { log.Fatal(err) } } @@ -764,9 +828,9 @@ func (srv *Server) CmdDebug(stdin io.ReadCloser, stdout io.Writer, args ...strin } func (srv *Server) CmdWeb(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - flags := rcli.Subcmd(stdout, "web", "[OPTIONS]", "A web UI for docker") - showurl := flags.Bool("u", false, "Return the URL of the web UI") - if err := flags.Parse(args); err != nil { + cmd := rcli.Subcmd(stdout, "web", "[OPTIONS]", "A web UI for docker") + showurl := cmd.Bool("u", false, "Return the URL of the web UI") + if err := cmd.Parse(args); err != nil { return nil } if *showurl { diff --git a/image/image.go b/image/image.go index e996ba33b3..7f635d1338 100644 --- a/image/image.go +++ b/image/image.go @@ -41,9 +41,16 @@ func New(root string) (*Store, error) { }, nil } +type Compression uint32 -func (store *Store) Import(name string, archive io.Reader, stderr io.Writer, parent *Image) (*Image, error) { - layer, err := store.Layers.AddLayer(archive, stderr) +const ( + Uncompressed Compression = iota + Bzip2 + Gzip +) + +func (store *Store) Import(name string, archive io.Reader, stderr io.Writer, parent *Image, compression Compression) (*Image, error) { + layer, err := store.Layers.AddLayer(archive, stderr, compression) if err != nil { return nil, err } diff --git a/image/layers.go b/image/layers.go index 3b43eed2e9..f856ff81d2 100644 --- a/image/layers.go +++ b/image/layers.go @@ -82,13 +82,19 @@ func (store *LayerStore) layerPath(id string) string { } -func (store *LayerStore) AddLayer(archive io.Reader, stderr io.Writer) (string, error) { +func (store *LayerStore) AddLayer(archive io.Reader, stderr io.Writer, compression Compression) (string, error) { tmp, err := store.Mktemp() defer os.RemoveAll(tmp) if err != nil { return "", err } - untarCmd := exec.Command("tar", "-C", tmp, "-x") + extractFlags := "-x" + if compression == Bzip2 { + extractFlags += "j" + } else if compression == Gzip { + extractFlags += "z" + } + untarCmd := exec.Command("tar", "-C", tmp, extractFlags) untarW, err := untarCmd.StdinPipe() if err != nil { return "", err diff --git a/puppet/manifests/quantal64.pp b/puppet/manifests/quantal64.pp new file mode 100644 index 0000000000..8ef0591650 --- /dev/null +++ b/puppet/manifests/quantal64.pp @@ -0,0 +1,17 @@ +node default { + exec { + "apt_update" : + command => "/usr/bin/apt-get update" + } + + Package { + require => Exec['apt_update'] + } + + group { "puppet": + ensure => "present" + } + + include "docker" + +} diff --git a/puppet/modules/docker/manifests/init.pp b/puppet/modules/docker/manifests/init.pp new file mode 100644 index 0000000000..79d5891308 --- /dev/null +++ b/puppet/modules/docker/manifests/init.pp @@ -0,0 +1,56 @@ +class docker { + + # update this with latest docker binary distro + $docker_url = "https://dl.dropbox.com/u/20637798/docker.tar.gz" + # update this with latest go binary distry + $go_url = "http://go.googlecode.com/files/go1.0.3.linux-amd64.tar.gz" + + + Package { ensure => "installed" } + + package { ["lxc", "debootstrap", "wget"]: } + + exec { "debootstrap" : + require => Package["debootstrap"], + command => "/usr/sbin/debootstrap --arch=amd64 quantal /var/lib/docker/images/ubuntu", + creates => "/var/lib/docker/images/ubuntu", + timeout => 0 + } + + exec { "fetch-go": + require => Package["wget"], + command => "/usr/bin/wget -O - $go_url | /bin/tar xz -C /usr/local", + creates => "/usr/local/go/bin/go", + } + + exec { "fetch-docker" : + require => Package["wget"], + command => "/usr/bin/wget -O - $docker_url | /bin/tar xz -C /home/vagrant", + creates => "/home/vagrant/docker/dockerd" + } + + file { "/etc/init/dockerd.conf": + mode => 600, + owner => "root", + group => "root", + content => template("docker/dockerd.conf"), + require => [Exec["fetch-docker"], Exec["debootstrap"]] + } + + exec { "copy-docker-bin" : + require => Exec["fetch-docker"], + command => "/bin/cp /home/vagrant/docker/docker /usr/local/bin", + creates => "/usr/local/bin/docker" + } + + service { "dockerd" : + ensure => "running", + start => "/sbin/initctl start dockerd", + stop => "/sbin/initctl stop dockerd", + require => File["/etc/init/dockerd.conf"], + name => "dockerd", + provider => "base" + } + + +} diff --git a/puppet/modules/docker/templates/dockerd.conf b/puppet/modules/docker/templates/dockerd.conf new file mode 100644 index 0000000000..a00d322bcb --- /dev/null +++ b/puppet/modules/docker/templates/dockerd.conf @@ -0,0 +1,11 @@ +description "Run dockerd" + +stop on runlevel [!2345] +start on runlevel [3] + +# if you want it to automatically restart if it crashes, leave the next line in +respawn + +script + /home/vagrant/dockerd/dockerd +end script diff --git a/rcli/tcp.go b/rcli/tcp.go index 0c1fa80f41..0a06d459ce 100644 --- a/rcli/tcp.go +++ b/rcli/tcp.go @@ -10,12 +10,15 @@ import ( "bufio" ) -func CallTCP(addr string, args ...string) (*net.TCPConn, error) { +// Connect to a remote endpoint using protocol `proto` and address `addr`, +// issue a single call, and return the result. +// `proto` may be "tcp", "unix", etc. See the `net` package for available protocols. +func Call(proto, addr string, args ...string) (*net.TCPConn, error) { cmd, err := json.Marshal(args) if err != nil { return nil, err } - conn, err := net.Dial("tcp", addr) + conn, err := net.Dial(proto, addr) if err != nil { return nil, err } @@ -25,12 +28,14 @@ func CallTCP(addr string, args ...string) (*net.TCPConn, error) { return conn.(*net.TCPConn), nil } -func ListenAndServeTCP(addr string, service Service) error { - listener, err := net.Listen("tcp", addr) +// Listen on `addr`, using protocol `proto`, for incoming rcli calls, +// and pass them to `service`. +func ListenAndServe(proto, addr string, service Service) error { + listener, err := net.Listen(proto, addr) if err != nil { return err } - log.Printf("Listening for RCLI/TCP on %s\n", addr) + log.Printf("Listening for RCLI/%s on %s\n", proto, addr) defer listener.Close() for { if conn, err := listener.Accept(); err != nil { @@ -48,6 +53,9 @@ func ListenAndServeTCP(addr string, service Service) error { return nil } + +// Parse an rcli call on a new connection, and pass it to `service` if it +// is valid. func Serve(conn io.ReadWriter, service Service) error { r := bufio.NewReader(conn) var args []string diff --git a/rcli/types.go b/rcli/types.go index b36aa75e8c..b8572cd896 100644 --- a/rcli/types.go +++ b/rcli/types.go @@ -1,5 +1,12 @@ package rcli +// rcli (Remote Command-Line Interface) is a simple protocol for... +// serving command-line interfaces remotely. +// +// rcli can be used over any transport capable of a) sending binary streams in +// both directions, and b) capable of half-closing a connection. TCP and Unix sockets +// are the usual suspects. + import ( "fmt" "io" @@ -20,6 +27,9 @@ type CmdMethod func(Service, io.ReadCloser, io.Writer, ...string) error func call(service Service, stdin io.ReadCloser, stdout io.Writer, args ...string) error { + if len(args) == 0 { + args = []string{"help"} + } flags := flag.NewFlagSet("main", flag.ContinueOnError) flags.SetOutput(stdout) flags.Usage = func() { stdout.Write([]byte(service.Help())) } @@ -33,7 +43,7 @@ func call(service Service, stdin io.ReadCloser, stdout io.Writer, args ...string } method := getMethod(service, cmd) if method != nil { - return method(stdin, stdout, args[1:]...) + return method(stdin, stdout, flags.Args()[1:]...) } return errors.New("No such command: " + cmd) }