Merge branch 'master' into postupload-endpoints-header
Conflicts: server.go
This commit is contained in:
commit
78e4a385f7
40 changed files with 5486 additions and 1746 deletions
1
AUTHORS
1
AUTHORS
|
@ -42,6 +42,7 @@ Ken Cochrane <kencochrane@gmail.com>
|
|||
Kevin J. Lynagh <kevin@keminglabs.com>
|
||||
Louis Opter <kalessin@kalessin.fr>
|
||||
Maxim Treskin <zerthurd@gmail.com>
|
||||
Michael Crosby <crosby.michael@gmail.com>
|
||||
Mikhail Sobolev <mss@mawhrin.net>
|
||||
Nate Jones <nate@endot.org>
|
||||
Nelson Chen <crazysim@gmail.com>
|
||||
|
|
18
FIXME
Normal file
18
FIXME
Normal file
|
@ -0,0 +1,18 @@
|
|||
|
||||
## FIXME
|
||||
|
||||
This file is a loose collection of things to improve in the codebase, for the internal
|
||||
use of the maintainers.
|
||||
|
||||
They are not big enough to be in the roadmap, not user-facing enough to be github issues,
|
||||
and not important enough to be discussed in the mailing list.
|
||||
|
||||
They are just like FIXME comments in the source code, except we're not sure where in the source
|
||||
to put them - so we put them here :)
|
||||
|
||||
|
||||
* Merge Runtime, Server and Builder into Runtime
|
||||
* Run linter on codebase
|
||||
* Unify build commands and regular commands
|
||||
* Move source code into src/ subdir for clarity
|
||||
* Clean up the Makefile, it's a mess
|
7
NOTICE
7
NOTICE
|
@ -3,4 +3,9 @@ Copyright 2012-2013 dotCloud, inc.
|
|||
|
||||
This product includes software developed at dotCloud, inc. (http://www.dotcloud.com).
|
||||
|
||||
This product contains software (https://github.com/kr/pty) developed by Keith Rarick, licensed under the MIT License.
|
||||
This product contains software (https://github.com/kr/pty) developed by Keith Rarick, licensed under the MIT License.
|
||||
|
||||
Transfers of Docker shall be in accordance with applicable export controls of any country and all other applicable
|
||||
legal requirements. Docker shall not be distributed or downloaded to or in Cuba, Iran, North Korea, Sudan or Syria
|
||||
and shall not be distributed or downloaded to any person on the Denied Persons List administered by the U.S.
|
||||
Department of Commerce.
|
||||
|
|
|
@ -373,5 +373,8 @@ Standard Container Specification
|
|||
|
||||
### Legal
|
||||
|
||||
Transfers Docker shall be in accordance with any applicable export control or other legal requirements.
|
||||
Transfers of Docker shall be in accordance with applicable export controls of any country and all other applicable
|
||||
legal requirements. Docker shall not be distributed or downloaded to or in Cuba, Iran, North Korea, Sudan or Syria
|
||||
and shall not be distributed or downloaded to any person on the Denied Persons List administered by the U.S.
|
||||
Department of Commerce.
|
||||
|
||||
|
|
123
api.go
123
api.go
|
@ -13,7 +13,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
const APIVERSION = 1.1
|
||||
const APIVERSION = 1.2
|
||||
|
||||
func hijackServer(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) {
|
||||
conn, _, err := w.(http.Hijacker).Hijack()
|
||||
|
@ -45,6 +45,8 @@ func httpError(w http.ResponseWriter, err error) {
|
|||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
} else if strings.HasPrefix(err.Error(), "Bad parameter") {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
} else if strings.HasPrefix(err.Error(), "Conflict") {
|
||||
http.Error(w, err.Error(), http.StatusConflict)
|
||||
} else if strings.HasPrefix(err.Error(), "Impossible") {
|
||||
http.Error(w, err.Error(), http.StatusNotAcceptable)
|
||||
} else {
|
||||
|
@ -69,8 +71,10 @@ func getBoolParam(value string) (bool, error) {
|
|||
}
|
||||
|
||||
func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
// FIXME: Handle multiple login at once
|
||||
// FIXME: return specific error code if config file missing?
|
||||
if version > 1.1 {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return nil
|
||||
}
|
||||
authConfig, err := auth.LoadConfig(srv.runtime.root)
|
||||
if err != nil {
|
||||
if err != auth.ErrConfigFileMissing {
|
||||
|
@ -87,29 +91,34 @@ func getAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Reques
|
|||
}
|
||||
|
||||
func postAuth(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
// FIXME: Handle multiple login at once
|
||||
config := &auth.AuthConfig{}
|
||||
if err := json.NewDecoder(r.Body).Decode(config); err != nil {
|
||||
authConfig := &auth.AuthConfig{}
|
||||
err := json.NewDecoder(r.Body).Decode(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authConfig, err := auth.LoadConfig(srv.runtime.root)
|
||||
if err != nil {
|
||||
if err != auth.ErrConfigFileMissing {
|
||||
status := ""
|
||||
if version > 1.1 {
|
||||
status, err = auth.Login(authConfig, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authConfig = &auth.AuthConfig{}
|
||||
}
|
||||
if config.Username == authConfig.Username {
|
||||
config.Password = authConfig.Password
|
||||
}
|
||||
} else {
|
||||
localAuthConfig, err := auth.LoadConfig(srv.runtime.root)
|
||||
if err != nil {
|
||||
if err != auth.ErrConfigFileMissing {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if authConfig.Username == localAuthConfig.Username {
|
||||
authConfig.Password = localAuthConfig.Password
|
||||
}
|
||||
|
||||
newAuthConfig := auth.NewAuthConfig(config.Username, config.Password, config.Email, srv.runtime.root)
|
||||
status, err := auth.Login(newAuthConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
newAuthConfig := auth.NewAuthConfig(authConfig.Username, authConfig.Password, authConfig.Email, srv.runtime.root)
|
||||
status, err = auth.Login(newAuthConfig, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if status != "" {
|
||||
b, err := json.Marshal(&APIAuth{Status: status})
|
||||
if err != nil {
|
||||
|
@ -320,7 +329,7 @@ func postImagesCreate(srv *Server, version float64, w http.ResponseWriter, r *ht
|
|||
sf := utils.NewStreamFormatter(version > 1.0)
|
||||
if image != "" { //pull
|
||||
registry := r.Form.Get("registry")
|
||||
if err := srv.ImagePull(image, tag, registry, w, sf); err != nil {
|
||||
if err := srv.ImagePull(image, tag, registry, w, sf, &auth.AuthConfig{}); err != nil {
|
||||
if sf.Used() {
|
||||
w.Write(sf.FormatError(err))
|
||||
return nil
|
||||
|
@ -388,6 +397,18 @@ func postImagesInsert(srv *Server, version float64, w http.ResponseWriter, r *ht
|
|||
}
|
||||
|
||||
func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
authConfig := &auth.AuthConfig{}
|
||||
if version > 1.1 {
|
||||
if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
localAuthConfig, err := auth.LoadConfig(srv.runtime.root)
|
||||
if err != nil && err != auth.ErrConfigFileMissing {
|
||||
return err
|
||||
}
|
||||
authConfig = localAuthConfig
|
||||
}
|
||||
if err := parseForm(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -401,7 +422,7 @@ func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http
|
|||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
sf := utils.NewStreamFormatter(version > 1.0)
|
||||
if err := srv.ImagePush(name, registry, w, sf); err != nil {
|
||||
if err := srv.ImagePush(name, registry, w, sf, authConfig); err != nil {
|
||||
if sf.Used() {
|
||||
w.Write(sf.FormatError(err))
|
||||
return nil
|
||||
|
@ -481,14 +502,30 @@ func deleteContainers(srv *Server, version float64, w http.ResponseWriter, r *ht
|
|||
}
|
||||
|
||||
func deleteImages(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
if err := parseForm(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if vars == nil {
|
||||
return fmt.Errorf("Missing parameter")
|
||||
}
|
||||
name := vars["name"]
|
||||
if err := srv.ImageDelete(name); err != nil {
|
||||
imgs, err := srv.ImageDelete(name, version > 1.1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
if imgs != nil {
|
||||
if len(*imgs) != 0 {
|
||||
b, err := json.Marshal(imgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJSON(w, b)
|
||||
} else {
|
||||
return fmt.Errorf("Conflict, %s wasn't deleted", name)
|
||||
}
|
||||
} else {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -703,9 +740,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 +791,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 +818,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 +829,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)
|
||||
}
|
||||
|
|
|
@ -23,6 +23,11 @@ type APIInfo struct {
|
|||
SwapLimit bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
type APIRmi struct {
|
||||
Deleted string `json:",omitempty"`
|
||||
Untagged string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type APIContainers struct {
|
||||
ID string `json:"Id"`
|
||||
Image string
|
||||
|
|
169
api_test.go
169
api_test.go
|
@ -6,7 +6,6 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/dotcloud/docker/auth"
|
||||
"github.com/dotcloud/docker/registry"
|
||||
"github.com/dotcloud/docker/utils"
|
||||
"io"
|
||||
"net"
|
||||
|
@ -18,7 +17,7 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
func TestGetAuth(t *testing.T) {
|
||||
func TestPostAuth(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -54,12 +53,6 @@ func TestGetAuth(t *testing.T) {
|
|||
if r.Code != http.StatusOK && r.Code != 0 {
|
||||
t.Fatalf("%d OK or 0 expected, received %d\n", http.StatusOK, r.Code)
|
||||
}
|
||||
|
||||
newAuthConfig := registry.NewRegistry(runtime.root).GetAuthConfig(false)
|
||||
if newAuthConfig.Username != authConfig.Username ||
|
||||
newAuthConfig.Email != authConfig.Email {
|
||||
t.Fatalf("The auth configuration hasn't been set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVersion(t *testing.T) {
|
||||
|
@ -494,40 +487,6 @@ func TestGetContainersByName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPostAuth(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
|
||||
srv := &Server{
|
||||
runtime: runtime,
|
||||
}
|
||||
|
||||
config := &auth.AuthConfig{
|
||||
Username: "utest",
|
||||
Email: "utest@yopmail.com",
|
||||
}
|
||||
|
||||
authStr := auth.EncodeAuth(config)
|
||||
auth.SaveConfig(runtime.root, authStr, config.Email)
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err := getAuth(srv, APIVERSION, r, nil, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
authConfig := &auth.AuthConfig{}
|
||||
if err := json.Unmarshal(r.Body.Bytes(), authConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if authConfig.Username != config.Username || authConfig.Email != config.Email {
|
||||
t.Errorf("The retrieve auth mismatch with the one set.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostCommit(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
|
@ -1239,9 +1198,131 @@ 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")
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
|
||||
srv := &Server{runtime: runtime}
|
||||
|
||||
if err := srv.runtime.repositories.Set("test", "test", unitTestImageName, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
images, err := srv.Images(false, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(images) != 2 {
|
||||
t.Errorf("Excepted 2 images, %d found", len(images))
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("DELETE", "/images/test:test", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := httptest.NewRecorder()
|
||||
if err := deleteImages(srv, APIVERSION, r, req, map[string]string{"name": "test:test"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.Code != http.StatusOK {
|
||||
t.Fatalf("%d OK expected, received %d\n", http.StatusOK, r.Code)
|
||||
}
|
||||
|
||||
var outs []APIRmi
|
||||
if err := json.Unmarshal(r.Body.Bytes(), &outs); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(outs) != 1 {
|
||||
t.Fatalf("Expected %d event (untagged), got %d", 1, len(outs))
|
||||
}
|
||||
images, err = srv.Images(false, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(images) != 1 {
|
||||
t.Errorf("Excepted 1 image, %d found", len(images))
|
||||
}
|
||||
|
||||
/* 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")
|
||||
} */
|
||||
}
|
||||
|
||||
// Mocked types for tests
|
||||
|
|
29
auth/auth.go
29
auth/auth.go
|
@ -48,7 +48,7 @@ func IndexServerAddress() string {
|
|||
}
|
||||
|
||||
// create a base64 encoded auth string to store in config
|
||||
func EncodeAuth(authConfig *AuthConfig) string {
|
||||
func encodeAuth(authConfig *AuthConfig) string {
|
||||
authStr := authConfig.Username + ":" + authConfig.Password
|
||||
msg := []byte(authStr)
|
||||
encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg)))
|
||||
|
@ -57,7 +57,7 @@ func EncodeAuth(authConfig *AuthConfig) string {
|
|||
}
|
||||
|
||||
// decode the auth string
|
||||
func DecodeAuth(authStr string) (*AuthConfig, error) {
|
||||
func decodeAuth(authStr string) (*AuthConfig, error) {
|
||||
decLen := base64.StdEncoding.DecodedLen(len(authStr))
|
||||
decoded := make([]byte, decLen)
|
||||
authByte := []byte(authStr)
|
||||
|
@ -82,7 +82,7 @@ func DecodeAuth(authStr string) (*AuthConfig, error) {
|
|||
func LoadConfig(rootPath string) (*AuthConfig, error) {
|
||||
confFile := path.Join(rootPath, CONFIGFILE)
|
||||
if _, err := os.Stat(confFile); err != nil {
|
||||
return nil, ErrConfigFileMissing
|
||||
return &AuthConfig{rootPath:rootPath}, ErrConfigFileMissing
|
||||
}
|
||||
b, err := ioutil.ReadFile(confFile)
|
||||
if err != nil {
|
||||
|
@ -94,7 +94,7 @@ func LoadConfig(rootPath string) (*AuthConfig, error) {
|
|||
}
|
||||
origAuth := strings.Split(arr[0], " = ")
|
||||
origEmail := strings.Split(arr[1], " = ")
|
||||
authConfig, err := DecodeAuth(origAuth[1])
|
||||
authConfig, err := decodeAuth(origAuth[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -104,13 +104,13 @@ func LoadConfig(rootPath string) (*AuthConfig, error) {
|
|||
}
|
||||
|
||||
// save the auth config
|
||||
func SaveConfig(rootPath, authStr string, email string) error {
|
||||
confFile := path.Join(rootPath, CONFIGFILE)
|
||||
if len(email) == 0 {
|
||||
func SaveConfig(authConfig *AuthConfig) error {
|
||||
confFile := path.Join(authConfig.rootPath, CONFIGFILE)
|
||||
if len(authConfig.Email) == 0 {
|
||||
os.Remove(confFile)
|
||||
return nil
|
||||
}
|
||||
lines := "auth = " + authStr + "\n" + "email = " + email + "\n"
|
||||
lines := "auth = " + encodeAuth(authConfig) + "\n" + "email = " + authConfig.Email + "\n"
|
||||
b := []byte(lines)
|
||||
err := ioutil.WriteFile(confFile, b, 0600)
|
||||
if err != nil {
|
||||
|
@ -120,7 +120,7 @@ func SaveConfig(rootPath, authStr string, email string) error {
|
|||
}
|
||||
|
||||
// try to register/login to the registry server
|
||||
func Login(authConfig *AuthConfig) (string, error) {
|
||||
func Login(authConfig *AuthConfig, store bool) (string, error) {
|
||||
storeConfig := false
|
||||
client := &http.Client{}
|
||||
reqStatusCode := 0
|
||||
|
@ -168,8 +168,10 @@ func Login(authConfig *AuthConfig) (string, error) {
|
|||
status = "Login Succeeded\n"
|
||||
storeConfig = true
|
||||
} else if resp.StatusCode == 401 {
|
||||
if err := SaveConfig(authConfig.rootPath, "", ""); err != nil {
|
||||
return "", err
|
||||
if store {
|
||||
if err := SaveConfig(authConfig); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("Wrong login/password, please try again")
|
||||
} else {
|
||||
|
@ -182,9 +184,8 @@ func Login(authConfig *AuthConfig) (string, error) {
|
|||
} else {
|
||||
return "", fmt.Errorf("Unexpected status code [%d] : %s", reqStatusCode, reqBody)
|
||||
}
|
||||
if storeConfig {
|
||||
authStr := EncodeAuth(authConfig)
|
||||
if err := SaveConfig(authConfig.rootPath, authStr, authConfig.Email); err != nil {
|
||||
if storeConfig && store {
|
||||
if err := SaveConfig(authConfig); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
|
31
buildfile.go
31
buildfile.go
|
@ -61,7 +61,7 @@ func (b *buildFile) CmdFrom(name string) error {
|
|||
remote = name
|
||||
}
|
||||
|
||||
if err := b.srv.ImagePull(remote, tag, "", b.out, utils.NewStreamFormatter(false)); err != nil {
|
||||
if err := b.srv.ImagePull(remote, tag, "", b.out, utils.NewStreamFormatter(false), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -176,16 +176,14 @@ func (b *buildFile) CmdAdd(args string) error {
|
|||
dest := strings.Trim(tmp[1], " ")
|
||||
|
||||
cmd := b.config.Cmd
|
||||
b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) ADD %s in %s", orig, dest)}
|
||||
cid, err := b.run()
|
||||
|
||||
// Create the container and start it
|
||||
container, err := b.builder.Create(b.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.tmpContainers[container.ID] = struct{}{}
|
||||
|
||||
container := b.runtime.Get(cid)
|
||||
if container == nil {
|
||||
return fmt.Errorf("Error while creating the container (CmdAdd)")
|
||||
}
|
||||
if err := container.EnsureMounted(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -220,7 +218,7 @@ func (b *buildFile) CmdAdd(args string) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if err := b.commit(cid, cmd, fmt.Sprintf("ADD %s in %s", orig, dest)); err != nil {
|
||||
if err := b.commit(container.ID, cmd, fmt.Sprintf("ADD %s in %s", orig, dest)); err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.Cmd = cmd
|
||||
|
@ -272,11 +270,19 @@ func (b *buildFile) commit(id string, autoCmd []string, comment string) error {
|
|||
utils.Debugf("[BUILDER] Cache miss")
|
||||
}
|
||||
|
||||
cid, err := b.run()
|
||||
// Create the container and start it
|
||||
container, err := b.builder.Create(b.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id = cid
|
||||
b.tmpContainers[container.ID] = struct{}{}
|
||||
|
||||
if err := container.EnsureMounted(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer container.Unmount()
|
||||
|
||||
id = container.ID
|
||||
}
|
||||
|
||||
container := b.runtime.Get(id)
|
||||
|
@ -313,10 +319,11 @@ func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) {
|
|||
for {
|
||||
line, err := file.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
if err == io.EOF && line == "" {
|
||||
break
|
||||
} else if err != io.EOF {
|
||||
return "", err
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
line = strings.Replace(strings.TrimSpace(line), " ", " ", 1)
|
||||
// Skip comments and empty line
|
||||
|
|
|
@ -15,58 +15,69 @@ run sh -c 'echo root:testpass > /tmp/passwd'
|
|||
run mkdir -p /var/run/sshd
|
||||
`
|
||||
|
||||
const DockerfileNoNewLine = `
|
||||
# VERSION 0.1
|
||||
# DOCKER-VERSION 0.2
|
||||
|
||||
from ` + unitTestImageName + `
|
||||
run sh -c 'echo root:testpass > /tmp/passwd'
|
||||
run mkdir -p /var/run/sshd`
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
dockerfiles := []string{Dockerfile, DockerfileNoNewLine}
|
||||
for _, Dockerfile := range dockerfiles {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
|
||||
srv := &Server{runtime: runtime}
|
||||
srv := &Server{runtime: runtime}
|
||||
|
||||
buildfile := NewBuildFile(srv, &utils.NopWriter{})
|
||||
buildfile := NewBuildFile(srv, &utils.NopWriter{})
|
||||
|
||||
imgID, err := buildfile.Build(strings.NewReader(Dockerfile), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
imgID, err := buildfile.Build(strings.NewReader(Dockerfile), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
builder := NewBuilder(runtime)
|
||||
container, err := builder.Create(
|
||||
&Config{
|
||||
Image: imgID,
|
||||
Cmd: []string{"cat", "/tmp/passwd"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer runtime.Destroy(container)
|
||||
builder := NewBuilder(runtime)
|
||||
container, err := builder.Create(
|
||||
&Config{
|
||||
Image: imgID,
|
||||
Cmd: []string{"cat", "/tmp/passwd"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer runtime.Destroy(container)
|
||||
|
||||
output, err := container.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(output) != "root:testpass\n" {
|
||||
t.Fatalf("Unexpected output. Read '%s', expected '%s'", output, "root:testpass\n")
|
||||
}
|
||||
output, err := container.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(output) != "root:testpass\n" {
|
||||
t.Fatalf("Unexpected output. Read '%s', expected '%s'", output, "root:testpass\n")
|
||||
}
|
||||
|
||||
container2, err := builder.Create(
|
||||
&Config{
|
||||
Image: imgID,
|
||||
Cmd: []string{"ls", "-d", "/var/run/sshd"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer runtime.Destroy(container2)
|
||||
container2, err := builder.Create(
|
||||
&Config{
|
||||
Image: imgID,
|
||||
Cmd: []string{"ls", "-d", "/var/run/sshd"},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer runtime.Destroy(container2)
|
||||
|
||||
output, err = container2.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(output) != "/var/run/sshd\n" {
|
||||
t.Fatal("/var/run/sshd has not been created")
|
||||
output, err = container2.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(output) != "/var/run/sshd\n" {
|
||||
t.Fatal("/var/run/sshd has not been created")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
192
commands.go
192
commands.go
|
@ -218,7 +218,10 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("error: %s", body)
|
||||
if len(body) == 0 {
|
||||
return fmt.Errorf("Error: %s", http.StatusText(resp.StatusCode))
|
||||
}
|
||||
return fmt.Errorf("Error: %s", body)
|
||||
}
|
||||
|
||||
// Output the result
|
||||
|
@ -284,27 +287,16 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
body, _, err := cli.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.Print("Username (", out.Username, "): ")
|
||||
fmt.Print("Username (", cli.authConfig.Username, "): ")
|
||||
username = readAndEchoString(os.Stdin, os.Stdout)
|
||||
if username == "" {
|
||||
username = out.Username
|
||||
username = cli.authConfig.Username
|
||||
}
|
||||
if username != out.Username {
|
||||
if username != cli.authConfig.Username {
|
||||
fmt.Print("Password: ")
|
||||
password = readString(os.Stdin, os.Stdout)
|
||||
|
||||
|
@ -312,20 +304,21 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
|
|||
return fmt.Errorf("Error : Password Required")
|
||||
}
|
||||
|
||||
fmt.Print("Email (", out.Email, "): ")
|
||||
fmt.Print("Email (", cli.authConfig.Email, "): ")
|
||||
email = readAndEchoString(os.Stdin, os.Stdout)
|
||||
if email == "" {
|
||||
email = out.Email
|
||||
email = cli.authConfig.Email
|
||||
}
|
||||
} else {
|
||||
email = out.Email
|
||||
email = cli.authConfig.Email
|
||||
}
|
||||
term.RestoreTerminal(oldState)
|
||||
|
||||
out.Username = username
|
||||
out.Password = password
|
||||
out.Email = email
|
||||
cli.authConfig.Username = username
|
||||
cli.authConfig.Password = password
|
||||
cli.authConfig.Email = email
|
||||
|
||||
body, _, err = cli.call("POST", "/auth", out)
|
||||
body, _, err := cli.call("POST", "/auth", cli.authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -333,10 +326,11 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
|
|||
var out2 APIAuth
|
||||
err = json.Unmarshal(body, &out2)
|
||||
if err != nil {
|
||||
auth.LoadConfig(os.Getenv("HOME"))
|
||||
return err
|
||||
}
|
||||
auth.SaveConfig(cli.authConfig)
|
||||
if out2.Status != "" {
|
||||
term.RestoreTerminal(oldState)
|
||||
fmt.Print(out2.Status)
|
||||
}
|
||||
return nil
|
||||
|
@ -457,7 +451,7 @@ func (cli *DockerCli) CmdStop(args ...string) error {
|
|||
for _, name := range cmd.Args() {
|
||||
_, _, err := cli.call("POST", "/containers/"+name+"/stop?"+v.Encode(), nil)
|
||||
if err != nil {
|
||||
fmt.Printf("%s", err)
|
||||
fmt.Fprintf(os.Stderr, "%s", err)
|
||||
} else {
|
||||
fmt.Println(name)
|
||||
}
|
||||
|
@ -482,7 +476,7 @@ func (cli *DockerCli) CmdRestart(args ...string) error {
|
|||
for _, name := range cmd.Args() {
|
||||
_, _, err := cli.call("POST", "/containers/"+name+"/restart?"+v.Encode(), nil)
|
||||
if err != nil {
|
||||
fmt.Printf("%s", err)
|
||||
fmt.Fprintf(os.Stderr, "%s", err)
|
||||
} else {
|
||||
fmt.Println(name)
|
||||
}
|
||||
|
@ -503,7 +497,7 @@ func (cli *DockerCli) CmdStart(args ...string) error {
|
|||
for _, name := range args {
|
||||
_, _, err := cli.call("POST", "/containers/"+name+"/start", nil)
|
||||
if err != nil {
|
||||
fmt.Printf("%s", err)
|
||||
fmt.Fprintf(os.Stderr, "%s", err)
|
||||
} else {
|
||||
fmt.Println(name)
|
||||
}
|
||||
|
@ -512,29 +506,38 @@ func (cli *DockerCli) CmdStart(args ...string) error {
|
|||
}
|
||||
|
||||
func (cli *DockerCli) CmdInspect(args ...string) error {
|
||||
cmd := Subcmd("inspect", "CONTAINER|IMAGE", "Return low-level information on a container/image")
|
||||
cmd := Subcmd("inspect", "CONTAINER|IMAGE [CONTAINER|IMAGE...]", "Return low-level information on a container/image")
|
||||
if err := cmd.Parse(args); err != nil {
|
||||
return nil
|
||||
}
|
||||
if cmd.NArg() != 1 {
|
||||
if cmd.NArg() < 1 {
|
||||
cmd.Usage()
|
||||
return nil
|
||||
}
|
||||
obj, _, err := cli.call("GET", "/containers/"+cmd.Arg(0)+"/json", nil)
|
||||
if err != nil {
|
||||
obj, _, err = cli.call("GET", "/images/"+cmd.Arg(0)+"/json", nil)
|
||||
fmt.Printf("[")
|
||||
for i, name := range args {
|
||||
if i > 0 {
|
||||
fmt.Printf(",")
|
||||
}
|
||||
obj, _, err := cli.call("GET", "/containers/"+name+"/json", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
obj, _, err = cli.call("GET", "/images/"+name+"/json", nil)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
indented := new(bytes.Buffer)
|
||||
if err = json.Indent(indented, obj, "", " "); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s", err)
|
||||
continue
|
||||
}
|
||||
if _, err := io.Copy(os.Stdout, indented); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s", 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
|
||||
}
|
||||
fmt.Printf("]")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -561,7 +564,7 @@ func (cli *DockerCli) CmdPort(args ...string) error {
|
|||
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 fmt.Errorf("Error: No private port '%s' allocated on %s", cmd.Arg(1), cmd.Arg(0))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -578,11 +581,22 @@ func (cli *DockerCli) CmdRmi(args ...string) error {
|
|||
}
|
||||
|
||||
for _, name := range cmd.Args() {
|
||||
_, _, err := cli.call("DELETE", "/images/"+name, nil)
|
||||
body, _, err := cli.call("DELETE", "/images/"+name, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("%s", err)
|
||||
fmt.Fprintf(os.Stderr, "%s", err)
|
||||
} else {
|
||||
fmt.Println(name)
|
||||
var outs []APIRmi
|
||||
err = json.Unmarshal(body, &outs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, out := range outs {
|
||||
if out.Deleted != "" {
|
||||
fmt.Println("Deleted:", out.Deleted)
|
||||
} else {
|
||||
fmt.Println("Untagged:", out.Untagged)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -701,18 +715,22 @@ func (cli *DockerCli) CmdPush(args ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
username, err := cli.checkIfLogged(*registry == "", "push")
|
||||
if err != nil {
|
||||
if err := cli.checkIfLogged(*registry == "", "push"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(strings.SplitN(name, "/", 2)) == 1 {
|
||||
return fmt.Errorf("Impossible to push a \"root\" repository. Please rename your repository in <user>/<repo> (ex: %s/%s)", username, name)
|
||||
return fmt.Errorf("Impossible to push a \"root\" repository. Please rename your repository in <user>/<repo> (ex: %s/%s)", cli.authConfig.Username, name)
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(cli.authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("registry", *registry)
|
||||
if err := cli.stream("POST", "/images/"+name+"/push?"+v.Encode(), nil, os.Stdout); err != nil {
|
||||
if err := cli.stream("POST", "/images/"+name+"/push?"+v.Encode(), bytes.NewBuffer(buf), os.Stdout); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -1027,7 +1045,9 @@ func (cli *DockerCli) CmdAttach(args ...string) error {
|
|||
connections += 1
|
||||
}
|
||||
chErrors := make(chan error, connections)
|
||||
cli.monitorTtySize(cmd.Arg(0))
|
||||
if container.Config.Tty {
|
||||
cli.monitorTtySize(cmd.Arg(0))
|
||||
}
|
||||
if splitStderr {
|
||||
go func() {
|
||||
chErrors <- cli.hijack("POST", "/containers/"+cmd.Arg(0)+"/attach?stream=1&stderr=1", false, nil, os.Stderr)
|
||||
|
@ -1079,7 +1099,12 @@ func (cli *DockerCli) CmdSearch(args ...string) error {
|
|||
w := tabwriter.NewWriter(os.Stdout, 20, 1, 3, ' ', 0)
|
||||
fmt.Fprintf(w, "NAME\tDESCRIPTION\n")
|
||||
for _, out := range outs {
|
||||
fmt.Fprintf(w, "%s\t%s\n", out.Name, out.Description)
|
||||
desc := strings.Replace(out.Description, "\n", " ", -1)
|
||||
desc = strings.Replace(desc, "\r", " ", -1)
|
||||
if len(desc) > 45 {
|
||||
desc = utils.Trunc(desc, 42) + "..."
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\n", out.Name, desc)
|
||||
}
|
||||
w.Flush()
|
||||
return nil
|
||||
|
@ -1233,7 +1258,9 @@ func (cli *DockerCli) CmdRun(args ...string) error {
|
|||
}
|
||||
if connections > 0 {
|
||||
chErrors := make(chan error, connections)
|
||||
cli.monitorTtySize(out.ID)
|
||||
if config.Tty {
|
||||
cli.monitorTtySize(out.ID)
|
||||
}
|
||||
|
||||
if splitStderr && config.AttachStderr {
|
||||
go func() {
|
||||
|
@ -1260,6 +1287,7 @@ func (cli *DockerCli) CmdRun(args ...string) error {
|
|||
for connections > 0 {
|
||||
err := <-chErrors
|
||||
if err != nil {
|
||||
utils.Debugf("Error hijack: %s", err)
|
||||
return err
|
||||
}
|
||||
connections -= 1
|
||||
|
@ -1268,38 +1296,17 @@ func (cli *DockerCli) CmdRun(args ...string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (cli *DockerCli) checkIfLogged(condition bool, action string) (string, error) {
|
||||
body, _, err := cli.call("GET", "/auth", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var out auth.AuthConfig
|
||||
err = json.Unmarshal(body, &out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (cli *DockerCli) checkIfLogged(condition bool, action string) error {
|
||||
// If condition AND the login failed
|
||||
if condition && out.Username == "" {
|
||||
if condition && cli.authConfig.Username == "" {
|
||||
if err := cli.CmdLogin(""); err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
body, _, err = cli.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 %s. ('docker login')", action)
|
||||
if cli.authConfig.Username == "" {
|
||||
return fmt.Errorf("Please login prior to %s. ('docker login')", action)
|
||||
}
|
||||
}
|
||||
return out.Username, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *DockerCli) call(method, path string, data interface{}) ([]byte, int, error) {
|
||||
|
@ -1335,7 +1342,10 @@ func (cli *DockerCli) call(method, path string, data interface{}) ([]byte, int,
|
|||
return nil, -1, err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
||||
return nil, resp.StatusCode, fmt.Errorf("error: %s", body)
|
||||
if len(body) == 0 {
|
||||
return nil, resp.StatusCode, fmt.Errorf("Error: %s", http.StatusText(resp.StatusCode))
|
||||
}
|
||||
return nil, resp.StatusCode, fmt.Errorf("Error: %s", body)
|
||||
}
|
||||
return body, resp.StatusCode, nil
|
||||
}
|
||||
|
@ -1365,7 +1375,10 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) e
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("error: %s", body)
|
||||
if len(body) == 0 {
|
||||
return fmt.Errorf("Error :%s", http.StatusText(resp.StatusCode))
|
||||
}
|
||||
return fmt.Errorf("Error: %s", body)
|
||||
}
|
||||
|
||||
if resp.Header.Get("Content-Type") == "application/json" {
|
||||
|
@ -1423,19 +1436,22 @@ func (cli *DockerCli) hijack(method, path string, setRawTerminal bool, in *os.Fi
|
|||
defer term.RestoreTerminal(oldState)
|
||||
}
|
||||
sendStdin := utils.Go(func() error {
|
||||
_, err := io.Copy(rwc, in)
|
||||
io.Copy(rwc, in)
|
||||
if err := rwc.(*net.TCPConn).CloseWrite(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't send EOF: %s\n", err)
|
||||
utils.Debugf("Couldn't send EOF: %s\n", err)
|
||||
}
|
||||
return err
|
||||
// Discard errors due to pipe interruption
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := <-receiveStdout; err != nil {
|
||||
utils.Debugf("Error receiveStdout: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !term.IsTerminal(os.Stdin.Fd()) {
|
||||
if !term.IsTerminal(in.Fd()) {
|
||||
if err := <-sendStdin; err != nil {
|
||||
utils.Debugf("Error sendStdin: %s", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -1480,10 +1496,12 @@ func Subcmd(name, signature, description string) *flag.FlagSet {
|
|||
}
|
||||
|
||||
func NewDockerCli(addr string, port int) *DockerCli {
|
||||
return &DockerCli{addr, port}
|
||||
authConfig, _ := auth.LoadConfig(os.Getenv("HOME"))
|
||||
return &DockerCli{addr, port, authConfig}
|
||||
}
|
||||
|
||||
type DockerCli struct {
|
||||
host string
|
||||
port int
|
||||
host string
|
||||
port int
|
||||
authConfig *auth.AuthConfig
|
||||
}
|
||||
|
|
25
container.go
25
container.go
|
@ -355,6 +355,18 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s
|
|||
errors <- err
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
if stdinCloser != nil {
|
||||
defer stdinCloser.Close()
|
||||
}
|
||||
|
||||
if cStdout, err := container.StdoutPipe(); err != nil {
|
||||
utils.Debugf("Error stdout pipe")
|
||||
} else {
|
||||
io.Copy(&utils.NopWriter{}, cStdout)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if stderr != nil {
|
||||
nJobs += 1
|
||||
|
@ -381,7 +393,20 @@ func (container *Container) Attach(stdin io.ReadCloser, stdinCloser io.Closer, s
|
|||
errors <- err
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
go func() {
|
||||
if stdinCloser != nil {
|
||||
defer stdinCloser.Close()
|
||||
}
|
||||
|
||||
if cStderr, err := container.StderrPipe(); err != nil {
|
||||
utils.Debugf("Error stdout pipe")
|
||||
} else {
|
||||
io.Copy(&utils.NopWriter{}, cStderr)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return utils.Go(func() error {
|
||||
if cStdout != nil {
|
||||
defer cStdout.Close()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
5
docs/sources/api/README.md
Normal file
5
docs/sources/api/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
This directory holds the authoritative specifications of APIs defined and implemented by Docker. Currently this includes:
|
||||
|
||||
* The remote API by which a docker node can be queried over HTTP
|
||||
* The registry API by which a docker node can download and upload container images for storage and sharing
|
||||
* The index search API by which a docker node can search the public index for images to download
|
File diff suppressed because it is too large
Load diff
1015
docs/sources/api/docker_remote_api_v1.0.rst
Normal file
1015
docs/sources/api/docker_remote_api_v1.0.rst
Normal file
File diff suppressed because it is too large
Load diff
1026
docs/sources/api/docker_remote_api_v1.1.rst
Normal file
1026
docs/sources/api/docker_remote_api_v1.1.rst
Normal file
File diff suppressed because it is too large
Load diff
1013
docs/sources/api/docker_remote_api_v1.2.rst
Normal file
1013
docs/sources/api/docker_remote_api_v1.2.rst
Normal file
File diff suppressed because it is too large
Load diff
|
@ -5,13 +5,14 @@
|
|||
APIs
|
||||
====
|
||||
|
||||
This following :
|
||||
Your programs and scripts can access Docker's functionality via these interfaces:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 3
|
||||
|
||||
registry_index_spec
|
||||
registry_api
|
||||
index_search_api
|
||||
index_api
|
||||
docker_remote_api
|
||||
|
||||
|
||||
|
|
553
docs/sources/api/index_api.rst
Normal file
553
docs/sources/api/index_api.rst
Normal file
|
@ -0,0 +1,553 @@
|
|||
:title: Index API
|
||||
:description: API Documentation for Docker Index
|
||||
:keywords: API, Docker, index, REST, documentation
|
||||
|
||||
=================
|
||||
Docker Index API
|
||||
=================
|
||||
|
||||
.. contents:: Table of Contents
|
||||
|
||||
1. Brief introduction
|
||||
=====================
|
||||
|
||||
- This is the REST API for the Docker index
|
||||
- Authorization is done with basic auth over SSL
|
||||
- Not all commands require authentication, only those noted as such.
|
||||
|
||||
2. Endpoints
|
||||
============
|
||||
|
||||
2.1 Repository
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
Repositories
|
||||
*************
|
||||
|
||||
User Repo
|
||||
~~~~~~~~~
|
||||
|
||||
.. http:put:: /v1/repositories/(namespace)/(repo_name)/
|
||||
|
||||
Create a user repository with the given ``namespace`` and ``repo_name``.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/repositories/foo/bar/ HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
X-Docker-Token: true
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”}]
|
||||
|
||||
:parameter namespace: the namespace for the repo
|
||||
:parameter repo_name: the name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
X-Docker-Endpoints: registry-1.docker.io [, registry-2.docker.io]
|
||||
|
||||
""
|
||||
|
||||
:statuscode 200: Created
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active
|
||||
|
||||
|
||||
.. http:delete:: /v1/repositories/(namespace)/(repo_name)/
|
||||
|
||||
Delete a user repository with the given ``namespace`` and ``repo_name``.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /v1/repositories/foo/bar/ HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
X-Docker-Token: true
|
||||
|
||||
""
|
||||
|
||||
:parameter namespace: the namespace for the repo
|
||||
:parameter repo_name: the name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 202
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=delete
|
||||
X-Docker-Endpoints: registry-1.docker.io [, registry-2.docker.io]
|
||||
|
||||
""
|
||||
|
||||
:statuscode 200: Deleted
|
||||
:statuscode 202: Accepted
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active
|
||||
|
||||
Library Repo
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. http:put:: /v1/repositories/(repo_name)/
|
||||
|
||||
Create a library repository with the given ``repo_name``.
|
||||
This is a restricted feature only available to docker admins.
|
||||
|
||||
When namespace is missing, it is assumed to be ``library``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/repositories/foobar/ HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
X-Docker-Token: true
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”}]
|
||||
|
||||
:parameter repo_name: the library name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
WWW-Authenticate: Token signature=123abc,repository=”library/foobar”,access=write
|
||||
X-Docker-Endpoints: registry-1.docker.io [, registry-2.docker.io]
|
||||
|
||||
""
|
||||
|
||||
:statuscode 200: Created
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active
|
||||
|
||||
.. http:delete:: /v1/repositories/(repo_name)/
|
||||
|
||||
Delete a library repository with the given ``repo_name``.
|
||||
This is a restricted feature only available to docker admins.
|
||||
|
||||
When namespace is missing, it is assumed to be ``library``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /v1/repositories/foobar/ HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
X-Docker-Token: true
|
||||
|
||||
""
|
||||
|
||||
:parameter repo_name: the library name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 202
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
WWW-Authenticate: Token signature=123abc,repository=”library/foobar”,access=delete
|
||||
X-Docker-Endpoints: registry-1.docker.io [, registry-2.docker.io]
|
||||
|
||||
""
|
||||
|
||||
:statuscode 200: Deleted
|
||||
:statuscode 202: Accepted
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active
|
||||
|
||||
Repository Images
|
||||
*****************
|
||||
|
||||
User Repo Images
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. http:put:: /v1/repositories/(namespace)/(repo_name)/images
|
||||
|
||||
Update the images for a user repo.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/repositories/foo/bar/images HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”,
|
||||
“checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}]
|
||||
|
||||
:parameter namespace: the namespace for the repo
|
||||
:parameter repo_name: the name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
""
|
||||
|
||||
:statuscode 204: Created
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active or permission denied
|
||||
|
||||
|
||||
.. http:get:: /v1/repositories/(namespace)/(repo_name)/images
|
||||
|
||||
get the images for a user repo.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/repositories/foo/bar/images HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
|
||||
:parameter namespace: the namespace for the repo
|
||||
:parameter repo_name: the name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”,
|
||||
“checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”},
|
||||
{“id”: “ertwetewtwe38722009fe6857087b486531f9a779a0c1dfddgfgsdgdsgds”,
|
||||
“checksum”: “34t23f23fc17e3ed29dae8f12c4f9e89cc6f0bsdfgfsdgdsgdsgerwgew”}]
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 404: Not found
|
||||
|
||||
Library Repo Images
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. http:put:: /v1/repositories/(repo_name)/images
|
||||
|
||||
Update the images for a library repo.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/repositories/foobar/images HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”,
|
||||
“checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}]
|
||||
|
||||
:parameter repo_name: the library name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
""
|
||||
|
||||
:statuscode 204: Created
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active or permission denied
|
||||
|
||||
|
||||
.. http:get:: /v1/repositories/(repo_name)/images
|
||||
|
||||
get the images for a library repo.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/repositories/foobar/images HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
|
||||
:parameter repo_name: the library name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”,
|
||||
“checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”},
|
||||
{“id”: “ertwetewtwe38722009fe6857087b486531f9a779a0c1dfddgfgsdgdsgds”,
|
||||
“checksum”: “34t23f23fc17e3ed29dae8f12c4f9e89cc6f0bsdfgfsdgdsgdsgerwgew”}]
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 404: Not found
|
||||
|
||||
|
||||
Repository Authorization
|
||||
************************
|
||||
|
||||
Library Repo
|
||||
~~~~~~~~~~~~
|
||||
|
||||
.. http:put:: /v1/repositories/(repo_name)/auth
|
||||
|
||||
authorize a token for a library repo
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/repositories/foobar/auth HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Authorization: Token signature=123abc,repository="library/foobar",access=write
|
||||
|
||||
:parameter repo_name: the library name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
"OK"
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 403: Permission denied
|
||||
:statuscode 404: Not found
|
||||
|
||||
|
||||
User Repo
|
||||
~~~~~~~~~
|
||||
|
||||
.. http:put:: /v1/repositories/(namespace)/(repo_name)/auth
|
||||
|
||||
authorize a token for a user repo
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/repositories/foo/bar/auth HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Authorization: Token signature=123abc,repository="foo/bar",access=write
|
||||
|
||||
:parameter namespace: the namespace for the repo
|
||||
:parameter repo_name: the name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
"OK"
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 403: Permission denied
|
||||
:statuscode 404: Not found
|
||||
|
||||
|
||||
2.2 Users
|
||||
^^^^^^^^^
|
||||
|
||||
User Login
|
||||
**********
|
||||
|
||||
.. http:get:: /v1/users
|
||||
|
||||
If you want to check your login, you can try this endpoint
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/users HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
OK
|
||||
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active
|
||||
|
||||
|
||||
User Register
|
||||
*************
|
||||
|
||||
.. http:post:: /v1/users
|
||||
|
||||
Registering a new account.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /v1/users HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
|
||||
{"email": "sam@dotcloud.com",
|
||||
"password": "toto42",
|
||||
"username": "foobar"'}
|
||||
|
||||
:jsonparameter email: valid email address, that needs to be confirmed
|
||||
:jsonparameter username: min 4 character, max 30 characters, must match the regular expression [a-z0-9_].
|
||||
:jsonparameter password: min 5 characters
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 201 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
"User Created"
|
||||
|
||||
:statuscode 201: User Created
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
|
||||
Update User
|
||||
***********
|
||||
|
||||
.. http:put:: /v1/users/(username)/
|
||||
|
||||
Change a password or email address for given user. If you pass in an email,
|
||||
it will add it to your account, it will not remove the old one. Passwords will
|
||||
be updated.
|
||||
|
||||
It is up to the client to verify that that password that is sent is the one that
|
||||
they want. Common approach is to have them type it twice.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/users/fakeuser/ HTTP/1.1
|
||||
Host: index.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic akmklmasadalkm==
|
||||
|
||||
{"email": "sam@dotcloud.com",
|
||||
"password": "toto42"}
|
||||
|
||||
:parameter username: username for the person you want to update
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 204
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
""
|
||||
|
||||
:statuscode 204: User Updated
|
||||
:statuscode 400: Errors (invalid json, missing or invalid fields, etc)
|
||||
:statuscode 401: Unauthorized
|
||||
:statuscode 403: Account is not Active
|
||||
:statuscode 404: User not found
|
||||
|
||||
|
||||
2.3 Search
|
||||
^^^^^^^^^^
|
||||
If you need to search the index, this is the endpoint you would use.
|
||||
|
||||
Search
|
||||
******
|
||||
|
||||
.. http:get:: /v1/search
|
||||
|
||||
Search the Index given a search term. It accepts :http:method:`get` only.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/search?q=search_term HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{"query":"search_term",
|
||||
"num_results": 2,
|
||||
"results" : [
|
||||
{"name": "dotcloud/base", "description": "A base ubuntu64 image..."},
|
||||
{"name": "base2", "description": "A base ubuntu64 image..."},
|
||||
]
|
||||
}
|
||||
|
||||
:query q: what you want to search for
|
||||
:statuscode 200: no error
|
||||
:statuscode 500: server error
|
|
@ -1,43 +0,0 @@
|
|||
:title: Docker Index documentation
|
||||
:description: Documentation for docker Index
|
||||
:keywords: docker, index, api
|
||||
|
||||
|
||||
=======================
|
||||
Docker Index Search API
|
||||
=======================
|
||||
|
||||
Search
|
||||
------
|
||||
|
||||
.. http:get:: /v1/search
|
||||
|
||||
Search the Index given a search term. It accepts :http:method:`get` only.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/search?q=search_term HTTP/1.1
|
||||
Host: example.com
|
||||
Accept: application/json
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{"query":"search_term",
|
||||
"num_results": 2,
|
||||
"results" : [
|
||||
{"name": "dotcloud/base", "description": "A base ubuntu64 image..."},
|
||||
{"name": "base2", "description": "A base ubuntu64 image..."},
|
||||
]
|
||||
}
|
||||
|
||||
:query q: what you want to search for
|
||||
:statuscode 200: no error
|
||||
:statuscode 500: server error
|
|
@ -1,7 +1,6 @@
|
|||
:title: Registry Documentation
|
||||
:description: Documentation for docker Registry and Registry API
|
||||
:keywords: docker, registry, api, index
|
||||
|
||||
:title: Registry API
|
||||
:description: API Documentation for Docker Registry
|
||||
:keywords: API, Docker, index, registry, REST, documentation
|
||||
|
||||
===================
|
||||
Docker Registry API
|
||||
|
@ -9,29 +8,10 @@ Docker Registry API
|
|||
|
||||
.. contents:: Table of Contents
|
||||
|
||||
1. The 3 roles
|
||||
===============
|
||||
1. Brief introduction
|
||||
=====================
|
||||
|
||||
1.1 Index
|
||||
---------
|
||||
|
||||
The Index is responsible for centralizing information about:
|
||||
- User accounts
|
||||
- Checksums of the images
|
||||
- Public namespaces
|
||||
|
||||
The Index has different components:
|
||||
- Web UI
|
||||
- Meta-data store (comments, stars, list public repositories)
|
||||
- Authentication service
|
||||
- Tokenization
|
||||
|
||||
The index is authoritative for those information.
|
||||
|
||||
We expect that there will be only one instance of the index, run and managed by dotCloud.
|
||||
|
||||
1.2 Registry
|
||||
------------
|
||||
- This is the REST API for the Docker Registry
|
||||
- It stores the images and the graph for a set of repositories
|
||||
- It does not have user accounts data
|
||||
- It has no notion of user accounts or authorization
|
||||
|
@ -60,418 +40,424 @@ We expect that there will be multiple registries out there. To help to grasp the
|
|||
|
||||
The latter would only require two new commands in docker, e.g. “registryget” and “registryput”, wrapping access to the local filesystem (and optionally doing consistency checks). Authentication and authorization are then delegated to SSH (e.g. with public keys).
|
||||
|
||||
1.3 Docker
|
||||
2. Endpoints
|
||||
============
|
||||
|
||||
2.1 Images
|
||||
----------
|
||||
|
||||
On top of being a runtime for LXC, Docker is the Registry client. It supports:
|
||||
- Push / Pull on the registry
|
||||
- Client authentication on the Index
|
||||
Layer
|
||||
*****
|
||||
|
||||
2. Workflow
|
||||
===========
|
||||
.. http:get:: /v1/images/(image_id)/layer
|
||||
|
||||
2.1 Pull
|
||||
get image layer for a given ``image_id``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/layer HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Token akmklmasadalkmsdfgsdgdge33
|
||||
|
||||
:parameter image_id: the id for the layer you want to get
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
{
|
||||
id: "088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c",
|
||||
parent: "aeee6396d62273d180a49c96c62e45438d87c7da4a5cf5d2be6bee4e21bc226f",
|
||||
created: "2013-04-30T17:46:10.843673+03:00",
|
||||
container: "8305672a76cc5e3d168f97221106ced35a76ec7ddbb03209b0f0d96bf74f6ef7",
|
||||
container_config: {
|
||||
Hostname: "host-test",
|
||||
User: "",
|
||||
Memory: 0,
|
||||
MemorySwap: 0,
|
||||
AttachStdin: false,
|
||||
AttachStdout: false,
|
||||
AttachStderr: false,
|
||||
PortSpecs: null,
|
||||
Tty: false,
|
||||
OpenStdin: false,
|
||||
StdinOnce: false,
|
||||
Env: null,
|
||||
Cmd: [
|
||||
"/bin/bash",
|
||||
"-c",
|
||||
"apt-get -q -yy -f install libevent-dev"
|
||||
],
|
||||
Dns: null,
|
||||
Image: "imagename/blah",
|
||||
Volumes: { },
|
||||
VolumesFrom: ""
|
||||
},
|
||||
docker_version: "0.1.7"
|
||||
}
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Image not found
|
||||
|
||||
|
||||
.. http:put:: /v1/images/(image_id)/layer
|
||||
|
||||
put image layer for a given ``image_id``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/layer HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Token akmklmasadalkmsdfgsdgdge33
|
||||
|
||||
{
|
||||
id: "088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c",
|
||||
parent: "aeee6396d62273d180a49c96c62e45438d87c7da4a5cf5d2be6bee4e21bc226f",
|
||||
created: "2013-04-30T17:46:10.843673+03:00",
|
||||
container: "8305672a76cc5e3d168f97221106ced35a76ec7ddbb03209b0f0d96bf74f6ef7",
|
||||
container_config: {
|
||||
Hostname: "host-test",
|
||||
User: "",
|
||||
Memory: 0,
|
||||
MemorySwap: 0,
|
||||
AttachStdin: false,
|
||||
AttachStdout: false,
|
||||
AttachStderr: false,
|
||||
PortSpecs: null,
|
||||
Tty: false,
|
||||
OpenStdin: false,
|
||||
StdinOnce: false,
|
||||
Env: null,
|
||||
Cmd: [
|
||||
"/bin/bash",
|
||||
"-c",
|
||||
"apt-get -q -yy -f install libevent-dev"
|
||||
],
|
||||
Dns: null,
|
||||
Image: "imagename/blah",
|
||||
Volumes: { },
|
||||
VolumesFrom: ""
|
||||
},
|
||||
docker_version: "0.1.7"
|
||||
}
|
||||
|
||||
:parameter image_id: the id for the layer you want to get
|
||||
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
""
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Image not found
|
||||
|
||||
|
||||
Image
|
||||
*****
|
||||
|
||||
.. http:put:: /v1/images/(image_id)/json
|
||||
|
||||
put image for a given ``image_id``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PUT /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/json HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
{
|
||||
“id”: “088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c”,
|
||||
“checksum”: “sha256:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”
|
||||
}
|
||||
|
||||
:parameter image_id: the id for the layer you want to get
|
||||
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
""
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
|
||||
.. http:get:: /v1/images/(image_id)/json
|
||||
|
||||
get image for a given ``image_id``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/json HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
:parameter image_id: the id for the layer you want to get
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
“id”: “088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c”,
|
||||
“checksum”: “sha256:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”
|
||||
}
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Image not found
|
||||
|
||||
|
||||
Ancestry
|
||||
********
|
||||
|
||||
.. http:get:: /v1/images/(image_id)/ancestry
|
||||
|
||||
get ancestry for an image given an ``image_id``
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/images/088b4505aa3adc3d35e79c031fa126b403200f02f51920fbd9b7c503e87c7a2c/ancestry HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
:parameter image_id: the id for the layer you want to get
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
["088b4502f51920fbd9b7c503e87c7a2c05aa3adc3d35e79c031fa126b403200f",
|
||||
"aeee63968d87c7da4a5cf5d2be6bee4e21bc226fd62273d180a49c96c62e4543",
|
||||
"bfa4c5326bc764280b0863b46a4b20d940bc1897ef9c1dfec060604bdc383280",
|
||||
"6ab5893c6927c15a15665191f2c6cf751f5056d8b95ceee32e43c5e8a3648544"]
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Image not found
|
||||
|
||||
|
||||
2.2 Tags
|
||||
--------
|
||||
|
||||
.. image:: /static_files/docker_pull_chart.png
|
||||
.. http:get:: /v1/repositories/(namespace)/(repository)/tags
|
||||
|
||||
1. Contact the Index to know where I should download “samalba/busybox”
|
||||
2. Index replies:
|
||||
a. “samalba/busybox” is on Registry A
|
||||
b. here are the checksums for “samalba/busybox” (for all layers)
|
||||
c. token
|
||||
3. Contact Registry A to receive the layers for “samalba/busybox” (all of them to the base image). Registry A is authoritative for “samalba/busybox” but keeps a copy of all inherited layers and serve them all from the same location.
|
||||
4. registry contacts index to verify if token/user is allowed to download images
|
||||
5. Index returns true/false lettings registry know if it should proceed or error out
|
||||
6. Get the payload for all layers
|
||||
get all of the tags for the given repo.
|
||||
|
||||
It’s possible to run docker pull \https://<registry>/repositories/samalba/busybox. In this case, docker bypasses the Index. However the security is not guaranteed (in case Registry A is corrupted) because there won’t be any checksum checks.
|
||||
**Example Request**:
|
||||
|
||||
Currently registry redirects to s3 urls for downloads, going forward all downloads need to be streamed through the registry. The Registry will then abstract the calls to S3 by a top-level class which implements sub-classes for S3 and local storage.
|
||||
.. sourcecode:: http
|
||||
|
||||
Token is only returned when the 'X-Docker-Token' header is sent with request.
|
||||
|
||||
Basic Auth is required to pull private repos. Basic auth isn't required for pulling public repos, but if one is provided, it needs to be valid and for an active account.
|
||||
|
||||
API (pulling repository foo/bar):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
1. (Docker -> Index) GET /v1/repositories/foo/bar/images
|
||||
**Headers**:
|
||||
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
X-Docker-Token: true
|
||||
**Action**:
|
||||
(looking up the foo/bar in db and gets images and checksums for that repo (all if no tag is specified, if tag, only checksums for those tags) see part 4.4.1)
|
||||
|
||||
2. (Index -> Docker) HTTP 200 OK
|
||||
|
||||
**Headers**:
|
||||
- Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
- X-Docker-Endpoints: registry.docker.io [, registry2.docker.io]
|
||||
**Body**:
|
||||
Jsonified checksums (see part 4.4.1)
|
||||
|
||||
3. (Docker -> Registry) GET /v1/repositories/foo/bar/tags/latest
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
|
||||
4. (Registry -> Index) GET /v1/repositories/foo/bar/images
|
||||
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
**Body**:
|
||||
<ids and checksums in payload>
|
||||
|
||||
**Action**:
|
||||
( Lookup token see if they have access to pull.)
|
||||
|
||||
If good:
|
||||
HTTP 200 OK
|
||||
Index will invalidate the token
|
||||
If bad:
|
||||
HTTP 401 Unauthorized
|
||||
|
||||
5. (Docker -> Registry) GET /v1/images/928374982374/ancestry
|
||||
**Action**:
|
||||
(for each image id returned in the registry, fetch /json + /layer)
|
||||
|
||||
.. note::
|
||||
|
||||
If someone makes a second request, then we will always give a new token, never reuse tokens.
|
||||
|
||||
2.2 Push
|
||||
--------
|
||||
|
||||
.. image:: /static_files/docker_push_chart.png
|
||||
|
||||
1. Contact the index to allocate the repository name “samalba/busybox” (authentication required with user credentials)
|
||||
2. If authentication works and namespace available, “samalba/busybox” is allocated and a temporary token is returned (namespace is marked as initialized in index)
|
||||
3. Push the image on the registry (along with the token)
|
||||
4. Registry A contacts the Index to verify the token (token must corresponds to the repository name)
|
||||
5. Index validates the token. Registry A starts reading the stream pushed by docker and store the repository (with its images)
|
||||
6. docker contacts the index to give checksums for upload images
|
||||
|
||||
.. note::
|
||||
|
||||
**It’s possible not to use the Index at all!** In this case, a deployed version of the Registry is deployed to store and serve images. Those images are not authentified and the security is not guaranteed.
|
||||
|
||||
.. note::
|
||||
|
||||
**Index can be replaced!** For a private Registry deployed, a custom Index can be used to serve and validate token according to different policies.
|
||||
|
||||
Docker computes the checksums and submit them to the Index at the end of the push. When a repository name does not have checksums on the Index, it means that the push is in progress (since checksums are submitted at the end).
|
||||
|
||||
API (pushing repos foo/bar):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
1. (Docker -> Index) PUT /v1/repositories/foo/bar/
|
||||
**Headers**:
|
||||
Authorization: Basic sdkjfskdjfhsdkjfh==
|
||||
X-Docker-Token: true
|
||||
|
||||
**Action**::
|
||||
- in index, we allocated a new repository, and set to initialized
|
||||
|
||||
**Body**::
|
||||
(The body contains the list of images that are going to be pushed, with empty checksums. The checksums will be set at the end of the push)::
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”}]
|
||||
|
||||
2. (Index -> Docker) 200 Created
|
||||
**Headers**:
|
||||
- WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
- X-Docker-Endpoints: registry.docker.io [, registry2.docker.io]
|
||||
|
||||
3. (Docker -> Registry) PUT /v1/images/98765432_parent/json
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
|
||||
4. (Registry->Index) GET /v1/repositories/foo/bar/images
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
**Action**::
|
||||
- Index:
|
||||
will invalidate the token.
|
||||
- Registry:
|
||||
grants a session (if token is approved) and fetches the images id
|
||||
|
||||
5. (Docker -> Registry) PUT /v1/images/98765432_parent/json
|
||||
**Headers**::
|
||||
- Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
- Cookie: (Cookie provided by the Registry)
|
||||
|
||||
6. (Docker -> Registry) PUT /v1/images/98765432/json
|
||||
**Headers**:
|
||||
GET /v1/repositories/foo/bar/tags HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
7. (Docker -> Registry) PUT /v1/images/98765432_parent/layer
|
||||
**Headers**:
|
||||
:parameter namespace: namespace for the repo
|
||||
:parameter repository: name for the repo
|
||||
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"latest": "9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f",
|
||||
“0.1.1”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”
|
||||
}
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Repository not found
|
||||
|
||||
|
||||
.. http:get:: /v1/repositories/(namespace)/(repository)/tags/(tag)
|
||||
|
||||
get a tag for the given repo.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/repositories/foo/bar/tags/latest HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
8. (Docker -> Registry) PUT /v1/images/98765432/layer
|
||||
**Headers**:
|
||||
X-Docker-Checksum: sha256:436745873465fdjkhdfjkgh
|
||||
:parameter namespace: namespace for the repo
|
||||
:parameter repository: name for the repo
|
||||
:parameter tag: name of tag you want to get
|
||||
|
||||
9. (Docker -> Registry) PUT /v1/repositories/foo/bar/tags/latest
|
||||
**Headers**:
|
||||
**Example Response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
"9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f"
|
||||
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Tag not found
|
||||
|
||||
.. http:delete:: /v1/repositories/(namespace)/(repository)/tags/(tag)
|
||||
|
||||
delete the tag for the repo
|
||||
|
||||
**Example Request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
DELETE /v1/repositories/foo/bar/tags/latest HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
**Body**:
|
||||
“98765432”
|
||||
|
||||
10. (Docker -> Index) PUT /v1/repositories/foo/bar/images
|
||||
:parameter namespace: namespace for the repo
|
||||
:parameter repository: name for the repo
|
||||
:parameter tag: name of tag you want to delete
|
||||
|
||||
**Headers**:
|
||||
Authorization: Basic 123oislifjsldfj==
|
||||
X-Docker-Endpoints: registry1.docker.io (no validation on this right now)
|
||||
**Example Response**:
|
||||
|
||||
**Body**:
|
||||
(The image, id’s, tags and checksums)
|
||||
.. sourcecode:: http
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”,
|
||||
“checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}]
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
**Return** HTTP 204
|
||||
""
|
||||
|
||||
.. note::
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Tag not found
|
||||
|
||||
If push fails and they need to start again, what happens in the index, there will already be a record for the namespace/name, but it will be initialized. Should we allow it, or mark as name already used? One edge case could be if someone pushes the same thing at the same time with two different shells.
|
||||
|
||||
If it's a retry on the Registry, Docker has a cookie (provided by the registry after token validation). So the Index won’t have to provide a new token.
|
||||
.. http:put:: /v1/repositories/(namespace)/(repository)/tags/(tag)
|
||||
|
||||
3. How to use the Registry in standalone mode
|
||||
=============================================
|
||||
put a tag for the given repo.
|
||||
|
||||
The Index has two main purposes (along with its fancy social features):
|
||||
**Example Request**:
|
||||
|
||||
- Resolve short names (to avoid passing absolute URLs all the time)
|
||||
- username/projectname -> \https://registry.docker.io/users/<username>/repositories/<projectname>/
|
||||
- Authenticate a user as a repos owner (for a central referenced repository)
|
||||
.. sourcecode:: http
|
||||
|
||||
3.1 Without an Index
|
||||
--------------------
|
||||
Using the Registry without the Index can be useful to store the images on a private network without having to rely on an external entity controlled by dotCloud.
|
||||
PUT /v1/repositories/foo/bar/tags/latest HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
In this case, the registry will be launched in a special mode (--standalone? --no-index?). In this mode, the only thing which changes is that Registry will never contact the Index to verify a token. It will be the Registry owner responsibility to authenticate the user who pushes (or even pulls) an image using any mechanism (HTTP auth, IP based, etc...).
|
||||
“9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”
|
||||
|
||||
In this scenario, the Registry is responsible for the security in case of data corruption since the checksums are not delivered by a trusted entity.
|
||||
:parameter namespace: namespace for the repo
|
||||
:parameter repository: name for the repo
|
||||
:parameter tag: name of tag you want to add
|
||||
|
||||
As hinted previously, a standalone registry can also be implemented by any HTTP server handling GET/PUT requests (or even only GET requests if no write access is necessary).
|
||||
**Example Response**:
|
||||
|
||||
3.2 With an Index
|
||||
-----------------
|
||||
.. sourcecode:: http
|
||||
|
||||
The Index data needed by the Registry are simple:
|
||||
- Serve the checksums
|
||||
- Provide and authorize a Token
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
In the scenario of a Registry running on a private network with the need of centralizing and authorizing, it’s easy to use a custom Index.
|
||||
""
|
||||
|
||||
The only challenge will be to tell Docker to contact (and trust) this custom Index. Docker will be configurable at some point to use a specific Index, it’ll be the private entity responsibility (basically the organization who uses Docker in a private environment) to maintain the Index and the Docker’s configuration among its consumers.
|
||||
:statuscode 200: OK
|
||||
:statuscode 400: Invalid data
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Image not found
|
||||
|
||||
4. The API
|
||||
==========
|
||||
2.3 Repositories
|
||||
----------------
|
||||
|
||||
The first version of the api is available here: https://github.com/jpetazzo/docker/blob/acd51ecea8f5d3c02b00a08176171c59442df8b3/docs/images-repositories-push-pull.md
|
||||
.. http:delete:: /v1/repositories/(namespace)/(repository)/
|
||||
|
||||
4.1 Images
|
||||
----------
|
||||
delete a repository
|
||||
|
||||
The format returned in the images is not defined here (for layer and json), basically because Registry stores exactly the same kind of information as Docker uses to manage them.
|
||||
**Example Request**:
|
||||
|
||||
The format of ancestry is a line-separated list of image ids, in age order. I.e. the image’s parent is on the last line, the parent of the parent on the next-to-last line, etc.; if the image has no parent, the file is empty.
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /v1/images/<image_id>/layer
|
||||
PUT /v1/images/<image_id>/layer
|
||||
GET /v1/images/<image_id>/json
|
||||
PUT /v1/images/<image_id>/json
|
||||
GET /v1/images/<image_id>/ancestry
|
||||
PUT /v1/images/<image_id>/ancestry
|
||||
DELETE /v1/repositories/foo/bar/ HTTP/1.1
|
||||
Host: registry-1.docker.io
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
4.2 Users
|
||||
---------
|
||||
""
|
||||
|
||||
4.2.1 Create a user (Index)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
:parameter namespace: namespace for the repo
|
||||
:parameter repository: name for the repo
|
||||
|
||||
POST /v1/users
|
||||
**Example Response**:
|
||||
|
||||
**Body**:
|
||||
{"email": "sam@dotcloud.com", "password": "toto42", "username": "foobar"'}
|
||||
.. sourcecode:: http
|
||||
|
||||
**Validation**:
|
||||
- **username** : min 4 character, max 30 characters, must match the regular expression [a-z0-9_].
|
||||
- **password**: min 5 characters
|
||||
HTTP/1.1 200
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
**Valid**: return HTTP 200
|
||||
""
|
||||
|
||||
Errors: HTTP 400 (we should create error codes for possible errors)
|
||||
- invalid json
|
||||
- missing field
|
||||
- wrong format (username, password, email, etc)
|
||||
- forbidden name
|
||||
- name already exists
|
||||
:statuscode 200: OK
|
||||
:statuscode 401: Requires authorization
|
||||
:statuscode 404: Repository not found
|
||||
|
||||
.. note::
|
||||
3.0 Authorization
|
||||
=================
|
||||
This is where we describe the authorization process, including the tokens and cookies.
|
||||
|
||||
A user account will be valid only if the email has been validated (a validation link is sent to the email address).
|
||||
|
||||
4.2.2 Update a user (Index)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
PUT /v1/users/<username>
|
||||
|
||||
**Body**:
|
||||
{"password": "toto"}
|
||||
|
||||
.. note::
|
||||
|
||||
We can also update email address, if they do, they will need to reverify their new email address.
|
||||
|
||||
4.2.3 Login (Index)
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
Does nothing else but asking for a user authentication. Can be used to validate credentials. HTTP Basic Auth for now, maybe change in future.
|
||||
|
||||
GET /v1/users
|
||||
|
||||
**Return**:
|
||||
- Valid: HTTP 200
|
||||
- Invalid login: HTTP 401
|
||||
- Account inactive: HTTP 403 Account is not Active
|
||||
|
||||
4.3 Tags (Registry)
|
||||
-------------------
|
||||
|
||||
The Registry does not know anything about users. Even though repositories are under usernames, it’s just a namespace for the registry. Allowing us to implement organizations or different namespaces per user later, without modifying the Registry’s API.
|
||||
|
||||
The following naming restrictions apply:
|
||||
|
||||
- Namespaces must match the same regular expression as usernames (See 4.2.1.)
|
||||
- Repository names must match the regular expression [a-zA-Z0-9-_.]
|
||||
|
||||
4.3.1 Get all tags
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
GET /v1/repositories/<namespace>/<repository_name>/tags
|
||||
|
||||
**Return**: HTTP 200
|
||||
{
|
||||
"latest": "9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f",
|
||||
“0.1.1”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”
|
||||
}
|
||||
|
||||
4.3.2 Read the content of a tag (resolve the image id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
GET /v1/repositories/<namespace>/<repo_name>/tags/<tag>
|
||||
|
||||
**Return**:
|
||||
"9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f"
|
||||
|
||||
4.3.3 Delete a tag (registry)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
DELETE /v1/repositories/<namespace>/<repo_name>/tags/<tag>
|
||||
|
||||
4.4 Images (Index)
|
||||
------------------
|
||||
|
||||
For the Index to “resolve” the repository name to a Registry location, it uses the X-Docker-Endpoints header. In other terms, this requests always add a “X-Docker-Endpoints” to indicate the location of the registry which hosts this repository.
|
||||
|
||||
4.4.1 Get the images
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
GET /v1/repositories/<namespace>/<repo_name>/images
|
||||
|
||||
**Return**: HTTP 200
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, “checksum”: “md5:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}]
|
||||
|
||||
|
||||
4.4.2 Add/update the images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You always add images, you never remove them.
|
||||
|
||||
PUT /v1/repositories/<namespace>/<repo_name>/images
|
||||
|
||||
**Body**:
|
||||
[ {“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, “checksum”: “sha256:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”} ]
|
||||
|
||||
**Return** 204
|
||||
|
||||
5. Chaining Registries
|
||||
======================
|
||||
|
||||
It’s possible to chain Registries server for several reasons:
|
||||
- Load balancing
|
||||
- Delegate the next request to another server
|
||||
|
||||
When a Registry is a reference for a repository, it should host the entire images chain in order to avoid breaking the chain during the download.
|
||||
|
||||
The Index and Registry use this mechanism to redirect on one or the other.
|
||||
|
||||
Example with an image download:
|
||||
On every request, a special header can be returned:
|
||||
|
||||
X-Docker-Endpoints: server1,server2
|
||||
|
||||
On the next request, the client will always pick a server from this list.
|
||||
|
||||
6. Authentication & Authorization
|
||||
=================================
|
||||
|
||||
6.1 On the Index
|
||||
-----------------
|
||||
|
||||
The Index supports both “Basic” and “Token” challenges. Usually when there is a “401 Unauthorized”, the Index replies this::
|
||||
|
||||
401 Unauthorized
|
||||
WWW-Authenticate: Basic realm="auth required",Token
|
||||
|
||||
You have 3 options:
|
||||
|
||||
1. Provide user credentials and ask for a token
|
||||
|
||||
**Header**:
|
||||
- Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
- X-Docker-Token: true
|
||||
|
||||
In this case, along with the 200 response, you’ll get a new token (if user auth is ok):
|
||||
If authorization isn't correct you get a 401 response.
|
||||
If account isn't active you will get a 403 response.
|
||||
|
||||
**Response**:
|
||||
- 200 OK
|
||||
- X-Docker-Token: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
2. Provide user credentials only
|
||||
|
||||
**Header**:
|
||||
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
|
||||
3. Provide Token
|
||||
|
||||
**Header**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
6.2 On the Registry
|
||||
-------------------
|
||||
|
||||
The Registry only supports the Token challenge::
|
||||
|
||||
401 Unauthorized
|
||||
WWW-Authenticate: Token
|
||||
|
||||
The only way is to provide a token on “401 Unauthorized” responses::
|
||||
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
Usually, the Registry provides a Cookie when a Token verification succeeded. Every time the Registry passes a Cookie, you have to pass it back the same cookie.::
|
||||
|
||||
200 OK
|
||||
Set-Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=×tamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4="; Path=/; HttpOnly
|
||||
|
||||
Next request::
|
||||
|
||||
GET /(...)
|
||||
Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=×tamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4="
|
||||
TODO: add more info.
|
||||
|
|
569
docs/sources/api/registry_index_spec.rst
Normal file
569
docs/sources/api/registry_index_spec.rst
Normal file
|
@ -0,0 +1,569 @@
|
|||
:title: Registry Documentation
|
||||
:description: Documentation for docker Registry and Registry API
|
||||
:keywords: docker, registry, api, index
|
||||
|
||||
|
||||
=====================
|
||||
Registry & index Spec
|
||||
=====================
|
||||
|
||||
.. contents:: Table of Contents
|
||||
|
||||
1. The 3 roles
|
||||
===============
|
||||
|
||||
1.1 Index
|
||||
---------
|
||||
|
||||
The Index is responsible for centralizing information about:
|
||||
- User accounts
|
||||
- Checksums of the images
|
||||
- Public namespaces
|
||||
|
||||
The Index has different components:
|
||||
- Web UI
|
||||
- Meta-data store (comments, stars, list public repositories)
|
||||
- Authentication service
|
||||
- Tokenization
|
||||
|
||||
The index is authoritative for those information.
|
||||
|
||||
We expect that there will be only one instance of the index, run and managed by dotCloud.
|
||||
|
||||
1.2 Registry
|
||||
------------
|
||||
- It stores the images and the graph for a set of repositories
|
||||
- It does not have user accounts data
|
||||
- It has no notion of user accounts or authorization
|
||||
- It delegates authentication and authorization to the Index Auth service using tokens
|
||||
- It supports different storage backends (S3, cloud files, local FS)
|
||||
- It doesn’t have a local database
|
||||
- It will be open-sourced at some point
|
||||
|
||||
We expect that there will be multiple registries out there. To help to grasp the context, here are some examples of registries:
|
||||
|
||||
- **sponsor registry**: such a registry is provided by a third-party hosting infrastructure as a convenience for their customers and the docker community as a whole. Its costs are supported by the third party, but the management and operation of the registry are supported by dotCloud. It features read/write access, and delegates authentication and authorization to the Index.
|
||||
- **mirror registry**: such a registry is provided by a third-party hosting infrastructure but is targeted at their customers only. Some mechanism (unspecified to date) ensures that public images are pulled from a sponsor registry to the mirror registry, to make sure that the customers of the third-party provider can “docker pull” those images locally.
|
||||
- **vendor registry**: such a registry is provided by a software vendor, who wants to distribute docker images. It would be operated and managed by the vendor. Only users authorized by the vendor would be able to get write access. Some images would be public (accessible for anyone), others private (accessible only for authorized users). Authentication and authorization would be delegated to the Index. The goal of vendor registries is to let someone do “docker pull basho/riak1.3” and automatically push from the vendor registry (instead of a sponsor registry); i.e. get all the convenience of a sponsor registry, while retaining control on the asset distribution.
|
||||
- **private registry**: such a registry is located behind a firewall, or protected by an additional security layer (HTTP authorization, SSL client-side certificates, IP address authorization...). The registry is operated by a private entity, outside of dotCloud’s control. It can optionally delegate additional authorization to the Index, but it is not mandatory.
|
||||
|
||||
.. note::
|
||||
|
||||
Mirror registries and private registries which do not use the Index don’t even need to run the registry code. They can be implemented by any kind of transport implementing HTTP GET and PUT. Read-only registries can be powered by a simple static HTTP server.
|
||||
|
||||
.. note::
|
||||
|
||||
The latter implies that while HTTP is the protocol of choice for a registry, multiple schemes are possible (and in some cases, trivial):
|
||||
- HTTP with GET (and PUT for read-write registries);
|
||||
- local mount point;
|
||||
- remote docker addressed through SSH.
|
||||
|
||||
The latter would only require two new commands in docker, e.g. “registryget” and “registryput”, wrapping access to the local filesystem (and optionally doing consistency checks). Authentication and authorization are then delegated to SSH (e.g. with public keys).
|
||||
|
||||
1.3 Docker
|
||||
----------
|
||||
|
||||
On top of being a runtime for LXC, Docker is the Registry client. It supports:
|
||||
- Push / Pull on the registry
|
||||
- Client authentication on the Index
|
||||
|
||||
2. Workflow
|
||||
===========
|
||||
|
||||
2.1 Pull
|
||||
--------
|
||||
|
||||
.. image:: /static_files/docker_pull_chart.png
|
||||
|
||||
1. Contact the Index to know where I should download “samalba/busybox”
|
||||
2. Index replies:
|
||||
a. “samalba/busybox” is on Registry A
|
||||
b. here are the checksums for “samalba/busybox” (for all layers)
|
||||
c. token
|
||||
3. Contact Registry A to receive the layers for “samalba/busybox” (all of them to the base image). Registry A is authoritative for “samalba/busybox” but keeps a copy of all inherited layers and serve them all from the same location.
|
||||
4. registry contacts index to verify if token/user is allowed to download images
|
||||
5. Index returns true/false lettings registry know if it should proceed or error out
|
||||
6. Get the payload for all layers
|
||||
|
||||
It’s possible to run docker pull \https://<registry>/repositories/samalba/busybox. In this case, docker bypasses the Index. However the security is not guaranteed (in case Registry A is corrupted) because there won’t be any checksum checks.
|
||||
|
||||
Currently registry redirects to s3 urls for downloads, going forward all downloads need to be streamed through the registry. The Registry will then abstract the calls to S3 by a top-level class which implements sub-classes for S3 and local storage.
|
||||
|
||||
Token is only returned when the 'X-Docker-Token' header is sent with request.
|
||||
|
||||
Basic Auth is required to pull private repos. Basic auth isn't required for pulling public repos, but if one is provided, it needs to be valid and for an active account.
|
||||
|
||||
API (pulling repository foo/bar):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
1. (Docker -> Index) GET /v1/repositories/foo/bar/images
|
||||
**Headers**:
|
||||
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
X-Docker-Token: true
|
||||
**Action**:
|
||||
(looking up the foo/bar in db and gets images and checksums for that repo (all if no tag is specified, if tag, only checksums for those tags) see part 4.4.1)
|
||||
|
||||
2. (Index -> Docker) HTTP 200 OK
|
||||
|
||||
**Headers**:
|
||||
- Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
- X-Docker-Endpoints: registry.docker.io [, registry2.docker.io]
|
||||
**Body**:
|
||||
Jsonified checksums (see part 4.4.1)
|
||||
|
||||
3. (Docker -> Registry) GET /v1/repositories/foo/bar/tags/latest
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
|
||||
4. (Registry -> Index) GET /v1/repositories/foo/bar/images
|
||||
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
**Body**:
|
||||
<ids and checksums in payload>
|
||||
|
||||
**Action**:
|
||||
( Lookup token see if they have access to pull.)
|
||||
|
||||
If good:
|
||||
HTTP 200 OK
|
||||
Index will invalidate the token
|
||||
If bad:
|
||||
HTTP 401 Unauthorized
|
||||
|
||||
5. (Docker -> Registry) GET /v1/images/928374982374/ancestry
|
||||
**Action**:
|
||||
(for each image id returned in the registry, fetch /json + /layer)
|
||||
|
||||
.. note::
|
||||
|
||||
If someone makes a second request, then we will always give a new token, never reuse tokens.
|
||||
|
||||
2.2 Push
|
||||
--------
|
||||
|
||||
.. image:: /static_files/docker_push_chart.png
|
||||
|
||||
1. Contact the index to allocate the repository name “samalba/busybox” (authentication required with user credentials)
|
||||
2. If authentication works and namespace available, “samalba/busybox” is allocated and a temporary token is returned (namespace is marked as initialized in index)
|
||||
3. Push the image on the registry (along with the token)
|
||||
4. Registry A contacts the Index to verify the token (token must corresponds to the repository name)
|
||||
5. Index validates the token. Registry A starts reading the stream pushed by docker and store the repository (with its images)
|
||||
6. docker contacts the index to give checksums for upload images
|
||||
|
||||
.. note::
|
||||
|
||||
**It’s possible not to use the Index at all!** In this case, a deployed version of the Registry is deployed to store and serve images. Those images are not authentified and the security is not guaranteed.
|
||||
|
||||
.. note::
|
||||
|
||||
**Index can be replaced!** For a private Registry deployed, a custom Index can be used to serve and validate token according to different policies.
|
||||
|
||||
Docker computes the checksums and submit them to the Index at the end of the push. When a repository name does not have checksums on the Index, it means that the push is in progress (since checksums are submitted at the end).
|
||||
|
||||
API (pushing repos foo/bar):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
1. (Docker -> Index) PUT /v1/repositories/foo/bar/
|
||||
**Headers**:
|
||||
Authorization: Basic sdkjfskdjfhsdkjfh==
|
||||
X-Docker-Token: true
|
||||
|
||||
**Action**::
|
||||
- in index, we allocated a new repository, and set to initialized
|
||||
|
||||
**Body**::
|
||||
(The body contains the list of images that are going to be pushed, with empty checksums. The checksums will be set at the end of the push)::
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”}]
|
||||
|
||||
2. (Index -> Docker) 200 Created
|
||||
**Headers**:
|
||||
- WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
- X-Docker-Endpoints: registry.docker.io [, registry2.docker.io]
|
||||
|
||||
3. (Docker -> Registry) PUT /v1/images/98765432_parent/json
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
|
||||
4. (Registry->Index) GET /v1/repositories/foo/bar/images
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
**Action**::
|
||||
- Index:
|
||||
will invalidate the token.
|
||||
- Registry:
|
||||
grants a session (if token is approved) and fetches the images id
|
||||
|
||||
5. (Docker -> Registry) PUT /v1/images/98765432_parent/json
|
||||
**Headers**::
|
||||
- Authorization: Token signature=123abc,repository=”foo/bar”,access=write
|
||||
- Cookie: (Cookie provided by the Registry)
|
||||
|
||||
6. (Docker -> Registry) PUT /v1/images/98765432/json
|
||||
**Headers**:
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
7. (Docker -> Registry) PUT /v1/images/98765432_parent/layer
|
||||
**Headers**:
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
|
||||
8. (Docker -> Registry) PUT /v1/images/98765432/layer
|
||||
**Headers**:
|
||||
X-Docker-Checksum: sha256:436745873465fdjkhdfjkgh
|
||||
|
||||
9. (Docker -> Registry) PUT /v1/repositories/foo/bar/tags/latest
|
||||
**Headers**:
|
||||
Cookie: (Cookie provided by the Registry)
|
||||
**Body**:
|
||||
“98765432”
|
||||
|
||||
10. (Docker -> Index) PUT /v1/repositories/foo/bar/images
|
||||
|
||||
**Headers**:
|
||||
Authorization: Basic 123oislifjsldfj==
|
||||
X-Docker-Endpoints: registry1.docker.io (no validation on this right now)
|
||||
|
||||
**Body**:
|
||||
(The image, id’s, tags and checksums)
|
||||
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”,
|
||||
“checksum”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}]
|
||||
|
||||
**Return** HTTP 204
|
||||
|
||||
.. note::
|
||||
|
||||
If push fails and they need to start again, what happens in the index, there will already be a record for the namespace/name, but it will be initialized. Should we allow it, or mark as name already used? One edge case could be if someone pushes the same thing at the same time with two different shells.
|
||||
|
||||
If it's a retry on the Registry, Docker has a cookie (provided by the registry after token validation). So the Index won’t have to provide a new token.
|
||||
|
||||
2.3 Delete
|
||||
----------
|
||||
|
||||
If you need to delete something from the index or registry, we need a nice clean way to do that. Here is the workflow.
|
||||
|
||||
1. Docker contacts the index to request a delete of a repository “samalba/busybox” (authentication required with user credentials)
|
||||
2. If authentication works and repository is valid, “samalba/busybox” is marked as deleted and a temporary token is returned
|
||||
3. Send a delete request to the registry for the repository (along with the token)
|
||||
4. Registry A contacts the Index to verify the token (token must corresponds to the repository name)
|
||||
5. Index validates the token. Registry A deletes the repository and everything associated to it.
|
||||
6. docker contacts the index to let it know it was removed from the registry, the index removes all records from the database.
|
||||
|
||||
.. note::
|
||||
|
||||
The Docker client should present an "Are you sure?" prompt to confirm the deletion before starting the process. Once it starts it can't be undone.
|
||||
|
||||
API (deleting repository foo/bar):
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
1. (Docker -> Index) DELETE /v1/repositories/foo/bar/
|
||||
**Headers**:
|
||||
Authorization: Basic sdkjfskdjfhsdkjfh==
|
||||
X-Docker-Token: true
|
||||
|
||||
**Action**::
|
||||
- in index, we make sure it is a valid repository, and set to deleted (logically)
|
||||
|
||||
**Body**::
|
||||
Empty
|
||||
|
||||
2. (Index -> Docker) 202 Accepted
|
||||
**Headers**:
|
||||
- WWW-Authenticate: Token signature=123abc,repository=”foo/bar”,access=delete
|
||||
- X-Docker-Endpoints: registry.docker.io [, registry2.docker.io] # list of endpoints where this repo lives.
|
||||
|
||||
3. (Docker -> Registry) DELETE /v1/repositories/foo/bar/
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=delete
|
||||
|
||||
4. (Registry->Index) PUT /v1/repositories/foo/bar/auth
|
||||
**Headers**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=delete
|
||||
**Action**::
|
||||
- Index:
|
||||
will invalidate the token.
|
||||
- Registry:
|
||||
deletes the repository (if token is approved)
|
||||
|
||||
5. (Registry -> Docker) 200 OK
|
||||
200 If success
|
||||
403 if forbidden
|
||||
400 if bad request
|
||||
404 if repository isn't found
|
||||
|
||||
6. (Docker -> Index) DELETE /v1/repositories/foo/bar/
|
||||
|
||||
**Headers**:
|
||||
Authorization: Basic 123oislifjsldfj==
|
||||
X-Docker-Endpoints: registry-1.docker.io (no validation on this right now)
|
||||
|
||||
**Body**:
|
||||
Empty
|
||||
|
||||
**Return** HTTP 200
|
||||
|
||||
|
||||
3. How to use the Registry in standalone mode
|
||||
=============================================
|
||||
|
||||
The Index has two main purposes (along with its fancy social features):
|
||||
|
||||
- Resolve short names (to avoid passing absolute URLs all the time)
|
||||
- username/projectname -> \https://registry.docker.io/users/<username>/repositories/<projectname>/
|
||||
- team/projectname -> \https://registry.docker.io/team/<team>/repositories/<projectname>/
|
||||
- Authenticate a user as a repos owner (for a central referenced repository)
|
||||
|
||||
3.1 Without an Index
|
||||
--------------------
|
||||
Using the Registry without the Index can be useful to store the images on a private network without having to rely on an external entity controlled by dotCloud.
|
||||
|
||||
In this case, the registry will be launched in a special mode (--standalone? --no-index?). In this mode, the only thing which changes is that Registry will never contact the Index to verify a token. It will be the Registry owner responsibility to authenticate the user who pushes (or even pulls) an image using any mechanism (HTTP auth, IP based, etc...).
|
||||
|
||||
In this scenario, the Registry is responsible for the security in case of data corruption since the checksums are not delivered by a trusted entity.
|
||||
|
||||
As hinted previously, a standalone registry can also be implemented by any HTTP server handling GET/PUT requests (or even only GET requests if no write access is necessary).
|
||||
|
||||
3.2 With an Index
|
||||
-----------------
|
||||
|
||||
The Index data needed by the Registry are simple:
|
||||
- Serve the checksums
|
||||
- Provide and authorize a Token
|
||||
|
||||
In the scenario of a Registry running on a private network with the need of centralizing and authorizing, it’s easy to use a custom Index.
|
||||
|
||||
The only challenge will be to tell Docker to contact (and trust) this custom Index. Docker will be configurable at some point to use a specific Index, it’ll be the private entity responsibility (basically the organization who uses Docker in a private environment) to maintain the Index and the Docker’s configuration among its consumers.
|
||||
|
||||
4. The API
|
||||
==========
|
||||
|
||||
The first version of the api is available here: https://github.com/jpetazzo/docker/blob/acd51ecea8f5d3c02b00a08176171c59442df8b3/docs/images-repositories-push-pull.md
|
||||
|
||||
4.1 Images
|
||||
----------
|
||||
|
||||
The format returned in the images is not defined here (for layer and json), basically because Registry stores exactly the same kind of information as Docker uses to manage them.
|
||||
|
||||
The format of ancestry is a line-separated list of image ids, in age order. I.e. the image’s parent is on the last line, the parent of the parent on the next-to-last line, etc.; if the image has no parent, the file is empty.
|
||||
|
||||
GET /v1/images/<image_id>/layer
|
||||
PUT /v1/images/<image_id>/layer
|
||||
GET /v1/images/<image_id>/json
|
||||
PUT /v1/images/<image_id>/json
|
||||
GET /v1/images/<image_id>/ancestry
|
||||
PUT /v1/images/<image_id>/ancestry
|
||||
|
||||
4.2 Users
|
||||
---------
|
||||
|
||||
4.2.1 Create a user (Index)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
POST /v1/users
|
||||
|
||||
**Body**:
|
||||
{"email": "sam@dotcloud.com", "password": "toto42", "username": "foobar"'}
|
||||
|
||||
**Validation**:
|
||||
- **username** : min 4 character, max 30 characters, must match the regular expression [a-z0-9_].
|
||||
- **password**: min 5 characters
|
||||
|
||||
**Valid**: return HTTP 200
|
||||
|
||||
Errors: HTTP 400 (we should create error codes for possible errors)
|
||||
- invalid json
|
||||
- missing field
|
||||
- wrong format (username, password, email, etc)
|
||||
- forbidden name
|
||||
- name already exists
|
||||
|
||||
.. note::
|
||||
|
||||
A user account will be valid only if the email has been validated (a validation link is sent to the email address).
|
||||
|
||||
4.2.2 Update a user (Index)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
PUT /v1/users/<username>
|
||||
|
||||
**Body**:
|
||||
{"password": "toto"}
|
||||
|
||||
.. note::
|
||||
|
||||
We can also update email address, if they do, they will need to reverify their new email address.
|
||||
|
||||
4.2.3 Login (Index)
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
Does nothing else but asking for a user authentication. Can be used to validate credentials. HTTP Basic Auth for now, maybe change in future.
|
||||
|
||||
GET /v1/users
|
||||
|
||||
**Return**:
|
||||
- Valid: HTTP 200
|
||||
- Invalid login: HTTP 401
|
||||
- Account inactive: HTTP 403 Account is not Active
|
||||
|
||||
4.3 Tags (Registry)
|
||||
-------------------
|
||||
|
||||
The Registry does not know anything about users. Even though repositories are under usernames, it’s just a namespace for the registry. Allowing us to implement organizations or different namespaces per user later, without modifying the Registry’s API.
|
||||
|
||||
The following naming restrictions apply:
|
||||
|
||||
- Namespaces must match the same regular expression as usernames (See 4.2.1.)
|
||||
- Repository names must match the regular expression [a-zA-Z0-9-_.]
|
||||
|
||||
4.3.1 Get all tags
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
GET /v1/repositories/<namespace>/<repository_name>/tags
|
||||
|
||||
**Return**: HTTP 200
|
||||
{
|
||||
"latest": "9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f",
|
||||
“0.1.1”: “b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”
|
||||
}
|
||||
|
||||
4.3.2 Read the content of a tag (resolve the image id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
GET /v1/repositories/<namespace>/<repo_name>/tags/<tag>
|
||||
|
||||
**Return**:
|
||||
"9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f"
|
||||
|
||||
4.3.3 Delete a tag (registry)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
DELETE /v1/repositories/<namespace>/<repo_name>/tags/<tag>
|
||||
|
||||
4.4 Images (Index)
|
||||
------------------
|
||||
|
||||
For the Index to “resolve” the repository name to a Registry location, it uses the X-Docker-Endpoints header. In other terms, this requests always add a “X-Docker-Endpoints” to indicate the location of the registry which hosts this repository.
|
||||
|
||||
4.4.1 Get the images
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
GET /v1/repositories/<namespace>/<repo_name>/images
|
||||
|
||||
**Return**: HTTP 200
|
||||
[{“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, “checksum”: “md5:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”}]
|
||||
|
||||
|
||||
4.4.2 Add/update the images
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You always add images, you never remove them.
|
||||
|
||||
PUT /v1/repositories/<namespace>/<repo_name>/images
|
||||
|
||||
**Body**:
|
||||
[ {“id”: “9e89cc6f0bc3c38722009fe6857087b486531f9a779a0c17e3ed29dae8f12c4f”, “checksum”: “sha256:b486531f9a779a0c17e3ed29dae8f12c4f9e89cc6f0bc3c38722009fe6857087”} ]
|
||||
|
||||
**Return** 204
|
||||
|
||||
4.5 Repositories
|
||||
----------------
|
||||
|
||||
4.5.1 Remove a Repository (Registry)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
DELETE /v1/repositories/<namespace>/<repo_name>
|
||||
|
||||
Return 200 OK
|
||||
|
||||
4.5.2 Remove a Repository (Index)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
This starts the delete process. see 2.3 for more details.
|
||||
|
||||
DELETE /v1/repositories/<namespace>/<repo_name>
|
||||
|
||||
Return 202 OK
|
||||
|
||||
5. Chaining Registries
|
||||
======================
|
||||
|
||||
It’s possible to chain Registries server for several reasons:
|
||||
- Load balancing
|
||||
- Delegate the next request to another server
|
||||
|
||||
When a Registry is a reference for a repository, it should host the entire images chain in order to avoid breaking the chain during the download.
|
||||
|
||||
The Index and Registry use this mechanism to redirect on one or the other.
|
||||
|
||||
Example with an image download:
|
||||
On every request, a special header can be returned:
|
||||
|
||||
X-Docker-Endpoints: server1,server2
|
||||
|
||||
On the next request, the client will always pick a server from this list.
|
||||
|
||||
6. Authentication & Authorization
|
||||
=================================
|
||||
|
||||
6.1 On the Index
|
||||
-----------------
|
||||
|
||||
The Index supports both “Basic” and “Token” challenges. Usually when there is a “401 Unauthorized”, the Index replies this::
|
||||
|
||||
401 Unauthorized
|
||||
WWW-Authenticate: Basic realm="auth required",Token
|
||||
|
||||
You have 3 options:
|
||||
|
||||
1. Provide user credentials and ask for a token
|
||||
|
||||
**Header**:
|
||||
- Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
- X-Docker-Token: true
|
||||
|
||||
In this case, along with the 200 response, you’ll get a new token (if user auth is ok):
|
||||
If authorization isn't correct you get a 401 response.
|
||||
If account isn't active you will get a 403 response.
|
||||
|
||||
**Response**:
|
||||
- 200 OK
|
||||
- X-Docker-Token: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
2. Provide user credentials only
|
||||
|
||||
**Header**:
|
||||
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
|
||||
|
||||
3. Provide Token
|
||||
|
||||
**Header**:
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
6.2 On the Registry
|
||||
-------------------
|
||||
|
||||
The Registry only supports the Token challenge::
|
||||
|
||||
401 Unauthorized
|
||||
WWW-Authenticate: Token
|
||||
|
||||
The only way is to provide a token on “401 Unauthorized” responses::
|
||||
|
||||
Authorization: Token signature=123abc,repository=”foo/bar”,access=read
|
||||
|
||||
Usually, the Registry provides a Cookie when a Token verification succeeded. Every time the Registry passes a Cookie, you have to pass it back the same cookie.::
|
||||
|
||||
200 OK
|
||||
Set-Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=×tamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4="; Path=/; HttpOnly
|
||||
|
||||
Next request::
|
||||
|
||||
GET /(...)
|
||||
Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=×tamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4="
|
||||
|
||||
|
||||
7.0 Document Version
|
||||
---------------------
|
||||
|
||||
- 1.0 : May 6th 2013 : initial release
|
||||
- 1.1 : June 1st 2013 : Added Delete Repository and way to handle new source namespace.
|
|
@ -19,10 +19,15 @@ Examples
|
|||
|
||||
docker build .
|
||||
|
||||
This will take the local Dockerfile
|
||||
| This will read the Dockerfile from the current directory. It will also send any other files and directories found in the current directory to the docker daemon.
|
||||
| The contents of this directory would be used by ADD commands found within the Dockerfile.
|
||||
| This will send a lot of data to the docker daemon if the current directory contains a lot of data.
|
||||
| If the absolute path is provided instead of '.', only the files and directories required by the ADD commands from the Dockerfile will be added to the context and transferred to the docker daemon.
|
||||
|
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker build -
|
||||
|
||||
This will read a Dockerfile form Stdin without context
|
||||
| This will read a Dockerfile from Stdin without context. Due to the lack of a context, no contents of any local directory will be sent to the docker daemon.
|
||||
| ADD doesn't work when running in this mode due to the absence of the context, thus having no source files to copy to the container.
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
Contributing to Docker
|
||||
======================
|
||||
|
||||
Want to hack on Docker? Awesome! The repository includes `all the instructions you need to get started <https://github.com/dotcloud/docker/blob/master/CONTRIBUTING.md>`.
|
||||
Want to hack on Docker? Awesome! The repository includes `all the instructions you need to get started <https://github.com/dotcloud/docker/blob/master/CONTRIBUTING.md>`_.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,6 +5,10 @@ In the design and development of Docker we try to follow these principles:
|
|||
(Work in progress)
|
||||
|
||||
* Don't try to replace every tool. Instead, be an ingredient to improve them.
|
||||
* Less code is better.
|
||||
* Less components is better. Do you really need to add one more class?
|
||||
* 50 lines of straightforward, readable code is better than 10 lines of magic that nobody can understand.
|
||||
* Don't do later what you can do now. "//FIXME: refactor" is not acceptable in new code.
|
||||
* When hesitating between 2 options, choose the one that is easier to reverse.
|
||||
* No is temporary, Yes is forever. If you're not sure about a new feature, say no. You can change your mind later.
|
||||
* Containers must be portable to the greatest possible number of machines. Be suspicious of any change which makes machines less interchangeable.
|
||||
|
|
|
@ -86,3 +86,20 @@ Production-ready
|
|||
Docker is still alpha software, and not suited for production.
|
||||
We are working hard to get there, and we are confident that it will be possible within a few months.
|
||||
|
||||
|
||||
Advanced port redirections
|
||||
--------------------------
|
||||
|
||||
Docker currently supports 2 flavors of port redirection: STATIC->STATIC (eg. "redirect public port 80 to private port 80")
|
||||
and RANDOM->STATIC (eg. "redirect any public port to private port 80").
|
||||
|
||||
With these 2 flavors, docker can support the majority of backend programs out there. But some applications have more exotic
|
||||
requirements, generally to implement custom clustering techniques. These applications include Hadoop, MongoDB, Riak, RabbitMQ,
|
||||
Disco, and all programs relying on Erlang's OTP.
|
||||
|
||||
To support these applications, Docker needs to support more advanced redirection flavors, including:
|
||||
|
||||
* RANDOM->RANDOM
|
||||
* STATIC1->STATIC2
|
||||
|
||||
These flavors should be implemented without breaking existing semantics, if at all possible.
|
||||
|
|
|
@ -13,7 +13,7 @@ run apt-get update
|
|||
# Packages required to checkout, build and upload docker
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q s3cmd
|
||||
run DEBIAN_FRONTEND=noninteractive apt-get install -y -q curl
|
||||
run curl -s -o /go.tar.gz https://go.googlecode.com/files/go1.1.linux-amd64.tar.gz
|
||||
run curl -s -o /go.tar.gz https://go.googlecode.com/files/go1.1.1.linux-amd64.tar.gz
|
||||
run tar -C /usr/local -xzf /go.tar.gz
|
||||
run echo "export PATH=/usr/local/go/bin:$PATH" > /.bashrc
|
||||
run echo "export PATH=/usr/local/go/bin:$PATH" > /.bash_profile
|
||||
|
|
2
image.go
2
image.go
|
@ -126,6 +126,8 @@ func MountAUFS(ro []string, rw string, target string) error {
|
|||
}
|
||||
branches := fmt.Sprintf("br:%v:%v", rwBranch, roBranches)
|
||||
|
||||
branches += ",xino=/dev/shm/aufs.xino"
|
||||
|
||||
//if error, try to load aufs kernel module
|
||||
if err := mount("none", target, "aufs", 0, branches); err != nil {
|
||||
log.Printf("Kernel does not support AUFS, trying to load the AUFS module with modprobe...")
|
||||
|
|
46
network.go
46
network.go
|
@ -485,20 +485,38 @@ type Nat struct {
|
|||
|
||||
func parseNat(spec string) (*Nat, error) {
|
||||
var nat Nat
|
||||
// If spec starts with ':', external and internal ports must be the same.
|
||||
// This might fail if the requested external port is not available.
|
||||
var sameFrontend bool
|
||||
if spec[0] == ':' {
|
||||
sameFrontend = true
|
||||
spec = spec[1:]
|
||||
}
|
||||
port, err := strconv.ParseUint(spec, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nat.Backend = int(port)
|
||||
if sameFrontend {
|
||||
nat.Frontend = nat.Backend
|
||||
|
||||
if strings.Contains(spec, ":") {
|
||||
specParts := strings.Split(spec, ":")
|
||||
if len(specParts) != 2 {
|
||||
return nil, fmt.Errorf("Invalid port format.")
|
||||
}
|
||||
// If spec starts with ':', external and internal ports must be the same.
|
||||
// This might fail if the requested external port is not available.
|
||||
var sameFrontend bool
|
||||
if len(specParts[0]) == 0 {
|
||||
sameFrontend = true
|
||||
} else {
|
||||
front, err := strconv.ParseUint(specParts[0], 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nat.Frontend = int(front)
|
||||
}
|
||||
back, err := strconv.ParseUint(specParts[1], 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nat.Backend = int(back)
|
||||
if sameFrontend {
|
||||
nat.Frontend = nat.Backend
|
||||
}
|
||||
} else {
|
||||
port, err := strconv.ParseUint(spec, 10, 16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nat.Backend = int(port)
|
||||
}
|
||||
nat.Proto = "tcp"
|
||||
return &nat, nil
|
||||
|
|
|
@ -18,6 +18,32 @@ func TestIptables(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseNat(t *testing.T) {
|
||||
if nat, err := parseNat("4500"); err == nil {
|
||||
if nat.Frontend != 0 || nat.Backend != 4500 {
|
||||
t.Errorf("-p 4500 should produce 0->4500, got %d->%d", nat.Frontend, nat.Backend)
|
||||
}
|
||||
} else {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if nat, err := parseNat(":4501"); err == nil {
|
||||
if nat.Frontend != 4501 || nat.Backend != 4501 {
|
||||
t.Errorf("-p :4501 should produce 4501->4501, got %d->%d", nat.Frontend, nat.Backend)
|
||||
}
|
||||
} else {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if nat, err := parseNat("4502:4503"); err == nil {
|
||||
if nat.Frontend != 4502 || nat.Backend != 4503 {
|
||||
t.Errorf("-p 4502:4503 should produce 4502->4503, got %d->%d", nat.Frontend, nat.Backend)
|
||||
}
|
||||
} else {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPortAllocation(t *testing.T) {
|
||||
allocator, err := newPortAllocator()
|
||||
if err != nil {
|
||||
|
|
|
@ -478,10 +478,7 @@ type Registry struct {
|
|||
authConfig *auth.AuthConfig
|
||||
}
|
||||
|
||||
func NewRegistry(root string) *Registry {
|
||||
// If the auth file does not exist, keep going
|
||||
authConfig, _ := auth.LoadConfig(root)
|
||||
|
||||
func NewRegistry(root string, authConfig *auth.AuthConfig) *Registry {
|
||||
httpTransport := &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
|
|
|
@ -17,7 +17,7 @@ import (
|
|||
)
|
||||
|
||||
const unitTestImageName string = "docker-ut"
|
||||
|
||||
const unitTestImageId string = "e9aa60c60128cad1"
|
||||
const unitTestStoreBase string = "/var/lib/docker/unit-tests"
|
||||
|
||||
func nuke(runtime *Runtime) error {
|
||||
|
@ -68,7 +68,7 @@ func init() {
|
|||
runtime: runtime,
|
||||
}
|
||||
// Retrieve the Image
|
||||
if err := srv.ImagePull(unitTestImageName, "", "", os.Stdout, utils.NewStreamFormatter(false)); err != nil {
|
||||
if err := srv.ImagePull(unitTestImageName, "", "", os.Stdout, utils.NewStreamFormatter(false), nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
159
server.go
159
server.go
|
@ -1,6 +1,7 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/dotcloud/docker/auth"
|
||||
"github.com/dotcloud/docker/registry"
|
||||
|
@ -54,7 +55,7 @@ func (srv *Server) ContainerExport(name string, out io.Writer) error {
|
|||
|
||||
func (srv *Server) ImagesSearch(term string) ([]APISearch, error) {
|
||||
|
||||
results, err := registry.NewRegistry(srv.runtime.root).SearchRepositories(term)
|
||||
results, err := registry.NewRegistry(srv.runtime.root, nil).SearchRepositories(term)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -63,9 +64,6 @@ func (srv *Server) ImagesSearch(term string) ([]APISearch, error) {
|
|||
for _, repo := range results.Results {
|
||||
var out APISearch
|
||||
out.Description = repo["description"]
|
||||
if len(out.Description) > 45 {
|
||||
out.Description = utils.Trunc(out.Description, 42) + "..."
|
||||
}
|
||||
out.Name = repo["name"]
|
||||
outs = append(outs, out)
|
||||
}
|
||||
|
@ -330,8 +328,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 +356,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 +384,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
|
||||
}
|
||||
}
|
||||
|
@ -397,8 +395,8 @@ func (srv *Server) pullRepository(r *registry.Registry, out io.Writer, remote, a
|
|||
return nil
|
||||
}
|
||||
|
||||
func (srv *Server) ImagePull(name, tag, endpoint string, out io.Writer, sf *utils.StreamFormatter) error {
|
||||
r := registry.NewRegistry(srv.runtime.root)
|
||||
func (srv *Server) ImagePull(name, tag, endpoint string, out io.Writer, sf *utils.StreamFormatter, authConfig *auth.AuthConfig) error {
|
||||
r := registry.NewRegistry(srv.runtime.root, authConfig)
|
||||
out = utils.NewWriteFlusher(out)
|
||||
if endpoint != "" {
|
||||
if err := srv.pullImage(r, out, name, endpoint, nil, sf); err != nil {
|
||||
|
@ -406,8 +404,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 +491,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, nil)
|
||||
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, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -506,14 +514,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, repoData.Endpoints); err != nil {
|
||||
if _, err := r.PushImageJSONIndex(srvName, imgList, true, repoData.Endpoints); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
@ -579,10 +587,10 @@ func (srv *Server) pushImage(r *registry.Registry, out io.Writer, remote, imgId,
|
|||
return nil
|
||||
}
|
||||
|
||||
func (srv *Server) ImagePush(name, endpoint string, out io.Writer, sf *utils.StreamFormatter) error {
|
||||
func (srv *Server) ImagePush(name, endpoint string, out io.Writer, sf *utils.StreamFormatter, authConfig *auth.AuthConfig) error {
|
||||
out = utils.NewWriteFlusher(out)
|
||||
img, err := srv.runtime.graph.Get(name)
|
||||
r := registry.NewRegistry(srv.runtime.root)
|
||||
r := registry.NewRegistry(srv.runtime.root, authConfig)
|
||||
|
||||
if err != nil {
|
||||
out.Write(sf.FormatStatus("The push refers to a repository [%s] (len: %d)", name, len(srv.runtime.repositories.Repositories[name])))
|
||||
|
@ -710,17 +718,112 @@ func (srv *Server) ContainerDestroy(name string, removeVolume bool) error {
|
|||
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)
|
||||
var ErrImageReferenced = errors.New("Image referenced by a repository")
|
||||
|
||||
func (srv *Server) deleteImageAndChildren(id string, imgs *[]APIRmi) error {
|
||||
// If the image is referenced by a repo, do not delete
|
||||
if len(srv.runtime.repositories.ByID()[id]) != 0 {
|
||||
return ErrImageReferenced
|
||||
}
|
||||
if err := srv.runtime.graph.Delete(img.ID); err != nil {
|
||||
return fmt.Errorf("Error deleting image %s: %s", name, err.Error())
|
||||
|
||||
// If the image is not referenced but has children, go recursive
|
||||
referenced := false
|
||||
byParents, err := srv.runtime.graph.ByParent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, img := range byParents[id] {
|
||||
if err := srv.deleteImageAndChildren(img.ID, imgs); err != nil {
|
||||
if err != ErrImageReferenced {
|
||||
return err
|
||||
}
|
||||
referenced = true
|
||||
}
|
||||
}
|
||||
if referenced {
|
||||
return ErrImageReferenced
|
||||
}
|
||||
|
||||
// If the image is not referenced and has no children, remove it
|
||||
byParents, err = srv.runtime.graph.ByParent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(byParents[id]) == 0 {
|
||||
if err := srv.runtime.repositories.DeleteAll(id); err != nil {
|
||||
return err
|
||||
}
|
||||
err := srv.runtime.graph.Delete(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*imgs = append(*imgs, APIRmi{Deleted: utils.TruncateID(id)})
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *Server) deleteImageParents(img *Image, imgs *[]APIRmi) error {
|
||||
if img.Parent != "" {
|
||||
parent, err := srv.runtime.graph.Get(img.Parent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Remove all children images
|
||||
if err := srv.deleteImageAndChildren(img.Parent, imgs); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.deleteImageParents(parent, imgs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *Server) deleteImage(img *Image, repoName, tag string) (*[]APIRmi, error) {
|
||||
//Untag the current image
|
||||
var imgs []APIRmi
|
||||
tagDeleted, err := srv.runtime.repositories.Delete(repoName, tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tagDeleted {
|
||||
imgs = append(imgs, APIRmi{Untagged: img.ShortID()})
|
||||
}
|
||||
if len(srv.runtime.repositories.ByID()[img.ID]) == 0 {
|
||||
if err := srv.deleteImageAndChildren(img.ID, &imgs); err != nil {
|
||||
if err != ErrImageReferenced {
|
||||
return &imgs, err
|
||||
}
|
||||
} else if err := srv.deleteImageParents(img, &imgs); err != nil {
|
||||
if err != ErrImageReferenced {
|
||||
return &imgs, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return &imgs, nil
|
||||
}
|
||||
|
||||
func (srv *Server) ImageDelete(name string, autoPrune bool) (*[]APIRmi, error) {
|
||||
img, err := srv.runtime.repositories.LookupImage(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("No such image: %s", name)
|
||||
}
|
||||
if !autoPrune {
|
||||
if err := srv.runtime.graph.Delete(img.ID); err != nil {
|
||||
return nil, fmt.Errorf("Error deleting image %s: %s", name, err.Error())
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var tag string
|
||||
if strings.Contains(name, ":") {
|
||||
nameParts := strings.Split(name, ":")
|
||||
name = nameParts[0]
|
||||
tag = nameParts[1]
|
||||
}
|
||||
|
||||
return srv.deleteImage(img, name, tag)
|
||||
}
|
||||
|
||||
func (srv *Server) ImageGetCached(imgId string, config *Config) (*Image, error) {
|
||||
|
||||
// Retrieve all images
|
||||
|
@ -869,7 +972,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 +981,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
|
||||
}
|
||||
|
|
|
@ -4,6 +4,58 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestContainerTagImageDelete(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
|
||||
srv := &Server{runtime: runtime}
|
||||
|
||||
if err := srv.runtime.repositories.Set("utest", "tag1", unitTestImageName, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := srv.runtime.repositories.Set("utest/docker", "tag2", unitTestImageName, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
images, err := srv.Images(false, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(images) != 3 {
|
||||
t.Errorf("Excepted 3 images, %d found", len(images))
|
||||
}
|
||||
|
||||
if _, err := srv.ImageDelete("utest/docker:tag2", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
images, err = srv.Images(false, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(images) != 2 {
|
||||
t.Errorf("Excepted 2 images, %d found", len(images))
|
||||
}
|
||||
|
||||
if _, err := srv.ImageDelete("utest:tag1", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
images, err = srv.Images(false, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(images) != 1 {
|
||||
t.Errorf("Excepted 1 image, %d found", len(images))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRm(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
|
|
58
tags.go
58
tags.go
|
@ -110,6 +110,52 @@ func (store *TagStore) ImageName(id string) string {
|
|||
return utils.TruncateID(id)
|
||||
}
|
||||
|
||||
func (store *TagStore) DeleteAll(id string) error {
|
||||
names, exists := store.ByID()[id]
|
||||
if !exists || len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, name := range names {
|
||||
if strings.Contains(name, ":") {
|
||||
nameParts := strings.Split(name, ":")
|
||||
if _, err := store.Delete(nameParts[0], nameParts[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := store.Delete(name, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (store *TagStore) Delete(repoName, tag string) (bool, error) {
|
||||
deleted := false
|
||||
if err := store.Reload(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if r, exists := store.Repositories[repoName]; exists {
|
||||
if tag != "" {
|
||||
if _, exists2 := r[tag]; exists2 {
|
||||
delete(r, tag)
|
||||
if len(r) == 0 {
|
||||
delete(store.Repositories, repoName)
|
||||
}
|
||||
deleted = true
|
||||
} else {
|
||||
return false, fmt.Errorf("No such tag: %s:%s", repoName, tag)
|
||||
}
|
||||
} else {
|
||||
delete(store.Repositories, repoName)
|
||||
deleted = true
|
||||
}
|
||||
} else {
|
||||
fmt.Errorf("No such repository: %s", repoName)
|
||||
}
|
||||
return deleted, store.Save()
|
||||
}
|
||||
|
||||
func (store *TagStore) Set(repoName, tag, imageName string, force bool) error {
|
||||
img, err := store.LookupImage(imageName)
|
||||
if err != nil {
|
||||
|
@ -133,7 +179,7 @@ func (store *TagStore) Set(repoName, tag, imageName string, force bool) error {
|
|||
} else {
|
||||
repo = make(map[string]string)
|
||||
if old, exists := store.Repositories[repoName]; exists && !force {
|
||||
return fmt.Errorf("Tag %s:%s is already set to %s", repoName, tag, old)
|
||||
return fmt.Errorf("Conflict: Tag %s:%s is already set to %s", repoName, tag, old)
|
||||
}
|
||||
store.Repositories[repoName] = repo
|
||||
}
|
||||
|
@ -151,14 +197,20 @@ func (store *TagStore) Get(repoName string) (Repository, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (store *TagStore) GetImage(repoName, tag string) (*Image, error) {
|
||||
func (store *TagStore) GetImage(repoName, tagOrId string) (*Image, error) {
|
||||
repo, err := store.Get(repoName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if repo == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if revision, exists := repo[tag]; exists {
|
||||
//go through all the tags, to see if tag is in fact an ID
|
||||
for _, revision := range repo {
|
||||
if strings.HasPrefix(revision, tagOrId) {
|
||||
return store.graph.Get(revision)
|
||||
}
|
||||
}
|
||||
if revision, exists := repo[tagOrId]; exists {
|
||||
return store.graph.Get(revision)
|
||||
}
|
||||
return nil, nil
|
||||
|
|
49
tags_test.go
Normal file
49
tags_test.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package docker
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLookupImage(t *testing.T) {
|
||||
runtime, err := newTestRuntime()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer nuke(runtime)
|
||||
|
||||
if img, err := runtime.repositories.LookupImage(unitTestImageName); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if img == nil {
|
||||
t.Errorf("Expected 1 image, none found")
|
||||
}
|
||||
|
||||
if img, err := runtime.repositories.LookupImage(unitTestImageName + ":" + DEFAULTTAG); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if img == nil {
|
||||
t.Errorf("Expected 1 image, none found")
|
||||
}
|
||||
|
||||
if img, err := runtime.repositories.LookupImage(unitTestImageName + ":" + "fail"); err == nil {
|
||||
t.Errorf("Expected error, none found")
|
||||
} else if img != nil {
|
||||
t.Errorf("Expected 0 image, 1 found")
|
||||
}
|
||||
|
||||
if img, err := runtime.repositories.LookupImage("fail:fail"); err == nil {
|
||||
t.Errorf("Expected error, none found")
|
||||
} else if img != nil {
|
||||
t.Errorf("Expected 0 image, 1 found")
|
||||
}
|
||||
|
||||
if img, err := runtime.repositories.LookupImage(unitTestImageId); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if img == nil {
|
||||
t.Errorf("Expected 1 image, none found")
|
||||
}
|
||||
|
||||
if img, err := runtime.repositories.LookupImage(unitTestImageName + ":" + unitTestImageId); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if img == nil {
|
||||
t.Errorf("Expected 1 image, none found")
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue