diff --git a/api/handlers.go b/api/handlers.go index e31ee3967a..6dcb38edbc 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -427,7 +427,15 @@ func postContainersCreate(c *context, w http.ResponseWriter, r *http.Request) { return } - container, err := c.cluster.CreateContainer(cluster.BuildContainerConfig(config), name) + // Pass auth information along if present + var authConfig *dockerclient.AuthConfig + buf, err := base64.URLEncoding.DecodeString(r.Header.Get("X-Registry-Auth")) + if err == nil { + authConfig = &dockerclient.AuthConfig{} + json.Unmarshal(buf, authConfig) + } + + container, err := c.cluster.CreateContainer(cluster.BuildContainerConfig(config), name, authConfig) if err != nil { if strings.HasPrefix(err.Error(), "Conflict") { httpError(w, err.Error(), http.StatusConflict) diff --git a/cluster/cluster.go b/cluster/cluster.go index 2b394f7bd9..48df473024 100644 --- a/cluster/cluster.go +++ b/cluster/cluster.go @@ -9,7 +9,7 @@ import ( // Cluster is exported type Cluster interface { // Create a container - CreateContainer(config *ContainerConfig, name string) (*Container, error) + CreateContainer(config *ContainerConfig, name string, authConfig *dockerclient.AuthConfig) (*Container, error) // Remove a container RemoveContainer(container *Container, force, volumes bool) error diff --git a/cluster/engine.go b/cluster/engine.go index 623c5eabcb..45ed7bf4ac 100644 --- a/cluster/engine.go +++ b/cluster/engine.go @@ -503,7 +503,7 @@ func (e *Engine) TotalCpus() int64 { } // Create a new container -func (e *Engine) Create(config *ContainerConfig, name string, pullImage bool) (*Container, error) { +func (e *Engine) Create(config *ContainerConfig, name string, pullImage bool, authConfig *dockerclient.AuthConfig) (*Container, error) { var ( err error id string @@ -521,17 +521,17 @@ func (e *Engine) Create(config *ContainerConfig, name string, pullImage bool) (* dockerConfig.CpuShares = int64(math.Ceil(float64(config.CpuShares*1024) / float64(e.Cpus))) dockerConfig.HostConfig.CpuShares = dockerConfig.CpuShares - if id, err = client.CreateContainer(&dockerConfig, name); err != nil { + if id, err = client.CreateContainer(&dockerConfig, name, nil); err != nil { // If the error is other than not found, abort immediately. if err != dockerclient.ErrImageNotFound || !pullImage { return nil, err } // Otherwise, try to pull the image... - if err = e.Pull(config.Image, nil); err != nil { + if err = e.Pull(config.Image, authConfig); err != nil { return nil, err } // ...And try again. - if id, err = client.CreateContainer(&dockerConfig, name); err != nil { + if id, err = client.CreateContainer(&dockerConfig, name, nil); err != nil { return nil, err } } diff --git a/cluster/engine_test.go b/cluster/engine_test.go index 1af7145032..730acc78b2 100644 --- a/cluster/engine_test.go +++ b/cluster/engine_test.go @@ -193,13 +193,14 @@ func TestCreateContainer(t *testing.T) { // Everything is ok name := "test1" id := "id1" - client.On("CreateContainer", &mockConfig, name).Return(id, nil).Once() + var auth *dockerclient.AuthConfig + client.On("CreateContainer", &mockConfig, name, auth).Return(id, nil).Once() client.On("ListContainers", true, false, fmt.Sprintf(`{"id":[%q]}`, id)).Return([]dockerclient.Container{{Id: id}}, nil).Once() client.On("ListImages", mock.Anything).Return([]*dockerclient.Image{}, nil).Once() client.On("ListVolumes", mock.Anything).Return([]*dockerclient.Volume{}, nil) client.On("ListNetworks", mock.Anything).Return([]*dockerclient.NetworkResource{}, nil) client.On("InspectContainer", id).Return(&dockerclient.ContainerInfo{Config: &config.ContainerConfig}, nil).Once() - container, err := engine.Create(config, name, false) + container, err := engine.Create(config, name, false, auth) assert.Nil(t, err) assert.Equal(t, container.Id, id) assert.Len(t, engine.Containers(), 1) @@ -207,8 +208,8 @@ func TestCreateContainer(t *testing.T) { // Image not found, pullImage == false name = "test2" mockConfig.CpuShares = int64(math.Ceil(float64(config.CpuShares*1024) / float64(mockInfo.NCPU))) - client.On("CreateContainer", &mockConfig, name).Return("", dockerclient.ErrImageNotFound).Once() - container, err = engine.Create(config, name, false) + client.On("CreateContainer", &mockConfig, name, auth).Return("", dockerclient.ErrImageNotFound).Once() + container, err = engine.Create(config, name, false, auth) assert.Equal(t, err, dockerclient.ErrImageNotFound) assert.Nil(t, container) @@ -217,14 +218,14 @@ func TestCreateContainer(t *testing.T) { id = "id3" mockConfig.CpuShares = int64(math.Ceil(float64(config.CpuShares*1024) / float64(mockInfo.NCPU))) client.On("PullImage", config.Image+":latest", mock.Anything).Return(nil).Once() - client.On("CreateContainer", &mockConfig, name).Return("", dockerclient.ErrImageNotFound).Once() - client.On("CreateContainer", &mockConfig, name).Return(id, nil).Once() + client.On("CreateContainer", &mockConfig, name, auth).Return("", dockerclient.ErrImageNotFound).Once() + client.On("CreateContainer", &mockConfig, name, auth).Return(id, nil).Once() client.On("ListContainers", true, false, fmt.Sprintf(`{"id":[%q]}`, id)).Return([]dockerclient.Container{{Id: id}}, nil).Once() client.On("ListImages", mock.Anything).Return([]*dockerclient.Image{}, nil).Once() client.On("ListVolumes", mock.Anything).Return([]*dockerclient.Volume{}, nil) client.On("ListNetworks", mock.Anything).Return([]*dockerclient.NetworkResource{}, nil) client.On("InspectContainer", id).Return(&dockerclient.ContainerInfo{Config: &config.ContainerConfig}, nil).Once() - container, err = engine.Create(config, name, true) + container, err = engine.Create(config, name, true, auth) assert.Nil(t, err) assert.Equal(t, container.Id, id) assert.Len(t, engine.Containers(), 2) diff --git a/cluster/mesos/cluster.go b/cluster/mesos/cluster.go index a8d9f38dda..8bbce91117 100644 --- a/cluster/mesos/cluster.go +++ b/cluster/mesos/cluster.go @@ -164,7 +164,7 @@ func (c *Cluster) RegisterEventHandler(h cluster.EventHandler) error { } // CreateContainer for container creation in Mesos task -func (c *Cluster) CreateContainer(config *cluster.ContainerConfig, name string) (*cluster.Container, error) { +func (c *Cluster) CreateContainer(config *cluster.ContainerConfig, name string, authConfig *dockerclient.AuthConfig) (*cluster.Container, error) { if config.Memory == 0 && config.CpuShares == 0 { return nil, errResourcesNeeded } diff --git a/cluster/swarm/cluster.go b/cluster/swarm/cluster.go index 7a4c4cfa4d..461d4a2833 100644 --- a/cluster/swarm/cluster.go +++ b/cluster/swarm/cluster.go @@ -115,8 +115,8 @@ func (c *Cluster) generateUniqueID() string { } // CreateContainer aka schedule a brand new container into the cluster. -func (c *Cluster) CreateContainer(config *cluster.ContainerConfig, name string) (*cluster.Container, error) { - container, err := c.createContainer(config, name, false) +func (c *Cluster) CreateContainer(config *cluster.ContainerConfig, name string, authConfig *dockerclient.AuthConfig) (*cluster.Container, error) { + container, err := c.createContainer(config, name, false, authConfig) // fails with image not found, then try to reschedule with soft-image-affinity if err != nil { @@ -125,14 +125,14 @@ func (c *Cluster) CreateContainer(config *cluster.ContainerConfig, name string) // Check if the image exists in the cluster // If exists, retry with a soft-image-affinity if image := c.Image(config.Image); image != nil { - container, err = c.createContainer(config, name, true) + container, err = c.createContainer(config, name, true, authConfig) } } } return container, err } -func (c *Cluster) createContainer(config *cluster.ContainerConfig, name string, withSoftImageAffinity bool) (*cluster.Container, error) { +func (c *Cluster) createContainer(config *cluster.ContainerConfig, name string, withSoftImageAffinity bool, authConfig *dockerclient.AuthConfig) (*cluster.Container, error) { c.scheduler.Lock() // Ensure the name is available @@ -170,7 +170,7 @@ func (c *Cluster) createContainer(config *cluster.ContainerConfig, name string, c.scheduler.Unlock() - container, err := engine.Create(config, name, true) + container, err := engine.Create(config, name, true, authConfig) c.scheduler.Lock() delete(c.pendingContainers, swarmID) diff --git a/docs/api/swarm-api.md b/docs/api/swarm-api.md index 4c54e5908e..dcfa98cb8f 100644 --- a/docs/api/swarm-api.md +++ b/docs/api/swarm-api.md @@ -45,6 +45,49 @@ POST "/images/create" : "docker import" flow not implement * `POST "/containers/create"`: `CpuShares` in `HostConfig` sets the number of CPU cores allocated to the container. +# Registry Authentication + +During container create calls, the swarm API will optionally accept a X-Registry-Config header. +If provided, this header will be passed down to the engine if the image must be pulled +to complete the create operation. + +The following two examples demonstrate how to utilize this using the existing docker CLI + +* CLI usage example using username/password: + + ```bash +# Calculate the header +REPO_USER=yourusername +read -s PASSWORD +HEADER=$(echo "{\"username\":\"${REPO_USER}\",\"password\":\"${PASSWORD}\"}"|base64 -w 0 ) +unset PASSWORD +echo HEADER=$HEADER + +# Then add the following to your ~/.docker/config.json +"HttpHeaders": { + "X-Registry-Auth": "
" +} + +# Now run a private image against swarm: +docker run --rm -it yourprivateimage:latest +``` + +* CLI usage example using registry tokens: (Requires engine 1.10 with new auth token support) + + ```bash +REPO=yourrepo/yourimage +REPO_USER=yourusername +read -s PASSWORD +AUTH_URL=https://auth.docker.io/token +TOKEN=$(curl -s -u "${REPO_USER}:${PASSWORD}" "${AUTH_URL}?scope=repository:${REPO}:pull&service=registry.docker.io" | + jq -r ".token") +HEADER=$(echo "{\"registrytoken\":\"${TOKEN}\"}"|base64 -w 0 ) +echo HEADER=$HEADER + +# Update the docker config as above, but the token will expire quickly... +``` + + ## Docker Swarm documentation index - [User guide](https://docs.docker.com/swarm/)