diff --git a/AUTHORS b/AUTHORS index 1599a1d0e..eb90cd02d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -42,6 +42,7 @@ Ken Cochrane Kevin J. Lynagh Louis Opter Maxim Treskin +Michael Crosby Mikhail Sobolev Nate Jones Nelson Chen diff --git a/README.md b/README.md index b65f5eeb2..bcad502ab 100644 --- a/README.md +++ b/README.md @@ -251,7 +251,7 @@ Note ---- We also keep the documentation in this repository. The website documentation is generated using sphinx using these sources. -Please find it under docs/sources/ and read more about it https://github.com/dotcloud/docker/master/docs/README.md +Please find it under docs/sources/ and read more about it https://github.com/dotcloud/docker/tree/master/docs/README.md Please feel free to fix / update the documentation and send us pull requests. More tutorials are also welcome. diff --git a/api.go b/api.go index 5a9bdbbe2..1fa40f016 100644 --- a/api.go +++ b/api.go @@ -703,9 +703,18 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ return nil } -func ListenAndServe(addr string, srv *Server, logging bool) error { +func optionsHandler(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + w.WriteHeader(http.StatusOK) + return nil +} +func writeCorsHeaders(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") + w.Header().Add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS") +} + +func createRouter(srv *Server, logging bool) (*mux.Router, error) { r := mux.NewRouter() - log.Printf("Listening for HTTP on %s\n", addr) m := map[string]map[string]func(*Server, float64, http.ResponseWriter, *http.Request, map[string]string) error{ "GET": { @@ -745,6 +754,9 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { "/containers/{name:.*}": deleteContainers, "/images/{name:.*}": deleteImages, }, + "OPTIONS": { + "": optionsHandler, + }, } for method, routes := range m { @@ -769,6 +781,9 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { if err != nil { version = APIVERSION } + if srv.enableCors { + writeCorsHeaders(w, r) + } if version == 0 || version > APIVERSION { w.WriteHeader(http.StatusNotFound) return @@ -777,9 +792,24 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { httpError(w, err) } } - r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f) - r.Path(localRoute).Methods(localMethod).HandlerFunc(f) + + if localRoute == "" { + r.Methods(localMethod).HandlerFunc(f) + } else { + r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f) + r.Path(localRoute).Methods(localMethod).HandlerFunc(f) + } } } + return r, nil +} + +func ListenAndServe(addr string, srv *Server, logging bool) error { + log.Printf("Listening for HTTP on %s\n", addr) + + r, err := createRouter(srv, logging) + if err != nil { + return err + } return http.ListenAndServe(addr, r) } diff --git a/api_test.go b/api_test.go index e122786b8..8785da68d 100644 --- a/api_test.go +++ b/api_test.go @@ -1239,6 +1239,73 @@ func TestDeleteContainers(t *testing.T) { } } +func TestOptionsRoute(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime, enableCors: true} + + r := httptest.NewRecorder() + router, err := createRouter(srv, false) + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest("OPTIONS", "/", nil) + if err != nil { + t.Fatal(err) + } + + router.ServeHTTP(r, req) + if r.Code != http.StatusOK { + t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code) + } +} + +func TestGetEnabledCors(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(runtime) + + srv := &Server{runtime: runtime, enableCors: true} + + r := httptest.NewRecorder() + + router, err := createRouter(srv, false) + if err != nil { + t.Fatal(err) + } + + req, err := http.NewRequest("GET", "/version", nil) + if err != nil { + t.Fatal(err) + } + + router.ServeHTTP(r, req) + if r.Code != http.StatusOK { + t.Errorf("Expected response for OPTIONS request to be \"200\", %v found.", r.Code) + } + + allowOrigin := r.Header().Get("Access-Control-Allow-Origin") + allowHeaders := r.Header().Get("Access-Control-Allow-Headers") + allowMethods := r.Header().Get("Access-Control-Allow-Methods") + + if allowOrigin != "*" { + t.Errorf("Expected header Access-Control-Allow-Origin to be \"*\", %s found.", allowOrigin) + } + if allowHeaders != "Origin, X-Requested-With, Content-Type, Accept" { + t.Errorf("Expected header Access-Control-Allow-Headers to be \"Origin, X-Requested-With, Content-Type, Accept\", %s found.", allowHeaders) + } + if allowMethods != "GET, POST, DELETE, PUT, OPTIONS" { + t.Errorf("Expected hearder Access-Control-Allow-Methods to be \"GET, POST, DELETE, PUT, OPTIONS\", %s found.", allowMethods) + } +} + func TestDeleteImages(t *testing.T) { //FIXME: Implement this test t.Log("Test not implemented") diff --git a/docker/docker.go b/docker/docker.go index dada16e11..74236613a 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -33,6 +33,7 @@ func main() { bridgeName := flag.String("b", "", "Attach containers to a pre-existing network bridge") pidfile := flag.String("p", "/var/run/docker.pid", "File containing process PID") flHost := flag.String("H", fmt.Sprintf("%s:%d", host, port), "Host:port to bind/connect to") + flEnableCors := flag.Bool("api-enable-cors", false, "Enable CORS requests in the remote api.") flag.Parse() if *bridgeName != "" { docker.NetworkBridgeIface = *bridgeName @@ -65,7 +66,7 @@ func main() { flag.Usage() return } - if err := daemon(*pidfile, host, port, *flAutoRestart); err != nil { + if err := daemon(*pidfile, host, port, *flAutoRestart, *flEnableCors); err != nil { log.Fatal(err) os.Exit(-1) } @@ -104,7 +105,7 @@ func removePidFile(pidfile string) { } } -func daemon(pidfile, addr string, port int, autoRestart bool) error { +func daemon(pidfile, addr string, port int, autoRestart, enableCors bool) error { if addr != "127.0.0.1" { log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\") } @@ -122,7 +123,7 @@ func daemon(pidfile, addr string, port int, autoRestart bool) error { os.Exit(0) }() - server, err := docker.NewServer(autoRestart) + server, err := docker.NewServer(autoRestart, enableCors) if err != nil { return err } diff --git a/docs/Makefile b/docs/Makefile index 517d01658..a97255f51 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -46,12 +46,11 @@ clean: -rm -rf $(BUILDDIR)/* docs: - #-rm -rf $(BUILDDIR)/* $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The documentation pages are now in $(BUILDDIR)/html." -server: +server: docs @cd $(BUILDDIR)/html; $(PYTHON) -m SimpleHTTPServer 8000 site: @@ -62,12 +61,13 @@ site: connect: @echo connecting dotcloud to www.docker.io website, make sure to use user 1 - @cd _build/website/ ; \ + @echo or create your own "dockerwebsite" app + @cd $(BUILDDIR)/website/ ; \ dotcloud connect dockerwebsite ; \ dotcloud list push: - @cd _build/website/ ; \ + @cd $(BUILDDIR)/website/ ; \ dotcloud push $(VERSIONS): diff --git a/docs/sources/api/docker_remote_api.rst b/docs/sources/api/docker_remote_api.rst index 1c46cf148..3a4c8dc02 100644 --- a/docs/sources/api/docker_remote_api.rst +++ b/docs/sources/api/docker_remote_api.rst @@ -839,9 +839,9 @@ Search images } ] - :query term: term to search - :statuscode 200: no error - :statuscode 500: server error + :query term: term to search + :statuscode 200: no error + :statuscode 500: server error 3.3 Misc @@ -1056,3 +1056,36 @@ Here are the steps of 'docker run' : 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. + +3.3 CORS Requests +----------------- + +To enable cross origin requests to the remote api add the flag "-api-enable-cors" when running docker in daemon mode. + + docker -d -H="192.168.1.9:4243" -api-enable-cors + + +================================== +Docker Remote API Client Libraries +================================== + +These libraries have been not tested by the Docker Maintainers for +compatibility. Please file issues with the library owners. If you +find more library implementations, please list them in Docker doc bugs +and we will add the libraries here. + ++----------------------+----------------+--------------------------------------------+ +| Language/Framework | Name | Repository | ++======================+================+============================================+ +| Python | docker-py | https://github.com/dotcloud/docker-py | ++----------------------+----------------+--------------------------------------------+ +| Ruby | docker-ruby | https://github.com/ActiveState/docker-ruby | ++----------------------+----------------+--------------------------------------------+ +| Ruby | docker-client | https://github.com/geku/docker-client | ++----------------------+----------------+--------------------------------------------+ +| Javascript | docker-js | https://github.com/dgoujard/docker-js | ++----------------------+----------------+--------------------------------------------+ +| Javascript (Angular) | dockerui | https://github.com/crosbymichael/dockerui | +| **WebUI** | | | ++----------------------+----------------+--------------------------------------------+ + diff --git a/docs/sources/api/index.rst b/docs/sources/api/index.rst index 85770f484..29de81d19 100644 --- a/docs/sources/api/index.rst +++ b/docs/sources/api/index.rst @@ -5,7 +5,7 @@ APIs ==== -This following : +Your programs and scripts can access Docker's functionality via these interfaces: .. toctree:: :maxdepth: 3 diff --git a/docs/sources/contributing/contributing.rst b/docs/sources/contributing/contributing.rst index 25b4df763..1913cec30 100644 --- a/docs/sources/contributing/contributing.rst +++ b/docs/sources/contributing/contributing.rst @@ -5,5 +5,5 @@ Contributing to Docker ====================== -Want to hack on Docker? Awesome! The repository includes `all the instructions you need to get started `. +Want to hack on Docker? Awesome! The repository includes `all the instructions you need to get started `_. diff --git a/docs/sources/faq.rst b/docs/sources/faq.rst index dfffa012f..12b975133 100644 --- a/docs/sources/faq.rst +++ b/docs/sources/faq.rst @@ -35,13 +35,16 @@ Most frequently asked questions. You can find more answers on: - * `IRC: docker on freenode`_ + * `Docker club mailinglist`_ + * `IRC, docker on freenode`_ * `Github`_ * `Ask questions on Stackoverflow`_ * `Join the conversation on Twitter`_ + + .. _Docker club mailinglist: https://groups.google.com/d/forum/docker-club .. _the repo: http://www.github.com/dotcloud/docker - .. _IRC: docker on freenode: docker on freenode: irc://chat.freenode.net#docker + .. _IRC, docker on freenode: irc://chat.freenode.net#docker .. _Github: http://www.github.com/dotcloud/docker .. _Ask questions on Stackoverflow: http://stackoverflow.com/search?q=docker .. _Join the conversation on Twitter: http://twitter.com/getdocker diff --git a/docs/sources/use/builder.rst b/docs/sources/use/builder.rst index abd5b9ecb..f2e9ce97c 100644 --- a/docs/sources/use/builder.rst +++ b/docs/sources/use/builder.rst @@ -18,7 +18,7 @@ steps and commit them along the way, giving you a final image. To use Docker Builder, assemble the steps into a text file (commonly referred to as a Dockerfile) and supply this to `docker build` on STDIN, like so: - ``docker build < Dockerfile`` + ``docker build - < Dockerfile`` Docker will run your steps one-by-one, committing the result if necessary, before finally outputting the ID of your new image. diff --git a/server.go b/server.go index 666612365..7e9e3ea31 100644 --- a/server.go +++ b/server.go @@ -330,8 +330,8 @@ func (srv *Server) pullImage(r *registry.Registry, out io.Writer, imgId, endpoin return nil } -func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, remote, askedTag string, sf *utils.StreamFormatter) error { - out.Write(sf.FormatStatus("Pulling repository %s from %s", remote, auth.IndexServerAddress())) +func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, local, remote, askedTag string, sf *utils.StreamFormatter) error { + out.Write(sf.FormatStatus("Pulling repository %s from %s", local, auth.IndexServerAddress())) repoData, err := r.GetRepositoryData(remote) if err != nil { return err @@ -358,7 +358,7 @@ func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, remote, a // Otherwise, check that the tag exists and use only that one id, exists := tagsList[askedTag] if !exists { - return fmt.Errorf("Tag %s not found in repositoy %s", askedTag, remote) + return fmt.Errorf("Tag %s not found in repositoy %s", askedTag, local) } repoData.ImgList[id].Tag = askedTag } @@ -386,7 +386,7 @@ func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, remote, a if askedTag != "" && tag != askedTag { continue } - if err := srv.runtime.repositories.Set(remote, tag, id, true); err != nil { + if err := srv.runtime.repositories.Set(local, tag, id, true); err != nil { return err } } @@ -406,8 +406,12 @@ func (srv *Server) ImagePull(name, tag, endpoint string, out io.Writer, sf *util } return nil } - - if err := srv.pullRepository(r, out, name, tag, sf); err != nil { + remote := name + parts := strings.Split(name, "/") + if len(parts) > 2 { + remote = fmt.Sprintf("src/%s", url.QueryEscape(strings.Join(parts, "/"))) + } + if err := srv.pullRepository(r, out, name, remote, tag, sf); err != nil { return err } @@ -489,7 +493,13 @@ func (srv *Server) pushRepository(r *registry.Registry, out io.Writer, name stri } out.Write(sf.FormatStatus("Sending image list")) - repoData, err := r.PushImageJSONIndex(name, imgList, false) + srvName := name + parts := strings.Split(name, "/") + if len(parts) > 2 { + srvName = fmt.Sprintf("src/%s", url.QueryEscape(strings.Join(parts, "/"))) + } + + repoData, err := r.PushImageJSONIndex(srvName, imgList, false) if err != nil { return err } @@ -506,14 +516,14 @@ func (srv *Server) pushRepository(r *registry.Registry, out io.Writer, name stri // FIXME: Continue on error? return err } - out.Write(sf.FormatStatus("Pushing tags for rev [%s] on {%s}", elem.ID, ep+"/users/"+name+"/"+elem.Tag)) - if err := r.PushRegistryTag(name, elem.ID, elem.Tag, ep, repoData.Tokens); err != nil { + out.Write(sf.FormatStatus("Pushing tags for rev [%s] on {%s}", elem.ID, ep+"/users/"+srvName+"/"+elem.Tag)) + if err := r.PushRegistryTag(srvName, elem.ID, elem.Tag, ep, repoData.Tokens); err != nil { return err } } } - if _, err := r.PushImageJSONIndex(name, imgList, true); err != nil { + if _, err := r.PushImageJSONIndex(srvName, imgList, true); err != nil { return err } return nil @@ -869,7 +879,7 @@ func (srv *Server) ImageInspect(name string) (*Image, error) { return nil, fmt.Errorf("No such image: %s", name) } -func NewServer(autoRestart bool) (*Server, error) { +func NewServer(autoRestart, enableCors 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) } @@ -878,12 +888,14 @@ func NewServer(autoRestart bool) (*Server, error) { return nil, err } srv := &Server{ - runtime: runtime, + runtime: runtime, + enableCors: enableCors, } runtime.srv = srv return srv, nil } type Server struct { - runtime *Runtime + runtime *Runtime + enableCors bool }