Merge branch 'master' into postupload-endpoints-header

Conflicts:
	server.go
This commit is contained in:
Guillaume J. Charmes 2013-06-14 11:50:58 -07:00
commit 78e4a385f7
40 changed files with 5486 additions and 1746 deletions

View file

@ -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
View 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
View file

@ -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.

View file

@ -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
View file

@ -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)
}

View file

@ -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

View file

@ -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

View file

@ -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
}
}

View file

@ -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

View file

@ -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")
}
}
}

View file

@ -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
}

View file

@ -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()

View file

@ -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
}

View file

@ -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):

View 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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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

View 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

View file

@ -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

View file

@ -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.
Its 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 wont 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::
**Its 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, ids, 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 wont 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, its 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, itll be the private entity responsibility (basically the organization who uses Docker in a private environment) to maintain the Index and the Dockers 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 images 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, its just a namespace for the registry. Allowing us to implement organizations or different namespaces per user later, without modifying the Registrys 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
======================
Its 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, youll 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=&timestamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4="; Path=/; HttpOnly
Next request::
GET /(...)
Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=&timestamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4="
TODO: add more info.

View 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 doesnt 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 dotClouds 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 dont 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
Its 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 wont 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::
**Its 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, ids, 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 wont 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, its 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, itll be the private entity responsibility (basically the organization who uses Docker in a private environment) to maintain the Index and the Dockers 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 images 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, its just a namespace for the registry. Allowing us to implement organizations or different namespaces per user later, without modifying the Registrys 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
======================
Its 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, youll 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=&timestamp=RjEzNjYzMTQ5NDcuNDc0NjQzCi4="; Path=/; HttpOnly
Next request::
GET /(...)
Cookie: session="wD/J7LqL5ctqw8haL10vgfhrb2Q=?foo=UydiYXInCnAxCi4=&timestamp=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.

View file

@ -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.

View file

@ -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>`_.

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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...")

View file

@ -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

View file

@ -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 {

View file

@ -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,

View file

@ -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
View file

@ -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
}

View file

@ -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
View file

@ -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
View 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")
}
}