diff --git a/components/engine/CHANGELOG.md b/components/engine/CHANGELOG.md index 5036d87c15..a9e2dab79e 100644 --- a/components/engine/CHANGELOG.md +++ b/components/engine/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## 0.3.2 (2013-05-09) + * Runtime: Store the actual archive on commit + * Registry: Improve the checksum process + * Registry: Use the size to have a good progress bar while pushing + * Registry: Use the actual archive if it exists in order to speed up the push + - Registry: Fix error 400 on push + +## 0.3.1 (2013-05-08) + + Builder: Implement the autorun capability within docker builder + + Builder: Add caching to docker builder + + Builder: Add support for docker builder with native API as top level command + + Runtime: Add go version to debug infos + + Builder: Implement ENV within docker builder + + Registry: Add docker search top level command in order to search a repository + + Images: output graph of images to dot (graphviz) + + Documentation: new introduction and high-level overview + + Documentation: Add the documentation for docker builder + + Website: new high-level overview + - Makefile: Swap "go get" for "go get -d", especially to compile on go1.1rc + - Images: fix ByParent function + - Builder: Check the command existance prior create and add Unit tests for the case + - Registry: Fix pull for official images with specific tag + - Registry: Fix issue when login in with a different user and trying to push + - Documentation: CSS fix for docker documentation to make REST API docs look better. + - Documentation: Fixed CouchDB example page header mistake + - Documentation: fixed README formatting + * Registry: Improve checksum - async calculation + * Runtime: kernel version - don't show the dash if flavor is empty + * Documentation: updated www.docker.io website. + * Builder: use any whitespaces instead of tabs + * Packaging: packaging ubuntu; issue #510: Use goland-stable PPA package to build docker + ## 0.3.0 (2013-05-06) + Registry: Implement the new registry + Documentation: new example: sharing data between 2 couchdb databases diff --git a/components/engine/Makefile b/components/engine/Makefile index bae7d64909..9527d3f750 100644 --- a/components/engine/Makefile +++ b/components/engine/Makefile @@ -39,7 +39,7 @@ $(DOCKER_BIN): $(DOCKER_DIR) $(DOCKER_DIR): @mkdir -p $(dir $@) @if [ -h $@ ]; then rm -f $@; fi; ln -sf $(CURDIR)/ $@ - @(cd $(DOCKER_MAIN); go get $(GO_OPTIONS)) + @(cd $(DOCKER_MAIN); go get -d $(GO_OPTIONS)) whichrelease: echo $(RELEASE_VERSION) diff --git a/components/engine/Vagrantfile b/components/engine/Vagrantfile index 06e8b47a4c..9ec0c83182 100644 --- a/components/engine/Vagrantfile +++ b/components/engine/Vagrantfile @@ -3,28 +3,47 @@ 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| # Setup virtual machine box. This VM configuration code is always executed. config.vm.box = BOX_NAME 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.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 - pkg_cmd << "; apt-get install linux-image-extra-3.2.0-40-virtual" + + # Provision docker and new kernel if deployment was not done + if Dir.glob("#{File.dirname(__FILE__)}/.vagrant/machines/default/*/id").empty? + # Add lxc-docker package + pkg_cmd = "apt-get update -qq; apt-get install -q -y python-software-properties; " \ + "add-apt-repository -y ppa:dotcloud/lxc-docker; apt-get update -qq; " \ + "apt-get install -q -y lxc-docker; " + # Add X.org Ubuntu backported 3.8 kernel + pkg_cmd << "add-apt-repository -y ppa:ubuntu-x-swat/r-lts-backport; " \ + "apt-get update -qq; apt-get install -q -y linux-image-3.8.0-19-generic; " + # Add guest additions if local vbox VM + is_vbox = true + ARGV.each do |arg| is_vbox &&= !arg.downcase.start_with?("--provider") end + if is_vbox + pkg_cmd << "apt-get install -q -y linux-headers-3.8.0-19-generic dkms; " \ + "echo 'Downloading VBox Guest Additions...'; " \ + "wget -q http://dlc.sun.com.edgesuite.net/virtualbox/4.2.12/VBoxGuestAdditions_4.2.12.iso; " + # Prepare the VM to add guest additions after reboot + pkg_cmd << "echo -e 'mount -o loop,ro /home/vagrant/VBoxGuestAdditions_4.2.12.iso /mnt\n" \ + "echo yes | /mnt/VBoxLinuxAdditions.run\numount /mnt\n" \ + "rm /root/guest_additions.sh; ' > /root/guest_additions.sh; " \ + "chmod 700 /root/guest_additions.sh; " \ + "sed -i -E 's#^exit 0#[ -x /root/guest_additions.sh ] \\&\\& /root/guest_additions.sh#' /etc/rc.local; " \ + "echo 'Installation of VBox Guest Additions is proceeding in the background.'; " \ + "echo '\"vagrant reload\" can be used in about 2 minutes to activate the new guest additions.'; " + end + # Activate new kernel + pkg_cmd << "shutdown -r +1; " + config.vm.provision :shell, :inline => pkg_cmd end - config.vm.provision :shell, :inline => pkg_cmd end + # Providers were added on Vagrant >= 1.1.0 Vagrant::VERSION >= "1.1.0" and Vagrant.configure("2") do |config| config.vm.provider :aws do |aws, override| - config.vm.box = "dummy" - config.vm.box_url = "https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box" aws.access_key_id = ENV["AWS_ACCESS_KEY_ID"] aws.secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"] aws.keypair_name = ENV["AWS_KEYPAIR_NAME"] @@ -36,8 +55,6 @@ Vagrant::VERSION >= "1.1.0" and Vagrant.configure("2") do |config| end config.vm.provider :rackspace do |rs| - config.vm.box = "dummy" - config.vm.box_url = "https://github.com/mitchellh/vagrant-rackspace/raw/master/dummy.box" config.ssh.private_key_path = ENV["RS_PRIVATE_KEY"] rs.username = ENV["RS_USERNAME"] rs.api_key = ENV["RS_API_KEY"] diff --git a/components/engine/api.go b/components/engine/api.go new file mode 100644 index 0000000000..9485fca051 --- /dev/null +++ b/components/engine/api.go @@ -0,0 +1,618 @@ +package docker + +import ( + "encoding/json" + "fmt" + "github.com/dotcloud/docker/auth" + "github.com/gorilla/mux" + "github.com/shin-/cookiejar" + "io" + "log" + "net/http" + "strconv" + "strings" +) + +func hijackServer(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) { + conn, _, err := w.(http.Hijacker).Hijack() + if err != nil { + return nil, nil, err + } + // Flush the options to make sure the client sets the raw mode + conn.Write([]byte{}) + return conn, conn, nil +} + +//If we don't do this, POST method without Content-type (even with empty body) will fail +func parseForm(r *http.Request) error { + if err := r.ParseForm(); err != nil && !strings.HasPrefix(err.Error(), "mime:") { + return err + } + return nil +} + +func httpError(w http.ResponseWriter, err error) { + if strings.HasPrefix(err.Error(), "No such") { + http.Error(w, err.Error(), http.StatusNotFound) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func getAuth(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + config := &auth.AuthConfig{ + Username: srv.runtime.authConfig.Username, + Email: srv.runtime.authConfig.Email, + } + b, err := json.Marshal(config) + if err != nil { + return nil, err + } + return b, nil +} + +func postAuth(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + config := &auth.AuthConfig{} + if err := json.NewDecoder(r.Body).Decode(config); err != nil { + return nil, err + } + + if config.Username == srv.runtime.authConfig.Username { + config.Password = srv.runtime.authConfig.Password + } + + newAuthConfig := auth.NewAuthConfig(config.Username, config.Password, config.Email, srv.runtime.root) + status, err := auth.Login(newAuthConfig) + if err != nil { + return nil, err + } else { + srv.runtime.graph.getHttpClient().Jar = cookiejar.NewCookieJar() + srv.runtime.authConfig = newAuthConfig + } + if status != "" { + b, err := json.Marshal(&ApiAuth{Status: status}) + if err != nil { + return nil, err + } + return b, nil + } + w.WriteHeader(http.StatusNoContent) + return nil, nil +} + +func getVersion(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + m := srv.DockerVersion() + b, err := json.Marshal(m) + if err != nil { + return nil, err + } + return b, nil +} + +func postContainersKill(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + if err := srv.ContainerKill(name); err != nil { + return nil, err + } + w.WriteHeader(http.StatusNoContent) + return nil, nil +} + +func getContainersExport(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + + if err := srv.ContainerExport(name, w); err != nil { + Debugf("%s", err.Error()) + //return nil, err + } + return nil, nil +} + +func getImagesJson(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if err := parseForm(r); err != nil { + return nil, err + } + + all := r.Form.Get("all") == "1" + filter := r.Form.Get("filter") + only_ids := r.Form.Get("only_ids") == "1" + + outs, err := srv.Images(all, only_ids, filter) + if err != nil { + return nil, err + } + b, err := json.Marshal(outs) + if err != nil { + return nil, err + } + return b, nil +} + +func getImagesViz(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if err := srv.ImagesViz(w); err != nil { + return nil, err + } + return nil, nil +} + +func getInfo(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + out := srv.DockerInfo() + b, err := json.Marshal(out) + if err != nil { + return nil, err + } + return b, nil +} + +func getImagesHistory(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + outs, err := srv.ImageHistory(name) + if err != nil { + return nil, err + } + b, err := json.Marshal(outs) + if err != nil { + return nil, err + } + return b, nil +} + +func getContainersChanges(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + changesStr, err := srv.ContainerChanges(name) + if err != nil { + return nil, err + } + b, err := json.Marshal(changesStr) + if err != nil { + return nil, err + } + return b, nil +} + +func getContainersPs(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if err := parseForm(r); err != nil { + return nil, err + } + all := r.Form.Get("all") == "1" + trunc_cmd := r.Form.Get("trunc_cmd") != "0" + only_ids := r.Form.Get("only_ids") == "1" + since := r.Form.Get("since") + before := r.Form.Get("before") + n, err := strconv.Atoi(r.Form.Get("limit")) + if err != nil { + n = -1 + } + + outs := srv.Containers(all, trunc_cmd, only_ids, n, since, before) + b, err := json.Marshal(outs) + if err != nil { + return nil, err + } + return b, nil +} + +func postImagesTag(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if err := parseForm(r); err != nil { + return nil, err + } + repo := r.Form.Get("repo") + tag := r.Form.Get("tag") + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + force := r.Form.Get("force") == "1" + + if err := srv.ContainerTag(name, repo, tag, force); err != nil { + return nil, err + } + w.WriteHeader(http.StatusCreated) + return nil, nil +} + +func postCommit(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if err := parseForm(r); err != nil { + return nil, err + } + config := &Config{} + if err := json.NewDecoder(r.Body).Decode(config); err != nil { + Debugf("%s", err.Error()) + } + repo := r.Form.Get("repo") + tag := r.Form.Get("tag") + container := r.Form.Get("container") + author := r.Form.Get("author") + comment := r.Form.Get("comment") + id, err := srv.ContainerCommit(container, repo, tag, author, comment, config) + if err != nil { + return nil, err + } + b, err := json.Marshal(&ApiId{id}) + if err != nil { + return nil, err + } + w.WriteHeader(http.StatusCreated) + return b, nil +} + +// Creates an image from Pull or from Import +func postImagesCreate(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if err := parseForm(r); err != nil { + return nil, err + } + + src := r.Form.Get("fromSrc") + image := r.Form.Get("fromImage") + repo := r.Form.Get("repo") + tag := r.Form.Get("tag") + + in, out, err := hijackServer(w) + if err != nil { + return nil, err + } + defer in.Close() + fmt.Fprintf(out, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") + if image != "" { //pull + registry := r.Form.Get("registry") + if err := srv.ImagePull(image, tag, registry, out); err != nil { + fmt.Fprintf(out, "Error: %s\n", err) + } + } else { //import + if err := srv.ImageImport(src, repo, tag, in, out); err != nil { + fmt.Fprintf(out, "Error: %s\n", err) + } + } + return nil, nil +} + +func getImagesSearch(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if err := parseForm(r); err != nil { + return nil, err + } + + term := r.Form.Get("term") + outs, err := srv.ImagesSearch(term) + if err != nil { + return nil, err + } + b, err := json.Marshal(outs) + if err != nil { + return nil, err + } + return b, nil +} + +func postImagesInsert(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if err := parseForm(r); err != nil { + return nil, err + } + + url := r.Form.Get("url") + path := r.Form.Get("path") + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + + in, out, err := hijackServer(w) + if err != nil { + return nil, err + } + defer in.Close() + fmt.Fprintf(out, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") + if err := srv.ImageInsert(name, url, path, out); err != nil { + fmt.Fprintf(out, "Error: %s\n", err) + } + return nil, nil +} + +func postImagesPush(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if err := parseForm(r); err != nil { + return nil, err + } + + registry := r.Form.Get("registry") + + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + + in, out, err := hijackServer(w) + if err != nil { + return nil, err + } + defer in.Close() + fmt.Fprintf(out, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") + if err := srv.ImagePush(name, registry, out); err != nil { + fmt.Fprintln(out, "Error: %s\n", err) + } + return nil, nil +} + +func postBuild(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + in, out, err := hijackServer(w) + if err != nil { + return nil, err + } + defer in.Close() + fmt.Fprintf(out, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") + if err := srv.ImageCreateFromFile(in, out); err != nil { + fmt.Fprintln(out, "Error: %s\n", err) + } + return nil, nil +} + +func postContainersCreate(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + config := &Config{} + if err := json.NewDecoder(r.Body).Decode(config); err != nil { + return nil, err + } + id, err := srv.ContainerCreate(config) + if err != nil { + return nil, err + } + + out := &ApiRun{ + Id: id, + } + if config.Memory > 0 && !srv.runtime.capabilities.MemoryLimit { + log.Println("WARNING: Your kernel does not support memory limit capabilities. Limitation discarded.") + out.Warnings = append(out.Warnings, "Your kernel does not support memory limit capabilities. Limitation discarded.") + } + if config.Memory > 0 && !srv.runtime.capabilities.SwapLimit { + log.Println("WARNING: Your kernel does not support swap limit capabilities. Limitation discarded.") + out.Warnings = append(out.Warnings, "Your kernel does not support memory swap capabilities. Limitation discarded.") + } + b, err := json.Marshal(out) + if err != nil { + return nil, err + } + w.WriteHeader(http.StatusCreated) + return b, nil +} + +func postContainersRestart(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if err := parseForm(r); err != nil { + return nil, err + } + t, err := strconv.Atoi(r.Form.Get("t")) + if err != nil || t < 0 { + t = 10 + } + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + if err := srv.ContainerRestart(name, t); err != nil { + return nil, err + } + w.WriteHeader(http.StatusNoContent) + return nil, nil +} + +func deleteContainers(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if err := parseForm(r); err != nil { + return nil, err + } + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + removeVolume := r.Form.Get("v") == "1" + + if err := srv.ContainerDestroy(name, removeVolume); err != nil { + return nil, err + } + w.WriteHeader(http.StatusNoContent) + return nil, nil +} + +func deleteImages(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + if err := srv.ImageDelete(name); err != nil { + return nil, err + } + w.WriteHeader(http.StatusNoContent) + return nil, nil +} + +func postContainersStart(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + if err := srv.ContainerStart(name); err != nil { + return nil, err + } + w.WriteHeader(http.StatusNoContent) + return nil, nil +} + +func postContainersStop(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if err := parseForm(r); err != nil { + return nil, err + } + t, err := strconv.Atoi(r.Form.Get("t")) + if err != nil || t < 0 { + t = 10 + } + + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + + if err := srv.ContainerStop(name, t); err != nil { + return nil, err + } + w.WriteHeader(http.StatusNoContent) + return nil, nil +} + +func postContainersWait(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + status, err := srv.ContainerWait(name) + if err != nil { + return nil, err + } + b, err := json.Marshal(&ApiWait{StatusCode: status}) + if err != nil { + return nil, err + } + return b, nil +} + +func postContainersAttach(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if err := parseForm(r); err != nil { + return nil, err + } + logs := r.Form.Get("logs") == "1" + stream := r.Form.Get("stream") == "1" + stdin := r.Form.Get("stdin") == "1" + stdout := r.Form.Get("stdout") == "1" + stderr := r.Form.Get("stderr") == "1" + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + + in, out, err := hijackServer(w) + if err != nil { + return nil, err + } + defer in.Close() + + fmt.Fprintf(out, "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n") + if err := srv.ContainerAttach(name, logs, stream, stdin, stdout, stderr, in, out); err != nil { + fmt.Fprintf(out, "Error: %s\n", err) + } + return nil, nil +} + +func getContainersByName(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + + container, err := srv.ContainerInspect(name) + if err != nil { + return nil, err + } + b, err := json.Marshal(container) + if err != nil { + return nil, err + } + return b, nil +} + +func getImagesByName(srv *Server, w http.ResponseWriter, r *http.Request, vars map[string]string) ([]byte, error) { + if vars == nil { + return nil, fmt.Errorf("Missing parameter") + } + name := vars["name"] + + image, err := srv.ImageInspect(name) + if err != nil { + return nil, err + } + b, err := json.Marshal(image) + if err != nil { + return nil, err + } + return b, nil +} + +func ListenAndServe(addr string, srv *Server, logging bool) error { + r := mux.NewRouter() + log.Printf("Listening for HTTP on %s\n", addr) + + m := map[string]map[string]func(*Server, http.ResponseWriter, *http.Request, map[string]string) ([]byte, error){ + "GET": { + "/auth": getAuth, + "/version": getVersion, + "/info": getInfo, + "/images/json": getImagesJson, + "/images/viz": getImagesViz, + "/images/search": getImagesSearch, + "/images/{name:.*}/history": getImagesHistory, + "/images/{name:.*}/json": getImagesByName, + "/containers/ps": getContainersPs, + "/containers/{name:.*}/export": getContainersExport, + "/containers/{name:.*}/changes": getContainersChanges, + "/containers/{name:.*}/json": getContainersByName, + }, + "POST": { + "/auth": postAuth, + "/commit": postCommit, + "/build": postBuild, + "/images/create": postImagesCreate, + "/images/{name:.*}/insert": postImagesInsert, + "/images/{name:.*}/push": postImagesPush, + "/images/{name:.*}/tag": postImagesTag, + "/containers/create": postContainersCreate, + "/containers/{name:.*}/kill": postContainersKill, + "/containers/{name:.*}/restart": postContainersRestart, + "/containers/{name:.*}/start": postContainersStart, + "/containers/{name:.*}/stop": postContainersStop, + "/containers/{name:.*}/wait": postContainersWait, + "/containers/{name:.*}/attach": postContainersAttach, + }, + "DELETE": { + "/containers/{name:.*}": deleteContainers, + "/images/{name:.*}": deleteImages, + }, + } + + for method, routes := range m { + for route, fct := range routes { + Debugf("Registering %s, %s", method, route) + // NOTE: scope issue, make sure the variables are local and won't be changed + localRoute := route + localMethod := method + localFct := fct + r.Path(localRoute).Methods(localMethod).HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Debugf("Calling %s %s", localMethod, localRoute) + if logging { + log.Println(r.Method, r.RequestURI) + } + if strings.Contains(r.Header.Get("User-Agent"), "Docker-Client/") { + userAgent := strings.Split(r.Header.Get("User-Agent"), "/") + if len(userAgent) == 2 && userAgent[1] != VERSION { + Debugf("Warning: client and server don't have the same version (client: %s, server: %s)", userAgent[1], VERSION) + } + } + json, err := localFct(srv, w, r, mux.Vars(r)) + if err != nil { + httpError(w, err) + } + if json != nil { + w.Header().Set("Content-Type", "application/json") + w.Write(json) + } + }) + } + } + + return http.ListenAndServe(addr, r) +} diff --git a/components/engine/api_params.go b/components/engine/api_params.go new file mode 100644 index 0000000000..c7c15585f9 --- /dev/null +++ b/components/engine/api_params.go @@ -0,0 +1,66 @@ +package docker + +type ApiHistory struct { + Id string + Created int64 + CreatedBy string +} + +type ApiImages struct { + Repository string `json:",omitempty"` + Tag string `json:",omitempty"` + Id string + Created int64 `json:",omitempty"` +} + +type ApiInfo struct { + Containers int + Version string + Images int + Debug bool + GoVersion string + NFd int `json:",omitempty"` + NGoroutines int `json:",omitempty"` +} + +type ApiContainers struct { + Id string + Image string `json:",omitempty"` + Command string `json:",omitempty"` + Created int64 `json:",omitempty"` + Status string `json:",omitempty"` + Ports string `json:",omitempty"` +} + +type ApiSearch struct { + Name string + Description string +} + +type ApiId struct { + Id string +} + +type ApiRun struct { + Id string + Warnings []string +} + +type ApiPort struct { + Port string +} + +type ApiVersion struct { + Version string + GitCommit string + MemoryLimit bool + SwapLimit bool +} + +type ApiWait struct { + StatusCode int +} + +type ApiAuth struct { + Status string +} diff --git a/components/engine/api_test.go b/components/engine/api_test.go new file mode 100644 index 0000000000..1b139b7d63 --- /dev/null +++ b/components/engine/api_test.go @@ -0,0 +1,1275 @@ +package docker + +import ( + "archive/tar" + "bufio" + "bytes" + "encoding/json" + "github.com/dotcloud/docker/auth" + "io" + "net" + "net/http" + "net/http/httptest" + "os" + "path" + "testing" + "time" +) + +func TestGetAuth(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + r := httptest.NewRecorder() + + authConfig := &auth.AuthConfig{ + Username: "utest", + Password: "utest", + Email: "utest@yopmail.com", + } + + authConfigJson, err := json.Marshal(authConfig) + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest("POST", "/auth", bytes.NewReader(authConfigJson)) + if err != nil { + t.Fatal(err) + } + + body, err := postAuth(srv, r, req, nil) + if err != nil { + t.Fatal(err) + } + if body == nil { + t.Fatalf("No body received\n") + } + if r.Code != http.StatusOK && r.Code != 0 { + t.Fatalf("%d OK or 0 expected, received %d\n", http.StatusOK, r.Code) + } + + if runtime.authConfig.Username != authConfig.Username || + runtime.authConfig.Password != authConfig.Password || + runtime.authConfig.Email != authConfig.Email { + t.Fatalf("The auth configuration hasn't been set correctly") + } +} + +func TestGetVersion(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + body, err := getVersion(srv, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + + v := &ApiVersion{} + + err = json.Unmarshal(body, v) + if err != nil { + t.Fatal(err) + } + if v.Version != VERSION { + t.Errorf("Excepted version %s, %s found", VERSION, v.Version) + } +} + +func TestGetInfo(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + body, err := getInfo(srv, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + infos := &ApiInfo{} + err = json.Unmarshal(body, infos) + if err != nil { + t.Fatal(err) + } + if infos.Version != VERSION { + t.Errorf("Excepted version %s, %s found", VERSION, infos.Version) + } +} + +func TestGetImagesJson(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + // only_ids=0&all=0 + req, err := http.NewRequest("GET", "/images/json?only_ids=0&all=0", nil) + if err != nil { + t.Fatal(err) + } + + body, err := getImagesJson(srv, nil, req, nil) + if err != nil { + t.Fatal(err) + } + + images := []ApiImages{} + err = json.Unmarshal(body, &images) + if err != nil { + t.Fatal(err) + } + + if len(images) != 1 { + t.Errorf("Excepted 1 image, %d found", len(images)) + } + + if images[0].Repository != unitTestImageName { + t.Errorf("Excepted image %s, %s found", unitTestImageName, images[0].Repository) + } + + // only_ids=1&all=1 + req2, err := http.NewRequest("GET", "/images/json?only_ids=1&all=1", nil) + if err != nil { + t.Fatal(err) + } + + body2, err := getImagesJson(srv, nil, req2, nil) + if err != nil { + t.Fatal(err) + } + + images2 := []ApiImages{} + err = json.Unmarshal(body2, &images2) + if err != nil { + t.Fatal(err) + } + + if len(images2) != 1 { + t.Errorf("Excepted 1 image, %d found", len(images2)) + } + + if images2[0].Repository != "" { + t.Errorf("Excepted no image Repository, %s found", images2[0].Repository) + } + + if images2[0].Id != GetTestImage(runtime).ShortId() { + t.Errorf("Retrieved image Id differs, expected %s, received %s", GetTestImage(runtime).ShortId(), images2[0].Id) + } + + // filter=a + req3, err := http.NewRequest("GET", "/images/json?filter=a", nil) + if err != nil { + t.Fatal(err) + } + + body3, err := getImagesJson(srv, nil, req3, nil) + if err != nil { + t.Fatal(err) + } + + images3 := []ApiImages{} + err = json.Unmarshal(body3, &images3) + if err != nil { + t.Fatal(err) + } + + if len(images3) != 0 { + t.Errorf("Excepted 1 image, %d found", len(images3)) + } +} + +func TestGetImagesViz(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + r := httptest.NewRecorder() + + _, err = getImagesViz(srv, r, nil, nil) + if err != nil { + t.Fatal(err) + } + + if r.Code != http.StatusOK { + t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code) + } + + reader := bufio.NewReader(r.Body) + line, err := reader.ReadString('\n') + if err != nil { + t.Fatal(err) + } + if line != "digraph docker {\n" { + t.Errorf("Excepted digraph docker {\n, %s found", line) + } +} + +func TestGetImagesSearch(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + req, err := http.NewRequest("GET", "/images/search?term=redis", nil) + if err != nil { + t.Fatal(err) + } + + body, err := getImagesSearch(srv, nil, req, nil) + if err != nil { + t.Fatal(err) + } + + results := []ApiSearch{} + err = json.Unmarshal(body, &results) + if err != nil { + t.Fatal(err) + } + if len(results) < 2 { + t.Errorf("Excepted at least 2 lines, %d found", len(results)) + } +} + +func TestGetImagesHistory(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + body, err := getImagesHistory(srv, nil, nil, map[string]string{"name": unitTestImageName}) + if err != nil { + t.Fatal(err) + } + + history := []ApiHistory{} + err = json.Unmarshal(body, &history) + if err != nil { + t.Fatal(err) + } + if len(history) != 1 { + t.Errorf("Excepted 1 line, %d found", len(history)) + } +} + +func TestGetImagesByName(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + body, err := getImagesByName(srv, nil, nil, map[string]string{"name": unitTestImageName}) + if err != nil { + t.Fatal(err) + } + + img := &Image{} + + err = json.Unmarshal(body, img) + if err != nil { + t.Fatal(err) + } + if img.Id != GetTestImage(runtime).Id || img.Comment != "Imported from http://get.docker.io/images/busybox" { + t.Errorf("Error inspecting image") + } +} + +func TestGetContainersPs(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + container, err := NewBuilder(runtime).Create(&Config{ + Image: GetTestImage(runtime).Id, + Cmd: []string{"echo", "test"}, + }) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + + req, err := http.NewRequest("GET", "/containers?quiet=1&all=1", nil) + if err != nil { + t.Fatal(err) + } + + body, err := getContainersPs(srv, nil, req, nil) + if err != nil { + t.Fatal(err) + } + containers := []ApiContainers{} + err = json.Unmarshal(body, &containers) + if err != nil { + t.Fatal(err) + } + if len(containers) != 1 { + t.Fatalf("Excepted %d container, %d found", 1, len(containers)) + } + if containers[0].Id != container.ShortId() { + t.Fatalf("Container ID mismatch. Expected: %s, received: %s\n", container.ShortId(), containers[0].Id) + } +} + +func TestGetContainersExport(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + builder := NewBuilder(runtime) + + // Create a container and remove a file + container, err := builder.Create( + &Config{ + Image: GetTestImage(runtime).Id, + Cmd: []string{"touch", "/test"}, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + + if err := container.Run(); err != nil { + t.Fatal(err) + } + + r := httptest.NewRecorder() + + _, err = getContainersExport(srv, r, nil, map[string]string{"name": container.Id}) + if err != nil { + t.Fatal(err) + } + + if r.Code != http.StatusOK { + t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code) + } + + found := false + for tarReader := tar.NewReader(r.Body); ; { + h, err := tarReader.Next() + if err != nil { + if err == io.EOF { + break + } + t.Fatal(err) + } + if h.Name == "./test" { + found = true + break + } + } + if !found { + t.Fatalf("The created test file has not been found in the exported image") + } +} + +func TestGetContainersChanges(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + builder := NewBuilder(runtime) + + // Create a container and remove a file + container, err := builder.Create( + &Config{ + Image: GetTestImage(runtime).Id, + Cmd: []string{"/bin/rm", "/etc/passwd"}, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + + if err := container.Run(); err != nil { + t.Fatal(err) + } + + body, err := getContainersChanges(srv, nil, nil, map[string]string{"name": container.Id}) + if err != nil { + t.Fatal(err) + } + changes := []Change{} + if err := json.Unmarshal(body, &changes); err != nil { + t.Fatal(err) + } + + // Check the changelog + success := false + for _, elem := range changes { + if elem.Path == "/etc/passwd" && elem.Kind == 2 { + success = true + } + } + if !success { + t.Fatalf("/etc/passwd as been removed but is not present in the diff") + } +} + +func TestGetContainersByName(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + builder := NewBuilder(runtime) + + // Create a container and remove a file + container, err := builder.Create( + &Config{ + Image: GetTestImage(runtime).Id, + Cmd: []string{"echo", "test"}, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + + body, err := getContainersByName(srv, nil, nil, map[string]string{"name": container.Id}) + if err != nil { + t.Fatal(err) + } + outContainer := &Container{} + if err := json.Unmarshal(body, outContainer); err != nil { + t.Fatal(err) + } + if outContainer.Id != container.Id { + t.Fatalf("Wrong containers retrieved. Expected %s, recieved %s", container.Id, outContainer.Id) + } +} + +func TestPostAuth(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + authConfigOrig := &auth.AuthConfig{ + Username: "utest", + Email: "utest@yopmail.com", + } + runtime.authConfig = authConfigOrig + + body, err := getAuth(srv, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + + authConfig := &auth.AuthConfig{} + err = json.Unmarshal(body, authConfig) + if err != nil { + t.Fatal(err) + } + + if authConfig.Username != authConfigOrig.Username || authConfig.Email != authConfigOrig.Email { + t.Errorf("The retrieve auth mismatch with the one set.") + } +} + +func TestPostCommit(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + r := httptest.NewRecorder() + + builder := NewBuilder(runtime) + + // Create a container and remove a file + container, err := builder.Create( + &Config{ + Image: GetTestImage(runtime).Id, + Cmd: []string{"touch", "/test"}, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + + if err := container.Run(); err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest("POST", "/commit?repo=testrepo&testtag=tag&container="+container.Id, bytes.NewReader([]byte{})) + if err != nil { + t.Fatal(err) + } + + body, err := postCommit(srv, r, req, nil) + if err != nil { + t.Fatal(err) + } + if r.Code != http.StatusCreated { + t.Fatalf("%d Created expected, received %d\n", http.StatusCreated, r.Code) + } + + apiId := &ApiId{} + if err := json.Unmarshal(body, apiId); err != nil { + t.Fatal(err) + } + if _, err := runtime.graph.Get(apiId.Id); err != nil { + t.Fatalf("The image has not been commited") + } +} + +func TestPostBuild(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + stdin, stdinPipe := io.Pipe() + stdout, stdoutPipe := io.Pipe() + + c1 := make(chan struct{}) + go func() { + r := &hijackTester{ + ResponseRecorder: httptest.NewRecorder(), + in: stdin, + out: stdoutPipe, + } + + body, err := postBuild(srv, r, nil, nil) + close(c1) + if err != nil { + t.Fatal(err) + } + if body != nil { + t.Fatalf("No body expected, received: %s\n", body) + } + }() + + // Acknowledge hijack + setTimeout(t, "hijack acknowledge timed out", 2*time.Second, func() { + stdout.Read([]byte{}) + stdout.Read(make([]byte, 4096)) + }) + + setTimeout(t, "read/write assertion timed out", 2*time.Second, func() { + if err := assertPipe("from docker-ut\n", "FROM docker-ut", stdout, stdinPipe, 15); err != nil { + t.Fatal(err) + } + }) + + // Close pipes (client disconnects) + if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil { + t.Fatal(err) + } + + // Wait for build to finish, the client disconnected, therefore, Build finished his job + setTimeout(t, "Waiting for CmdBuild timed out", 2*time.Second, func() { + <-c1 + }) + +} + +func TestPostImagesCreate(t *testing.T) { + // FIXME: Use the staging in order to perform tests + + // runtime, err := newTestRuntime() + // if err != nil { + // t.Fatal(err) + // } + // defer nuke(runtime) + + // srv := &Server{runtime: runtime} + + // stdin, stdinPipe := io.Pipe() + // stdout, stdoutPipe := io.Pipe() + + // c1 := make(chan struct{}) + // go func() { + // defer close(c1) + + // r := &hijackTester{ + // ResponseRecorder: httptest.NewRecorder(), + // in: stdin, + // out: stdoutPipe, + // } + + // req, err := http.NewRequest("POST", "/images/create?fromImage="+unitTestImageName, bytes.NewReader([]byte{})) + // if err != nil { + // t.Fatal(err) + // } + + // body, err := postImagesCreate(srv, r, req, nil) + // if err != nil { + // t.Fatal(err) + // } + // if body != nil { + // t.Fatalf("No body expected, received: %s\n", body) + // } + // }() + + // // Acknowledge hijack + // setTimeout(t, "hijack acknowledge timed out", 2*time.Second, func() { + // stdout.Read([]byte{}) + // stdout.Read(make([]byte, 4096)) + // }) + + // setTimeout(t, "Waiting for imagesCreate output", 5*time.Second, func() { + // reader := bufio.NewReader(stdout) + // line, err := reader.ReadString('\n') + // if err != nil { + // t.Fatal(err) + // } + // if !strings.HasPrefix(line, "Pulling repository d from") { + // t.Fatalf("Expected Pulling repository docker-ut from..., found %s", line) + // } + // }) + + // // Close pipes (client disconnects) + // if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil { + // t.Fatal(err) + // } + + // // Wait for imagesCreate to finish, the client disconnected, therefore, Create finished his job + // setTimeout(t, "Waiting for imagesCreate timed out", 10*time.Second, func() { + // <-c1 + // }) +} + +// func TestPostImagesInsert(t *testing.T) { +// //FIXME: Implement this test (or remove this endpoint) +// t.Log("Test not implemented") +// } + +func TestPostImagesPush(t *testing.T) { + //FIXME: Use staging in order to perform tests + // runtime, err := newTestRuntime() + // if err != nil { + // t.Fatal(err) + // } + // defer nuke(runtime) + + // srv := &Server{runtime: runtime} + + // stdin, stdinPipe := io.Pipe() + // stdout, stdoutPipe := io.Pipe() + + // c1 := make(chan struct{}) + // go func() { + // r := &hijackTester{ + // ResponseRecorder: httptest.NewRecorder(), + // in: stdin, + // out: stdoutPipe, + // } + + // req, err := http.NewRequest("POST", "/images/docker-ut/push", bytes.NewReader([]byte{})) + // if err != nil { + // t.Fatal(err) + // } + + // body, err := postImagesPush(srv, r, req, map[string]string{"name": "docker-ut"}) + // close(c1) + // if err != nil { + // t.Fatal(err) + // } + // if body != nil { + // t.Fatalf("No body expected, received: %s\n", body) + // } + // }() + + // // Acknowledge hijack + // setTimeout(t, "hijack acknowledge timed out", 2*time.Second, func() { + // stdout.Read([]byte{}) + // stdout.Read(make([]byte, 4096)) + // }) + + // setTimeout(t, "Waiting for imagesCreate output", 5*time.Second, func() { + // reader := bufio.NewReader(stdout) + // line, err := reader.ReadString('\n') + // if err != nil { + // t.Fatal(err) + // } + // if !strings.HasPrefix(line, "Processing checksum") { + // t.Fatalf("Processing checksum..., found %s", line) + // } + // }) + + // // Close pipes (client disconnects) + // if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil { + // t.Fatal(err) + // } + + // // Wait for imagesPush to finish, the client disconnected, therefore, Push finished his job + // setTimeout(t, "Waiting for imagesPush timed out", 10*time.Second, func() { + // <-c1 + // }) +} + +func TestPostImagesTag(t *testing.T) { + // FIXME: Use staging in order to perform tests + + // runtime, err := newTestRuntime() + // if err != nil { + // t.Fatal(err) + // } + // defer nuke(runtime) + + // srv := &Server{runtime: runtime} + + // r := httptest.NewRecorder() + + // req, err := http.NewRequest("POST", "/images/docker-ut/tag?repo=testrepo&tag=testtag", bytes.NewReader([]byte{})) + // if err != nil { + // t.Fatal(err) + // } + + // body, err := postImagesTag(srv, r, req, map[string]string{"name": "docker-ut"}) + // if err != nil { + // t.Fatal(err) + // } + + // if body != nil { + // t.Fatalf("No body expected, received: %s\n", body) + // } + // if r.Code != http.StatusCreated { + // t.Fatalf("%d Created expected, received %d\n", http.StatusCreated, r.Code) + // } +} + +func TestPostContainersCreate(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + r := httptest.NewRecorder() + + configJson, err := json.Marshal(&Config{ + Image: GetTestImage(runtime).Id, + Memory: 33554432, + Cmd: []string{"touch", "/test"}, + }) + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest("POST", "/containers/create", bytes.NewReader(configJson)) + if err != nil { + t.Fatal(err) + } + + body, err := postContainersCreate(srv, r, req, nil) + if err != nil { + t.Fatal(err) + } + if r.Code != http.StatusCreated { + t.Fatalf("%d Created expected, received %d\n", http.StatusCreated, r.Code) + } + + apiRun := &ApiRun{} + if err := json.Unmarshal(body, apiRun); err != nil { + t.Fatal(err) + } + + container := srv.runtime.Get(apiRun.Id) + if container == nil { + t.Fatalf("Container not created") + } + + if err := container.Run(); err != nil { + t.Fatal(err) + } + + if _, err := os.Stat(path.Join(container.rwPath(), "test")); err != nil { + if os.IsNotExist(err) { + Debugf("Err: %s", err) + t.Fatalf("The test file has not been created") + } + t.Fatal(err) + } +} + +func TestPostContainersKill(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + container, err := NewBuilder(runtime).Create( + &Config{ + Image: GetTestImage(runtime).Id, + Cmd: []string{"/bin/cat"}, + OpenStdin: true, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + + if err := container.Start(); err != nil { + t.Fatal(err) + } + + // Give some time to the process to start + container.WaitTimeout(500 * time.Millisecond) + + if !container.State.Running { + t.Errorf("Container should be running") + } + + r := httptest.NewRecorder() + + body, err := postContainersKill(srv, r, nil, map[string]string{"name": container.Id}) + if err != nil { + t.Fatal(err) + } + if body != nil { + t.Fatalf("No body expected, received: %s\n", body) + } + if r.Code != http.StatusNoContent { + t.Fatalf("%d NO CONTENT expected, received %d\n", http.StatusNoContent, r.Code) + } + if container.State.Running { + t.Fatalf("The container hasn't been killed") + } +} + +func TestPostContainersRestart(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + container, err := NewBuilder(runtime).Create( + &Config{ + Image: GetTestImage(runtime).Id, + Cmd: []string{"/bin/cat"}, + OpenStdin: true, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + + if err := container.Start(); err != nil { + t.Fatal(err) + } + + // Give some time to the process to start + container.WaitTimeout(500 * time.Millisecond) + + if !container.State.Running { + t.Errorf("Container should be running") + } + + r := httptest.NewRecorder() + + req, err := http.NewRequest("POST", "/containers/"+container.Id+"/restart?t=1", bytes.NewReader([]byte{})) + if err != nil { + t.Fatal(err) + } + body, err := postContainersRestart(srv, r, req, map[string]string{"name": container.Id}) + if err != nil { + t.Fatal(err) + } + if body != nil { + t.Fatalf("No body expected, received: %s\n", body) + } + if r.Code != http.StatusNoContent { + t.Fatalf("%d NO CONTENT expected, received %d\n", http.StatusNoContent, r.Code) + } + + // Give some time to the process to restart + container.WaitTimeout(500 * time.Millisecond) + + if !container.State.Running { + t.Fatalf("Container should be running") + } + + if err := container.Kill(); err != nil { + t.Fatal(err) + } +} + +func TestPostContainersStart(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + container, err := NewBuilder(runtime).Create( + &Config{ + Image: GetTestImage(runtime).Id, + Cmd: []string{"/bin/cat"}, + OpenStdin: true, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + + r := httptest.NewRecorder() + + body, err := postContainersStart(srv, r, nil, map[string]string{"name": container.Id}) + if err != nil { + t.Fatal(err) + } + if body != nil { + t.Fatalf("No body expected, received: %s\n", body) + } + if r.Code != http.StatusNoContent { + t.Fatalf("%d NO CONTENT expected, received %d\n", http.StatusNoContent, r.Code) + } + + // Give some time to the process to start + container.WaitTimeout(500 * time.Millisecond) + + if !container.State.Running { + t.Errorf("Container should be running") + } + + if _, err = postContainersStart(srv, r, nil, map[string]string{"name": container.Id}); err == nil { + t.Fatalf("A running containter should be able to be started") + } + + if err := container.Kill(); err != nil { + t.Fatal(err) + } +} + +func TestPostContainersStop(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + container, err := NewBuilder(runtime).Create( + &Config{ + Image: GetTestImage(runtime).Id, + Cmd: []string{"/bin/cat"}, + OpenStdin: true, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + + if err := container.Start(); err != nil { + t.Fatal(err) + } + + // Give some time to the process to start + container.WaitTimeout(500 * time.Millisecond) + + if !container.State.Running { + t.Errorf("Container should be running") + } + + r := httptest.NewRecorder() + + // Note: as it is a POST request, it requires a body. + req, err := http.NewRequest("POST", "/containers/"+container.Id+"/stop?t=1", bytes.NewReader([]byte{})) + if err != nil { + t.Fatal(err) + } + body, err := postContainersStop(srv, r, req, map[string]string{"name": container.Id}) + if err != nil { + t.Fatal(err) + } + if body != nil { + t.Fatalf("No body expected, received: %s\n", body) + } + if r.Code != http.StatusNoContent { + t.Fatalf("%d NO CONTENT expected, received %d\n", http.StatusNoContent, r.Code) + } + if container.State.Running { + t.Fatalf("The container hasn't been stopped") + } +} + +func TestPostContainersWait(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + container, err := NewBuilder(runtime).Create( + &Config{ + Image: GetTestImage(runtime).Id, + Cmd: []string{"/bin/sleep", "1"}, + OpenStdin: true, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + + if err := container.Start(); err != nil { + t.Fatal(err) + } + + setTimeout(t, "Wait timed out", 3*time.Second, func() { + body, err := postContainersWait(srv, nil, nil, map[string]string{"name": container.Id}) + if err != nil { + t.Fatal(err) + } + apiWait := &ApiWait{} + if err := json.Unmarshal(body, apiWait); err != nil { + t.Fatal(err) + } + if apiWait.StatusCode != 0 { + t.Fatalf("Non zero exit code for sleep: %d\n", apiWait.StatusCode) + } + }) + + if container.State.Running { + t.Fatalf("The container should be stopped after wait") + } +} + +func TestPostContainersAttach(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + container, err := NewBuilder(runtime).Create( + &Config{ + Image: GetTestImage(runtime).Id, + Cmd: []string{"/bin/cat"}, + OpenStdin: true, + }, + ) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + + // Start the process + if err := container.Start(); err != nil { + t.Fatal(err) + } + + stdin, stdinPipe := io.Pipe() + stdout, stdoutPipe := io.Pipe() + + // Attach to it + c1 := make(chan struct{}) + go func() { + // We're simulating a disconnect so the return value doesn't matter. What matters is the + // fact that CmdAttach returns. + + r := &hijackTester{ + ResponseRecorder: httptest.NewRecorder(), + in: stdin, + out: stdoutPipe, + } + + req, err := http.NewRequest("POST", "/containers/"+container.Id+"/attach?stream=1&stdin=1&stdout=1&stderr=1", bytes.NewReader([]byte{})) + if err != nil { + t.Fatal(err) + } + + body, err := postContainersAttach(srv, r, req, map[string]string{"name": container.Id}) + close(c1) + if err != nil { + t.Fatal(err) + } + if body != nil { + t.Fatalf("No body expected, received: %s\n", body) + } + }() + + // Acknowledge hijack + setTimeout(t, "hijack acknowledge timed out", 2*time.Second, func() { + stdout.Read([]byte{}) + stdout.Read(make([]byte, 4096)) + }) + + setTimeout(t, "read/write assertion timed out", 2*time.Second, func() { + if err := assertPipe("hello\n", "hello", stdout, stdinPipe, 15); err != nil { + t.Fatal(err) + } + }) + + // Close pipes (client disconnects) + if err := closeWrap(stdin, stdinPipe, stdout, stdoutPipe); err != nil { + t.Fatal(err) + } + + // Wait for attach to finish, the client disconnected, therefore, Attach finished his job + setTimeout(t, "Waiting for CmdAttach timed out", 2*time.Second, func() { + <-c1 + }) + + // We closed stdin, expect /bin/cat to still be running + // Wait a little bit to make sure container.monitor() did his thing + err = container.WaitTimeout(500 * time.Millisecond) + if err == nil || !container.State.Running { + t.Fatalf("/bin/cat is not running after closing stdin") + } + + // Try to avoid the timeoout in destroy. Best effort, don't check error + cStdin, _ := container.StdinPipe() + cStdin.Close() + container.Wait() +} + +// FIXME: Test deleting running container +// FIXME: Test deleting container with volume +// FIXME: Test deleting volume in use by other container +func TestDeleteContainers(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + container, err := NewBuilder(runtime).Create(&Config{ + Image: GetTestImage(runtime).Id, + Cmd: []string{"touch", "/test"}, + }) + if err != nil { + t.Fatal(err) + } + defer runtime.Destroy(container) + + if err := container.Run(); err != nil { + t.Fatal(err) + } + + r := httptest.NewRecorder() + + req, err := http.NewRequest("DELETE", "/containers/"+container.Id, nil) + if err != nil { + t.Fatal(err) + } + + body, err := deleteContainers(srv, r, req, map[string]string{"name": container.Id}) + if err != nil { + t.Fatal(err) + } + if body != nil { + t.Fatalf("No body expected, received: %s\n", body) + } + if r.Code != http.StatusNoContent { + t.Fatalf("%d NO CONTENT expected, received %d\n", http.StatusNoContent, r.Code) + } + + if c := runtime.Get(container.Id); c != nil { + t.Fatalf("The container as not been deleted") + } + + if _, err := os.Stat(path.Join(container.rwPath(), "test")); err == nil { + t.Fatalf("The test file has not been deleted") + } +} + +func TestDeleteImages(t *testing.T) { + //FIXME: Implement this test + t.Log("Test not implemented") +} + +// Mocked types for tests +type NopConn struct { + io.ReadCloser + io.Writer +} + +func (c *NopConn) LocalAddr() net.Addr { return nil } +func (c *NopConn) RemoteAddr() net.Addr { return nil } +func (c *NopConn) SetDeadline(t time.Time) error { return nil } +func (c *NopConn) SetReadDeadline(t time.Time) error { return nil } +func (c *NopConn) SetWriteDeadline(t time.Time) error { return nil } + +type hijackTester struct { + *httptest.ResponseRecorder + in io.ReadCloser + out io.Writer +} + +func (t *hijackTester) Hijack() (net.Conn, *bufio.ReadWriter, error) { + bufrw := bufio.NewReadWriter(bufio.NewReader(t.in), bufio.NewWriter(t.out)) + conn := &NopConn{ + ReadCloser: t.in, + Writer: t.out, + } + return conn, bufrw, nil +} diff --git a/components/engine/builder.go b/components/engine/builder.go index b22c853730..5c51d62b9e 100644 --- a/components/engine/builder.go +++ b/components/engine/builder.go @@ -75,7 +75,7 @@ func (builder *Builder) Create(config *Config) (*Container, error) { builder.mergeConfig(config, img.Config) } - if config.Cmd == nil { + if config.Cmd == nil || len(config.Cmd) == 0 { return nil, fmt.Errorf("No command specified") } @@ -269,7 +269,7 @@ func (builder *Builder) Build(dockerfile io.Reader, stdout io.Writer) (*Image, e if image == nil { return nil, fmt.Errorf("Please provide a source image with `from` prior to run") } - config, err := ParseRun([]string{image.Id, "/bin/sh", "-c", arguments}, nil, builder.runtime.capabilities) + config, _, err := ParseRun([]string{image.Id, "/bin/sh", "-c", arguments}, builder.runtime.capabilities) if err != nil { return nil, err } @@ -416,7 +416,7 @@ func (builder *Builder) Build(dockerfile io.Reader, stdout io.Writer) (*Image, e } defer file.Body.Close() - config, err := ParseRun([]string{base.Id, "echo", "insert", sourceUrl, destPath}, nil, builder.runtime.capabilities) + config, _, err := ParseRun([]string{base.Id, "echo", "insert", sourceUrl, destPath}, builder.runtime.capabilities) if err != nil { return nil, err } diff --git a/components/engine/commands.go b/components/engine/commands.go index dba8cd8572..dc3f8c4e87 100644 --- a/components/engine/commands.go +++ b/components/engine/commands.go @@ -3,16 +3,18 @@ package docker import ( "bytes" "encoding/json" + "flag" "fmt" "github.com/dotcloud/docker/auth" - "github.com/dotcloud/docker/rcli" - "github.com/shin-/cookiejar" + "github.com/dotcloud/docker/term" "io" - "log" + "io/ioutil" + "net" "net/http" + "net/http/httputil" "net/url" + "os" "path/filepath" - "runtime" "strconv" "strings" "text/tabwriter" @@ -20,18 +22,57 @@ import ( "unicode" ) -const VERSION = "0.3.0" +const VERSION = "0.3.2" var ( GIT_COMMIT string ) -func (srv *Server) Name() string { - return "docker" +func ParseCommands(args ...string) error { + + cmds := map[string]func(args ...string) error{ + "attach": CmdAttach, + "build": CmdBuild, + "commit": CmdCommit, + "diff": CmdDiff, + "export": CmdExport, + "images": CmdImages, + "info": CmdInfo, + "insert": CmdInsert, + "inspect": CmdInspect, + "import": CmdImport, + "history": CmdHistory, + "kill": CmdKill, + "login": CmdLogin, + "logs": CmdLogs, + "port": CmdPort, + "ps": CmdPs, + "pull": CmdPull, + "push": CmdPush, + "restart": CmdRestart, + "rm": CmdRm, + "rmi": CmdRmi, + "run": CmdRun, + "tag": CmdTag, + "search": CmdSearch, + "start": CmdStart, + "stop": CmdStop, + "version": CmdVersion, + "wait": CmdWait, + } + + if len(args) > 0 { + cmd, exists := cmds[args[0]] + if !exists { + fmt.Println("Error: Command not found:", args[0]) + return cmdHelp(args...) + } + return cmd(args[1:]...) + } + return cmdHelp(args...) } -// FIXME: Stop violating DRY by repeating usage here and in Subcmd declarations -func (srv *Server) Help() string { +func cmdHelp(args ...string) error { help := "Usage: docker COMMAND [arg...]\n\nA self-sufficient runtime for linux containers.\n\nCommands:\n" for _, cmd := range [][]string{ {"attach", "Attach to a running container"}, @@ -65,12 +106,12 @@ func (srv *Server) Help() string { } { help += fmt.Sprintf(" %-10.10s%s\n", cmd[0], cmd[1]) } - return help + fmt.Println(help) + return nil } -func (srv *Server) CmdInsert(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { - stdout.Flush() - cmd := rcli.Subcmd(stdout, "insert", "IMAGE URL PATH", "Insert a file from URL in the IMAGE at PATH") +func CmdInsert(args ...string) error { + cmd := Subcmd("insert", "IMAGE URL PATH", "Insert a file from URL in the IMAGE at PATH") if err := cmd.Parse(args); err != nil { return nil } @@ -78,67 +119,33 @@ func (srv *Server) CmdInsert(stdin io.ReadCloser, stdout rcli.DockerConn, args . cmd.Usage() return nil } - imageId := cmd.Arg(0) - url := cmd.Arg(1) - path := cmd.Arg(2) - img, err := srv.runtime.repositories.LookupImage(imageId) - if err != nil { - return err - } - file, err := Download(url, stdout) - if err != nil { - return err - } - defer file.Body.Close() + v := url.Values{} + v.Set("url", cmd.Arg(1)) + v.Set("path", cmd.Arg(2)) - config, err := ParseRun([]string{img.Id, "echo", "insert", url, path}, nil, srv.runtime.capabilities) + err := hijack("POST", "/images/"+cmd.Arg(0)+"?"+v.Encode(), false) if err != nil { return err } - - b := NewBuilder(srv.runtime) - c, err := b.Create(config) - if err != nil { - return err - } - - if err := c.Inject(ProgressReader(file.Body, int(file.ContentLength), stdout, "Downloading %v/%v (%v)"), path); err != nil { - return err - } - // FIXME: Handle custom repo, tag comment, author - img, err = b.Commit(c, "", "", img.Comment, img.Author, nil) - if err != nil { - return err - } - fmt.Fprintf(stdout, "%s\n", img.Id) return nil } -func (srv *Server) CmdBuild(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { - stdout.Flush() - cmd := rcli.Subcmd(stdout, "build", "-", "Build a container from Dockerfile via stdin") +func CmdBuild(args ...string) error { + cmd := Subcmd("build", "-", "Build an image from Dockerfile via stdin") if err := cmd.Parse(args); err != nil { return nil } - img, err := NewBuilder(srv.runtime).Build(stdin, stdout) + + err := hijack("POST", "/build", false) if err != nil { return err } - fmt.Fprintf(stdout, "%s\n", img.ShortId()) return nil } // 'docker login': login / register a user to registry service. -func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { - // Read a line on raw terminal with support for simple backspace - // sequences and echo. - // - // This function is necessary because the login command must be done in a - // raw terminal for two reasons: - // - we have to read a password (without echoing it); - // - the rcli "protocol" only supports cannonical and raw modes and you - // can't tune it once the command as been started. +func CmdLogin(args ...string) error { var readStringOnRawTerminal = func(stdin io.Reader, stdout io.Writer, echo bool) string { char := make([]byte, 1) buffer := make([]byte, 64) @@ -181,56 +188,79 @@ func (srv *Server) CmdLogin(stdin io.ReadCloser, stdout rcli.DockerConn, args .. return readStringOnRawTerminal(stdin, stdout, false) } - stdout.SetOptionRawTerminal() + oldState, err := SetRawTerminal() + if err != nil { + return err + } else { + defer RestoreTerminal(oldState) + } - cmd := rcli.Subcmd(stdout, "login", "", "Register or Login to the docker registry server") + cmd := Subcmd("login", "", "Register or Login to the docker registry server") if err := cmd.Parse(args); err != nil { return nil } + body, _, err := call("GET", "/auth", nil) + if err != nil { + return err + } + + var out auth.AuthConfig + err = json.Unmarshal(body, &out) + if err != nil { + return err + } + var username string var password string var email string - fmt.Fprint(stdout, "Username (", srv.runtime.authConfig.Username, "): ") - username = readAndEchoString(stdin, stdout) + fmt.Print("Username (", out.Username, "): ") + username = readAndEchoString(os.Stdin, os.Stdout) if username == "" { - username = srv.runtime.authConfig.Username + username = out.Username } - if username != srv.runtime.authConfig.Username { - fmt.Fprint(stdout, "Password: ") - password = readString(stdin, stdout) + if username != out.Username { + fmt.Print("Password: ") + password = readString(os.Stdin, os.Stdout) if password == "" { return fmt.Errorf("Error : Password Required") } - fmt.Fprint(stdout, "Email (", srv.runtime.authConfig.Email, "): ") - email = readAndEchoString(stdin, stdout) + fmt.Print("Email (", out.Email, "): ") + email = readAndEchoString(os.Stdin, os.Stdout) if email == "" { - email = srv.runtime.authConfig.Email + email = out.Email } } else { - password = srv.runtime.authConfig.Password - email = srv.runtime.authConfig.Email + email = out.Email } - newAuthConfig := auth.NewAuthConfig(username, password, email, srv.runtime.root) - status, err := auth.Login(newAuthConfig) + + out.Username = username + out.Password = password + out.Email = email + + body, _, err = call("POST", "/auth", out) if err != nil { - fmt.Fprintf(stdout, "Error: %s\r\n", err) - } else { - srv.runtime.graph.getHttpClient().Jar = cookiejar.NewCookieJar() - srv.runtime.authConfig = newAuthConfig + return err } - if status != "" { - fmt.Fprint(stdout, status) + + var out2 ApiAuth + err = json.Unmarshal(body, &out2) + if err != nil { + return err + } + if out2.Status != "" { + RestoreTerminal(oldState) + fmt.Print(out2.Status) } return nil } // 'docker wait': block until a container stops -func (srv *Server) CmdWait(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, "wait", "CONTAINER [CONTAINER...]", "Block until a container stops, then print its exit code.") +func CmdWait(args ...string) error { + cmd := Subcmd("wait", "CONTAINER [CONTAINER...]", "Block until a container stops, then print its exit code.") if err := cmd.Parse(args); err != nil { return nil } @@ -239,39 +269,24 @@ func (srv *Server) CmdWait(stdin io.ReadCloser, stdout io.Writer, args ...string return nil } for _, name := range cmd.Args() { - if container := srv.runtime.Get(name); container != nil { - fmt.Fprintln(stdout, container.Wait()) + body, _, err := call("POST", "/containers/"+name+"/wait", nil) + if err != nil { + fmt.Printf("%s", err) } else { - return fmt.Errorf("No such container: %s", name) + var out ApiWait + err = json.Unmarshal(body, &out) + if err != nil { + return err + } + fmt.Println(out.StatusCode) } } return nil } // 'docker version': show version information -func (srv *Server) CmdVersion(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - fmt.Fprintf(stdout, "Version: %s\n", VERSION) - fmt.Fprintf(stdout, "Git Commit: %s\n", GIT_COMMIT) - fmt.Fprintf(stdout, "Kernel: %s\n", srv.runtime.kernelVersion) - if !srv.runtime.capabilities.MemoryLimit { - fmt.Fprintf(stdout, "WARNING: No memory limit support\n") - } - if !srv.runtime.capabilities.SwapLimit { - fmt.Fprintf(stdout, "WARNING: No swap limit support\n") - } - return nil -} - -// 'docker info': display system-wide information. -func (srv *Server) CmdInfo(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - images, _ := srv.runtime.graph.All() - var imgcount int - if images == nil { - imgcount = 0 - } else { - imgcount = len(images) - } - cmd := rcli.Subcmd(stdout, "info", "", "Display system-wide information.") +func CmdVersion(args ...string) error { + cmd := Subcmd("version", "", "Show the docker version information.") if err := cmd.Parse(args); err != nil { return nil } @@ -279,148 +294,62 @@ func (srv *Server) CmdInfo(stdin io.ReadCloser, stdout io.Writer, args ...string cmd.Usage() return nil } - fmt.Fprintf(stdout, "containers: %d\nversion: %s\nimages: %d\n", - len(srv.runtime.List()), - VERSION, - imgcount) - if !rcli.DEBUG_FLAG { - return nil - } - fmt.Fprintln(stdout, "debug mode enabled") - fmt.Fprintf(stdout, "fds: %d\ngoroutines: %d\n", getTotalUsedFds(), runtime.NumGoroutine()) - return nil -} - -func (srv *Server) CmdStop(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, "stop", "[OPTIONS] CONTAINER [CONTAINER...]", "Stop a running container") - nSeconds := cmd.Int("t", 10, "wait t seconds before killing the container") - if err := cmd.Parse(args); err != nil { - return nil - } - if cmd.NArg() < 1 { - cmd.Usage() - return nil - } - for _, name := range cmd.Args() { - if container := srv.runtime.Get(name); container != nil { - if err := container.Stop(*nSeconds); err != nil { - return err - } - fmt.Fprintln(stdout, container.ShortId()) - } else { - return fmt.Errorf("No such container: %s", name) - } - } - return nil -} - -func (srv *Server) CmdRestart(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, "restart", "CONTAINER [CONTAINER...]", "Restart a running container") - nSeconds := cmd.Int("t", 10, "wait t seconds before killing the container") - if err := cmd.Parse(args); err != nil { - return nil - } - if cmd.NArg() < 1 { - cmd.Usage() - return nil - } - for _, name := range cmd.Args() { - if container := srv.runtime.Get(name); container != nil { - if err := container.Restart(*nSeconds); err != nil { - return err - } - fmt.Fprintln(stdout, container.ShortId()) - } else { - return fmt.Errorf("No such container: %s", name) - } - } - return nil -} - -func (srv *Server) CmdStart(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, "start", "CONTAINER [CONTAINER...]", "Start a stopped container") - if err := cmd.Parse(args); err != nil { - return nil - } - if cmd.NArg() < 1 { - cmd.Usage() - return nil - } - for _, name := range cmd.Args() { - if container := srv.runtime.Get(name); container != nil { - if err := container.Start(); err != nil { - return err - } - fmt.Fprintln(stdout, container.ShortId()) - } else { - return fmt.Errorf("No such container: %s", name) - } - } - return nil -} - -func (srv *Server) CmdInspect(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, "inspect", "CONTAINER", "Return low-level information on a container") - if err := cmd.Parse(args); err != nil { - return nil - } - if cmd.NArg() < 1 { - cmd.Usage() - return nil - } - name := cmd.Arg(0) - var obj interface{} - if container := srv.runtime.Get(name); container != nil { - obj = container - } else if image, err := srv.runtime.repositories.LookupImage(name); err == nil && image != nil { - obj = image - } else { - // No output means the object does not exist - // (easier to script since stdout and stderr are not differentiated atm) - return nil - } - data, err := json.Marshal(obj) + body, _, err := call("GET", "/version", nil) if err != nil { return err } - indented := new(bytes.Buffer) - if err = json.Indent(indented, data, "", " "); err != nil { + + var out ApiVersion + err = json.Unmarshal(body, &out) + if err != nil { + Debugf("Error unmarshal: body: %s, err: %s\n", body, err) return err } - if _, err := io.Copy(stdout, indented); err != nil { - return err + fmt.Println("Version:", out.Version) + fmt.Println("Git Commit:", out.GitCommit) + if !out.MemoryLimit { + fmt.Println("WARNING: No memory limit support") } - stdout.Write([]byte{'\n'}) + if !out.SwapLimit { + fmt.Println("WARNING: No swap limit support") + } + return nil } -func (srv *Server) CmdPort(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, "port", "CONTAINER PRIVATE_PORT", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT") +// 'docker info': display system-wide information. +func CmdInfo(args ...string) error { + cmd := Subcmd("info", "", "Display system-wide information") if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() != 2 { + if cmd.NArg() > 0 { cmd.Usage() return nil } - name := cmd.Arg(0) - privatePort := cmd.Arg(1) - if container := srv.runtime.Get(name); container == nil { - return fmt.Errorf("No such container: %s", name) - } else { - if frontend, exists := container.NetworkSettings.PortMapping[privatePort]; !exists { - return fmt.Errorf("No private port '%s' allocated on %s", privatePort, name) - } else { - fmt.Fprintln(stdout, frontend) - } + + body, _, err := call("GET", "/info", nil) + if err != nil { + return err + } + + var out ApiInfo + err = json.Unmarshal(body, &out) + if err != nil { + return err + } + fmt.Printf("containers: %d\nversion: %s\nimages: %d\nGo version: %s\n", out.Containers, out.Version, out.Images, out.GoVersion) + if out.Debug { + fmt.Println("debug mode enabled") + fmt.Printf("fds: %d\ngoroutines: %d\n", out.NFd, out.NGoroutines) } return nil } -// 'docker rmi IMAGE' removes all images with the name IMAGE -func (srv *Server) CmdRmi(stdin io.ReadCloser, stdout io.Writer, args ...string) (err error) { - cmd := rcli.Subcmd(stdout, "rmimage", "IMAGE [IMAGE...]", "Remove an image") +func CmdStop(args ...string) error { + cmd := Subcmd("stop", "[OPTIONS] CONTAINER [CONTAINER...]", "Stop a running container") + nSeconds := cmd.Int("t", 10, "wait t seconds before killing the container") if err := cmd.Parse(args); err != nil { return nil } @@ -428,20 +357,69 @@ func (srv *Server) CmdRmi(stdin io.ReadCloser, stdout io.Writer, args ...string) cmd.Usage() return nil } + + v := url.Values{} + v.Set("t", strconv.Itoa(*nSeconds)) + for _, name := range cmd.Args() { - img, err := srv.runtime.repositories.LookupImage(name) + _, _, err := call("POST", "/containers/"+name+"/stop?"+v.Encode(), nil) if err != nil { - return err - } - if err := srv.runtime.graph.Delete(img.Id); err != nil { - return err + fmt.Printf("%s", err) + } else { + fmt.Println(name) } } return nil } -func (srv *Server) CmdHistory(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, "history", "IMAGE", "Show the history of an image") +func CmdRestart(args ...string) error { + cmd := Subcmd("restart", "[OPTIONS] CONTAINER [CONTAINER...]", "Restart a running container") + nSeconds := cmd.Int("t", 10, "wait t seconds before killing the container") + if err := cmd.Parse(args); err != nil { + return nil + } + if cmd.NArg() < 1 { + cmd.Usage() + return nil + } + + v := url.Values{} + v.Set("t", strconv.Itoa(*nSeconds)) + + for _, name := range cmd.Args() { + _, _, err := call("POST", "/containers/"+name+"/restart?"+v.Encode(), nil) + if err != nil { + fmt.Printf("%s", err) + } else { + fmt.Println(name) + } + } + return nil +} + +func CmdStart(args ...string) error { + cmd := Subcmd("start", "CONTAINER [CONTAINER...]", "Restart a stopped container") + if err := cmd.Parse(args); err != nil { + return nil + } + if cmd.NArg() < 1 { + cmd.Usage() + return nil + } + + for _, name := range args { + _, _, err := call("POST", "/containers/"+name+"/start", nil) + if err != nil { + fmt.Printf("%s", err) + } else { + fmt.Println(name) + } + } + return nil +} + +func CmdInspect(args ...string) error { + cmd := Subcmd("inspect", "CONTAINER|IMAGE", "Return low-level information on a container/image") if err := cmd.Parse(args); err != nil { return nil } @@ -449,25 +427,106 @@ func (srv *Server) CmdHistory(stdin io.ReadCloser, stdout io.Writer, args ...str cmd.Usage() return nil } - image, err := srv.runtime.repositories.LookupImage(cmd.Arg(0)) + obj, _, err := call("GET", "/containers/"+cmd.Arg(0)+"/json", nil) + if err != nil { + obj, _, err = call("GET", "/images/"+cmd.Arg(0)+"/json", nil) + if err != nil { + return err + } + } + + indented := new(bytes.Buffer) + if err = json.Indent(indented, obj, "", " "); err != nil { + return err + } + if _, err := io.Copy(os.Stdout, indented); err != nil { + return err + } + return nil +} + +func CmdPort(args ...string) error { + cmd := Subcmd("port", "CONTAINER PRIVATE_PORT", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT") + if err := cmd.Parse(args); err != nil { + return nil + } + if cmd.NArg() != 2 { + cmd.Usage() + return nil + } + + body, _, err := call("GET", "/containers/"+cmd.Arg(0)+"/json", nil) if err != nil { return err } - w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0) - defer w.Flush() - fmt.Fprintln(w, "ID\tCREATED\tCREATED BY") - return image.WalkHistory(func(img *Image) error { - fmt.Fprintf(w, "%s\t%s\t%s\n", - srv.runtime.repositories.ImageName(img.ShortId()), - HumanDuration(time.Now().Sub(img.Created))+" ago", - strings.Join(img.ContainerConfig.Cmd, " "), - ) - return nil - }) + var out Container + err = json.Unmarshal(body, &out) + if err != nil { + return err + } + + if frontend, exists := out.NetworkSettings.PortMapping[cmd.Arg(1)]; exists { + fmt.Println(frontend) + } else { + return fmt.Errorf("error: No private port '%s' allocated on %s", cmd.Arg(1), cmd.Arg(0)) + } + return nil } -func (srv *Server) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, "rm", "[OPTIONS] CONTAINER [CONTAINER...]", "Remove a container") +// 'docker rmi IMAGE' removes all images with the name IMAGE +func CmdRmi(args ...string) error { + cmd := Subcmd("rmi", "IMAGE [IMAGE...]", "Remove an image") + if err := cmd.Parse(args); err != nil { + return nil + } + if cmd.NArg() < 1 { + cmd.Usage() + return nil + } + + for _, name := range cmd.Args() { + _, _, err := call("DELETE", "/images/"+name, nil) + if err != nil { + fmt.Printf("%s", err) + } else { + fmt.Println(name) + } + } + return nil +} + +func CmdHistory(args ...string) error { + cmd := Subcmd("history", "IMAGE", "Show the history of an image") + if err := cmd.Parse(args); err != nil { + return nil + } + if cmd.NArg() != 1 { + cmd.Usage() + return nil + } + + body, _, err := call("GET", "/images/"+cmd.Arg(0)+"/history", nil) + if err != nil { + return err + } + + var outs []ApiHistory + err = json.Unmarshal(body, &outs) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) + fmt.Fprintln(w, "ID\tCREATED\tCREATED BY") + + for _, out := range outs { + fmt.Fprintf(w, "%s\t%s ago\t%s\n", out.Id, HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.CreatedBy) + } + w.Flush() + return nil +} + +func CmdRm(args ...string) error { + 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 @@ -476,46 +535,24 @@ func (srv *Server) CmdRm(stdin io.ReadCloser, stdout io.Writer, args ...string) cmd.Usage() return nil } - volumes := make(map[string]struct{}) - for _, name := range cmd.Args() { - container := srv.runtime.Get(name) - if container == nil { - return fmt.Errorf("No such container: %s", name) - } - // Store all the deleted containers volumes - for _, volumeId := range container.Volumes { - volumes[volumeId] = struct{}{} - } - if err := srv.runtime.Destroy(container); err != nil { - fmt.Fprintln(stdout, "Error destroying container "+name+": "+err.Error()) - } - } + val := url.Values{} 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 { - fmt.Fprintf(stdout, "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 - } + 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 { + fmt.Println(name) } } 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", "CONTAINER [CONTAINER...]", "Kill a running container") +func CmdKill(args ...string) error { + cmd := Subcmd("kill", "CONTAINER [CONTAINER...]", "Kill a running container") if err := cmd.Parse(args); err != nil { return nil } @@ -523,23 +560,20 @@ func (srv *Server) CmdKill(stdin io.ReadCloser, stdout io.Writer, args ...string cmd.Usage() return nil } - for _, name := range cmd.Args() { - container := srv.runtime.Get(name) - if container == nil { - return fmt.Errorf("No such container: %s", name) - } - if err := container.Kill(); err != nil { - fmt.Fprintln(stdout, "Error killing container "+name+": "+err.Error()) + + for _, name := range args { + _, _, err := call("POST", "/containers/"+name+"/kill", nil) + if err != nil { + fmt.Printf("%s", err) + } else { + fmt.Println(name) } } return nil } -func (srv *Server) CmdImport(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { - stdout.Flush() - cmd := rcli.Subcmd(stdout, "import", "URL|- [REPOSITORY [TAG]]", "Create a new filesystem image from the contents of a tarball") - var archive io.Reader - var resp *http.Response +func CmdImport(args ...string) error { + cmd := Subcmd("import", "URL|- [REPOSITORY [TAG]]", "Create a new filesystem image from the contents of a tarball") if err := cmd.Parse(args); err != nil { return nil @@ -548,254 +582,163 @@ func (srv *Server) CmdImport(stdin io.ReadCloser, stdout rcli.DockerConn, args . cmd.Usage() return nil } - src := cmd.Arg(0) - if src == "-" { - archive = stdin - } else { - u, err := url.Parse(src) - if err != nil { - return err - } - if u.Scheme == "" { - u.Scheme = "http" - u.Host = src - u.Path = "" - } - fmt.Fprintln(stdout, "Downloading from", u) - // Download with curl (pretty progress bar) - // If curl is not available, fallback to http.Get() - resp, err = Download(u.String(), stdout) - if err != nil { - return err - } - archive = ProgressReader(resp.Body, int(resp.ContentLength), stdout, "Importing %v/%v (%v)") - } - img, err := srv.runtime.graph.Create(archive, nil, "Imported from "+src, "", nil) + src, repository, tag := cmd.Arg(0), cmd.Arg(1), cmd.Arg(2) + v := url.Values{} + v.Set("repo", repository) + v.Set("tag", tag) + v.Set("fromSrc", src) + + err := hijack("POST", "/images/create?"+v.Encode(), false) if err != nil { return err } - // Optionally register the image at REPO/TAG - if repository := cmd.Arg(1); repository != "" { - tag := cmd.Arg(2) // Repository will handle an empty tag properly - if err := srv.runtime.repositories.Set(repository, tag, img.Id, true); err != nil { - return err - } - } - fmt.Fprintln(stdout, img.ShortId()) return nil } -func (srv *Server) CmdPush(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { - cmd := rcli.Subcmd(stdout, "push", "NAME", "Push an image or a repository to the registry") +func CmdPush(args ...string) error { + cmd := Subcmd("push", "[OPTION] NAME", "Push an image or a repository to the registry") registry := cmd.String("registry", "", "Registry host to push the image to") if err := cmd.Parse(args); err != nil { return nil } - local := cmd.Arg(0) + name := cmd.Arg(0) - if local == "" { + if name == "" { cmd.Usage() return nil } + body, _, err := call("GET", "/auth", nil) + if err != nil { + return err + } + + var out auth.AuthConfig + err = json.Unmarshal(body, &out) + if err != nil { + return err + } + // If the login failed AND we're using the index, abort - if *registry == "" && (srv.runtime.authConfig == nil || srv.runtime.authConfig.Username == "") { - if err := srv.CmdLogin(stdin, stdout, args...); err != nil { + if *registry == "" && out.Username == "" { + if err := CmdLogin(args...); err != nil { return err } - if srv.runtime.authConfig == nil || srv.runtime.authConfig.Username == "" { + + body, _, err = call("GET", "/auth", nil) + if err != nil { + return err + } + err = json.Unmarshal(body, &out) + if err != nil { + return err + } + + if out.Username == "" { return fmt.Errorf("Please login prior to push. ('docker login')") } } - var remote string - - tmp := strings.SplitN(local, "/", 2) - if len(tmp) == 1 { - return fmt.Errorf( - "Impossible to push a \"root\" repository. Please rename your repository in / (ex: %s/%s)", - srv.runtime.authConfig.Username, local) - } else { - remote = local + if len(strings.SplitN(name, "/", 2)) == 1 { + return fmt.Errorf("Impossible to push a \"root\" repository. Please rename your repository in / (ex: %s/%s)", out.Username, name) } - Debugf("Pushing [%s] to [%s]\n", local, remote) - - // Try to get the image - img, err := srv.runtime.graph.Get(local) - if err != nil { - Debugf("The push refers to a repository [%s] (len: %d)\n", local, len(srv.runtime.repositories.Repositories[local])) - // If it fails, try to get the repository - if localRepo, exists := srv.runtime.repositories.Repositories[local]; exists { - if err := srv.runtime.graph.PushRepository(stdout, remote, localRepo, srv.runtime.authConfig); err != nil { - return err - } - return nil - } - - return err - } - err = srv.runtime.graph.PushImage(stdout, img, *registry, nil) - if err != nil { + v := url.Values{} + v.Set("registry", *registry) + if err := hijack("POST", "/images/"+name+"/push?"+v.Encode(), false); err != nil { return err } return nil } -func (srv *Server) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, "pull", "NAME", "Pull an image or a repository from the registry") +func CmdPull(args ...string) error { + cmd := Subcmd("pull", "NAME", "Pull an image or a repository from the registry") tag := cmd.String("t", "", "Download tagged image in repository") registry := cmd.String("registry", "", "Registry to download from. Necessary if image is pulled by ID") if err := cmd.Parse(args); err != nil { return nil } - remote := cmd.Arg(0) - if remote == "" { + + if cmd.NArg() != 1 { cmd.Usage() return nil } + remote := cmd.Arg(0) if strings.Contains(remote, ":") { remoteParts := strings.Split(remote, ":") tag = &remoteParts[1] remote = remoteParts[0] } - // FIXME: CmdPull should be a wrapper around Runtime.Pull() - if *registry != "" { - if err := srv.runtime.graph.PullImage(stdout, remote, *registry, nil); err != nil { - return err - } - return nil - } - if err := srv.runtime.graph.PullRepository(stdout, remote, *tag, srv.runtime.repositories, srv.runtime.authConfig); err != nil { + v := url.Values{} + v.Set("fromImage", remote) + v.Set("tag", *tag) + v.Set("registry", *registry) + + if err := hijack("POST", "/images/create?"+v.Encode(), false); err != nil { return err } + return nil } -func (srv *Server) CmdImages(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, "images", "[OPTIONS] [NAME]", "List images") - //limit := cmd.Int("l", 0, "Only show the N most recent versions of each image") +func CmdImages(args ...string) error { + cmd := Subcmd("images", "[OPTIONS] [NAME]", "List images") quiet := cmd.Bool("q", false, "only show numeric IDs") - flAll := cmd.Bool("a", false, "show all images") + all := cmd.Bool("a", false, "show all images") flViz := cmd.Bool("viz", false, "output graph in graphviz format") + if err := cmd.Parse(args); err != nil { return nil } + if cmd.NArg() > 1 { + cmd.Usage() + return nil + } if *flViz { - images, _ := srv.runtime.graph.All() - if images == nil { - return nil - } - - fmt.Fprintf(stdout, "digraph docker {\n") - - var parentImage *Image - var err error - for _, image := range images { - parentImage, err = image.GetParent() - if err != nil { - fmt.Errorf("Error while getting parent image: %v", err) - return nil - } - if parentImage != nil { - fmt.Fprintf(stdout, " \"%s\" -> \"%s\"\n", parentImage.ShortId(), image.ShortId()) - } else { - fmt.Fprintf(stdout, " base -> \"%s\" [style=invis]\n", image.ShortId()) - } - } - - reporefs := make(map[string][]string) - - for name, repository := range srv.runtime.repositories.Repositories { - for tag, id := range repository { - reporefs[TruncateId(id)] = append(reporefs[TruncateId(id)], fmt.Sprintf("%s:%s", name, tag)) - } - } - - for id, repos := range reporefs { - fmt.Fprintf(stdout, " \"%s\" [label=\"%s\\n%s\",shape=box,fillcolor=\"paleturquoise\",style=\"filled,rounded\"];\n", id, id, strings.Join(repos, "\\n")) - } - - fmt.Fprintf(stdout, " base [style=invisible]\n") - fmt.Fprintf(stdout, "}\n") - } else { - if cmd.NArg() > 1 { - cmd.Usage() - return nil - } - var nameFilter string - if cmd.NArg() == 1 { - nameFilter = cmd.Arg(0) - } - w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0) - if !*quiet { - fmt.Fprintln(w, "REPOSITORY\tTAG\tID\tCREATED") - } - var allImages map[string]*Image - var err error - if *flAll { - allImages, err = srv.runtime.graph.Map() - } else { - allImages, err = srv.runtime.graph.Heads() - } + body, _, err := call("GET", "/images/viz", false) if err != nil { return err } - for name, repository := range srv.runtime.repositories.Repositories { - if nameFilter != "" && name != nameFilter { - continue - } - for tag, id := range repository { - image, err := srv.runtime.graph.Get(id) - if err != nil { - log.Printf("Warning: couldn't load %s from %s/%s: %s", id, name, tag, err) - continue - } - delete(allImages, id) - if !*quiet { - for idx, field := range []string{ - /* REPOSITORY */ name, - /* TAG */ tag, - /* ID */ TruncateId(id), - /* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago", - } { - if idx == 0 { - w.Write([]byte(field)) - } else { - w.Write([]byte("\t" + field)) - } - } - w.Write([]byte{'\n'}) - } else { - stdout.Write([]byte(image.ShortId() + "\n")) - } - } - } - // Display images which aren't part of a - if nameFilter == "" { - for id, image := range allImages { - if !*quiet { - for idx, field := range []string{ - /* REPOSITORY */ "", - /* TAG */ "", - /* ID */ TruncateId(id), - /* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago", - } { - if idx == 0 { - w.Write([]byte(field)) - } else { - w.Write([]byte("\t" + field)) - } - } - w.Write([]byte{'\n'}) - } else { - stdout.Write([]byte(image.ShortId() + "\n")) - } + fmt.Printf("%s", body) + } else { + v := url.Values{} + if cmd.NArg() == 1 { + v.Set("filter", cmd.Arg(0)) + } + if *quiet { + v.Set("only_ids", "1") + } + if *all { + v.Set("all", "1") + } + + body, _, err := call("GET", "/images/json?"+v.Encode(), nil) + if err != nil { + return err + } + + var outs []ApiImages + err = json.Unmarshal(body, &outs) + if err != nil { + return err + } + + w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) + if !*quiet { + fmt.Fprintln(w, "REPOSITORY\tTAG\tID\tCREATED") + } + + for _, out := range outs { + if !*quiet { + fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\n", out.Repository, out.Tag, out.Id, HumanDuration(time.Now().Sub(time.Unix(out.Created, 0)))) + } else { + fmt.Fprintln(w, out.Id) } } + if !*quiet { w.Flush() } @@ -803,78 +746,91 @@ func (srv *Server) CmdImages(stdin io.ReadCloser, stdout io.Writer, args ...stri return nil } -func (srv *Server) CmdPs(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, - "ps", "[OPTIONS]", "List containers") +func CmdPs(args ...string) error { + cmd := Subcmd("ps", "[OPTIONS]", "List containers") quiet := cmd.Bool("q", false, "Only display numeric IDs") - flAll := cmd.Bool("a", false, "Show all containers. Only running containers are shown by default.") - flFull := cmd.Bool("notrunc", false, "Don't truncate output") - latest := cmd.Bool("l", false, "Show only the latest created container, include non-running ones.") - nLast := cmd.Int("n", -1, "Show n last created containers, include non-running ones.") + all := cmd.Bool("a", false, "Show all containers. Only running containers are shown by default.") + noTrunc := cmd.Bool("notrunc", false, "Don't truncate output") + nLatest := cmd.Bool("l", false, "Show only the latest created container, include non-running ones.") + since := cmd.String("sinceId", "", "Show only containers created since Id, include non-running ones.") + before := cmd.String("beforeId", "", "Show only container created before Id, include non-running ones.") + last := cmd.Int("n", -1, "Show n last created containers, include non-running ones.") + if err := cmd.Parse(args); err != nil { return nil } - if *nLast == -1 && *latest { - *nLast = 1 + v := url.Values{} + if *last == -1 && *nLatest { + *last = 1 } - w := tabwriter.NewWriter(stdout, 12, 1, 3, ' ', 0) + if *quiet { + v.Set("only_ids", "1") + } + if *all { + v.Set("all", "1") + } + if *noTrunc { + v.Set("trunc_cmd", "0") + } + if *last != -1 { + v.Set("limit", strconv.Itoa(*last)) + } + if *since != "" { + v.Set("since", *since) + } + if *before != "" { + v.Set("before", *before) + } + + body, _, err := call("GET", "/containers/ps?"+v.Encode(), nil) + if err != nil { + return err + } + + var outs []ApiContainers + err = json.Unmarshal(body, &outs) + if err != nil { + return err + } + w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) if !*quiet { - fmt.Fprintln(w, "ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tCOMMENT\tPORTS") + fmt.Fprintln(w, "ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS") } - for i, container := range srv.runtime.List() { - if !container.State.Running && !*flAll && *nLast == -1 { - continue - } - if i == *nLast { - break - } + + for _, out := range outs { if !*quiet { - command := fmt.Sprintf("%s %s", container.Path, strings.Join(container.Args, " ")) - if !*flFull { - command = Trunc(command, 20) - } - for idx, field := range []string{ - /* ID */ container.ShortId(), - /* IMAGE */ srv.runtime.repositories.ImageName(container.Image), - /* COMMAND */ command, - /* CREATED */ HumanDuration(time.Now().Sub(container.Created)) + " ago", - /* STATUS */ container.State.String(), - /* COMMENT */ "", - /* PORTS */ container.NetworkSettings.PortMappingHuman(), - } { - if idx == 0 { - w.Write([]byte(field)) - } else { - w.Write([]byte("\t" + field)) - } - } - w.Write([]byte{'\n'}) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s ago\t%s\n", out.Id, out.Image, out.Command, out.Status, HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Ports) } else { - stdout.Write([]byte(container.ShortId() + "\n")) + fmt.Fprintln(w, out.Id) } } + if !*quiet { w.Flush() } return nil } -func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, - "commit", "[OPTIONS] CONTAINER [REPOSITORY [TAG]]", - "Create a new image from a container's changes") +func CmdCommit(args ...string) error { + cmd := Subcmd("commit", "[OPTIONS] CONTAINER [REPOSITORY [TAG]]", "Create a new image from a container's changes") flComment := cmd.String("m", "", "Commit message") flAuthor := cmd.String("author", "", "Author (eg. \"John Hannibal Smith \"") flConfig := cmd.String("run", "", "Config automatically applied when the image is run. "+`(ex: {"Cmd": ["cat", "/world"], "PortSpecs": ["22"]}')`) if err := cmd.Parse(args); err != nil { return nil } - containerName, repository, tag := cmd.Arg(0), cmd.Arg(1), cmd.Arg(2) - if containerName == "" { + name, repository, tag := cmd.Arg(0), cmd.Arg(1), cmd.Arg(2) + if name == "" { cmd.Usage() return nil } + v := url.Values{} + v.Set("container", name) + v.Set("repo", repository) + v.Set("tag", tag) + v.Set("comment", *flComment) + v.Set("author", *flAuthor) var config *Config if *flConfig != "" { config = &Config{} @@ -882,69 +838,40 @@ func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...stri return err } } - - container := srv.runtime.Get(containerName) - if container == nil { - return fmt.Errorf("No such container: %s", containerName) - } - - img, err := NewBuilder(srv.runtime).Commit(container, repository, tag, *flComment, *flAuthor, config) + body, _, err := call("POST", "/commit?"+v.Encode(), config) if err != nil { return err } - fmt.Fprintln(stdout, img.ShortId()) + + apiId := &ApiId{} + err = json.Unmarshal(body, apiId) + if err != nil { + return err + } + + fmt.Println(apiId.Id) return nil } -func (srv *Server) CmdExport(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, - "export", "CONTAINER", - "Export the contents of a filesystem as a tar archive") +func CmdExport(args ...string) error { + cmd := Subcmd("export", "CONTAINER", "Export the contents of a filesystem as a tar archive") if err := cmd.Parse(args); err != nil { return nil } - name := cmd.Arg(0) - if container := srv.runtime.Get(name); container != nil { - data, err := container.Export() - if err != nil { - return err - } - // Stream the entire contents of the container (basically a volatile snapshot) - if _, err := io.Copy(stdout, data); err != nil { - return err - } - return nil - } - return fmt.Errorf("No such container: %s", name) -} -func (srv *Server) CmdDiff(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, - "diff", "CONTAINER", - "Inspect changes on a container's filesystem") - if err := cmd.Parse(args); err != nil { - return nil - } - if cmd.NArg() < 1 { + if cmd.NArg() != 1 { cmd.Usage() return nil } - if container := srv.runtime.Get(cmd.Arg(0)); container == nil { - return fmt.Errorf("No such container") - } else { - changes, err := container.Changes() - if err != nil { - return err - } - for _, change := range changes { - fmt.Fprintln(stdout, change.String()) - } + + if err := stream("GET", "/containers/"+cmd.Arg(0)+"/export"); err != nil { + return err } return nil } -func (srv *Server) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, "logs", "CONTAINER", "Fetch the logs of a container") +func CmdDiff(args ...string) error { + cmd := Subcmd("diff", "CONTAINER", "Inspect changes on a container's filesystem") if err := cmd.Parse(args); err != nil { return nil } @@ -952,79 +879,104 @@ func (srv *Server) CmdLogs(stdin io.ReadCloser, stdout io.Writer, args ...string cmd.Usage() return nil } - name := cmd.Arg(0) - if container := srv.runtime.Get(name); container != nil { - logStdout, err := container.ReadLog("stdout") - if err != nil { - return err - } - logStderr, err := container.ReadLog("stderr") - if err != nil { - return err - } - // FIXME: Interpolate stdout and stderr instead of concatenating them - // FIXME: Differentiate stdout and stderr in the remote protocol - if _, err := io.Copy(stdout, logStdout); err != nil { - return err - } - if _, err := io.Copy(stdout, logStderr); err != nil { - return err - } - return nil - } - return fmt.Errorf("No such container: %s", cmd.Arg(0)) -} -func (srv *Server) CmdAttach(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { - cmd := rcli.Subcmd(stdout, "attach", "CONTAINER", "Attach to a running container") - if err := cmd.Parse(args); err != nil { - return nil - } - if cmd.NArg() != 1 { - cmd.Usage() - return nil - } - name := cmd.Arg(0) - container := srv.runtime.Get(name) - if container == nil { - return fmt.Errorf("No such container: %s", name) - } - - if container.State.Ghost { - return fmt.Errorf("Impossible to attach to a ghost container") - } - - if container.Config.Tty { - stdout.SetOptionRawTerminal() - } - // Flush the options to make sure the client sets the raw mode - stdout.Flush() - return <-container.Attach(stdin, nil, stdout, stdout) -} - -func (srv *Server) CmdSearch(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { - cmd := rcli.Subcmd(stdout, "search", "NAME", "Search the docker index for images") - if err := cmd.Parse(args); err != nil { - return nil - } - if cmd.NArg() != 1 { - cmd.Usage() - return nil - } - term := cmd.Arg(0) - results, err := srv.runtime.graph.SearchRepositories(stdout, term) + body, _, err := call("GET", "/containers/"+cmd.Arg(0)+"/changes", nil) if err != nil { return err } - fmt.Fprintf(stdout, "Found %d results matching your query (\"%s\")\n", results.NumResults, results.Query) - w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0) + + changes := []Change{} + err = json.Unmarshal(body, &changes) + if err != nil { + return err + } + for _, change := range changes { + fmt.Println(change.String()) + } + return nil +} + +func CmdLogs(args ...string) error { + cmd := Subcmd("logs", "CONTAINER", "Fetch the logs of a container") + if err := cmd.Parse(args); err != nil { + return nil + } + if cmd.NArg() != 1 { + cmd.Usage() + return nil + } + + v := url.Values{} + v.Set("logs", "1") + v.Set("stdout", "1") + v.Set("stderr", "1") + + if err := hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), false); err != nil { + return err + } + return nil +} + +func CmdAttach(args ...string) error { + cmd := Subcmd("attach", "CONTAINER", "Attach to a running container") + if err := cmd.Parse(args); err != nil { + return nil + } + if cmd.NArg() != 1 { + cmd.Usage() + return nil + } + + body, _, err := call("GET", "/containers/"+cmd.Arg(0)+"/json", nil) + if err != nil { + return err + } + + container := &Container{} + err = json.Unmarshal(body, container) + if err != nil { + return err + } + + v := url.Values{} + v.Set("stream", "1") + v.Set("stdout", "1") + v.Set("stderr", "1") + v.Set("stdin", "1") + + if err := hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?"+v.Encode(), container.Config.Tty); err != nil { + return err + } + return nil +} + +func CmdSearch(args ...string) error { + cmd := Subcmd("search", "NAME", "Search the docker index for images") + if err := cmd.Parse(args); err != nil { + return nil + } + if cmd.NArg() != 1 { + cmd.Usage() + return nil + } + + v := url.Values{} + v.Set("term", cmd.Arg(0)) + body, _, err := call("GET", "/images/search?"+v.Encode(), nil) + if err != nil { + return err + } + + outs := []ApiSearch{} + err = json.Unmarshal(body, &outs) + if err != nil { + return err + } + fmt.Printf("Found %d results matching your query (\"%s\")\n", len(outs), cmd.Arg(0)) + w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0) fmt.Fprintf(w, "NAME\tDESCRIPTION\n") - for _, repo := range results.Results { - description := repo["description"] - if len(description) > 45 { - description = Trunc(description, 42) + "..." - } - fmt.Fprintf(w, "%s\t%s\n", repo["name"], description) + for _, out := range outs { + fmt.Fprintf(w, "%s\t%s\n", out.Name, out.Description) } w.Flush() return nil @@ -1033,19 +985,6 @@ func (srv *Server) CmdSearch(stdin io.ReadCloser, stdout rcli.DockerConn, args . // Ports type - Used to parse multiple -p flags type ports []int -func (p *ports) String() string { - return fmt.Sprint(*p) -} - -func (p *ports) Set(value string) error { - port, err := strconv.Atoi(value) - if err != nil { - return fmt.Errorf("Invalid port: %v", value) - } - *p = append(*p, port) - return nil -} - // ListOpts type type ListOpts []string @@ -1104,108 +1043,221 @@ func (opts PathOpts) Set(val string) error { return nil } -func (srv *Server) CmdTag(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - cmd := rcli.Subcmd(stdout, "tag", "[OPTIONS] IMAGE REPOSITORY [TAG]", "Tag an image into a repository") +func CmdTag(args ...string) error { + cmd := Subcmd("tag", "[OPTIONS] IMAGE REPOSITORY [TAG]", "Tag an image into a repository") force := cmd.Bool("f", false, "Force") if err := cmd.Parse(args); err != nil { return nil } - if cmd.NArg() < 2 { + if cmd.NArg() != 2 && cmd.NArg() != 3 { cmd.Usage() return nil } - return srv.runtime.repositories.Set(cmd.Arg(1), cmd.Arg(2), cmd.Arg(0), *force) -} -func (srv *Server) CmdRun(stdin io.ReadCloser, stdout rcli.DockerConn, args ...string) error { - config, err := ParseRun(args, stdout, srv.runtime.capabilities) - if err != nil { + v := url.Values{} + v.Set("repo", cmd.Arg(1)) + if cmd.NArg() == 3 { + v.Set("tag", cmd.Arg(2)) + } + + if *force { + v.Set("force", "1") + } + + if _, _, err := call("POST", "/images/"+cmd.Arg(0)+"/tag?"+v.Encode(), nil); err != nil { return err } - if config.Image == "" { - fmt.Fprintln(stdout, "Error: Image not specified") - return fmt.Errorf("Image not specified") - } - - if config.Tty { - stdout.SetOptionRawTerminal() - } - // Flush the options to make sure the client sets the raw mode - // or tell the client there is no options - stdout.Flush() - - b := NewBuilder(srv.runtime) - - // Create new container - container, err := b.Create(config) - if err != nil { - // If container not found, try to pull it - if srv.runtime.graph.IsNotExist(err) { - fmt.Fprintf(stdout, "Image %s not found, trying to pull it from registry.\r\n", config.Image) - if err = srv.CmdPull(stdin, stdout, config.Image); err != nil { - return err - } - if container, err = b.Create(config); err != nil { - return err - } - } else { - return err - } - } - var ( - cStdin io.ReadCloser - cStdout, cStderr io.Writer - ) - if config.AttachStdin { - r, w := io.Pipe() - go func() { - defer w.Close() - defer Debugf("Closing buffered stdin pipe") - io.Copy(w, stdin) - }() - cStdin = r - } - if config.AttachStdout { - cStdout = stdout - } - if config.AttachStderr { - cStderr = stdout // FIXME: rcli can't differentiate stdout from stderr - } - - attachErr := container.Attach(cStdin, stdin, cStdout, cStderr) - Debugf("Starting\n") - if err := container.Start(); err != nil { - return err - } - if cStdout == nil && cStderr == nil { - fmt.Fprintln(stdout, container.ShortId()) - } - Debugf("Waiting for attach to return\n") - <-attachErr - // Expecting I/O pipe error, discarding - - // If we are in stdinonce mode, wait for the process to end - // otherwise, simply return - if config.StdinOnce && !config.Tty { - container.Wait() - } return nil } -func NewServer(autoRestart bool) (*Server, error) { - if runtime.GOARCH != "amd64" { - log.Fatalf("The docker runtime currently only supports amd64 (not %s). This will change in the future. Aborting.", runtime.GOARCH) - } - runtime, err := NewRuntime(autoRestart) +func CmdRun(args ...string) error { + config, cmd, err := ParseRun(args, nil) if err != nil { - return nil, err + return err } - srv := &Server{ - runtime: runtime, + if config.Image == "" { + cmd.Usage() + return nil } - return srv, nil + + //create the container + body, statusCode, err := call("POST", "/containers/create", config) + //if image not found try to pull it + if statusCode == 404 { + v := url.Values{} + v.Set("fromImage", config.Image) + err = hijack("POST", "/images/create?"+v.Encode(), false) + if err != nil { + return err + } + body, _, err = call("POST", "/containers/create", config) + if err != nil { + return err + } + } + if err != nil { + return err + } + + out := &ApiRun{} + err = json.Unmarshal(body, out) + if err != nil { + return err + } + + for _, warning := range out.Warnings { + fmt.Fprintln(os.Stderr, "WARNING: ", warning) + } + + v := url.Values{} + v.Set("logs", "1") + v.Set("stream", "1") + + if config.AttachStdin { + v.Set("stdin", "1") + } + if config.AttachStdout { + v.Set("stdout", "1") + } + if config.AttachStderr { + v.Set("stderr", "1") + + } + + //start the container + _, _, err = call("POST", "/containers/"+out.Id+"/start", nil) + if err != nil { + return err + } + + if config.AttachStdin || config.AttachStdout || config.AttachStderr { + if err := hijack("POST", "/containers/"+out.Id+"/attach?"+v.Encode(), config.Tty); err != nil { + return err + } + } + if !config.AttachStdout && !config.AttachStderr { + fmt.Println(out.Id) + } + return nil } -type Server struct { - runtime *Runtime +func call(method, path string, data interface{}) ([]byte, int, error) { + var params io.Reader + if data != nil { + buf, err := json.Marshal(data) + if err != nil { + return nil, -1, err + } + params = bytes.NewBuffer(buf) + } + + req, err := http.NewRequest(method, "http://0.0.0.0:4243"+path, params) + if err != nil { + return nil, -1, err + } + req.Header.Set("User-Agent", "Docker-Client/"+VERSION) + if data != nil { + req.Header.Set("Content-Type", "application/json") + } else if method == "POST" { + req.Header.Set("Content-Type", "plain/text") + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + if strings.Contains(err.Error(), "connection refused") { + return nil, -1, fmt.Errorf("Can't connect to docker daemon. Is 'docker -d' running on this host?") + } + return nil, -1, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, -1, err + } + if resp.StatusCode < 200 || resp.StatusCode >= 400 { + return nil, resp.StatusCode, fmt.Errorf("error: %s", body) + } + return body, resp.StatusCode, nil +} + +func stream(method, path string) error { + req, err := http.NewRequest(method, "http://0.0.0.0:4243"+path, nil) + if err != nil { + return err + } + req.Header.Set("User-Agent", "Docker-Client/"+VERSION) + if method == "POST" { + req.Header.Set("Content-Type", "plain/text") + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + if strings.Contains(err.Error(), "connection refused") { + return fmt.Errorf("Can't connect to docker daemon. Is 'docker -d' running on this host?") + } + return err + } + defer resp.Body.Close() + if _, err := io.Copy(os.Stdout, resp.Body); err != nil { + return err + } + return nil +} + +func hijack(method, path string, setRawTerminal bool) error { + req, err := http.NewRequest(method, path, nil) + if err != nil { + return err + } + req.Header.Set("Content-Type", "plain/text") + dial, err := net.Dial("tcp", "0.0.0.0:4243") + if err != nil { + return err + } + clientconn := httputil.NewClientConn(dial, nil) + clientconn.Do(req) + defer clientconn.Close() + + rwc, br := clientconn.Hijack() + defer rwc.Close() + + receiveStdout := Go(func() error { + _, err := io.Copy(os.Stdout, br) + return err + }) + + if setRawTerminal && term.IsTerminal(int(os.Stdin.Fd())) && os.Getenv("NORAW") == "" { + if oldState, err := SetRawTerminal(); err != nil { + return err + } else { + defer RestoreTerminal(oldState) + } + } + + sendStdin := Go(func() error { + _, err := io.Copy(rwc, os.Stdin) + if err := rwc.(*net.TCPConn).CloseWrite(); err != nil { + fmt.Fprintf(os.Stderr, "Couldn't send EOF: %s\n", err) + } + return err + }) + + if err := <-receiveStdout; err != nil { + return err + } + + if !term.IsTerminal(int(os.Stdin.Fd())) { + if err := <-sendStdin; err != nil { + return err + } + } + return nil + +} + +func Subcmd(name, signature, description string) *flag.FlagSet { + flags := flag.NewFlagSet(name, flag.ContinueOnError) + flags.Usage = func() { + fmt.Printf("\nUsage: docker %s %s\n\n%s\n\n", name, signature, description) + flags.PrintDefaults() + } + return flags } diff --git a/components/engine/commands_test.go b/components/engine/commands_test.go index 999a241ce7..05ece80dac 100644 --- a/components/engine/commands_test.go +++ b/components/engine/commands_test.go @@ -3,9 +3,8 @@ package docker import ( "bufio" "fmt" - "github.com/dotcloud/docker/rcli" "io" - "io/ioutil" + _ "io/ioutil" "strings" "testing" "time" @@ -59,6 +58,7 @@ func assertPipe(input, output string, r io.Reader, w io.Writer, count int) error return nil } +/*TODO func cmdWait(srv *Server, container *Container) error { stdout, stdoutPipe := io.Pipe() @@ -468,3 +468,4 @@ func TestAttachDisconnect(t *testing.T) { cStdin.Close() container.Wait() } +*/ diff --git a/components/engine/container.go b/components/engine/container.go index 9315d3347a..d4ebc60c8b 100644 --- a/components/engine/container.go +++ b/components/engine/container.go @@ -2,8 +2,8 @@ package docker import ( "encoding/json" + "flag" "fmt" - "github.com/dotcloud/docker/rcli" "github.com/kr/pty" "io" "io/ioutil" @@ -72,8 +72,8 @@ type Config struct { VolumesFrom string } -func ParseRun(args []string, stdout io.Writer, capabilities *Capabilities) (*Config, error) { - cmd := rcli.Subcmd(stdout, "run", "[OPTIONS] IMAGE COMMAND [ARG...]", "Run a command in a new container") +func ParseRun(args []string, capabilities *Capabilities) (*Config, *flag.FlagSet, error) { + cmd := Subcmd("run", "[OPTIONS] IMAGE COMMAND [ARG...]", "Run a command in a new container") if len(args) > 0 && args[0] != "--help" { cmd.SetOutput(ioutil.Discard) } @@ -87,8 +87,8 @@ func ParseRun(args []string, stdout io.Writer, capabilities *Capabilities) (*Con flTty := cmd.Bool("t", false, "Allocate a pseudo-tty") flMemory := cmd.Int64("m", 0, "Memory limit (in bytes)") - if *flMemory > 0 && !capabilities.MemoryLimit { - fmt.Fprintf(stdout, "WARNING: Your kernel does not support memory limit capabilities. Limitation discarded.\n") + if capabilities != nil && *flMemory > 0 && !capabilities.MemoryLimit { + //fmt.Fprintf(stdout, "WARNING: Your kernel does not support memory limit capabilities. Limitation discarded.\n") *flMemory = 0 } @@ -109,10 +109,10 @@ func ParseRun(args []string, stdout io.Writer, capabilities *Capabilities) (*Con flVolumesFrom := cmd.String("volumes-from", "", "Mount volumes from the specified container") if err := cmd.Parse(args); err != nil { - return nil, err + return nil, cmd, err } if *flDetach && len(flAttach) > 0 { - return nil, fmt.Errorf("Conflicting options: -a and -d") + return nil, cmd, fmt.Errorf("Conflicting options: -a and -d") } // If neither -d or -a are set, attach to everything by default if len(flAttach) == 0 && !*flDetach { @@ -152,8 +152,8 @@ func ParseRun(args []string, stdout io.Writer, capabilities *Capabilities) (*Con VolumesFrom: *flVolumesFrom, } - if *flMemory > 0 && !capabilities.SwapLimit { - fmt.Fprintf(stdout, "WARNING: Your kernel does not support swap limit capabilities. Limitation discarded.\n") + if capabilities != nil && *flMemory > 0 && !capabilities.SwapLimit { + //fmt.Fprintf(stdout, "WARNING: Your kernel does not support swap limit capabilities. Limitation discarded.\n") config.MemorySwap = -1 } @@ -161,7 +161,7 @@ func ParseRun(args []string, stdout io.Writer, capabilities *Capabilities) (*Con if config.OpenStdin && config.AttachStdin { config.StdinOnce = true } - return config, nil + return config, cmd, nil } type NetworkSettings struct { diff --git a/components/engine/container_test.go b/components/engine/container_test.go index 35fa82e659..3ed1763a3e 100644 --- a/components/engine/container_test.go +++ b/components/engine/container_test.go @@ -400,6 +400,11 @@ func TestStart(t *testing.T) { } defer runtime.Destroy(container) + cStdin, err := container.StdinPipe() + if err != nil { + t.Fatal(err) + } + if err := container.Start(); err != nil { t.Fatal(err) } @@ -415,7 +420,6 @@ func TestStart(t *testing.T) { } // Try to avoid the timeoout in destroy. Best effort, don't check error - cStdin, _ := container.StdinPipe() cStdin.Close() container.WaitTimeout(2 * time.Second) } diff --git a/components/engine/docker/docker.go b/components/engine/docker/docker.go index dfd234609a..778326a810 100644 --- a/components/engine/docker/docker.go +++ b/components/engine/docker/docker.go @@ -4,9 +4,6 @@ import ( "flag" "fmt" "github.com/dotcloud/docker" - "github.com/dotcloud/docker/rcli" - "github.com/dotcloud/docker/term" - "io" "io/ioutil" "log" "os" @@ -48,10 +45,12 @@ func main() { } if err := daemon(*pidfile, *flAutoRestart); err != nil { log.Fatal(err) + os.Exit(-1) } } else { - if err := runCommand(flag.Args()); err != nil { + if err := docker.ParseCommands(flag.Args()...); err != nil { log.Fatal(err) + os.Exit(-1) } } } @@ -98,50 +97,10 @@ func daemon(pidfile string, autoRestart bool) error { os.Exit(0) }() - service, err := docker.NewServer(autoRestart) + server, err := docker.NewServer(autoRestart) if err != nil { return err } - return rcli.ListenAndServe("tcp", "127.0.0.1:4242", service) -} -func runCommand(args []string) error { - // 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 conn, err := rcli.Call("tcp", "127.0.0.1:4242", args...); err == nil { - options := conn.GetOptions() - if options.RawTerminal && - term.IsTerminal(int(os.Stdin.Fd())) && - os.Getenv("NORAW") == "" { - if oldState, err := rcli.SetRawTerminal(); err != nil { - return err - } else { - defer rcli.RestoreTerminal(oldState) - } - } - receiveStdout := docker.Go(func() error { - _, err := io.Copy(os.Stdout, conn) - return err - }) - sendStdin := docker.Go(func() error { - _, err := io.Copy(conn, os.Stdin) - if err := conn.CloseWrite(); err != nil { - log.Printf("Couldn't send EOF: " + err.Error()) - } - return err - }) - if err := <-receiveStdout; err != nil { - return err - } - if !term.IsTerminal(int(os.Stdin.Fd())) { - if err := <-sendStdin; err != nil { - return err - } - } - } else { - return fmt.Errorf("Can't connect to docker daemon. Is 'docker -d' running on this host?") - } - return nil + return docker.ListenAndServe("0.0.0.0:4243", server, true) } diff --git a/components/engine/docs/Makefile b/components/engine/docs/Makefile index 77f14ee92f..9298123f7f 100644 --- a/components/engine/docs/Makefile +++ b/components/engine/docs/Makefile @@ -46,23 +46,24 @@ clean: docs: -rm -rf $(BUILDDIR)/* $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/html - cp sources/index.html $(BUILDDIR)/html/ - cp -r sources/gettingstarted $(BUILDDIR)/html/ - cp sources/dotcloud.yml $(BUILDDIR)/html/ - cp sources/CNAME $(BUILDDIR)/html/ - cp sources/.nojekyll $(BUILDDIR)/html/ - cp sources/nginx.conf $(BUILDDIR)/html/ @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + @echo "Build finished. The documentation pages are now in $(BUILDDIR)/html." + + +site: + cp -r website $(BUILDDIR)/ + cp -r theme/docker/static/ $(BUILDDIR)/website/ + @echo + @echo "The Website pages are in $(BUILDDIR)/site." connect: - @echo pushing changes to staging site - @cd _build/html/ ; \ - @dotcloud list ; \ + @echo connecting dotcloud to www.docker.io website, make sure to use user 1 + @cd _build/website/ ; \ + dotcloud list ; \ dotcloud connect dockerwebsite push: - @cd _build/html/ ; \ + @cd _build/website/ ; \ dotcloud push github-deploy: docs diff --git a/components/engine/docs/sources/builder/basics.rst b/components/engine/docs/sources/builder/basics.rst index 0d726e93c1..f7ce07926f 100644 --- a/components/engine/docs/sources/builder/basics.rst +++ b/components/engine/docs/sources/builder/basics.rst @@ -103,7 +103,7 @@ The `INSERT` instruction will download the file at the given url and place it wi run echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list run apt-get update - run apt-get install -y inotify-tools nginx apache openssh-server + run apt-get install -y inotify-tools nginx apache2 openssh-server insert https://raw.github.com/creack/docker-vps/master/nginx-wrapper.sh /usr/sbin/nginx-wrapper :: diff --git a/components/engine/docs/sources/commandline/command/build.rst b/components/engine/docs/sources/commandline/command/build.rst index 6415f11f7b..8d07c725c2 100644 --- a/components/engine/docs/sources/commandline/command/build.rst +++ b/components/engine/docs/sources/commandline/command/build.rst @@ -1,6 +1,6 @@ -=========================================== +======================================================== ``build`` -- Build a container from Dockerfile via stdin -=========================================== +======================================================== :: diff --git a/components/engine/docs/sources/commandline/index.rst b/components/engine/docs/sources/commandline/index.rst index d19d39ab60..72290fa7a8 100644 --- a/components/engine/docs/sources/commandline/index.rst +++ b/components/engine/docs/sources/commandline/index.rst @@ -13,4 +13,4 @@ Contents: basics workingwithrepository - cli \ No newline at end of file + cli diff --git a/components/engine/docs/sources/concepts/containers.rst b/components/engine/docs/sources/concepts/containers.rst index f432c4363d..8378a7e29f 100644 --- a/components/engine/docs/sources/concepts/containers.rst +++ b/components/engine/docs/sources/concepts/containers.rst @@ -5,124 +5,4 @@ :note: This version of the introduction is temporary, just to make sure we don't break the links from the website when the documentation is updated - -Introduction -============ - -Docker - The Linux container runtime ------------------------------------- - -Docker complements LXC with a high-level API which operates at the process level. It runs unix processes with strong guarantees of isolation and repeatability across servers. - -Docker is a great building block for automating distributed systems: large-scale web deployments, database clusters, continuous deployment systems, private PaaS, service-oriented architectures, etc. - - -- **Heterogeneous payloads** Any combination of binaries, libraries, configuration files, scripts, virtualenvs, jars, gems, tarballs, you name it. No more juggling between domain-specific tools. Docker can deploy and run them all. -- **Any server** Docker can run on any x64 machine with a modern linux kernel - whether it's a laptop, a bare metal server or a VM. This makes it perfect for multi-cloud deployments. -- **Isolation** docker isolates processes from each other and from the underlying host, using lightweight containers. -- **Repeatability** Because containers are isolated in their own filesystem, they behave the same regardless of where, when, and alongside what they run. - - - -What is a Standard Container? ------------------------------ - -Docker defines a unit of software delivery called a Standard Container. The goal of a Standard Container is to encapsulate a software component and all its dependencies in -a format that is self-describing and portable, so that any compliant runtime can run it without extra dependency, regardless of the underlying machine and the contents of the container. - -The spec for Standard Containers is currently work in progress, but it is very straightforward. It mostly defines 1) an image format, 2) a set of standard operations, and 3) an execution environment. - -A great analogy for this is the shipping container. Just like Standard Containers are a fundamental unit of software delivery, shipping containers (http://bricks.argz.com/ins/7823-1/12) are a fundamental unit of physical delivery. - -Standard operations -~~~~~~~~~~~~~~~~~~~ - -Just like shipping containers, Standard Containers define a set of STANDARD OPERATIONS. Shipping containers can be lifted, stacked, locked, loaded, unloaded and labelled. Similarly, standard containers can be started, stopped, copied, snapshotted, downloaded, uploaded and tagged. - - -Content-agnostic -~~~~~~~~~~~~~~~~~~~ - -Just like shipping containers, Standard Containers are CONTENT-AGNOSTIC: all standard operations have the same effect regardless of the contents. A shipping container will be stacked in exactly the same way whether it contains Vietnamese powder coffee or spare Maserati parts. Similarly, Standard Containers are started or uploaded in the same way whether they contain a postgres database, a php application with its dependencies and application server, or Java build artifacts. - - -Infrastructure-agnostic -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Both types of containers are INFRASTRUCTURE-AGNOSTIC: they can be transported to thousands of facilities around the world, and manipulated by a wide variety of equipment. A shipping container can be packed in a factory in Ukraine, transported by truck to the nearest routing center, stacked onto a train, loaded into a German boat by an Australian-built crane, stored in a warehouse at a US facility, etc. Similarly, a standard container can be bundled on my laptop, uploaded to S3, downloaded, run and snapshotted by a build server at Equinix in Virginia, uploaded to 10 staging servers in a home-made Openstack cluster, then sent to 30 production instances across 3 EC2 regions. - - -Designed for automation -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Because they offer the same standard operations regardless of content and infrastructure, Standard Containers, just like their physical counterpart, are extremely well-suited for automation. In fact, you could say automation is their secret weapon. - -Many things that once required time-consuming and error-prone human effort can now be programmed. Before shipping containers, a bag of powder coffee was hauled, dragged, dropped, rolled and stacked by 10 different people in 10 different locations by the time it reached its destination. 1 out of 50 disappeared. 1 out of 20 was damaged. The process was slow, inefficient and cost a fortune - and was entirely different depending on the facility and the type of goods. - -Similarly, before Standard Containers, by the time a software component ran in production, it had been individually built, configured, bundled, documented, patched, vendored, templated, tweaked and instrumented by 10 different people on 10 different computers. Builds failed, libraries conflicted, mirrors crashed, post-it notes were lost, logs were misplaced, cluster updates were half-broken. The process was slow, inefficient and cost a fortune - and was entirely different depending on the language and infrastructure provider. - - -Industrial-grade delivery -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There are 17 million shipping containers in existence, packed with every physical good imaginable. Every single one of them can be loaded on the same boats, by the same cranes, in the same facilities, and sent anywhere in the World with incredible efficiency. It is embarrassing to think that a 30 ton shipment of coffee can safely travel half-way across the World in *less time* than it takes a software team to deliver its code from one datacenter to another sitting 10 miles away. - -With Standard Containers we can put an end to that embarrassment, by making INDUSTRIAL-GRADE DELIVERY of software a reality. - - -Standard Container Specification -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -(TODO) - -Image format -~~~~~~~~~~~~ - -Standard operations -~~~~~~~~~~~~~~~~~~~ - -- Copy -- Run -- Stop -- Wait -- Commit -- Attach standard streams -- List filesystem changes -- ... - -Execution environment -~~~~~~~~~~~~~~~~~~~~~ - -Root filesystem -^^^^^^^^^^^^^^^ - -Environment variables -^^^^^^^^^^^^^^^^^^^^^ - -Process arguments -^^^^^^^^^^^^^^^^^ - -Networking -^^^^^^^^^^ - -Process namespacing -^^^^^^^^^^^^^^^^^^^ - -Resource limits -^^^^^^^^^^^^^^^ - -Process monitoring -^^^^^^^^^^^^^^^^^^ - -Logging -^^^^^^^ - -Signals -^^^^^^^ - -Pseudo-terminal allocation -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Security -^^^^^^^^ - +This document has been moved to :ref:`introduction`, please update your bookmarks. \ No newline at end of file diff --git a/components/engine/docs/sources/concepts/introduction.rst b/components/engine/docs/sources/concepts/introduction.rst index 46698b4015..b7e1b04f05 100644 --- a/components/engine/docs/sources/concepts/introduction.rst +++ b/components/engine/docs/sources/concepts/introduction.rst @@ -2,7 +2,7 @@ :description: An introduction to docker and standard containers? :keywords: containers, lxc, concepts, explanation - +.. _introduction: Introduction ============ diff --git a/components/engine/docs/sources/index.rst b/components/engine/docs/sources/index.rst index 9a272d2a34..4c46653808 100644 --- a/components/engine/docs/sources/index.rst +++ b/components/engine/docs/sources/index.rst @@ -18,6 +18,7 @@ This documentation has the following resources: registry/index index/index builder/index + remote-api/index faq diff --git a/components/engine/docs/sources/remote-api/api.rst b/components/engine/docs/sources/remote-api/api.rst new file mode 100644 index 0000000000..a6f0662644 --- /dev/null +++ b/components/engine/docs/sources/remote-api/api.rst @@ -0,0 +1,1005 @@ +================= +Docker Remote API +================= + +.. contents:: Table of Contents + +1. Brief introduction +===================== + +- The Remote API is replacing rcli +- Default port in the docker deamon is 4243 +- The API tends to be REST, but for some complex commands, like attach or pull, the HTTP connection in hijacked to transport stdout stdin and stderr + +2. Endpoints +============ + +2.1 Containers +-------------- + +List containers +*************** + +.. http:get:: /containers/ps + + List containers + + **Example request**: + + .. sourcecode:: http + + GET /containers/ps?trunc_cmd=0&all=1&only_ids=0&before=8dfafdbc3a40 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id": "8dfafdbc3a40", + "Image": "base:latest", + "Command": "echo 1", + "Created": 1367854155, + "Status": "Exit 0" + }, + { + "Id": "9cd87474be90", + "Image": "base:latest", + "Command": "echo 222222", + "Created": 1367854155, + "Status": "Exit 0" + }, + { + "Id": "3176a2479c92", + "Image": "base:latest", + "Command": "echo 3333333333333333", + "Created": 1367854154, + "Status": "Exit 0" + }, + { + "Id": "4cb07b47f9fb", + "Image": "base:latest", + "Command": "echo 444444444444444444444444444444444", + "Created": 1367854152, + "Status": "Exit 0" + } + ] + + :query only_ids: 1 or 0, Only display numeric IDs. Default 0 + :query all: 1 or 0, Show all containers. Only running containers are shown by default + :query trunc_cmd: 1 or 0, Truncate output. Output is truncated by default + :query limit: Show ``limit`` last created containers, include non-running ones. + :query since: Show only containers created since Id, include non-running ones. + :query before: Show only containers created before Id, include non-running ones. + :statuscode 200: no error + :statuscode 500: server error + + +Create a container +****************** + +.. http:post:: /containers/create + + Create a container + + **Example request**: + + .. sourcecode:: http + + POST /containers/create HTTP/1.1 + Content-Type: application/json + + { + "Hostname":"", + "User":"", + "Memory":0, + "MemorySwap":0, + "AttachStdin":false, + "AttachStdout":true, + "AttachStderr":true, + "PortSpecs":null, + "Tty":false, + "OpenStdin":false, + "StdinOnce":false, + "Env":null, + "Cmd":[ + "date" + ], + "Dns":null, + "Image":"base", + "Volumes":{}, + "VolumesFrom":"" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 OK + + { + "Id":"e90e34656806" + "Warnings":[] + } + + :jsonparam config: the container's configuration + :statuscode 201: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Inspect a container +******************* + +.. http:get:: /containers/(id)/json + + Return low-level information on the container ``id`` + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/json HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2", + "Created": "2013-05-07T14:51:42.041847+02:00", + "Path": "date", + "Args": [], + "Config": { + "Hostname": "4fa6e0f0c678", + "User": "", + "Memory": 0, + "MemorySwap": 0, + "AttachStdin": false, + "AttachStdout": true, + "AttachStderr": true, + "PortSpecs": null, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": [ + "date" + ], + "Dns": null, + "Image": "base", + "Volumes": {}, + "VolumesFrom": "" + }, + "State": { + "Running": false, + "Pid": 0, + "ExitCode": 0, + "StartedAt": "2013-05-07T14:51:42.087658+02:01360", + "Ghost": false + }, + "Image": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "NetworkSettings": { + "IpAddress": "", + "IpPrefixLen": 0, + "Gateway": "", + "Bridge": "", + "PortMapping": null + }, + "SysInitPath": "/home/kitty/go/src/github.com/dotcloud/docker/bin/docker", + "ResolvConfPath": "/etc/resolv.conf", + "Volumes": {} + } + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Inspect changes on a container's filesystem +******************************************* + +.. http:get:: /containers/(id)/changes + + Inspect changes on container ``id`` 's filesystem + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/changes HTTP/1.1 + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Path":"/dev", + "Kind":0 + }, + { + "Path":"/dev/kmsg", + "Kind":1 + }, + { + "Path":"/test", + "Kind":1 + } + ] + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Export a container +****************** + +.. http:get:: /containers/(id)/export + + Export the contents of container ``id`` + + **Example request**: + + .. sourcecode:: http + + GET /containers/4fa6e0f0c678/export HTTP/1.1 + + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/octet-stream + + {{ STREAM }} + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Start a container +***************** + +.. http:post:: /containers/(id)/start + + Start the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/start HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Stop a contaier +*************** + +.. http:post:: /containers/(id)/stop + + Stop the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/stop?t=5 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query t: number of seconds to wait before killing the container + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Restart a container +******************* + +.. http:post:: /containers/(id)/restart + + Restart the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/restart?t=5 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query t: number of seconds to wait before killing the container + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Kill a container +**************** + +.. http:post:: /containers/(id)/kill + + Kill the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/e90e34656806/kill HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Attach to a container +********************* + +.. http:post:: /containers/(id)/attach + + Stop the container ``id`` + + **Example request**: + + .. sourcecode:: http + + POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + + :query logs: 1 or 0, return logs. Default 0 + :query stream: 1 or 0, return stream. Default 0 + :query stdin: 1 or 0, if stream=1, attach to stdin. Default 0 + :query stdout: 1 or 0, if logs=1, return stdout log, if stream=1, attach to stdout. Default 0 + :query stderr: 1 or 0, if logs=1, return stderr log, if stream=1, attach to stderr. Default 0 + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Wait a container +**************** + +.. http:post:: /containers/(id)/wait + + Block until container ``id`` stops, then returns the exit code + + **Example request**: + + .. sourcecode:: http + + POST /containers/16253994b7c4/wait HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + {"StatusCode":0} + + :statuscode 200: no error + :statuscode 404: no such container + :statuscode 500: server error + + +Remove a container +******************* + +.. http:delete:: /containers/(id) + + Remove the container ``id`` from the filesystem + + **Example request**: + + .. sourcecode:: http + + DELETE /containers/16253994b7c4?v=1 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :query v: 1 or 0, Remove the volumes associated to the container. Default 0 + :statuscode 204: no error + :statuscode 404: no such container + :statuscode 500: server error + + +2.2 Images +---------- + +List Images +*********** + +.. http:get:: /images/(format) + + List images ``format`` could be json or viz (json default) + + **Example request**: + + .. sourcecode:: http + + GET /images/json?all=0&only_ids=0 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Repository":"base", + "Tag":"ubuntu-12.10", + "Id":"b750fe79269d", + "Created":1364102658 + }, + { + "Repository":"base", + "Tag":"ubuntu-quantal", + "Id":"b750fe79269d", + "Created":1364102658 + } + ] + + + **Example request**: + + .. sourcecode:: http + + GET /images/viz HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: text/plain + + digraph docker { + "d82cbacda43a" -> "074be284591f" + "1496068ca813" -> "08306dc45919" + "08306dc45919" -> "0e7893146ac2" + "b750fe79269d" -> "1496068ca813" + base -> "27cf78414709" [style=invis] + "f71189fff3de" -> "9a33b36209ed" + "27cf78414709" -> "b750fe79269d" + "0e7893146ac2" -> "d6434d954665" + "d6434d954665" -> "d82cbacda43a" + base -> "e9aa60c60128" [style=invis] + "074be284591f" -> "f71189fff3de" + "b750fe79269d" [label="b750fe79269d\nbase",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; + "e9aa60c60128" [label="e9aa60c60128\nbase2",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; + "9a33b36209ed" [label="9a33b36209ed\ntest",shape=box,fillcolor="paleturquoise",style="filled,rounded"]; + base [style=invisible] + } + + :query only_ids: 1 or 0, Only display numeric IDs. Default 0 + :query all: 1 or 0, Show all containers. Only running containers are shown by default + :statuscode 200: no error + :statuscode 500: server error + + +Create an image +*************** + +.. http:post:: /images/create + + Create an image, either by pull it from the registry or by importing it + + **Example request**: + + .. sourcecode:: http + + POST /images/create?fromImage=base HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + + :query fromImage: name of the image to pull + :query fromSrc: source to import, - means stdin + :query repo: repository + :query tag: tag + :query registry: the registry to pull from + :statuscode 200: no error + :statuscode 500: server error + + +Insert a file in a image +************************ + +.. http:post:: /images/(name)/insert + + Insert a file from ``url`` in the image ``name`` at ``path`` + + **Example request**: + + .. sourcecode:: http + + POST /images/test/insert?path=/usr&url=myurl HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + {{ STREAM }} + + :statuscode 200: no error + :statuscode 500: server error + + +Inspect an image +**************** + +.. http:get:: /images/(name)/json + + Return low-level information on the image ``name`` + + **Example request**: + + .. sourcecode:: http + + GET /images/base/json HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc", + "parent":"27cf784147099545", + "created":"2013-03-23T22:24:18.818426-07:00", + "container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0", + "container_config": + { + "Hostname":"", + "User":"", + "Memory":0, + "MemorySwap":0, + "AttachStdin":false, + "AttachStdout":false, + "AttachStderr":false, + "PortSpecs":null, + "Tty":true, + "OpenStdin":true, + "StdinOnce":false, + "Env":null, + "Cmd": ["/bin/bash"] + ,"Dns":null, + "Image":"base", + "Volumes":null, + "VolumesFrom":"" + } + } + + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Get the history of an image +*************************** + +.. http:get:: /images/(name)/history + + Return the history of the image ``name`` + + **Example request**: + + .. sourcecode:: http + + GET /images/base/history HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Id":"b750fe79269d", + "Created":1364102658, + "CreatedBy":"/bin/bash" + }, + { + "Id":"27cf78414709", + "Created":1364068391, + "CreatedBy":"" + } + ] + + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Push an image on the registry +***************************** + +.. http:post:: /images/(name)/push + + Push the image ``name`` on the registry + + **Example request**: + + .. sourcecode:: http + + POST /images/test/push HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/vnd.docker.raw-stream + + {{ STREAM }} + + :query registry: the registry you wan to push, optional + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Tag an image into a repository +****************************** + +.. http:post:: /images/(name)/tag + + Tag the image ``name`` into a repository + + **Example request**: + + .. sourcecode:: http + + POST /images/test/tag?repo=myrepo&force=0 HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :query repo: The repository to tag in + :query force: 1 or 0, default 0 + :statuscode 200: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Remove an image +*************** + +.. http:delete:: /images/(name) + + Remove the image ``name`` from the filesystem + + **Example request**: + + .. sourcecode:: http + + DELETE /images/test HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 204 OK + + :statuscode 204: no error + :statuscode 404: no such image + :statuscode 500: server error + + +Search images +************* + +.. http:get:: /images/search + + Search for an image in the docker index + + **Example request**: + + .. sourcecode:: http + + GET /images/search?term=sshd HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "Name":"cespare/sshd", + "Description":"" + }, + { + "Name":"johnfuller/sshd", + "Description":"" + }, + { + "Name":"dhrp/mongodb-sshd", + "Description":"" + } + ] + + :query term: term to search + :statuscode 200: no error + :statuscode 500: server error + + +2.3 Misc +-------- + +Build an image from Dockerfile via stdin +**************************************** + +.. http:post:: /build + + Build an image from Dockerfile via stdin + + **Example request**: + + .. sourcecode:: http + + POST /build HTTP/1.1 + + {{ STREAM }} + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + {{ STREAM }} + + :statuscode 200: no error + :statuscode 500: server error + + +Get default username and email +****************************** + +.. http:get:: /auth + + Get the default username and email + + **Example request**: + + .. sourcecode:: http + + GET /auth HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "username":"hannibal", + "email":"hannibal@a-team.com" + } + + :statuscode 200: no error + :statuscode 500: server error + + +Set auth configuration +********************** + +.. http:post:: /auth + + Get the default username and email + + **Example request**: + + .. sourcecode:: http + + POST /auth HTTP/1.1 + Content-Type: application/json + + { + "username":"hannibal", + "password:"xxxx", + "email":"hannibal@a-team.com" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :statuscode 200: no error + :statuscode 204: no error + :statuscode 500: server error + + +Display system-wide information +******************************* + +.. http:get:: /info + + Display system-wide information + + **Example request**: + + .. sourcecode:: http + + GET /info HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Containers":11, + "Version":"0.2.2", + "Images":16, + "GoVersion":"go1.0.3", + "Debug":false + } + + :statuscode 200: no error + :statuscode 500: server error + + +Show the docker version information +*********************************** + +.. http:get:: /version + + Show the docker version information + + **Example request**: + + .. sourcecode:: http + + GET /version HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "Version":"0.2.2", + "GitCommit":"5a2a5cc+CHANGES", + "MemoryLimit":true, + "SwapLimit":false + } + + :statuscode 200: no error + :statuscode 500: server error + + +Create a new image from a container's changes +********************************************* + +.. http:post:: /commit + + Create a new image from a container's changes + + **Example request**: + + .. sourcecode:: http + + POST /commit?container=44c004db4b17&m=message&repo=myrepo HTTP/1.1 + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 OK + Content-Type: application/vnd.docker.raw-stream + + {"Id":"596069db4bf5"} + + :query container: source container + :query repo: repository + :query tag: tag + :query m: commit message + :query author: author (eg. "John Hannibal Smith ") + :query run: config automatically applied when the image is run. (ex: {"Cmd": ["cat", "/world"], "PortSpecs":["22"]}) + :statuscode 201: no error + :statuscode 404: no such container + :statuscode 500: server error + + +3. Going further +================ + +3.1 Inside 'docker run' +----------------------- + +Here are the steps of 'docker run' : + +* Create the container +* If the status code is 404, it means the image doesn't exists: + * Try to pull it + * Then retry to create the container +* Start the container +* If you are not in detached mode: + * Attach to the container, using logs=1 (to have stdout and stderr from the container's start) and stream=1 +* If in detached mode or only stdin is attached: + * Display the container's id + + +3.2 Hijacking +------------- + +In this first version of the API, some of the endpoints, like /attach, /pull or /push uses hijacking to transport stdin, +stdout and stderr on the same socket. This might change in the future. diff --git a/components/engine/docs/sources/remote-api/index.rst b/components/engine/docs/sources/remote-api/index.rst new file mode 100644 index 0000000000..5b3b790b56 --- /dev/null +++ b/components/engine/docs/sources/remote-api/index.rst @@ -0,0 +1,15 @@ +:title: docker Remote API documentation +:description: Documentation for docker Remote API +:keywords: docker, rest, api, http + + + +Remote API +========== + +Contents: + +.. toctree:: + :maxdepth: 2 + + api diff --git a/components/engine/docs/theme/docker/static/css/main.css b/components/engine/docs/theme/docker/static/css/main.css index 3948acc55d..0629efeb48 100755 --- a/components/engine/docs/theme/docker/static/css/main.css +++ b/components/engine/docs/theme/docker/static/css/main.css @@ -330,3 +330,7 @@ section.header { @media (max-width: 480px) { } +/* Misc fixes */ +table th { + text-align: left; +} diff --git a/components/engine/docs/theme/docker/static/css/main.less b/components/engine/docs/theme/docker/static/css/main.less index 69f53f9e1b..50c8fe6b4b 100644 --- a/components/engine/docs/theme/docker/static/css/main.less +++ b/components/engine/docs/theme/docker/static/css/main.less @@ -449,4 +449,9 @@ section.header { @media (max-width: 480px) { +} + +/* Misc fixes */ +table th { + text-align: left; } \ No newline at end of file diff --git a/components/engine/docs/website/dotcloud.yml b/components/engine/docs/website/dotcloud.yml new file mode 100644 index 0000000000..5a8f50f9e9 --- /dev/null +++ b/components/engine/docs/website/dotcloud.yml @@ -0,0 +1,2 @@ +www: + type: static \ No newline at end of file diff --git a/components/engine/docs/website/gettingstarted/index.html b/components/engine/docs/website/gettingstarted/index.html new file mode 100644 index 0000000000..c005cfc9f9 --- /dev/null +++ b/components/engine/docs/website/gettingstarted/index.html @@ -0,0 +1,210 @@ + + + + + + + + + + Docker - the Linux container runtime + + + + + + + + + + + + + + + + + + + + + + + +
+
+

GETTING STARTED

+
+
+ +
+ +
+
+ Docker is still under heavy development. It should not yet be used in production. Check the repo for recent progress. +
+
+
+
+

+ + Installing on Ubuntu

+ +

Requirements

+
    +
  • Ubuntu 12.04 (LTS) (64-bit)
  • +
  • or Ubuntu 12.10 (quantal) (64-bit)
  • +
+
    +
  1. +

    Install dependencies

    + The linux-image-extra package is only needed on standard Ubuntu EC2 AMIs in order to install the aufs kernel module. +
    sudo apt-get install linux-image-extra-`uname -r`
    + + +
  2. +
  3. +

    Install Docker

    +

    Add the Ubuntu PPA (Personal Package Archive) sources to your apt sources list, update and install.

    +

    You may see some warnings that the GPG keys cannot be verified.

    +
    +
    sudo sh -c "echo 'deb http://ppa.launchpad.net/dotcloud/lxc-docker/ubuntu precise main' >> /etc/apt/sources.list"
    +
    sudo apt-get update
    +
    sudo apt-get install lxc-docker
    +
    + + +
  4. + +
  5. +

    Run!

    + +
    +
    docker run -i -t ubuntu /bin/bash
    +
    +
  6. + Continue with the Hello world example. +
+
+ +
+

