diff --git a/api.go b/api.go new file mode 100644 index 0000000000..d5396a5851 --- /dev/null +++ b/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 + } + var 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/api_params.go b/api_params.go new file mode 100644 index 0000000000..c7c15585f9 --- /dev/null +++ b/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/api_test.go b/api_test.go new file mode 100644 index 0000000000..5ac3d7ad05 --- /dev/null +++ b/api_test.go @@ -0,0 +1,877 @@ +package docker + +import ( + "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 { + t.Fatalf("%d OK 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} + + // FIXME: Do more tests with filter + req, err := http.NewRequest("GET", "/images/json?quiet=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 != "docker-ut" { + t.Errorf("Excepted image docker-ut, %s found", images[0].Repository) + } +} + +func TestGetImagesViz(t *testing.T) { + //FIXME: Implement this test (or remove this endpoint) + t.Log("Test not implemented") +} + +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.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) { + //FIXME: Implement this test + t.Log("Test not implemented") +} + +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) { + //FIXME: Implement this test + t.Log("Test not implemented") +} + +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) { + //FIXME: Implement this test + t.Log("Test not implemented") +} + +func TestPostBuild(t *testing.T) { + //FIXME: Implement this test + t.Log("Test not implemented") +} + +func TestPostImagesCreate(t *testing.T) { + //FIXME: Implement this test + t.Log("Test not implemented") +} + +func TestPostImagesInsert(t *testing.T) { + //FIXME: Implement this test (or remove this endpoint) + t.Log("Test not implemented") +} + +func TestPostImagesPush(t *testing.T) { + //FIXME: Implement this test + t.Log("Test not implemented") +} + +func TestPostImagesTag(t *testing.T) { + //FIXME: Implement this test + t.Log("Test not implemented") +} + +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 runnign 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/builder.go b/builder.go index d8a22ac3c8..cae8b55827 100644 --- a/builder.go +++ b/builder.go @@ -266,7 +266,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 } @@ -413,7 +413,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/commands.go b/commands.go index 3846033c83..b9175d731a 100644 --- a/commands.go +++ b/commands.go @@ -3,17 +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" @@ -27,12 +28,51 @@ 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"}, @@ -66,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 } @@ -79,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) @@ -182,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 } @@ -240,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 } @@ -280,150 +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) - fmt.Fprintf(stdout, "Go version: %s\n", runtime.Version()) - if os.Getenv("DEBUG") == "" { - 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 } @@ -431,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 } @@ -452,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 @@ -479,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 } @@ -526,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 @@ -551,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() } @@ -806,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{} @@ -885,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 } @@ -955,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 @@ -1036,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 @@ -1107,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?"+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/commands_test.go b/commands_test.go index 307f90bf7f..80f31e4f76 100644 --- a/commands_test.go +++ b/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() @@ -467,3 +467,4 @@ func TestAttachDisconnect(t *testing.T) { cStdin.Close() container.Wait() } +*/ diff --git a/container.go b/container.go index a4bee6a6fe..c49441e5da 100644 --- a/container.go +++ b/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" @@ -71,8 +71,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) } @@ -86,8 +86,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 } @@ -106,10 +106,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 { @@ -148,8 +148,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 } @@ -157,7 +157,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/container_test.go b/container_test.go index 5b63b2a0e7..9770e800ff 100644 --- a/container_test.go +++ b/container_test.go @@ -399,6 +399,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) } @@ -414,7 +419,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/docker/docker.go b/docker/docker.go index dfd234609a..778326a810 100644 --- a/docker/docker.go +++ b/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/docs/sources/.nojekyll b/docs/sources/.nojekyll new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/sources/CNAME b/docs/sources/CNAME new file mode 100644 index 0000000000..243e482261 --- /dev/null +++ b/docs/sources/CNAME @@ -0,0 +1 @@ +docker.io diff --git a/docs/sources/commandline/index.rst b/docs/sources/commandline/index.rst index d19d39ab60..72290fa7a8 100644 --- a/docs/sources/commandline/index.rst +++ b/docs/sources/commandline/index.rst @@ -13,4 +13,4 @@ Contents: basics workingwithrepository - cli \ No newline at end of file + cli diff --git a/docs/sources/dotcloud.yml b/docs/sources/dotcloud.yml new file mode 100644 index 0000000000..5a8f50f9e9 --- /dev/null +++ b/docs/sources/dotcloud.yml @@ -0,0 +1,2 @@ +www: + type: static \ No newline at end of file diff --git a/docs/sources/gettingstarted/index.html b/docs/sources/gettingstarted/index.html new file mode 100644 index 0000000000..96175d6dec --- /dev/null +++ b/docs/sources/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/docs/sources/index.html b/docs/sources/index.html new file mode 100644 index 0000000000..44a1cc737c --- /dev/null +++ b/docs/sources/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/docs/sources/index.rst b/docs/sources/index.rst index 9a272d2a34..4c46653808 100644 --- a/docs/sources/index.rst +++ b/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/docs/sources/nginx.conf b/docs/sources/nginx.conf new file mode 100644 index 0000000000..97ffd2c0e5 --- /dev/null +++ b/docs/sources/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/docs/sources/remote-api/api.rst b/docs/sources/remote-api/api.rst new file mode 100644 index 0000000000..a6f0662644 --- /dev/null +++ b/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/docs/sources/remote-api/index.rst b/docs/sources/remote-api/index.rst new file mode 100644 index 0000000000..5b3b790b56 --- /dev/null +++ b/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/rcli/tcp.go b/rcli/tcp.go deleted file mode 100644 index cf111cdf71..0000000000 --- a/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/rcli/types.go b/rcli/types.go deleted file mode 100644 index 38f4a8c008..0000000000 --- a/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/rcli/utils.go b/rcli/utils.go deleted file mode 100644 index dbd579ffcd..0000000000 --- a/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/runtime.go b/runtime.go index 7db3b40325..f2914dba21 100644 --- a/runtime.go +++ b/runtime.go @@ -187,7 +187,7 @@ func (runtime *Runtime) Destroy(container *Container) error { 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/runtime_test.go b/runtime_test.go index 68e1213497..64956baa67 100644 --- a/runtime_test.go +++ b/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) } } diff --git a/server.go b/server.go new file mode 100644 index 0000000000..3e95d5849d --- /dev/null +++ b/server.go @@ -0,0 +1,591 @@ +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.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/server_test.go b/server_test.go new file mode 100644 index 0000000000..7b90252864 --- /dev/null +++ b/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/utils.go b/utils.go index 217fad0322..4b416cd1e6 100644 --- a/utils.go +++ b/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 {