diff --git a/pkg/kubelet/dockertools/config.go b/pkg/kubelet/dockertools/config.go new file mode 100644 index 00000000000..33efce30ffa --- /dev/null +++ b/pkg/kubelet/dockertools/config.go @@ -0,0 +1,116 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dockertools + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/url" + "strings" + + "github.com/fsouza/go-dockerclient" + "github.com/golang/glog" +) + +const ( + dockerConfigFileLocation = ".dockercfg" +) + +func readDockerConfigFile() (cfg dockerConfig, err error) { + contents, err := ioutil.ReadFile(dockerConfigFileLocation) + err = json.Unmarshal(contents, &cfg) + return +} + +// dockerConfig represents the config file used by the docker CLI. +// This config that represents the credentials that should be used +// when pulling images from specific image repositories. +type dockerConfig map[string]dockerConfigEntry + +func (dc dockerConfig) addToKeyring(dk *dockerKeyring) { + for loc, ident := range dc { + creds := docker.AuthConfiguration{ + Username: ident.Username, + Password: ident.Password, + Email: ident.Email, + } + + parsed, err := url.Parse(loc) + if err != nil { + glog.Errorf("Entry %q in dockercfg invalid (%v), ignoring", loc, err) + continue + } + + dk.add(parsed.Host+parsed.Path, creds) + } +} + +type dockerConfigEntry struct { + Username string + Password string + Email string +} + +// dockerConfigEntryWithAuth is used solely for deserializing the Auth field +// into a dockerConfigEntry during JSON deserialization. +type dockerConfigEntryWithAuth struct { + Username string + Password string + Email string + Auth string +} + +func (ident *dockerConfigEntry) UnmarshalJSON(data []byte) error { + var tmp dockerConfigEntryWithAuth + err := json.Unmarshal(data, &tmp) + if err != nil { + return err + } + + ident.Username = tmp.Username + ident.Password = tmp.Password + ident.Email = tmp.Email + + if len(tmp.Auth) == 0 { + return nil + } + + ident.Username, ident.Password, err = decodeDockerConfigFieldAuth(tmp.Auth) + return err +} + +// decodeDockerConfigFieldAuth deserializes the "auth" field from dockercfg into a +// username and a password. The format of the auth field is base64(:). +func decodeDockerConfigFieldAuth(field string) (username, password string, err error) { + decoded, err := base64.StdEncoding.DecodeString(field) + if err != nil { + return + } + + parts := strings.SplitN(string(decoded), ":", 2) + if len(parts) != 2 { + err = fmt.Errorf("unable to parse auth field") + return + } + + username = parts[0] + password = parts[1] + + return +} diff --git a/pkg/kubelet/dockertools/config_test.go b/pkg/kubelet/dockertools/config_test.go new file mode 100644 index 00000000000..9c7a6af8480 --- /dev/null +++ b/pkg/kubelet/dockertools/config_test.go @@ -0,0 +1,208 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dockertools + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/fsouza/go-dockerclient" +) + +func TestDockerConfigJSONDecode(t *testing.T) { + input := []byte(`{"http://foo.example.com":{"username": "foo", "password": "bar", "email": "foo@example.com"}, "http://bar.example.com":{"username": "bar", "password": "baz", "email": "bar@example.com"}}`) + + expect := dockerConfig(map[string]dockerConfigEntry{ + "http://foo.example.com": dockerConfigEntry{ + Username: "foo", + Password: "bar", + Email: "foo@example.com", + }, + "http://bar.example.com": dockerConfigEntry{ + Username: "bar", + Password: "baz", + Email: "bar@example.com", + }, + }) + + var output dockerConfig + err := json.Unmarshal(input, &output) + if err != nil { + t.Errorf("Received unexpected error: %v", err) + } + + if !reflect.DeepEqual(expect, output) { + t.Errorf("Received unexpected output. Expected %#v, got %#v", expect, output) + } +} + +func TestDockerConfigEntryJSONDecode(t *testing.T) { + tests := []struct { + input []byte + expect dockerConfigEntry + fail bool + }{ + // simple case, just decode the fields + { + input: []byte(`{"username": "foo", "password": "bar", "email": "foo@example.com"}`), + expect: dockerConfigEntry{ + Username: "foo", + Password: "bar", + Email: "foo@example.com", + }, + fail: false, + }, + + // auth field decodes to username & password + { + input: []byte(`{"auth": "Zm9vOmJhcg==", "email": "foo@example.com"}`), + expect: dockerConfigEntry{ + Username: "foo", + Password: "bar", + Email: "foo@example.com", + }, + fail: false, + }, + + // auth field overrides username & password + { + input: []byte(`{"username": "foo", "password": "bar", "auth": "cGluZzpwb25n", "email": "foo@example.com"}`), + expect: dockerConfigEntry{ + Username: "ping", + Password: "pong", + Email: "foo@example.com", + }, + fail: false, + }, + + // poorly-formatted auth causes failure + { + input: []byte(`{"auth": "pants", "email": "foo@example.com"}`), + expect: dockerConfigEntry{ + Username: "", + Password: "", + Email: "foo@example.com", + }, + fail: true, + }, + + // invalid JSON causes failure + { + input: []byte(`{"email": false}`), + expect: dockerConfigEntry{ + Username: "", + Password: "", + Email: "", + }, + fail: true, + }, + } + + for i, tt := range tests { + var output dockerConfigEntry + err := json.Unmarshal(tt.input, &output) + if (err != nil) != tt.fail { + t.Errorf("case %d: expected fail=%t, got err=%v", i, tt.fail, err) + } + + if !reflect.DeepEqual(tt.expect, output) { + t.Errorf("case %d: expected output %#v, got %#v", i, tt.expect, output) + } + } +} + +func TestDecodeDockerConfigFieldAuth(t *testing.T) { + tests := []struct { + input string + username string + password string + fail bool + }{ + // auth field decodes to username & password + { + input: "Zm9vOmJhcg==", + username: "foo", + password: "bar", + }, + + // good base64 data, but no colon separating username & password + { + input: "cGFudHM=", + fail: true, + }, + + // bad base64 data + { + input: "pants", + fail: true, + }, + } + + for i, tt := range tests { + username, password, err := decodeDockerConfigFieldAuth(tt.input) + if (err != nil) != tt.fail { + t.Errorf("case %d: expected fail=%t, got err=%v", i, tt.fail, err) + } + + if tt.username != username { + t.Errorf("case %d: expected username %q, got %q", i, tt.username, username) + } + + if tt.password != password { + t.Errorf("case %d: expected password %q, got %q", i, tt.password, password) + } + } +} + +func TestDockerKeyringFromConfig(t *testing.T) { + cfg := dockerConfig(map[string]dockerConfigEntry{ + "http://foo.example.com": dockerConfigEntry{ + Username: "foo", + Password: "bar", + Email: "foo@example.com", + }, + "https://bar.example.com": dockerConfigEntry{ + Username: "bar", + Password: "baz", + Email: "bar@example.com", + }, + }) + + dk := newDockerKeyring() + cfg.addToKeyring(dk) + + expect := newDockerKeyring() + expect.add("foo.example.com", + docker.AuthConfiguration{ + Username: "foo", + Password: "bar", + Email: "foo@example.com", + }, + ) + expect.add("bar.example.com", + docker.AuthConfiguration{ + Username: "bar", + Password: "baz", + Email: "bar@example.com", + }, + ) + + if !reflect.DeepEqual(expect, dk) { + t.Errorf("Received unexpected output. Expected %#v, got %#v", expect, dk) + } +} diff --git a/pkg/kubelet/dockertools/docker.go b/pkg/kubelet/dockertools/docker.go index f1690af964a..b42e2ce9a36 100644 --- a/pkg/kubelet/dockertools/docker.go +++ b/pkg/kubelet/dockertools/docker.go @@ -22,6 +22,7 @@ import ( "hash/adler32" "math/rand" "os/exec" + "sort" "strconv" "strings" @@ -57,14 +58,29 @@ type DockerPuller interface { // dockerPuller is the default implementation of DockerPuller. type dockerPuller struct { - client DockerInterface + client DockerInterface + keyring *dockerKeyring } // NewDockerPuller creates a new instance of the default implementation of DockerPuller. func NewDockerPuller(client DockerInterface) DockerPuller { - return dockerPuller{ - client: client, + dp := dockerPuller{ + client: client, + keyring: newDockerKeyring(), } + + cfg, err := readDockerConfigFile() + if err == nil { + cfg.addToKeyring(dp.keyring) + } else { + glog.Errorf("Unable to parse docker config file: %v", err) + } + + if dp.keyring.count() == 0 { + glog.Infof("Continuing with empty docker keyring") + } + + return dp } type dockerContainerCommandRunner struct{} @@ -103,7 +119,13 @@ func (p dockerPuller) Pull(image string) error { Repository: image, Tag: tag, } - return p.client.PullImage(opts, docker.AuthConfiguration{}) + + creds, ok := p.keyring.lookup(image) + if !ok { + glog.V(1).Infof("Pulling image %s without credentials", image) + } + + return p.client.PullImage(opts, creds) } // DockerContainers is a map of containers @@ -323,3 +345,55 @@ func parseImageName(image string) (string, string) { type ContainerCommandRunner interface { RunInContainer(containerID string, cmd []string) ([]byte, error) } + +// dockerKeyring tracks a set of docker registry credentials, maintaining a +// reverse index across the registry endpoints. A registry endpoint is made +// up of a host (e.g. registry.example.com), but it may also contain a path +// (e.g. registry.example.com/foo) This index is important for two reasons: +// - registry endpoints may overlap, and when this happens we must find the +// most specific match for a given image +// - iterating a map does not yield predictable results +type dockerKeyring struct { + index []string + creds map[string]docker.AuthConfiguration +} + +func newDockerKeyring() *dockerKeyring { + return &dockerKeyring{ + index: make([]string, 0), + creds: make(map[string]docker.AuthConfiguration), + } +} + +func (dk *dockerKeyring) add(registry string, creds docker.AuthConfiguration) { + dk.creds[registry] = creds + + dk.index = append(dk.index, registry) + dk.reindex() +} + +// reindex updates the index used to identify which credentials to use for +// a given image. The index is reverse-sorted so more specific paths are +// matched first. For example, if for the given image "quay.io/coreos/etcd", +// credentials for "quay.io/coreos" should match before "quay.io". +func (dk *dockerKeyring) reindex() { + sort.Sort(sort.Reverse(sort.StringSlice(dk.index))) +} + +func (dk *dockerKeyring) lookup(image string) (docker.AuthConfiguration, bool) { + // range over the index as iterating over a map does not provide + // a predictable ordering + for _, k := range dk.index { + if !strings.HasPrefix(image, k) { + continue + } + + return dk.creds[k], true + } + + return docker.AuthConfiguration{}, false +} + +func (dk dockerKeyring) count() int { + return len(dk.creds) +} diff --git a/pkg/kubelet/dockertools/docker_test.go b/pkg/kubelet/dockertools/docker_test.go index 6822b3a0c5f..66305492753 100644 --- a/pkg/kubelet/dockertools/docker_test.go +++ b/pkg/kubelet/dockertools/docker_test.go @@ -151,3 +151,59 @@ func TestParseImageName(t *testing.T) { } } } + +func TestDockerKeyringLookup(t *testing.T) { + empty := docker.AuthConfiguration{} + + ada := docker.AuthConfiguration{ + Username: "ada", + Password: "smash", + Email: "ada@example.com", + } + + grace := docker.AuthConfiguration{ + Username: "grace", + Password: "squash", + Email: "grace@example.com", + } + + dk := newDockerKeyring() + dk.add("bar.example.com/pong", grace) + dk.add("bar.example.com", ada) + + tests := []struct { + image string + match docker.AuthConfiguration + ok bool + }{ + // direct match + {"bar.example.com", ada, true}, + + // direct match deeper than other possible matches + {"bar.example.com/pong", grace, true}, + + // no direct match, deeper path ignored + {"bar.example.com/ping", ada, true}, + + // match first part of path token + {"bar.example.com/pongz", grace, true}, + + // match regardless of sub-path + {"bar.example.com/pong/pang", grace, true}, + + // no host match + {"example.com", empty, false}, + {"foo.example.com", empty, false}, + } + + for i, tt := range tests { + match, ok := dk.lookup(tt.image) + if tt.ok != ok { + t.Errorf("case %d: expected ok=%t, got %t", i, tt.ok, ok) + } + + if !reflect.DeepEqual(tt.match, match) { + t.Errorf("case %d: expected match=%#v, got %#v", i, tt.match, match) + } + } +}