Contributing to Docker

+ +

Want to hack on Docker? Awesome! We have some instructions to get you started. They are probably not perfect, please let us know if anything feels wrong or incomplete.

+
+ +
+
+
+

Quick install on other operating systems

+

For other operating systems we recommend and provide a streamlined install with virtualbox, + vagrant and an Ubuntu virtual machine.

+ + + +
+ +
+

More resources

+ +
+ + +
+
+ Fill out my online form. +
+ +
+ +
+
+
+ + +
+
+
+ +
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + diff --git a/components/engine/docs/website/index.html b/components/engine/docs/website/index.html new file mode 100644 index 0000000000..90de706881 --- /dev/null +++ b/components/engine/docs/website/index.html @@ -0,0 +1,314 @@ + + + + + + + + + + + Docker - the Linux container engine + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ +
+
+ + +

The Linux container engine

+
+ +
+ +
+ Docker is an open-source engine which automates the deployment of applications as highly portable, self-sufficient containers which are independent of hardware, language, framework, packaging system and hosting provider. +
+ +
+ + + + +
+ +
+
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+

Heterogeneous payloads

+

Any combination of binaries, libraries, configuration files, scripts, virtualenvs, jars, gems, tarballs, you name it. No more juggling between domain-specific tools. Docker can deploy and run them all.

