mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-19 00:31:00 +00:00
kubelet: generate keyring from .dockercfg
This commit is contained in:
parent
0bf4fabc19
commit
2f87857b0f
116
pkg/kubelet/dockertools/config.go
Normal file
116
pkg/kubelet/dockertools/config.go
Normal file
@ -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(<username>:<password>).
|
||||||
|
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
|
||||||
|
}
|
208
pkg/kubelet/dockertools/config_test.go
Normal file
208
pkg/kubelet/dockertools/config_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ import (
|
|||||||
"hash/adler32"
|
"hash/adler32"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -57,14 +58,29 @@ type DockerPuller interface {
|
|||||||
|
|
||||||
// dockerPuller is the default implementation of DockerPuller.
|
// dockerPuller is the default implementation of DockerPuller.
|
||||||
type dockerPuller struct {
|
type dockerPuller struct {
|
||||||
client DockerInterface
|
client DockerInterface
|
||||||
|
keyring *dockerKeyring
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDockerPuller creates a new instance of the default implementation of DockerPuller.
|
// NewDockerPuller creates a new instance of the default implementation of DockerPuller.
|
||||||
func NewDockerPuller(client DockerInterface) DockerPuller {
|
func NewDockerPuller(client DockerInterface) DockerPuller {
|
||||||
return dockerPuller{
|
dp := dockerPuller{
|
||||||
client: client,
|
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{}
|
type dockerContainerCommandRunner struct{}
|
||||||
@ -103,7 +119,13 @@ func (p dockerPuller) Pull(image string) error {
|
|||||||
Repository: image,
|
Repository: image,
|
||||||
Tag: tag,
|
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
|
// DockerContainers is a map of containers
|
||||||
@ -323,3 +345,55 @@ func parseImageName(image string) (string, string) {
|
|||||||
type ContainerCommandRunner interface {
|
type ContainerCommandRunner interface {
|
||||||
RunInContainer(containerID string, cmd []string) ([]byte, error)
|
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)
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user