Merge pull request #1796 from shin-/api_1_5

*Remote API: Bumped API version to 1.5
*Registry: Implement login with private registry
*Remote API: Improve port mapping information
This commit is contained in:
Victor Vieux 2013-09-09 16:58:54 -07:00
commit 2801624462
11 changed files with 1545 additions and 66 deletions

48
api.go
View file

@ -2,6 +2,7 @@ package docker
import (
"code.google.com/p/go.net/websocket"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/dotcloud/docker/auth"
@ -20,7 +21,7 @@ import (
"strings"
)
const APIVERSION = 1.4
const APIVERSION = 1.5
const DEFAULTHTTPHOST = "127.0.0.1"
const DEFAULTHTTPPORT = 4243
const DEFAULTUNIXSOCKET = "/var/run/docker.sock"
@ -326,8 +327,18 @@ func getContainersJSON(srv *Server, version float64, w http.ResponseWriter, r *h
n = -1
}
var b []byte
outs := srv.Containers(all, size, n, since, before)
b, err := json.Marshal(outs)
if version < 1.5 {
outs2 := []APIContainersOld{}
for _, ctnr := range outs {
outs2 = append(outs2, ctnr.ToLegacy())
}
b, err = json.Marshal(outs2)
} else {
b, err = json.Marshal(outs)
}
if err != nil {
return err
}
@ -394,6 +405,16 @@ func postImagesCreate(srv *Server, version float64, w http.ResponseWriter, r *ht
tag := r.Form.Get("tag")
repo := r.Form.Get("repo")
authEncoded := r.Header.Get("X-Registry-Auth")
authConfig := &auth.AuthConfig{}
if authEncoded != "" {
authJson := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
if err := json.NewDecoder(authJson).Decode(authConfig); err != nil {
// for a pull it is not an error if no auth was given
// to increase compatibility with the existing api it is defaulting to be empty
authConfig = &auth.AuthConfig{}
}
}
if version > 1.0 {
w.Header().Set("Content-Type", "application/json")
}
@ -405,7 +426,7 @@ func postImagesCreate(srv *Server, version float64, w http.ResponseWriter, r *ht
metaHeaders[k] = v
}
}
if err := srv.ImagePull(image, tag, w, sf, &auth.AuthConfig{}, metaHeaders, version > 1.3); err != nil {
if err := srv.ImagePull(image, tag, w, sf, authConfig, metaHeaders, version > 1.3); err != nil {
if sf.Used() {
w.Write(sf.FormatError(err))
return nil
@ -473,19 +494,32 @@ 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{}
metaHeaders := map[string][]string{}
for k, v := range r.Header {
if strings.HasPrefix(k, "X-Meta-") {
metaHeaders[k] = v
}
}
if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
return err
}
if err := parseForm(r); err != nil {
return err
}
authConfig := &auth.AuthConfig{}
authEncoded := r.Header.Get("X-Registry-Auth")
if authEncoded != "" {
// the new format is to handle the authConfig as a header
authJson := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
if err := json.NewDecoder(authJson).Decode(authConfig); err != nil {
// to increase compatibility to existing api it is defaulting to be empty
authConfig = &auth.AuthConfig{}
}
} else {
// the old format is supported for compatibility if there was no authConfig header
if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {
return err
}
}
if vars == nil {
return fmt.Errorf("Missing parameter")

View file

@ -1,5 +1,7 @@
package docker
import "encoding/json"
type APIHistory struct {
ID string `json:"Id"`
Tags []string `json:",omitempty"`
@ -42,6 +44,30 @@ type APIRmi struct {
}
type APIContainers struct {
ID string `json:"Id"`
Image string
Command string
Created int64
Status string
Ports []APIPort
SizeRw int64
SizeRootFs int64
}
func (self *APIContainers) ToLegacy() APIContainersOld {
return APIContainersOld{
ID: self.ID,
Image: self.Image,
Command: self.Command,
Created: self.Created,
Status: self.Status,
Ports: displayablePorts(self.Ports),
SizeRw: self.SizeRw,
SizeRootFs: self.SizeRootFs,
}
}
type APIContainersOld struct {
ID string `json:"Id"`
Image string
Command string
@ -67,7 +93,17 @@ type APIRun struct {
}
type APIPort struct {
Port string
PrivatePort int64
PublicPort int64
Type string
}
func (port *APIPort) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"PrivatePort": port.PrivatePort,
"PublicPort": port.PublicPort,
"Type": port.Type,
})
}
type APIVersion struct {

View file

@ -26,10 +26,11 @@ var (
)
type AuthConfig struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Auth string `json:"auth"`
Email string `json:"email"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Auth string `json:"auth"`
Email string `json:"email"`
ServerAddress string `json:"serveraddress,omitempty"`
}
type ConfigFile struct {
@ -96,6 +97,7 @@ func LoadConfig(rootPath string) (*ConfigFile, error) {
}
origEmail := strings.Split(arr[1], " = ")
authConfig.Email = origEmail[1]
authConfig.ServerAddress = IndexServerAddress()
configFile.Configs[IndexServerAddress()] = authConfig
} else {
for k, authConfig := range configFile.Configs {
@ -105,6 +107,7 @@ func LoadConfig(rootPath string) (*ConfigFile, error) {
}
authConfig.Auth = ""
configFile.Configs[k] = authConfig
authConfig.ServerAddress = k
}
}
return &configFile, nil
@ -125,7 +128,7 @@ func SaveConfig(configFile *ConfigFile) error {
authCopy.Auth = encodeAuth(&authCopy)
authCopy.Username = ""
authCopy.Password = ""
authCopy.ServerAddress = ""
configs[k] = authCopy
}
@ -146,14 +149,26 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e
reqStatusCode := 0
var status string
var reqBody []byte
jsonBody, err := json.Marshal(authConfig)
serverAddress := authConfig.ServerAddress
if serverAddress == "" {
serverAddress = IndexServerAddress()
}
loginAgainstOfficialIndex := serverAddress == IndexServerAddress()
// to avoid sending the server address to the server it should be removed before marshalled
authCopy := *authConfig
authCopy.ServerAddress = ""
jsonBody, err := json.Marshal(authCopy)
if err != nil {
return "", fmt.Errorf("Config Error: %s", err)
}
// using `bytes.NewReader(jsonBody)` here causes the server to respond with a 411 status.
b := strings.NewReader(string(jsonBody))
req1, err := http.Post(IndexServerAddress()+"users/", "application/json; charset=utf-8", b)
req1, err := http.Post(serverAddress+"users/", "application/json; charset=utf-8", b)
if err != nil {
return "", fmt.Errorf("Server Error: %s", err)
}
@ -165,14 +180,23 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e
}
if reqStatusCode == 201 {
status = "Account created. Please use the confirmation link we sent" +
" to your e-mail to activate it."
if loginAgainstOfficialIndex {
status = "Account created. Please use the confirmation link we sent" +
" to your e-mail to activate it."
} else {
status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it."
}
} else if reqStatusCode == 403 {
return "", fmt.Errorf("Login: Your account hasn't been activated. " +
"Please check your e-mail for a confirmation link.")
if loginAgainstOfficialIndex {
return "", fmt.Errorf("Login: Your account hasn't been activated. " +
"Please check your e-mail for a confirmation link.")
} else {
return "", fmt.Errorf("Login: Your account hasn't been activated. " +
"Please see the documentation of the registry " + serverAddress + " for instructions how to activate it.")
}
} else if reqStatusCode == 400 {
if string(reqBody) == "\"Username or email already exists\"" {
req, err := factory.NewRequest("GET", IndexServerAddress()+"users/", nil)
req, err := factory.NewRequest("GET", serverAddress+"users/", nil)
req.SetBasicAuth(authConfig.Username, authConfig.Password)
resp, err := client.Do(req)
if err != nil {
@ -199,3 +223,52 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e
}
return status, nil
}
// this method matches a auth configuration to a server address or a url
func (config *ConfigFile) ResolveAuthConfig(registry string) AuthConfig {
if registry == IndexServerAddress() || len(registry) == 0 {
// default to the index server
return config.Configs[IndexServerAddress()]
}
// if its not the index server there are three cases:
//
// 1. this is a full config url -> it should be used as is
// 2. it could be a full url, but with the wrong protocol
// 3. it can be the hostname optionally with a port
//
// as there is only one auth entry which is fully qualified we need to start
// parsing and matching
swapProtocoll := func(url string) string {
if strings.HasPrefix(url, "http:") {
return strings.Replace(url, "http:", "https:", 1)
}
if strings.HasPrefix(url, "https:") {
return strings.Replace(url, "https:", "http:", 1)
}
return url
}
resolveIgnoringProtocol := func(url string) AuthConfig {
if c, found := config.Configs[url]; found {
return c
}
registrySwappedProtocoll := swapProtocoll(url)
// now try to match with the different protocol
if c, found := config.Configs[registrySwappedProtocoll]; found {
return c
}
return AuthConfig{}
}
// match both protocols as it could also be a server name like httpfoo
if strings.HasPrefix(registry, "http:") || strings.HasPrefix(registry, "https:") {
return resolveIgnoringProtocol(registry)
}
url := "https://" + registry
if !strings.Contains(registry, "/") {
url = url + "/v1/"
}
return resolveIgnoringProtocol(url)
}

View file

@ -4,10 +4,12 @@ import (
"archive/tar"
"bufio"
"bytes"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"github.com/dotcloud/docker/auth"
"github.com/dotcloud/docker/registry"
"github.com/dotcloud/docker/term"
"github.com/dotcloud/docker/utils"
"io"
@ -21,6 +23,7 @@ import (
"path/filepath"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
"syscall"
@ -91,6 +94,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error {
{"login", "Register or Login to the docker registry server"},
{"logs", "Fetch the logs of a container"},
{"port", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT"},
{"top", "Lookup the running processes of a container"},
{"ps", "List containers"},
{"pull", "Pull an image or a repository from the docker registry server"},
{"push", "Push an image or a repository to the docker registry server"},
@ -102,7 +106,6 @@ func (cli *DockerCli) CmdHelp(args ...string) error {
{"start", "Start a stopped container"},
{"stop", "Stop a running container"},
{"tag", "Tag an image into a repository"},
{"top", "Lookup the running processes of a container"},
{"version", "Show the docker version information"},
{"wait", "Block until a container stops, then print its exit code"},
} {
@ -126,7 +129,7 @@ func (cli *DockerCli) CmdInsert(args ...string) error {
v.Set("url", cmd.Arg(1))
v.Set("path", cmd.Arg(2))
if err := cli.stream("POST", "/images/"+cmd.Arg(0)+"/insert?"+v.Encode(), nil, cli.out); err != nil {
if err := cli.stream("POST", "/images/"+cmd.Arg(0)+"/insert?"+v.Encode(), nil, cli.out, nil); err != nil {
return err
}
return nil
@ -187,10 +190,8 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
} else if utils.IsURL(cmd.Arg(0)) || utils.IsGIT(cmd.Arg(0)) {
isRemote = true
} else {
if fi, err := os.Stat(cmd.Arg(0)); err != nil {
if _, err := os.Stat(cmd.Arg(0)); err != nil {
return err
} else if !fi.IsDir() {
return fmt.Errorf("\"%s\" is not a path or URL. Please provide a path to a directory containing a Dockerfile.", cmd.Arg(0))
}
context, err = Tar(cmd.Arg(0), Uncompressed)
}
@ -254,7 +255,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
// 'docker login': login / register a user to registry service.
func (cli *DockerCli) CmdLogin(args ...string) error {
cmd := Subcmd("login", "[OPTIONS]", "Register or Login to the docker registry server")
cmd := Subcmd("login", "[OPTIONS] [SERVER]", "Register or Login to a docker registry server, if no server is specified \""+auth.IndexServerAddress()+"\" is the default.")
var username, password, email string
@ -262,10 +263,17 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
cmd.StringVar(&password, "p", "", "password")
cmd.StringVar(&email, "e", "", "email")
err := cmd.Parse(args)
if err != nil {
return nil
}
serverAddress := auth.IndexServerAddress()
if len(cmd.Args()) > 0 {
serverAddress, err = registry.ExpandAndVerifyRegistryUrl(cmd.Arg(0))
if err != nil {
return err
}
fmt.Fprintf(cli.out, "Login against server at %s\n", serverAddress)
}
promptDefault := func(prompt string, configDefault string) {
if configDefault == "" {
@ -298,19 +306,16 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
username = authconfig.Username
}
}
if username != authconfig.Username {
if password == "" {
oldState, _ := term.SaveState(cli.terminalFd)
fmt.Fprintf(cli.out, "Password: ")
term.DisableEcho(cli.terminalFd, oldState)
password = readInput(cli.in, cli.out)
fmt.Fprint(cli.out, "\n")
term.RestoreTerminal(cli.terminalFd, oldState)
if password == "" {
return fmt.Errorf("Error : Password Required")
}
@ -327,15 +332,15 @@ func (cli *DockerCli) CmdLogin(args ...string) error {
password = authconfig.Password
email = authconfig.Email
}
authconfig.Username = username
authconfig.Password = password
authconfig.Email = email
cli.configFile.Configs[auth.IndexServerAddress()] = authconfig
authconfig.ServerAddress = serverAddress
cli.configFile.Configs[serverAddress] = authconfig
body, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[auth.IndexServerAddress()])
body, statusCode, err := cli.call("POST", "/auth", cli.configFile.Configs[serverAddress])
if statusCode == 401 {
delete(cli.configFile.Configs, auth.IndexServerAddress())
delete(cli.configFile.Configs, serverAddress)
auth.SaveConfig(cli.configFile)
return err
}
@ -786,7 +791,7 @@ func (cli *DockerCli) CmdImport(args ...string) error {
v.Set("tag", tag)
v.Set("fromSrc", src)
err := cli.stream("POST", "/images/create?"+v.Encode(), cli.in, cli.out)
err := cli.stream("POST", "/images/create?"+v.Encode(), cli.in, cli.out, nil)
if err != nil {
return err
}
@ -807,6 +812,13 @@ func (cli *DockerCli) CmdPush(args ...string) error {
cli.LoadConfigFile()
// Resolve the Repository name from fqn to endpoint + name
endpoint, _, err := registry.ResolveRepositoryName(name)
if err != nil {
return err
}
// Resolve the Auth config relevant for this server
authConfig := cli.configFile.ResolveAuthConfig(endpoint)
// If we're not using a custom registry, we know the restrictions
// applied to repository names and can warn the user in advance.
// Custom repositories can have different rules, and we must also
@ -820,22 +832,28 @@ func (cli *DockerCli) CmdPush(args ...string) error {
}
v := url.Values{}
push := func() error {
buf, err := json.Marshal(cli.configFile.Configs[auth.IndexServerAddress()])
push := func(authConfig auth.AuthConfig) error {
buf, err := json.Marshal(authConfig)
if err != nil {
return err
}
registryAuthHeader := []string{
base64.URLEncoding.EncodeToString(buf),
}
return cli.stream("POST", "/images/"+name+"/push?"+v.Encode(), bytes.NewBuffer(buf), cli.out)
return cli.stream("POST", "/images/"+name+"/push?"+v.Encode(), nil, cli.out, map[string][]string{
"X-Registry-Auth": registryAuthHeader,
})
}
if err := push(); err != nil {
if err.Error() == "Authentication is required." {
if err := push(authConfig); err != nil {
if err.Error() == registry.ErrLoginRequired.Error() {
fmt.Fprintln(cli.out, "\nPlease login prior to push:")
if err := cli.CmdLogin(""); err != nil {
if err := cli.CmdLogin(endpoint); err != nil {
return err
}
return push()
authConfig := cli.configFile.ResolveAuthConfig(endpoint)
return push(authConfig)
}
return err
}
@ -859,11 +877,43 @@ func (cli *DockerCli) CmdPull(args ...string) error {
*tag = parsedTag
}
// Resolve the Repository name from fqn to endpoint + name
endpoint, _, err := registry.ResolveRepositoryName(remote)
if err != nil {
return err
}
cli.LoadConfigFile()
// Resolve the Auth config relevant for this server
authConfig := cli.configFile.ResolveAuthConfig(endpoint)
v := url.Values{}
v.Set("fromImage", remote)
v.Set("tag", *tag)
if err := cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.out); err != nil {
pull := func(authConfig auth.AuthConfig) error {
buf, err := json.Marshal(authConfig)
if err != nil {
return err
}
registryAuthHeader := []string{
base64.URLEncoding.EncodeToString(buf),
}
return cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.out, map[string][]string{
"X-Registry-Auth": registryAuthHeader,
})
}
if err := pull(authConfig); err != nil {
if err.Error() == registry.ErrLoginRequired.Error() {
fmt.Fprintln(cli.out, "\nPlease login prior to push:")
if err := cli.CmdLogin(endpoint); err != nil {
return err
}
authConfig := cli.configFile.ResolveAuthConfig(endpoint)
return pull(authConfig)
}
return err
}
@ -953,6 +1003,19 @@ func (cli *DockerCli) CmdImages(args ...string) error {
return nil
}
func displayablePorts(ports []APIPort) string {
result := []string{}
for _, port := range ports {
if port.Type == "tcp" {
result = append(result, fmt.Sprintf("%d->%d", port.PublicPort, port.PrivatePort))
} else {
result = append(result, fmt.Sprintf("%d->%d/%s", port.PublicPort, port.PrivatePort, port.Type))
}
}
sort.Strings(result)
return strings.Join(result, ", ")
}
func (cli *DockerCli) CmdPs(args ...string) error {
cmd := Subcmd("ps", "[OPTIONS]", "List containers")
quiet := cmd.Bool("q", false, "Only display numeric IDs")
@ -1010,9 +1073,9 @@ func (cli *DockerCli) CmdPs(args ...string) error {
for _, out := range outs {
if !*quiet {
if *noTrunc {
fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t", out.ID, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, out.Ports)
fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t", out.ID, out.Image, out.Command, utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports))
} else {
fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t", utils.TruncateID(out.ID), out.Image, utils.Trunc(out.Command, 20), utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, out.Ports)
fmt.Fprintf(w, "%s\t%s\t%s\t%s ago\t%s\t%s\t", utils.TruncateID(out.ID), out.Image, utils.Trunc(out.Command, 20), utils.HumanDuration(time.Now().Sub(time.Unix(out.Created, 0))), out.Status, displayablePorts(out.Ports))
}
if *size {
if out.SizeRootFs > 0 {
@ -1097,7 +1160,7 @@ func (cli *DockerCli) CmdEvents(args ...string) error {
v.Set("since", *since)
}
if err := cli.stream("GET", "/events?"+v.Encode(), nil, cli.out); err != nil {
if err := cli.stream("GET", "/events?"+v.Encode(), nil, cli.out, nil); err != nil {
return err
}
return nil
@ -1114,7 +1177,7 @@ func (cli *DockerCli) CmdExport(args ...string) error {
return nil
}
if err := cli.stream("GET", "/containers/"+cmd.Arg(0)+"/export", nil, cli.out); err != nil {
if err := cli.stream("GET", "/containers/"+cmd.Arg(0)+"/export", nil, cli.out, nil); err != nil {
return err
}
return nil
@ -1386,7 +1449,30 @@ func (cli *DockerCli) CmdRun(args ...string) error {
repos, tag := utils.ParseRepositoryTag(config.Image)
v.Set("fromImage", repos)
v.Set("tag", tag)
err = cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.err)
// Resolve the Repository name from fqn to endpoint + name
var endpoint string
endpoint, _, err = registry.ResolveRepositoryName(repos)
if err != nil {
return err
}
// Load the auth config file, to be able to pull the image
cli.LoadConfigFile()
// Resolve the Auth config relevant for this server
authConfig := cli.configFile.ResolveAuthConfig(endpoint)
buf, err := json.Marshal(authConfig)
if err != nil {
return err
}
registryAuthHeader := []string{
base64.URLEncoding.EncodeToString(buf),
}
err = cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.err, map[string][]string{
"X-Registry-Auth": registryAuthHeader,
})
if err != nil {
return err
}
@ -1573,7 +1659,7 @@ func (cli *DockerCli) call(method, path string, data interface{}) ([]byte, int,
return body, resp.StatusCode, nil
}
func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) error {
func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, headers map[string][]string) error {
if (method == "POST" || method == "PUT") && in == nil {
in = bytes.NewReader([]byte{})
}
@ -1586,6 +1672,13 @@ func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer) e
if method == "POST" {
req.Header.Set("Content-Type", "plain/text")
}
if headers != nil {
for k, v := range headers {
req.Header[k] = v
}
}
dial, err := net.Dial(cli.proto, cli.addr)
if err != nil {
if strings.Contains(err.Error(), "connection refused") {

View file

@ -15,7 +15,6 @@ import (
"os/exec"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"syscall"
@ -253,17 +252,28 @@ type NetworkSettings struct {
PortMapping map[string]PortMapping
}
// String returns a human-readable description of the port mapping defined in the settings
func (settings *NetworkSettings) PortMappingHuman() string {
var mapping []string
// returns a more easy to process description of the port mapping defined in the settings
func (settings *NetworkSettings) PortMappingAPI() []APIPort {
var mapping []APIPort
for private, public := range settings.PortMapping["Tcp"] {
mapping = append(mapping, fmt.Sprintf("%s->%s", public, private))
pubint, _ := strconv.ParseInt(public, 0, 0)
privint, _ := strconv.ParseInt(private, 0, 0)
mapping = append(mapping, APIPort{
PrivatePort: privint,
PublicPort: pubint,
Type: "tcp",
})
}
for private, public := range settings.PortMapping["Udp"] {
mapping = append(mapping, fmt.Sprintf("%s->%s/udp", public, private))
pubint, _ := strconv.ParseInt(public, 0, 0)
privint, _ := strconv.ParseInt(private, 0, 0)
mapping = append(mapping, APIPort{
PrivatePort: privint,
PublicPort: pubint,
Type: "udp",
})
}
sort.Strings(mapping)
return strings.Join(mapping, ", ")
return mapping
}
// Inject the io.Reader at the given path. Note: do not close the reader

View file

@ -27,14 +27,36 @@ Docker Remote API
2. Versions
===========
The current version of the API is 1.4
The current version of the API is 1.5
Calling /images/<name>/insert is the same as calling
/v1.4/images/<name>/insert
/v1.5/images/<name>/insert
You can still call an old version of the api using
/v1.0/images/<name>/insert
:doc:`docker_remote_api_v1.5`
*****************************
What's new
----------
.. http:post:: /images/create
**New!** You can now pass registry credentials (via an AuthConfig object)
through the `X-Registry-Auth` header
.. http:post:: /images/(name)/push
**New!** The AuthConfig object now needs to be passed through
the `X-Registry-Auth` header
.. http:get:: /containers/json
**New!** The format of the `Ports` entry has been changed to a list of
dicts each containing `PublicPort`, `PrivatePort` and `Type` describing a
port mapping.
:doc:`docker_remote_api_v1.4`
*****************************

View file

@ -993,7 +993,8 @@ Check auth configuration
{
"username":"hannibal",
"password:"xxxx",
"email":"hannibal@a-team.com"
"email":"hannibal@a-team.com",
"serveraddress":"https://index.docker.io/v1/"
}
**Example response**:

File diff suppressed because it is too large Load diff

View file

@ -8,10 +8,17 @@
::
Usage: docker login [OPTIONS]
Usage: docker login [OPTIONS] [SERVER]
Register or Login to the docker registry server
-e="": email
-p="": password
-u="": username
If you want to login to a private registry you can
specify this by adding the server name.
example:
docker login localhost:8080

View file

@ -22,6 +22,7 @@ import (
var (
ErrAlreadyExists = errors.New("Image already exists")
ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
ErrLoginRequired = errors.New("Authentication is required.")
)
func pingRegistryEndpoint(endpoint string) error {
@ -102,17 +103,38 @@ func ResolveRepositoryName(reposName string) (string, string, error) {
if err := validateRepositoryName(reposName); err != nil {
return "", "", err
}
endpoint, err := ExpandAndVerifyRegistryUrl(hostname)
if err != nil {
return "", "", err
}
return endpoint, reposName, err
}
// this method expands the registry name as used in the prefix of a repo
// to a full url. if it already is a url, there will be no change.
// The registry is pinged to test if it http or https
func ExpandAndVerifyRegistryUrl(hostname string) (string, error) {
if strings.HasPrefix(hostname, "http:") || strings.HasPrefix(hostname, "https:") {
// if there is no slash after https:// (8 characters) then we have no path in the url
if strings.LastIndex(hostname, "/") < 9 {
// there is no path given. Expand with default path
hostname = hostname + "/v1/"
}
if err := pingRegistryEndpoint(hostname); err != nil {
return "", errors.New("Invalid Registry endpoint: " + err.Error())
}
return hostname, nil
}
endpoint := fmt.Sprintf("https://%s/v1/", hostname)
if err := pingRegistryEndpoint(endpoint); err != nil {
utils.Debugf("Registry %s does not work (%s), falling back to http", endpoint, err)
endpoint = fmt.Sprintf("http://%s/v1/", hostname)
if err = pingRegistryEndpoint(endpoint); err != nil {
//TODO: triggering highland build can be done there without "failing"
return "", "", errors.New("Invalid Registry endpoint: " + err.Error())
return "", errors.New("Invalid Registry endpoint: " + err.Error())
}
}
err := validateRepositoryName(reposName)
return endpoint, reposName, err
return endpoint, nil
}
func doWithCookies(c *http.Client, req *http.Request) (*http.Response, error) {
@ -139,6 +161,9 @@ func (r *Registry) GetRemoteHistory(imgID, registry string, token []string) ([]s
req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
res, err := doWithCookies(r.client, req)
if err != nil || res.StatusCode != 200 {
if res.StatusCode == 401 {
return nil, ErrLoginRequired
}
if res != nil {
return nil, utils.NewHTTPRequestError(fmt.Sprintf("Internal server error: %d trying to fetch remote history for %s", res.StatusCode, imgID), res)
}
@ -282,7 +307,7 @@ func (r *Registry) GetRepositoryData(indexEp, remote string) (*RepositoryData, e
}
defer res.Body.Close()
if res.StatusCode == 401 {
return nil, utils.NewHTTPRequestError(fmt.Sprintf("Please login first (HTTP code %d)", res.StatusCode), res)
return nil, ErrLoginRequired
}
// TODO: Right now we're ignoring checksums in the response body.
// In the future, we need to use them to check image validity.

View file

@ -387,7 +387,7 @@ func (srv *Server) Containers(all, size bool, n int, since, before string) []API
c.Command = fmt.Sprintf("%s %s", container.Path, strings.Join(container.Args, " "))
c.Created = container.Created.Unix()
c.Status = container.State.String()
c.Ports = container.NetworkSettings.PortMappingHuman()
c.Ports = container.NetworkSettings.PortMappingAPI()
if size {
c.SizeRw, c.SizeRootFs = container.GetSize()
}
@ -656,6 +656,9 @@ func (srv *Server) ImagePull(localName string, tag string, out io.Writer, sf *ut
out = utils.NewWriteFlusher(out)
err = srv.pullRepository(r, out, localName, remoteName, tag, endpoint, sf, parallel)
if err == registry.ErrLoginRequired {
return err
}
if err != nil {
if err := srv.pullImage(r, out, remoteName, endpoint, nil, sf); err != nil {
return err