+

Any server

+

Docker can run on any x64 machine with a modern linux kernel - whether it's a laptop, a bare metal server or a VM. This makes it perfect for multi-cloud deployments.

+

Isolation

+

Docker isolates processes from each other and from the underlying host, using lightweight containers.

+

Repeatability

+

Because each container is isolated in its own filesystem, they behave the same regardless of where, when, and alongside what they run.

+
+
+
+
+

New! Docker Index

+ On the Docker Index you can find and explore pre-made container images. It allows you to share your images and download them. + +

+ +
+ DOCKER index +
+
+   + + +
+
+
+ Fill out my online form. +
+ +
+
+
+ +
+ + + + +
+
+
+
+ + John Willis @botchagalupe: IMHO docker is to paas what chef was to Iaas 4 years ago +
+
+
+
+ + John Feminella ‏@superninjarobot: So, @getdocker is pure excellence. If you've ever wished for arbitrary, PaaS-agnostic, lxc/aufs Linux containers, this is your jam! +
+
+
+
+
+
+ + David Romulan ‏@destructuring: I haven't had this much fun since AWS +
+
+
+
+ + Ricardo Gladwell ‏@rgladwell: wow @getdocker is either amazing or totally stupid +
+
+ +
+
+ +
+
+
+ +
+ +

Notable features

+ +
    +
  • Filesystem isolation: each process container runs in a completely separate root filesystem.
  • +
  • Resource isolation: system resources like cpu and memory can be allocated differently to each process container, using cgroups.
  • +
  • Network isolation: each process container runs in its own network namespace, with a virtual interface and IP address of its own.
  • +
  • Copy-on-write: root filesystems are created using copy-on-write, which makes deployment extremeley fast, memory-cheap and disk-cheap.
  • +
  • Logging: the standard streams (stdout/stderr/stdin) of each process container is collected and logged for real-time or batch retrieval.
  • +
  • Change management: changes to a container's filesystem can be committed into a new image and re-used to create more containers. No templating or manual configuration required.
  • +
  • Interactive shell: docker can allocate a pseudo-tty and attach to the standard input of any container, for example to run a throwaway interactive shell.
  • +
+ +

Under the hood

+ +

Under the hood, Docker is built on the following components:

+ +
    +
  • The cgroup and namespacing capabilities of the Linux kernel;
  • +
  • AUFS, a powerful union filesystem with copy-on-write capabilities;
  • +
  • The Go programming language;
  • +
  • lxc, a set of convenience scripts to simplify the creation of linux containers.
  • +
+ +

Who started it

+

+ Docker is an open-source implementation of the deployment engine which powers dotCloud, a popular Platform-as-a-Service.

+ +

It benefits directly from the experience accumulated over several years of large-scale operation and support of hundreds of thousands + of applications and databases. +

+ +
+
+ +
+ + +
+

Twitter

+ + +
+ +
+
+ +
+ + +
+
+
+
+ + Docker is a project by dotCloud + +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + diff --git a/components/engine/docs/website/nginx.conf b/components/engine/docs/website/nginx.conf new file mode 100644 index 0000000000..97ffd2c0e5 --- /dev/null +++ b/components/engine/docs/website/nginx.conf @@ -0,0 +1,6 @@ + +# rule to redirect original links created when hosted on github pages +rewrite ^/documentation/(.*).html http://docs.docker.io/en/latest/$1/ permanent; + +# rewrite the stuff which was on the current page +rewrite ^/gettingstarted.html$ /gettingstarted/ permanent; diff --git a/components/engine/docs/website/static b/components/engine/docs/website/static new file mode 120000 index 0000000000..95bc97aa10 --- /dev/null +++ b/components/engine/docs/website/static @@ -0,0 +1 @@ +../theme/docker/static \ No newline at end of file diff --git a/components/engine/graph.go b/components/engine/graph.go index 21d9d9407c..d9b4f5ac5c 100644 --- a/components/engine/graph.go +++ b/components/engine/graph.go @@ -1,6 +1,7 @@ package docker import ( + "encoding/json" "fmt" "io" "io/ioutil" @@ -9,14 +10,18 @@ import ( "path" "path/filepath" "strings" + "sync" "time" ) // A Graph is a store for versioned filesystem images and the relationship between them. type Graph struct { - Root string - idIndex *TruncIndex - httpClient *http.Client + Root string + idIndex *TruncIndex + httpClient *http.Client + checksumLock map[string]*sync.Mutex + lockSumFile *sync.Mutex + lockSumMap *sync.Mutex } // NewGraph instantiates a new graph at the given root path in the filesystem. @@ -27,12 +32,15 @@ func NewGraph(root string) (*Graph, error) { return nil, err } // Create the root directory if it doesn't exists - if err := os.Mkdir(root, 0700); err != nil && !os.IsExist(err) { + if err := os.MkdirAll(root, 0700); err != nil && !os.IsExist(err) { return nil, err } graph := &Graph{ - Root: abspath, - idIndex: NewTruncIndex(), + Root: abspath, + idIndex: NewTruncIndex(), + checksumLock: make(map[string]*sync.Mutex), + lockSumFile: &sync.Mutex{}, + lockSumMap: &sync.Mutex{}, } if err := graph.restore(); err != nil { return nil, err @@ -55,7 +63,7 @@ func (graph *Graph) restore() error { // FIXME: Implement error subclass instead of looking at the error text // Note: This is the way golang implements os.IsNotExists on Plan9 func (graph *Graph) IsNotExist(err error) bool { - return err != nil && strings.Contains(err.Error(), "does not exist") + return err != nil && (strings.Contains(err.Error(), "does not exist") || strings.Contains(err.Error(), "No such")) } // Exists returns true if an image is registered at the given id. @@ -82,6 +90,11 @@ func (graph *Graph) Get(name string) (*Image, error) { return nil, fmt.Errorf("Image stored at '%s' has wrong id '%s'", id, img.Id) } img.graph = graph + graph.lockSumMap.Lock() + defer graph.lockSumMap.Unlock() + if _, exists := graph.checksumLock[img.Id]; !exists { + graph.checksumLock[img.Id] = &sync.Mutex{} + } return img, nil } @@ -100,16 +113,16 @@ func (graph *Graph) Create(layerData Archive, container *Container, comment, aut img.Container = container.Id img.ContainerConfig = *container.Config } - if err := graph.Register(layerData, img); err != nil { + if err := graph.Register(layerData, true, img); err != nil { return nil, err } - img.Checksum() + go img.Checksum() return img, nil } // Register imports a pre-existing image into the graph. // FIXME: pass img as first argument -func (graph *Graph) Register(layerData Archive, img *Image) error { +func (graph *Graph) Register(layerData Archive, store bool, img *Image) error { if err := ValidateId(img.Id); err != nil { return err } @@ -122,7 +135,7 @@ func (graph *Graph) Register(layerData Archive, img *Image) error { if err != nil { return fmt.Errorf("Mktemp failed: %s", err) } - if err := StoreImage(img, layerData, tmp); err != nil { + if err := StoreImage(img, layerData, tmp, store); err != nil { return err } // Commit @@ -131,6 +144,7 @@ func (graph *Graph) Register(layerData Archive, img *Image) error { } img.graph = graph graph.idIndex.Add(img.Id) + graph.checksumLock[img.Id] = &sync.Mutex{} return nil } @@ -253,14 +267,14 @@ func (graph *Graph) WalkAll(handler func(*Image)) error { func (graph *Graph) ByParent() (map[string][]*Image, error) { byParent := make(map[string][]*Image) err := graph.WalkAll(func(image *Image) { - image, err := graph.Get(image.Parent) + parent, err := graph.Get(image.Parent) if err != nil { return } - if children, exists := byParent[image.Parent]; exists { - byParent[image.Parent] = []*Image{image} + if children, exists := byParent[parent.Id]; exists { + byParent[parent.Id] = []*Image{image} } else { - byParent[image.Parent] = append(children, image) + byParent[parent.Id] = append(children, image) } }) return byParent, err @@ -287,3 +301,26 @@ func (graph *Graph) Heads() (map[string]*Image, error) { func (graph *Graph) imageRoot(id string) string { return path.Join(graph.Root, id) } + +func (graph *Graph) getStoredChecksums() (map[string]string, error) { + checksums := make(map[string]string) + // FIXME: Store the checksum in memory + + if checksumDict, err := ioutil.ReadFile(path.Join(graph.Root, "checksums")); err == nil { + if err := json.Unmarshal(checksumDict, &checksums); err != nil { + return nil, err + } + } + return checksums, nil +} + +func (graph *Graph) storeChecksums(checksums map[string]string) error { + checksumJson, err := json.Marshal(checksums) + if err != nil { + return err + } + if err := ioutil.WriteFile(path.Join(graph.Root, "checksums"), checksumJson, 0600); err != nil { + return err + } + return nil +} diff --git a/components/engine/graph_test.go b/components/engine/graph_test.go index b7ec81698f..19c6c07cf2 100644 --- a/components/engine/graph_test.go +++ b/components/engine/graph_test.go @@ -37,7 +37,7 @@ func TestInterruptedRegister(t *testing.T) { Comment: "testing", Created: time.Now(), } - go graph.Register(badArchive, image) + go graph.Register(badArchive, false, image) time.Sleep(200 * time.Millisecond) w.CloseWithError(errors.New("But I'm not a tarball!")) // (Nobody's perfect, darling) if _, err := graph.Get(image.Id); err == nil { @@ -48,7 +48,7 @@ func TestInterruptedRegister(t *testing.T) { if err != nil { t.Fatal(err) } - if err := graph.Register(goodArchive, image); err != nil { + if err := graph.Register(goodArchive, false, image); err != nil { t.Fatal(err) } } @@ -94,7 +94,7 @@ func TestRegister(t *testing.T) { Comment: "testing", Created: time.Now(), } - err = graph.Register(archive, image) + err = graph.Register(archive, false, image) if err != nil { t.Fatal(err) } @@ -212,7 +212,7 @@ func TestDelete(t *testing.T) { assertNImages(graph, t, 1) // Test delete twice (pull -> rm -> pull -> rm) - if err := graph.Register(archive, img1); err != nil { + if err := graph.Register(archive, false, img1); err != nil { t.Fatal(err) } if err := graph.Delete(img1.Id); err != nil { diff --git a/components/engine/image.go b/components/engine/image.go index bf86e2e7f7..413d95673b 100644 --- a/components/engine/image.go +++ b/components/engine/image.go @@ -35,8 +35,9 @@ func LoadImage(root string) (*Image, error) { if err != nil { return nil, err } - var img Image - if err := json.Unmarshal(jsonData, &img); err != nil { + img := &Image{} + + if err := json.Unmarshal(jsonData, img); err != nil { return nil, err } if err := ValidateId(img.Id); err != nil { @@ -52,11 +53,10 @@ func LoadImage(root string) (*Image, error) { } else if !stat.IsDir() { return nil, fmt.Errorf("Couldn't load image %s: %s is not a directory", img.Id, layerPath(root)) } - - return &img, nil + return img, nil } -func StoreImage(img *Image, layerData Archive, root string) error { +func StoreImage(img *Image, layerData Archive, root string, store bool) error { // Check that root doesn't already exist if _, err := os.Stat(root); err == nil { return fmt.Errorf("Image %s already exists", img.Id) @@ -68,6 +68,28 @@ func StoreImage(img *Image, layerData Archive, root string) error { if err := os.MkdirAll(layer, 0700); err != nil { return err } + + if store { + layerArchive := layerArchivePath(root) + file, err := os.OpenFile(layerArchive, os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + // FIXME: Retrieve the image layer size from here? + if _, err := io.Copy(file, layerData); err != nil { + return err + } + // FIXME: Don't close/open, read/write instead of Copy + file.Close() + + file, err = os.Open(layerArchive) + if err != nil { + return err + } + defer file.Close() + layerData = file + } + if err := Untar(layerData, layer); err != nil { return err } @@ -86,6 +108,10 @@ func layerPath(root string) string { return path.Join(root, "layer") } +func layerArchivePath(root string) string { + return path.Join(root, "layer.tar.xz") +} + func jsonPath(root string) string { return path.Join(root, "json") } @@ -261,21 +287,20 @@ func (img *Image) layer() (string, error) { } func (img *Image) Checksum() (string, error) { + img.graph.checksumLock[img.Id].Lock() + defer img.graph.checksumLock[img.Id].Unlock() + root, err := img.root() if err != nil { return "", err } - checksumDictPth := path.Join(root, "..", "..", "checksums") - checksums := new(map[string]string) - - if checksumDict, err := ioutil.ReadFile(checksumDictPth); err == nil { - if err := json.Unmarshal(checksumDict, checksums); err != nil { - return "", err - } - if checksum, ok := (*checksums)[img.Id]; ok { - return checksum, nil - } + checksums, err := img.graph.getStoredChecksums() + if err != nil { + return "", err + } + if checksum, ok := checksums[img.Id]; ok { + return checksum, nil } layer, err := img.layer() @@ -287,9 +312,20 @@ func (img *Image) Checksum() (string, error) { return "", err } - layerData, err := Tar(layer, Xz) - if err != nil { - return "", err + var layerData io.Reader + + if file, err := os.Open(layerArchivePath(root)); err != nil { + if os.IsNotExist(err) { + layerData, err = Tar(layer, Xz) + if err != nil { + return "", err + } + } else { + return "", err + } + } else { + defer file.Close() + layerData = file } h := sha256.New() @@ -299,22 +335,27 @@ func (img *Image) Checksum() (string, error) { if _, err := h.Write([]byte("\n")); err != nil { return "", err } + if _, err := io.Copy(h, layerData); err != nil { return "", err } - hash := "sha256:" + hex.EncodeToString(h.Sum(nil)) - if *checksums == nil { - *checksums = map[string]string{} - } - (*checksums)[img.Id] = hash - checksumJson, err := json.Marshal(checksums) + + // Reload the json file to make sure not to overwrite faster sums + img.graph.lockSumFile.Lock() + defer img.graph.lockSumFile.Unlock() + + checksums, err = img.graph.getStoredChecksums() if err != nil { + return "", err + } + + checksums[img.Id] = hash + + // Dump the checksums to disc + if err := img.graph.storeChecksums(checksums); err != nil { return hash, err } - if err := ioutil.WriteFile(checksumDictPth, checksumJson, 0600); err != nil { - return hash, err - } return hash, nil } diff --git a/components/engine/packaging/ubuntu/changelog b/components/engine/packaging/ubuntu/changelog index b3e68558ff..2e4907f200 100644 --- a/components/engine/packaging/ubuntu/changelog +++ b/components/engine/packaging/ubuntu/changelog @@ -1,3 +1,41 @@ +lxc-docker (0.3.2-1) precise; urgency=low + - Runtime: Store the actual archive on commit + - Registry: Improve the checksum process + - Registry: Use the size to have a good progress bar while pushing + - Registry: Use the actual archive if it exists in order to speed up the push + - Registry: Fix error 400 on push + + -- dotCloud Fri, 9 May 2013 00:00:00 -0700 + + +lxc-docker (0.3.1-1) precise; urgency=low + - Builder: Implement the autorun capability within docker builder + - Builder: Add caching to docker builder + - Builder: Add support for docker builder with native API as top level command + - Runtime: Add go version to debug infos + - Builder: Implement ENV within docker builder + - Registry: Add docker search top level command in order to search a repository + - Images: output graph of images to dot (graphviz) + - Documentation: new introduction and high-level overview + - Documentation: Add the documentation for docker builder + - Website: new high-level overview + - Makefile: Swap "go get" for "go get -d", especially to compile on go1.1rc + - Images: fix ByParent function + - Builder: Check the command existance prior create and add Unit tests for the case + - Registry: Fix pull for official images with specific tag + - Registry: Fix issue when login in with a different user and trying to push + - Documentation: CSS fix for docker documentation to make REST API docs look better. + - Documentation: Fixed CouchDB example page header mistake + - Documentation: fixed README formatting + - Registry: Improve checksum - async calculation + - Runtime: kernel version - don't show the dash if flavor is empty + - Documentation: updated www.docker.io website. + - Builder: use any whitespaces instead of tabs + - Packaging: packaging ubuntu; issue #510: Use goland-stable PPA package to build docker + + -- dotCloud Fri, 8 May 2013 00:00:00 -0700 + + lxc-docker (0.3.0-1) precise; urgency=low - Registry: Implement the new registry - Documentation: new example: sharing data between 2 couchdb databases diff --git a/components/engine/rcli/tcp.go b/components/engine/rcli/tcp.go deleted file mode 100644 index cf111cdf71..0000000000 --- a/components/engine/rcli/tcp.go +++ /dev/null @@ -1,169 +0,0 @@ -package rcli - -import ( - "bufio" - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "log" - "net" -) - -// Note: the globals are here to avoid import cycle -// FIXME: Handle debug levels mode? -var DEBUG_FLAG bool = false -var CLIENT_SOCKET io.Writer = nil - -type DockerTCPConn struct { - conn *net.TCPConn - options *DockerConnOptions - optionsBuf *[]byte - handshaked bool - client bool -} - -func NewDockerTCPConn(conn *net.TCPConn, client bool) *DockerTCPConn { - return &DockerTCPConn{ - conn: conn, - options: &DockerConnOptions{}, - client: client, - } -} - -func (c *DockerTCPConn) SetOptionRawTerminal() { - c.options.RawTerminal = true -} - -func (c *DockerTCPConn) GetOptions() *DockerConnOptions { - if c.client && !c.handshaked { - // Attempt to parse options encoded as a JSON dict and store - // the reminder of what we read from the socket in a buffer. - // - // bufio (and its ReadBytes method) would have been nice here, - // but if json.Unmarshal() fails (which will happen if we speak - // to a version of docker that doesn't send any option), then - // we can't put the data back in it for the next Read(). - c.handshaked = true - buf := make([]byte, 4096) - if n, _ := c.conn.Read(buf); n > 0 { - buf = buf[:n] - if nl := bytes.IndexByte(buf, '\n'); nl != -1 { - if err := json.Unmarshal(buf[:nl], c.options); err == nil { - buf = buf[nl+1:] - } - } - c.optionsBuf = &buf - } - } - - return c.options -} - -func (c *DockerTCPConn) Read(b []byte) (int, error) { - if c.optionsBuf != nil { - // Consume what we buffered in GetOptions() first: - optionsBuf := *c.optionsBuf - optionsBuflen := len(optionsBuf) - copied := copy(b, optionsBuf) - if copied < optionsBuflen { - optionsBuf = optionsBuf[copied:] - c.optionsBuf = &optionsBuf - return copied, nil - } - c.optionsBuf = nil - return copied, nil - } - return c.conn.Read(b) -} - -func (c *DockerTCPConn) Write(b []byte) (int, error) { - optionsLen := 0 - if !c.client && !c.handshaked { - c.handshaked = true - options, _ := json.Marshal(c.options) - options = append(options, '\n') - if optionsLen, err := c.conn.Write(options); err != nil { - return optionsLen, err - } - } - n, err := c.conn.Write(b) - return n + optionsLen, err -} - -func (c *DockerTCPConn) Flush() error { - _, err := c.Write([]byte{}) - return err -} - -func (c *DockerTCPConn) Close() error { return c.conn.Close() } - -func (c *DockerTCPConn) CloseWrite() error { return c.conn.CloseWrite() } - -func (c *DockerTCPConn) CloseRead() error { return c.conn.CloseRead() } - -// 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) (DockerConn, error) { - cmd, err := json.Marshal(args) - if err != nil { - return nil, err - } - conn, err := dialDocker(proto, addr) - if err != nil { - return nil, err - } - if _, err := fmt.Fprintln(conn, string(cmd)); err != nil { - return nil, err - } - return conn, nil -} - -// 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/%s on %s\n", proto, addr) - defer listener.Close() - for { - if conn, err := listener.Accept(); err != nil { - return err - } else { - conn, err := newDockerServerConn(conn) - if err != nil { - return err - } - go func(conn DockerConn) { - defer conn.Close() - if DEBUG_FLAG { - CLIENT_SOCKET = conn - } - if err := Serve(conn, service); err != nil { - log.Println("Error:", err.Error()) - fmt.Fprintln(conn, "Error:", err.Error()) - } - }(conn) - } - } - return nil -} - -// Parse an rcli call on a new connection, and pass it to `service` if it -// is valid. -func Serve(conn DockerConn, service Service) error { - r := bufio.NewReader(conn) - var args []string - if line, err := r.ReadString('\n'); err != nil { - return err - } else if err := json.Unmarshal([]byte(line), &args); err != nil { - return err - } else { - return call(service, ioutil.NopCloser(r), conn, args...) - } - return nil -} diff --git a/components/engine/rcli/types.go b/components/engine/rcli/types.go deleted file mode 100644 index 38f4a8c008..0000000000 --- a/components/engine/rcli/types.go +++ /dev/null @@ -1,181 +0,0 @@ -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 ( - "flag" - "fmt" - "github.com/dotcloud/docker/term" - "io" - "log" - "net" - "os" - "reflect" - "strings" -) - -type DockerConnOptions struct { - RawTerminal bool -} - -type DockerConn interface { - io.ReadWriteCloser - CloseWrite() error - CloseRead() error - GetOptions() *DockerConnOptions - SetOptionRawTerminal() - Flush() error -} - -type DockerLocalConn struct { - writer io.WriteCloser - savedState *term.State -} - -func NewDockerLocalConn(w io.WriteCloser) *DockerLocalConn { - return &DockerLocalConn{ - writer: w, - } -} - -func (c *DockerLocalConn) Read(b []byte) (int, error) { - return 0, fmt.Errorf("DockerLocalConn does not implement Read()") -} - -func (c *DockerLocalConn) Write(b []byte) (int, error) { return c.writer.Write(b) } - -func (c *DockerLocalConn) Close() error { - if c.savedState != nil { - RestoreTerminal(c.savedState) - c.savedState = nil - } - return c.writer.Close() -} - -func (c *DockerLocalConn) Flush() error { return nil } - -func (c *DockerLocalConn) CloseWrite() error { return nil } - -func (c *DockerLocalConn) CloseRead() error { return nil } - -func (c *DockerLocalConn) GetOptions() *DockerConnOptions { return nil } - -func (c *DockerLocalConn) SetOptionRawTerminal() { - if state, err := SetRawTerminal(); err != nil { - if os.Getenv("DEBUG") != "" { - log.Printf("Can't set the terminal in raw mode: %s", err) - } - } else { - c.savedState = state - } -} - -var UnknownDockerProto = fmt.Errorf("Only TCP is actually supported by Docker at the moment") - -func dialDocker(proto string, addr string) (DockerConn, error) { - conn, err := net.Dial(proto, addr) - if err != nil { - return nil, err - } - switch i := conn.(type) { - case *net.TCPConn: - return NewDockerTCPConn(i, true), nil - } - return nil, UnknownDockerProto -} - -func newDockerFromConn(conn net.Conn, client bool) (DockerConn, error) { - switch i := conn.(type) { - case *net.TCPConn: - return NewDockerTCPConn(i, client), nil - } - return nil, UnknownDockerProto -} - -func newDockerServerConn(conn net.Conn) (DockerConn, error) { - return newDockerFromConn(conn, false) -} - -type Service interface { - Name() string - Help() string -} - -type Cmd func(io.ReadCloser, io.Writer, ...string) error -type CmdMethod func(Service, io.ReadCloser, io.Writer, ...string) error - -// FIXME: For reverse compatibility -func call(service Service, stdin io.ReadCloser, stdout DockerConn, args ...string) error { - return LocalCall(service, stdin, stdout, args...) -} - -func LocalCall(service Service, stdin io.ReadCloser, stdout DockerConn, 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())) } - if err := flags.Parse(args); err != nil { - return err - } - cmd := flags.Arg(0) - log.Printf("%s\n", strings.Join(append(append([]string{service.Name()}, cmd), flags.Args()[1:]...), " ")) - if cmd == "" { - cmd = "help" - } - method := getMethod(service, cmd) - if method != nil { - return method(stdin, stdout, flags.Args()[1:]...) - } - return fmt.Errorf("No such command: %s", cmd) -} - -func getMethod(service Service, name string) Cmd { - if name == "help" { - return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - if len(args) == 0 { - stdout.Write([]byte(service.Help())) - } else { - if method := getMethod(service, args[0]); method == nil { - return fmt.Errorf("No such command: %s", args[0]) - } else { - method(stdin, stdout, "--help") - } - } - return nil - } - } - methodName := "Cmd" + strings.ToUpper(name[:1]) + strings.ToLower(name[1:]) - method, exists := reflect.TypeOf(service).MethodByName(methodName) - if !exists { - return nil - } - return func(stdin io.ReadCloser, stdout io.Writer, args ...string) error { - ret := method.Func.CallSlice([]reflect.Value{ - reflect.ValueOf(service), - reflect.ValueOf(stdin), - reflect.ValueOf(stdout), - reflect.ValueOf(args), - })[0].Interface() - if ret == nil { - return nil - } - return ret.(error) - } -} - -func Subcmd(output io.Writer, name, signature, description string) *flag.FlagSet { - flags := flag.NewFlagSet(name, flag.ContinueOnError) - flags.SetOutput(output) - flags.Usage = func() { - fmt.Fprintf(output, "\nUsage: docker %s %s\n\n%s\n\n", name, signature, description) - flags.PrintDefaults() - } - return flags -} diff --git a/components/engine/rcli/utils.go b/components/engine/rcli/utils.go deleted file mode 100644 index dbd579ffcd..0000000000 --- a/components/engine/rcli/utils.go +++ /dev/null @@ -1,27 +0,0 @@ -package rcli - -import ( - "github.com/dotcloud/docker/term" - "os" - "os/signal" -) - -//FIXME: move these function to utils.go (in rcli to avoid import loop) -func SetRawTerminal() (*term.State, error) { - oldState, err := term.MakeRaw(int(os.Stdin.Fd())) - if err != nil { - return nil, err - } - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt) - go func() { - _ = <-c - term.Restore(int(os.Stdin.Fd()), oldState) - os.Exit(0) - }() - return oldState, err -} - -func RestoreTerminal(state *term.State) { - term.Restore(int(os.Stdin.Fd()), state) -} diff --git a/components/engine/registry.go b/components/engine/registry.go index cc8ee68153..b8a3e10599 100644 --- a/components/engine/registry.go +++ b/components/engine/registry.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "net/http" "net/url" + "os" "path" "strings" ) @@ -194,18 +195,16 @@ func (graph *Graph) getRemoteTags(stdout io.Writer, registries []string, reposit return nil, fmt.Errorf("Repository not found") } - result := new(map[string]string) + result := make(map[string]string) rawJson, err := ioutil.ReadAll(res.Body) if err != nil { return nil, err } - if err = json.Unmarshal(rawJson, result); err != nil { + if err = json.Unmarshal(rawJson, &result); err != nil { return nil, err } - - return *result, nil - + return result, nil } return nil, fmt.Errorf("Could not reach any registry endpoint") } @@ -261,7 +260,7 @@ func (graph *Graph) PullImage(stdout io.Writer, imgId, registry string, token [] // FIXME: Keep goging in case of error? return err } - if err = graph.Register(layer, img); err != nil { + if err = graph.Register(layer, false, img); err != nil { return err } } @@ -308,6 +307,47 @@ func (graph *Graph) PullRepository(stdout io.Writer, remote, askedTag string, re return fmt.Errorf("Index response didn't contain any endpoints") } + checksumsJson, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + + // Reload the json file to make sure not to overwrite faster sums + err = func() error { + localChecksums := make(map[string]string) + remoteChecksums := []ImgListJson{} + checksumDictPth := path.Join(graph.Root, "checksums") + + if err := json.Unmarshal(checksumsJson, &remoteChecksums); err != nil { + return err + } + + graph.lockSumFile.Lock() + defer graph.lockSumFile.Unlock() + + if checksumDict, err := ioutil.ReadFile(checksumDictPth); err == nil { + if err := json.Unmarshal(checksumDict, &localChecksums); err != nil { + return err + } + } + + for _, elem := range remoteChecksums { + localChecksums[elem.Id] = elem.Checksum + } + + checksumsJson, err = json.Marshal(localChecksums) + if err != nil { + return err + } + if err := ioutil.WriteFile(checksumDictPth, checksumsJson, 0600); err != nil { + return err + } + return nil + }() + if err != nil { + return err + } + var tagsList map[string]string if askedTag == "" { tagsList, err = graph.getRemoteTags(stdout, endpoints, remote, token) @@ -353,14 +393,10 @@ func (graph *Graph) PullRepository(stdout io.Writer, remote, askedTag string, re return nil } -func pushImageRec(graph *Graph, stdout io.Writer, img *Image, registry string, token []string) error { - if parent, err := img.GetParent(); err != nil { - return err - } else if parent != nil { - if err := pushImageRec(graph, stdout, parent, registry, token); err != nil { - return err - } - } +// Push a local image to the registry +func (graph *Graph) PushImage(stdout io.Writer, img *Image, registry string, token []string) error { + registry = "https://" + registry + "/v1" + client := graph.getHttpClient() jsonRaw, err := ioutil.ReadFile(path.Join(graph.Root, img.Id, "json")) if err != nil { @@ -383,6 +419,7 @@ func pushImageRec(graph *Graph, stdout io.Writer, img *Image, registry string, t return fmt.Errorf("Error while retrieving checksum for %s: %v", img.Id, err) } req.Header.Set("X-Docker-Checksum", checksum) + Debugf("Setting checksum for %s: %s", img.ShortId(), checksum) res, err := doWithCookies(client, req) if err != nil { return fmt.Errorf("Failed to upload metadata: %s", err) @@ -408,14 +445,35 @@ func pushImageRec(graph *Graph, stdout io.Writer, img *Image, registry string, t } fmt.Fprintf(stdout, "Pushing %s fs layer\r\n", img.Id) - - layerData, err := graph.TempLayerArchive(img.Id, Xz, stdout) + root, err := img.root() if err != nil { - return fmt.Errorf("Failed to generate layer archive: %s", err) + return err + } + + var layerData *TempArchive + // If the archive exists, use it + file, err := os.Open(layerArchivePath(root)) + if err != nil { + if os.IsNotExist(err) { + // If the archive does not exist, create one from the layer + layerData, err = graph.TempLayerArchive(img.Id, Xz, stdout) + if err != nil { + return fmt.Errorf("Failed to generate layer archive: %s", err) + } + } else { + return err + } + } else { + defer file.Close() + st, err := file.Stat() + if err != nil { + return err + } + layerData = &TempArchive{file, st.Size()} } req3, err := http.NewRequest("PUT", registry+"/images/"+img.Id+"/layer", - ProgressReader(layerData, -1, stdout, "")) + ProgressReader(layerData, int(layerData.Size), stdout, "")) if err != nil { return err } @@ -427,19 +485,19 @@ func pushImageRec(graph *Graph, stdout io.Writer, img *Image, registry string, t if err != nil { return fmt.Errorf("Failed to upload layer: %s", err) } - res3.Body.Close() + defer res3.Body.Close() + if res3.StatusCode != 200 { - return fmt.Errorf("Received HTTP code %d while uploading layer", res3.StatusCode) + errBody, err := ioutil.ReadAll(res3.Body) + if err != nil { + return fmt.Errorf("HTTP code %d while uploading metadata and error when"+ + " trying to parse response body: %v", res.StatusCode, err) + } + return fmt.Errorf("Received HTTP code %d while uploading layer: %s", res3.StatusCode, errBody) } return nil } -// Push a local image to the registry with its history if needed -func (graph *Graph) PushImage(stdout io.Writer, imgOrig *Image, registry string, token []string) error { - registry = "https://" + registry + "/v1" - return pushImageRec(graph, stdout, imgOrig, registry, token) -} - // push a tag on the registry. // Remote has the format '/ func (graph *Graph) pushTag(remote, revision, tag, registry string, token []string) error { @@ -489,48 +547,89 @@ func (graph *Graph) pushPrimitive(stdout io.Writer, remote, tag, imgId, registry return nil } +// Retrieve the checksum of an image +// Priority: +// - Check on the stored checksums +// - Check if the archive exists, if it does not, ask the registry +// - If the archive does exists, process the checksum from it +// - If the archive does not exists and not found on registry, process checksum from layer +func (graph *Graph) getChecksum(imageId string) (string, error) { + // FIXME: Use in-memory map instead of reading the file each time + if sums, err := graph.getStoredChecksums(); err != nil { + return "", err + } else if checksum, exists := sums[imageId]; exists { + return checksum, nil + } + + img, err := graph.Get(imageId) + if err != nil { + return "", err + } + + if _, err := os.Stat(layerArchivePath(graph.imageRoot(imageId))); err != nil { + if os.IsNotExist(err) { + // TODO: Ask the registry for the checksum + // As the archive is not there, it is supposed to come from a pull. + } else { + return "", err + } + } + + checksum, err := img.Checksum() + if err != nil { + return "", err + } + return checksum, nil +} + +type ImgListJson struct { + Id string `json:"id"` + Checksum string `json:"checksum,omitempty"` + tag string +} + // Push a repository to the registry. // Remote has the format '/ func (graph *Graph) PushRepository(stdout io.Writer, remote string, localRepo Repository, authConfig *auth.AuthConfig) error { client := graph.getHttpClient() + // FIXME: Do not reset the cookie each time? (need to reset it in case updating latest of a repo and repushing) + client.Jar = cookiejar.NewCookieJar() + var imgList []*ImgListJson - checksums, err := graph.Checksums(stdout, localRepo) - if err != nil { - return err - } + fmt.Fprintf(stdout, "Processing checksums\n") + imageSet := make(map[string]struct{}) - imgList := make([]map[string]string, len(checksums)) - checksums2 := make([]map[string]string, len(checksums)) - - uploadedImages, err := graph.getImagesInRepository(remote, authConfig) - if err != nil { - return fmt.Errorf("Error occured while fetching the list: %s", err) - } - - // Filter list to only send images/checksums not already uploaded - i := 0 - for _, obj := range checksums { - found := false - for _, uploadedImg := range uploadedImages { - if obj["id"] == uploadedImg["id"] && uploadedImg["checksum"] != "" { - found = true - break + for tag, id := range localRepo { + img, err := graph.Get(id) + if err != nil { + return err + } + img.WalkHistory(func(img *Image) error { + if _, exists := imageSet[img.Id]; exists { + return nil } - } - if !found { - imgList[i] = map[string]string{"id": obj["id"]} - checksums2[i] = obj - i += 1 - } + imageSet[img.Id] = struct{}{} + checksum, err := graph.getChecksum(img.Id) + if err != nil { + return err + } + imgList = append([]*ImgListJson{{ + Id: img.Id, + Checksum: checksum, + tag: tag, + }}, imgList...) + return nil + }) } - checksums = checksums2[:i] - imgList = imgList[:i] imgListJson, err := json.Marshal(imgList) if err != nil { return err } + Debugf("json sent: %s\n", imgListJson) + + fmt.Fprintf(stdout, "Sending image list\n") req, err := http.NewRequest("PUT", INDEX_ENDPOINT+"/repositories/"+remote+"/", bytes.NewReader(imgListJson)) if err != nil { return err @@ -538,11 +637,13 @@ func (graph *Graph) PushRepository(stdout io.Writer, remote string, localRepo Re req.SetBasicAuth(authConfig.Username, authConfig.Password) req.ContentLength = int64(len(imgListJson)) req.Header.Set("X-Docker-Token", "true") + res, err := client.Do(req) if err != nil { return err } defer res.Body.Close() + for res.StatusCode >= 300 && res.StatusCode < 400 { Debugf("Redirected to %s\n", res.Header.Get("Location")) req, err = http.NewRequest("PUT", res.Header.Get("Location"), bytes.NewReader(imgListJson)) @@ -552,6 +653,7 @@ func (graph *Graph) PushRepository(stdout io.Writer, remote string, localRepo Re req.SetBasicAuth(authConfig.Username, authConfig.Password) req.ContentLength = int64(len(imgListJson)) req.Header.Set("X-Docker-Token", "true") + res, err = client.Do(req) if err != nil { return err @@ -560,7 +662,11 @@ func (graph *Graph) PushRepository(stdout io.Writer, remote string, localRepo Re } if res.StatusCode != 200 && res.StatusCode != 201 { - return fmt.Errorf("Error: Status %d trying to push repository %s", res.StatusCode, remote) + errBody, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + return fmt.Errorf("Error: Status %d trying to push repository %s: %s", res.StatusCode, remote, errBody) } var token, endpoints []string @@ -576,75 +682,41 @@ func (graph *Graph) PushRepository(stdout io.Writer, remote string, localRepo Re return fmt.Errorf("Index response didn't contain any endpoints") } + // FIXME: Send only needed images for _, registry := range endpoints { - fmt.Fprintf(stdout, "Pushing repository %s to %s (%d tags)\r\n", remote, registry, - len(localRepo)) + fmt.Fprintf(stdout, "Pushing repository %s to %s (%d tags)\r\n", remote, registry, len(localRepo)) // For each image within the repo, push them - for tag, imgId := range localRepo { - if err := graph.pushPrimitive(stdout, remote, tag, imgId, registry, token); err != nil { + for _, elem := range imgList { + if err := graph.pushPrimitive(stdout, remote, elem.tag, elem.Id, registry, token); err != nil { // FIXME: Continue on error? return err } } } - checksumsJson, err := json.Marshal(checksums) - if err != nil { - return err - } - req2, err := http.NewRequest("PUT", INDEX_ENDPOINT+"/repositories/"+remote+"/images", bytes.NewReader(checksumsJson)) + req2, err := http.NewRequest("PUT", INDEX_ENDPOINT+"/repositories/"+remote+"/images", bytes.NewReader(imgListJson)) if err != nil { return err } req2.SetBasicAuth(authConfig.Username, authConfig.Password) req2.Header["X-Docker-Endpoints"] = endpoints - req2.ContentLength = int64(len(checksumsJson)) + req2.ContentLength = int64(len(imgListJson)) res2, err := client.Do(req2) if err != nil { return err } - res2.Body.Close() + defer res2.Body.Close() if res2.StatusCode != 204 { - return fmt.Errorf("Error: Status %d trying to push checksums %s", res.StatusCode, remote) + if errBody, err := ioutil.ReadAll(res2.Body); err != nil { + return err + } else { + return fmt.Errorf("Error: Status %d trying to push checksums %s: %s", res2.StatusCode, remote, errBody) + } } return nil } -func (graph *Graph) Checksums(output io.Writer, repo Repository) ([]map[string]string, error) { - var result []map[string]string - checksums := map[string]string{} - for _, id := range repo { - img, err := graph.Get(id) - if err != nil { - return nil, err - } - err = img.WalkHistory(func(image *Image) error { - fmt.Fprintf(output, "Computing checksum for image %s\n", image.Id) - if _, exists := checksums[image.Id]; !exists { - checksums[image.Id], err = image.Checksum() - if err != nil { - return err - } - } - return nil - }) - if err != nil { - return nil, err - } - } - i := 0 - result = make([]map[string]string, len(checksums)) - for id, sum := range checksums { - result[i] = map[string]string{ - "id": id, - "checksum": sum, - } - i++ - } - return result, nil -} - type SearchResults struct { Query string `json:"query"` NumResults int `json:"num_results"` diff --git a/components/engine/runtime.go b/components/engine/runtime.go index 5958aa1811..f2914dba21 100644 --- a/components/engine/runtime.go +++ b/components/engine/runtime.go @@ -178,12 +178,16 @@ func (runtime *Runtime) LogToDisk(src *writeBroadcaster, dst string) error { } func (runtime *Runtime) Destroy(container *Container) error { + if container == nil { + return fmt.Errorf("The given container is ") + } + element := runtime.getContainerElement(container.Id) if element == nil { return fmt.Errorf("Container %v not found - maybe it was already destroyed?", container.Id) } - if err := container.Stop(10); err != nil { + if err := container.Stop(3); err != nil { return err } if mounted, err := container.Mounted(); err != nil { diff --git a/components/engine/runtime_test.go b/components/engine/runtime_test.go index 8e21f57bc5..64956baa67 100644 --- a/components/engine/runtime_test.go +++ b/components/engine/runtime_test.go @@ -2,7 +2,6 @@ package docker import ( "fmt" - "github.com/dotcloud/docker/rcli" "io" "io/ioutil" "net" @@ -67,12 +66,13 @@ func init() { if err != nil { panic(err) } + // Create the "Server" srv := &Server{ runtime: runtime, } // Retrieve the Image - if err := srv.CmdPull(os.Stdin, rcli.NewDockerLocalConn(os.Stdout), unitTestImageName); err != nil { + if err := srv.ImagePull(unitTestImageName, "", "", os.Stdout); err != nil { panic(err) } } @@ -118,7 +118,10 @@ func TestRuntimeCreate(t *testing.T) { if len(runtime.List()) != 0 { t.Errorf("Expected 0 containers, %v found", len(runtime.List())) } - container, err := NewBuilder(runtime).Create(&Config{ + + builder := NewBuilder(runtime) + + container, err := builder.Create(&Config{ Image: GetTestImage(runtime).Id, Cmd: []string{"ls", "-al"}, }, @@ -157,6 +160,26 @@ func TestRuntimeCreate(t *testing.T) { if !runtime.Exists(container.Id) { t.Errorf("Exists() returned false for a newly created container") } + + // Make sure crete with bad parameters returns an error + _, err = builder.Create( + &Config{ + Image: GetTestImage(runtime).Id, + }, + ) + if err == nil { + t.Fatal("Builder.Create should throw an error when Cmd is missing") + } + + _, err = builder.Create( + &Config{ + Image: GetTestImage(runtime).Id, + Cmd: []string{}, + }, + ) + if err == nil { + t.Fatal("Builder.Create should throw an error when Cmd is empty") + } } func TestDestroy(t *testing.T) { diff --git a/components/engine/server.go b/components/engine/server.go new file mode 100644 index 0000000000..e96497bff3 --- /dev/null +++ b/components/engine/server.go @@ -0,0 +1,596 @@ +package docker + +import ( + "fmt" + "io" + "log" + "net/http" + "net/url" + "os" + "runtime" + "strings" +) + +func (srv *Server) DockerVersion() ApiVersion { + return ApiVersion{VERSION, GIT_COMMIT, srv.runtime.capabilities.MemoryLimit, srv.runtime.capabilities.SwapLimit} +} + +func (srv *Server) ContainerKill(name string) error { + if container := srv.runtime.Get(name); container != nil { + if err := container.Kill(); err != nil { + return fmt.Errorf("Error restarting container %s: %s", name, err.Error()) + } + } else { + return fmt.Errorf("No such container: %s", name) + } + return nil +} + +func (srv *Server) ContainerExport(name string, out io.Writer) error { + if container := srv.runtime.Get(name); container != nil { + + data, err := container.Export() + if err != nil { + return err + } + + // Stream the entire contents of the container (basically a volatile snapshot) + if _, err := io.Copy(out, data); err != nil { + return err + } + return nil + } + return fmt.Errorf("No such container: %s", name) +} + +func (srv *Server) ImagesSearch(term string) ([]ApiSearch, error) { + results, err := srv.runtime.graph.SearchRepositories(nil, term) + if err != nil { + return nil, err + } + + var outs []ApiSearch + for _, repo := range results.Results { + var out ApiSearch + out.Description = repo["description"] + if len(out.Description) > 45 { + out.Description = Trunc(out.Description, 42) + "..." + } + out.Name = repo["name"] + outs = append(outs, out) + } + return outs, nil +} + +func (srv *Server) ImageInsert(name, url, path string, out io.Writer) error { + img, err := srv.runtime.repositories.LookupImage(name) + if err != nil { + return err + } + + file, err := Download(url, out) + if err != nil { + return err + } + defer file.Body.Close() + + config, _, err := ParseRun([]string{img.Id, "echo", "insert", url, path}, srv.runtime.capabilities) + if err != nil { + return err + } + + b := NewBuilder(srv.runtime) + c, err := b.Create(config) + if err != nil { + return err + } + + if err := c.Inject(ProgressReader(file.Body, int(file.ContentLength), out, "Downloading %v/%v (%v)"), path); err != nil { + return err + } + // FIXME: Handle custom repo, tag comment, author + img, err = b.Commit(c, "", "", img.Comment, img.Author, nil) + if err != nil { + return err + } + fmt.Fprintf(out, "%s\n", img.Id) + return nil +} + +func (srv *Server) ImagesViz(out io.Writer) error { + images, _ := srv.runtime.graph.All() + if images == nil { + return nil + } + out.Write([]byte("digraph docker {\n")) + + var ( + parentImage *Image + err error + ) + for _, image := range images { + parentImage, err = image.GetParent() + if err != nil { + return fmt.Errorf("Error while getting parent image: %v", err) + } + if parentImage != nil { + out.Write([]byte(" \"" + parentImage.ShortId() + "\" -> \"" + image.ShortId() + "\"\n")) + } else { + out.Write([]byte(" base -> \"" + image.ShortId() + "\" [style=invis]\n")) + } + } + + reporefs := make(map[string][]string) + + for name, repository := range srv.runtime.repositories.Repositories { + for tag, id := range repository { + reporefs[TruncateId(id)] = append(reporefs[TruncateId(id)], fmt.Sprintf("%s:%s", name, tag)) + } + } + + for id, repos := range reporefs { + out.Write([]byte(" \"" + id + "\" [label=\"" + id + "\\n" + strings.Join(repos, "\\n") + "\",shape=box,fillcolor=\"paleturquoise\",style=\"filled,rounded\"];\n")) + } + out.Write([]byte(" base [style=invisible]\n}\n")) + return nil +} + +func (srv *Server) Images(all, only_ids bool, filter string) ([]ApiImages, error) { + var allImages map[string]*Image + var err error + if all { + allImages, err = srv.runtime.graph.Map() + } else { + allImages, err = srv.runtime.graph.Heads() + } + if err != nil { + return nil, err + } + var outs []ApiImages = []ApiImages{} //produce [] when empty instead of 'null' + for name, repository := range srv.runtime.repositories.Repositories { + if filter != "" && name != filter { + continue + } + for tag, id := range repository { + var out ApiImages + image, err := srv.runtime.graph.Get(id) + if err != nil { + log.Printf("Warning: couldn't load %s from %s/%s: %s", id, name, tag, err) + continue + } + delete(allImages, id) + if !only_ids { + out.Repository = name + out.Tag = tag + out.Id = TruncateId(id) + out.Created = image.Created.Unix() + } else { + out.Id = image.ShortId() + } + outs = append(outs, out) + } + } + // Display images which aren't part of a + if filter == "" { + for id, image := range allImages { + var out ApiImages + if !only_ids { + out.Repository = "" + out.Tag = "" + out.Id = TruncateId(id) + out.Created = image.Created.Unix() + } else { + out.Id = image.ShortId() + } + outs = append(outs, out) + } + } + return outs, nil +} + +func (srv *Server) DockerInfo() ApiInfo { + images, _ := srv.runtime.graph.All() + var imgcount int + if images == nil { + imgcount = 0 + } else { + imgcount = len(images) + } + var out ApiInfo + out.Containers = len(srv.runtime.List()) + out.Version = VERSION + out.Images = imgcount + out.GoVersion = runtime.Version() + if os.Getenv("DEBUG") != "" { + out.Debug = true + out.NFd = getTotalUsedFds() + out.NGoroutines = runtime.NumGoroutine() + } + return out +} + +func (srv *Server) ImageHistory(name string) ([]ApiHistory, error) { + image, err := srv.runtime.repositories.LookupImage(name) + if err != nil { + return nil, err + } + + var outs []ApiHistory = []ApiHistory{} //produce [] when empty instead of 'null' + err = image.WalkHistory(func(img *Image) error { + var out ApiHistory + out.Id = srv.runtime.repositories.ImageName(img.ShortId()) + out.Created = img.Created.Unix() + out.CreatedBy = strings.Join(img.ContainerConfig.Cmd, " ") + outs = append(outs, out) + return nil + }) + return outs, nil + +} + +func (srv *Server) ContainerChanges(name string) ([]Change, error) { + if container := srv.runtime.Get(name); container != nil { + return container.Changes() + } + return nil, fmt.Errorf("No such container: %s", name) +} + +func (srv *Server) Containers(all, trunc_cmd, only_ids bool, n int, since, before string) []ApiContainers { + var foundBefore bool + var displayed int + retContainers := []ApiContainers{} + + for _, container := range srv.runtime.List() { + if !container.State.Running && !all && n == -1 && since == "" && before == "" { + continue + } + if before != "" { + if container.ShortId() == before { + foundBefore = true + continue + } + if !foundBefore { + continue + } + } + if displayed == n { + break + } + if container.ShortId() == since { + break + } + displayed++ + + c := ApiContainers{ + Id: container.Id, + } + if trunc_cmd { + c = ApiContainers{ + Id: container.ShortId(), + } + } + + if !only_ids { + command := fmt.Sprintf("%s %s", container.Path, strings.Join(container.Args, " ")) + if trunc_cmd { + command = Trunc(command, 20) + } + c.Image = srv.runtime.repositories.ImageName(container.Image) + c.Command = command + c.Created = container.Created.Unix() + c.Status = container.State.String() + c.Ports = container.NetworkSettings.PortMappingHuman() + } + retContainers = append(retContainers, c) + } + return retContainers +} + +func (srv *Server) ContainerCommit(name, repo, tag, author, comment string, config *Config) (string, error) { + container := srv.runtime.Get(name) + if container == nil { + return "", fmt.Errorf("No such container: %s", name) + } + img, err := NewBuilder(srv.runtime).Commit(container, repo, tag, comment, author, config) + if err != nil { + return "", err + } + return img.ShortId(), err +} + +func (srv *Server) ContainerTag(name, repo, tag string, force bool) error { + if err := srv.runtime.repositories.Set(repo, tag, name, force); err != nil { + return err + } + return nil +} + +func (srv *Server) ImagePull(name, tag, registry string, out io.Writer) error { + if registry != "" { + if err := srv.runtime.graph.PullImage(out, name, registry, nil); err != nil { + return err + } + return nil + } + if err := srv.runtime.graph.PullRepository(out, name, tag, srv.runtime.repositories, srv.runtime.authConfig); err != nil { + return err + } + return nil +} + +func (srv *Server) ImagePush(name, registry string, out io.Writer) error { + img, err := srv.runtime.graph.Get(name) + if err != nil { + Debugf("The push refers to a repository [%s] (len: %d)\n", name, len(srv.runtime.repositories.Repositories[name])) + // If it fails, try to get the repository + if localRepo, exists := srv.runtime.repositories.Repositories[name]; exists { + if err := srv.runtime.graph.PushRepository(out, name, localRepo, srv.runtime.authConfig); err != nil { + return err + } + return nil + } + + return err + } + err = srv.runtime.graph.PushImage(out, img, registry, nil) + if err != nil { + return err + } + return nil +} + +func (srv *Server) ImageImport(src, repo, tag string, in io.Reader, out io.Writer) error { + var archive io.Reader + var resp *http.Response + + if src == "-" { + archive = in + } else { + u, err := url.Parse(src) + if err != nil { + fmt.Fprintf(out, "Error: %s\n", err) + } + if u.Scheme == "" { + u.Scheme = "http" + u.Host = src + u.Path = "" + } + fmt.Fprintln(out, "Downloading from", u) + // Download with curl (pretty progress bar) + // If curl is not available, fallback to http.Get() + resp, err = Download(u.String(), out) + if err != nil { + return err + } + archive = ProgressReader(resp.Body, int(resp.ContentLength), out, "Importing %v/%v (%v)") + } + img, err := srv.runtime.graph.Create(archive, nil, "Imported from "+src, "", nil) + if err != nil { + return err + } + // Optionally register the image at REPO/TAG + if repo != "" { + if err := srv.runtime.repositories.Set(repo, tag, img.Id, true); err != nil { + return err + } + } + fmt.Fprintln(out, img.ShortId()) + return nil +} + +func (srv *Server) ContainerCreate(config *Config) (string, error) { + + if config.Memory > 0 && !srv.runtime.capabilities.MemoryLimit { + config.Memory = 0 + } + + if config.Memory > 0 && !srv.runtime.capabilities.SwapLimit { + config.MemorySwap = -1 + } + b := NewBuilder(srv.runtime) + container, err := b.Create(config) + if err != nil { + if srv.runtime.graph.IsNotExist(err) { + return "", fmt.Errorf("No such image: %s", config.Image) + } + return "", err + } + return container.ShortId(), nil +} + +func (srv *Server) ImageCreateFromFile(dockerfile io.Reader, out io.Writer) error { + img, err := NewBuilder(srv.runtime).Build(dockerfile, out) + if err != nil { + return err + } + fmt.Fprintf(out, "%s\n", img.ShortId()) + return nil +} + +func (srv *Server) ContainerRestart(name string, t int) error { + if container := srv.runtime.Get(name); container != nil { + if err := container.Restart(t); err != nil { + return fmt.Errorf("Error restarting container %s: %s", name, err.Error()) + } + } else { + return fmt.Errorf("No such container: %s", name) + } + return nil +} + +func (srv *Server) ContainerDestroy(name string, removeVolume 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 removeVolume { + // 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) + } + return nil +} + +func (srv *Server) ImageDelete(name string) error { + img, err := srv.runtime.repositories.LookupImage(name) + if err != nil { + return fmt.Errorf("No such image: %s", name) + } else { + if err := srv.runtime.graph.Delete(img.Id); err != nil { + return fmt.Errorf("Error deleting image %s: %s", name, err.Error()) + } + } + return nil +} + +func (srv *Server) ContainerStart(name string) error { + if container := srv.runtime.Get(name); container != nil { + if err := container.Start(); err != nil { + return fmt.Errorf("Error starting container %s: %s", name, err.Error()) + } + } else { + return fmt.Errorf("No such container: %s", name) + } + return nil +} + +func (srv *Server) ContainerStop(name string, t int) error { + if container := srv.runtime.Get(name); container != nil { + if err := container.Stop(t); err != nil { + return fmt.Errorf("Error stopping container %s: %s", name, err.Error()) + } + } else { + return fmt.Errorf("No such container: %s", name) + } + return nil +} + +func (srv *Server) ContainerWait(name string) (int, error) { + if container := srv.runtime.Get(name); container != nil { + return container.Wait(), nil + } + return 0, fmt.Errorf("No such container: %s", name) +} + +func (srv *Server) ContainerAttach(name string, logs, stream, stdin, stdout, stderr bool, in io.ReadCloser, out io.Writer) error { + container := srv.runtime.Get(name) + if container == nil { + return fmt.Errorf("No such container: %s", name) + } + + //logs + if logs { + if stdout { + cLog, err := container.ReadLog("stdout") + if err != nil { + Debugf(err.Error()) + } else if _, err := io.Copy(out, cLog); err != nil { + Debugf(err.Error()) + } + } + if stderr { + cLog, err := container.ReadLog("stderr") + if err != nil { + Debugf(err.Error()) + } else if _, err := io.Copy(out, cLog); err != nil { + Debugf(err.Error()) + } + } + } + + //stream + if stream { + if container.State.Ghost { + return fmt.Errorf("Impossible to attach to a ghost container") + } + + var ( + cStdin io.ReadCloser + cStdout, cStderr io.Writer + cStdinCloser io.Closer + ) + + if stdin { + r, w := io.Pipe() + go func() { + defer w.Close() + defer Debugf("Closing buffered stdin pipe") + io.Copy(w, in) + }() + cStdin = r + cStdinCloser = in + } + if stdout { + cStdout = out + } + if stderr { + cStderr = out + } + + <-container.Attach(cStdin, cStdinCloser, cStdout, cStderr) + + // If we are in stdinonce mode, wait for the process to end + // otherwise, simply return + if container.Config.StdinOnce && !container.Config.Tty { + container.Wait() + } + } + return nil +} + +func (srv *Server) ContainerInspect(name string) (*Container, error) { + if container := srv.runtime.Get(name); container != nil { + return container, nil + } + return nil, fmt.Errorf("No such container: %s", name) +} + +func (srv *Server) ImageInspect(name string) (*Image, error) { + if image, err := srv.runtime.repositories.LookupImage(name); err == nil && image != nil { + return image, nil + } + return nil, fmt.Errorf("No such image: %s", name) +} + +func NewServer(autoRestart bool) (*Server, error) { + if runtime.GOARCH != "amd64" { + log.Fatalf("The docker runtime currently only supports amd64 (not %s). This will change in the future. Aborting.", runtime.GOARCH) + } + runtime, err := NewRuntime(autoRestart) + if err != nil { + return nil, err + } + srv := &Server{ + runtime: runtime, + } + return srv, nil +} + +type Server struct { + runtime *Runtime +} diff --git a/components/engine/server_test.go b/components/engine/server_test.go new file mode 100644 index 0000000000..7b90252864 --- /dev/null +++ b/components/engine/server_test.go @@ -0,0 +1,96 @@ +package docker + +import ( + "testing" +) + +func TestCreateRm(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + config, _, err := ParseRun([]string{GetTestImage(runtime).Id, "echo test"}, nil) + if err != nil { + t.Fatal(err) + } + + id, err := srv.ContainerCreate(config) + if err != nil { + t.Fatal(err) + } + + if len(runtime.List()) != 1 { + t.Errorf("Expected 1 container, %v found", len(runtime.List())) + } + + if err = srv.ContainerDestroy(id, true); err != nil { + t.Fatal(err) + } + + if len(runtime.List()) != 0 { + t.Errorf("Expected 0 container, %v found", len(runtime.List())) + } + +} + +func TestCreateStartRestartStopStartKillRm(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime} + + config, _, err := ParseRun([]string{GetTestImage(runtime).Id, "/bin/cat"}, nil) + if err != nil { + t.Fatal(err) + } + + id, err := srv.ContainerCreate(config) + if err != nil { + t.Fatal(err) + } + + if len(runtime.List()) != 1 { + t.Errorf("Expected 1 container, %v found", len(runtime.List())) + } + + err = srv.ContainerStart(id) + if err != nil { + t.Fatal(err) + } + + err = srv.ContainerRestart(id, 1) + if err != nil { + t.Fatal(err) + } + + err = srv.ContainerStop(id, 1) + if err != nil { + t.Fatal(err) + } + + err = srv.ContainerStart(id) + if err != nil { + t.Fatal(err) + } + + err = srv.ContainerKill(id) + if err != nil { + t.Fatal(err) + } + + if err = srv.ContainerDestroy(id, true); err != nil { + t.Fatal(err) + } + + if len(runtime.List()) != 0 { + t.Errorf("Expected 0 container, %v found", len(runtime.List())) + } + +} diff --git a/components/engine/utils.go b/components/engine/utils.go index 832d89441b..e7f7d319d9 100644 --- a/components/engine/utils.go +++ b/components/engine/utils.go @@ -6,13 +6,14 @@ import ( "encoding/hex" "errors" "fmt" - "github.com/dotcloud/docker/rcli" + "github.com/dotcloud/docker/term" "index/suffixarray" "io" "io/ioutil" "net/http" "os" "os/exec" + "os/signal" "path/filepath" "runtime" "strings" @@ -58,9 +59,6 @@ func Debugf(format string, a ...interface{}) { } fmt.Fprintf(os.Stderr, fmt.Sprintf("[debug] %s:%d %s\n", file, line, format), a...) - if rcli.CLIENT_SOCKET != nil { - fmt.Fprintf(rcli.CLIENT_SOCKET, fmt.Sprintf("[debug] %s:%d %s\n", file, line, format), a...) - } } } @@ -404,6 +402,25 @@ func CopyEscapable(dst io.Writer, src io.ReadCloser) (written int64, err error) return written, err } +func SetRawTerminal() (*term.State, error) { + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + return nil, err + } + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + _ = <-c + term.Restore(int(os.Stdin.Fd()), oldState) + os.Exit(0) + }() + return oldState, err +} + +func RestoreTerminal(state *term.State) { + term.Restore(int(os.Stdin.Fd()), state) +} + func HashData(src io.Reader) (string, error) { h := sha256.New() if _, err := io.Copy(h, src); err != nil { @@ -425,7 +442,11 @@ func GetKernelVersion() (*KernelVersionInfo, error) { } func (k *KernelVersionInfo) String() string { - return fmt.Sprintf("%d.%d.%d-%s", k.Kernel, k.Major, k.Minor, k.Flavor) + flavor := "" + if len(k.Flavor) > 0 { + flavor = fmt.Sprintf("-%s", k.Flavor) + } + return fmt.Sprintf("%d.%d.%d%s", k.Kernel, k.Major, k.Minor, flavor) } // Compare two KernelVersionInfo struct.