Implements a credentialprovider library for use by DockerPuller.

This change refactors the way Kubelet's DockerPuller handles the docker config credentials to utilize a new credentialprovider library.

The credentialprovider library is based on several of the files from the Kubelet's dockertools directory, but supports a new pluggable model for retrieving a .dockercfg-compatible JSON blob with credentials.

With this change, the Kubelet will lazily ask for the docker config from a set of DockerConfigProvider extensions each time it needs a credential.

This change provides common implementations of DockerConfigProvider for:
 - "Default": load .dockercfg from disk
 - "Caching": wraps another provider in a cache that expires after a pre-specified lifetime.

GCP-only:
 - "google-dockercfg": reads a .dockercfg from a GCE instance's metadata
 - "google-dockercfg-url": reads a .dockercfg from a URL specified in a GCE instance's metadata.
 - "google-container-registry": reads an access token from GCE metadata into a password field.
pull/6/head
Matt Moore 2014-11-15 05:50:59 -08:00
parent 931cd3a2df
commit 0c5d9ed0d2
17 changed files with 1341 additions and 167 deletions

View File

@ -31,7 +31,7 @@ MASTER_TAG="${INSTANCE_PREFIX}-master"
MINION_TAG="${INSTANCE_PREFIX}-minion"
MINION_NAMES=($(eval echo ${INSTANCE_PREFIX}-minion-{1..${NUM_MINIONS}}))
MINION_IP_RANGES=($(eval echo "10.244.{1..${NUM_MINIONS}}.0/24"))
MINION_SCOPES="compute-rw"
MINION_SCOPES="storage-ro,compute-rw"
# Increase the sleep interval value if concerned about API rate limits. 3, in seconds, is the default.
POLL_SLEEP_INTERVAL=3
PORTAL_NET="10.0.0.0/16"

24
cmd/kubelet/plugins.go Normal file
View File

@ -0,0 +1,24 @@
/*
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 main
// This file exists to force the desired plugin implementations to be linked.
// This should probably be part of some configuration fed into the build for a
// given binary target.
import (
_ "github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider/gcp"
)

86
hack/e2e-suite/private.sh Executable file
View File

@ -0,0 +1,86 @@
#!/bin/bash
# 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.
# Launches a container and verifies it can be reached. Assumes that
# we're being called by hack/e2e-test.sh (we use some env vars it sets up).
set -o errexit
set -o nounset
set -o pipefail
if [[ "${KUBERNETES_PROVIDER:-gce}" != "gce" ]]; then
echo WARNING: Skipping private.sh for cloud provider: $KUBERNETES_PROVIDER.
exit 0
fi
KUBE_ROOT=$(dirname "${BASH_SOURCE}")/../..
source "${KUBE_ROOT}/cluster/kube-env.sh"
source "${KUBE_ROOT}/cluster/$KUBERNETES_PROVIDER/util.sh"
# Launch some pods.
num_pods=2
$KUBECFG -p 8080:9376 run container.cloud.google.com/_b_k8s_test/serve_hostname ${num_pods} my-hostname
function teardown() {
echo "Cleaning up test artifacts"
$KUBECFG stop my-hostname
$KUBECFG rm my-hostname
}
trap "teardown" EXIT
pod_id_list=$($KUBECFG '-template={{range.items}}{{.id}} {{end}}' -l replicationController=my-hostname list pods)
# Pod turn up on a clean cluster can take a while for the docker image pull.
all_running=0
for i in $(seq 1 24); do
echo "Waiting for pods to come up."
sleep 5
all_running=1
for id in $pod_id_list; do
current_status=$($KUBECFG -template '{{.currentState.status}}' get pods/$id) || true
if [[ "$current_status" != "Running" ]]; then
all_running=0
break
fi
done
if [[ "${all_running}" == 1 ]]; then
break
fi
done
if [[ "${all_running}" == 0 ]]; then
echo "Pods did not come up in time"
exit 1
fi
# Get minion IP addresses
detect-minions
# let images stabilize
echo "Letting images stabilize"
sleep 5
# Verify that something is listening.
for id in ${pod_id_list}; do
ip=$($KUBECFG -template '{{.currentState.hostIP}}' get pods/$id)
echo "Trying to reach server that should be running at ${ip}:8080..."
ok=0
for i in $(seq 1 5); do
curl --connect-timeout 1 "http://${ip}:8080" >/dev/null 2>&1 && ok=1 && break
sleep 2
done
done
exit 0

View File

@ -14,26 +14,38 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package dockertools
package credentialprovider
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"net/http"
"path/filepath"
"strings"
"github.com/fsouza/go-dockerclient"
"github.com/golang/glog"
)
// 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
type DockerConfigEntry struct {
Username string
Password string
Email string
}
const (
dockerConfigFileLocation = ".dockercfg"
)
func readDockerConfigFile() (cfg dockerConfig, err error) {
func ReadDockerConfigFile() (cfg DockerConfig, err error) {
// TODO(mattmoor): This causes the Kubelet to read /.dockercfg,
// which is incorrect. It should come from $HOME/.dockercfg.
absDockerConfigFileLocation, err := filepath.Abs(dockerConfigFileLocation)
if err != nil {
glog.Errorf("while trying to canonicalize %s: %v", dockerConfigFileLocation, err)
@ -45,42 +57,58 @@ func readDockerConfigFile() (cfg dockerConfig, err error) {
glog.Errorf("while trying to read %s: %v", absDockerConfigFileLocation, err)
return nil, err
}
return readDockerConfigFileFromBytes(contents)
}
func ReadUrl(url string, client *http.Client, header *http.Header) (body []byte, err error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
glog.Errorf("while creating request to read %s: %v", url, err)
return nil, err
}
if header != nil {
req.Header = *header
}
resp, err := client.Do(req)
if err != nil {
glog.Errorf("while trying to read %s: %v", url, err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err := fmt.Errorf("http status code: %d while fetching url: %v", resp.StatusCode)
glog.Errorf("while trying to read %s: %v", url, err)
glog.V(2).Infof("body of failing http response: %v", resp.Body)
return nil, err
}
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
glog.Errorf("while trying to read %s: %v", url, err)
return nil, err
}
return contents, nil
}
func ReadDockerConfigFileFromUrl(url string, client *http.Client, header *http.Header) (cfg DockerConfig, err error) {
if contents, err := ReadUrl(url, client, header); err != nil {
return nil, err
} else {
return readDockerConfigFileFromBytes(contents)
}
}
func readDockerConfigFileFromBytes(contents []byte) (cfg DockerConfig, err error) {
if err = json.Unmarshal(contents, &cfg); err != nil {
glog.Errorf("while trying to parse %s: %v", absDockerConfigFileLocation, err)
glog.Errorf("while trying to parse blob %q: %v", contents, err)
return nil, err
}
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 {
@ -90,7 +118,7 @@ type dockerConfigEntryWithAuth struct {
Auth string
}
func (ident *dockerConfigEntry) UnmarshalJSON(data []byte) error {
func (ident *DockerConfigEntry) UnmarshalJSON(data []byte) error {
var tmp dockerConfigEntryWithAuth
err := json.Unmarshal(data, &tmp)
if err != nil {

View File

@ -14,20 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package dockertools
package credentialprovider
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{
expect := DockerConfig(map[string]DockerConfigEntry{
"http://foo.example.com": {
Username: "foo",
Password: "bar",
@ -40,7 +38,7 @@ func TestDockerConfigJSONDecode(t *testing.T) {
},
})
var output dockerConfig
var output DockerConfig
err := json.Unmarshal(input, &output)
if err != nil {
t.Errorf("Received unexpected error: %v", err)
@ -54,13 +52,13 @@ func TestDockerConfigJSONDecode(t *testing.T) {
func TestDockerConfigEntryJSONDecode(t *testing.T) {
tests := []struct {
input []byte
expect dockerConfigEntry
expect DockerConfigEntry
fail bool
}{
// simple case, just decode the fields
{
input: []byte(`{"username": "foo", "password": "bar", "email": "foo@example.com"}`),
expect: dockerConfigEntry{
expect: DockerConfigEntry{
Username: "foo",
Password: "bar",
Email: "foo@example.com",
@ -71,7 +69,7 @@ func TestDockerConfigEntryJSONDecode(t *testing.T) {
// auth field decodes to username & password
{
input: []byte(`{"auth": "Zm9vOmJhcg==", "email": "foo@example.com"}`),
expect: dockerConfigEntry{
expect: DockerConfigEntry{
Username: "foo",
Password: "bar",
Email: "foo@example.com",
@ -82,7 +80,7 @@ func TestDockerConfigEntryJSONDecode(t *testing.T) {
// auth field overrides username & password
{
input: []byte(`{"username": "foo", "password": "bar", "auth": "cGluZzpwb25n", "email": "foo@example.com"}`),
expect: dockerConfigEntry{
expect: DockerConfigEntry{
Username: "ping",
Password: "pong",
Email: "foo@example.com",
@ -93,7 +91,7 @@ func TestDockerConfigEntryJSONDecode(t *testing.T) {
// poorly-formatted auth causes failure
{
input: []byte(`{"auth": "pants", "email": "foo@example.com"}`),
expect: dockerConfigEntry{
expect: DockerConfigEntry{
Username: "",
Password: "",
Email: "foo@example.com",
@ -104,7 +102,7 @@ func TestDockerConfigEntryJSONDecode(t *testing.T) {
// invalid JSON causes failure
{
input: []byte(`{"email": false}`),
expect: dockerConfigEntry{
expect: DockerConfigEntry{
Username: "",
Password: "",
Email: "",
@ -114,7 +112,7 @@ func TestDockerConfigEntryJSONDecode(t *testing.T) {
}
for i, tt := range tests {
var output dockerConfigEntry
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)
@ -168,41 +166,3 @@ func TestDecodeDockerConfigFieldAuth(t *testing.T) {
}
}
}
func TestDockerKeyringFromConfig(t *testing.T) {
cfg := dockerConfig(map[string]dockerConfigEntry{
"http://foo.example.com": {
Username: "foo",
Password: "bar",
Email: "foo@example.com",
},
"https://bar.example.com": {
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)
}
}

View File

@ -0,0 +1,19 @@
/*
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 credentialprovider supplies interfaces and implementations for
// docker registry providers to expose their authentication scheme.
package credentialprovider

View File

@ -0,0 +1,19 @@
/*
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 gcp_credentials contains implementations of DockerConfigProvider
// for Google Cloud Platform.
package gcp_credentials

View File

@ -0,0 +1,190 @@
/*
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 gcp_credentials
import (
"encoding/json"
"net/http"
"strings"
"time"
"github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider"
"github.com/golang/glog"
)
const (
metadataUrl = "http://metadata.google.internal./computeMetadata/v1/"
metadataAttributes = metadataUrl + "instance/attributes/"
dockerConfigKey = metadataAttributes + "google-dockercfg"
dockerConfigUrlKey = metadataAttributes + "google-dockercfg-url"
metadataScopes = metadataUrl + "instance/service-accounts/default/scopes"
metadataToken = metadataUrl + "instance/service-accounts/default/token"
metadataEmail = metadataUrl + "instance/service-accounts/default/email"
storageScopePrefix = "https://www.googleapis.com/auth/devstorage"
)
var containerRegistryUrls = []string{"container.cloud.google.com"}
var metadataHeader = &http.Header{
"Metadata-Flavor": []string{"Google"},
}
// A DockerConfigProvider that reads its configuration from Google
// Compute Engine metadata.
type metadataProvider struct {
Client *http.Client
}
// A DockerConfigProvider that reads its configuration from a specific
// Google Compute Engine metadata key: 'google-dockercfg'.
type dockerConfigKeyProvider struct {
metadataProvider
}
// A DockerConfigProvider that reads its configuration from a URL read from
// a specific Google Compute Engine metadata key: 'google-dockercfg-url'.
type dockerConfigUrlKeyProvider struct {
metadataProvider
}
// A DockerConfigProvider that provides a dockercfg with:
// Username: "_token"
// Password: "{access token from metadata}"
type containerRegistryProvider struct {
metadataProvider
}
// init registers the various means by which credentials may
// be resolved on GCP.
func init() {
credentialprovider.RegisterCredentialProvider("google-dockercfg",
&credentialprovider.CachingDockerConfigProvider{
Provider: &dockerConfigKeyProvider{
metadataProvider{Client: http.DefaultClient},
},
Lifetime: 60 * time.Second,
})
credentialprovider.RegisterCredentialProvider("google-dockercfg-url",
&credentialprovider.CachingDockerConfigProvider{
Provider: &dockerConfigUrlKeyProvider{
metadataProvider{Client: http.DefaultClient},
},
Lifetime: 60 * time.Second,
})
credentialprovider.RegisterCredentialProvider("google-container-registry",
// Never cache this. The access token is already
// cached by the metadata service.
&containerRegistryProvider{
metadataProvider{Client: http.DefaultClient},
})
}
// Enabled implements DockerConfigProvider for all of the Google implementations.
func (g *metadataProvider) Enabled() bool {
_, err := credentialprovider.ReadUrl(metadataUrl, g.Client, metadataHeader)
return err == nil
}
// Provide implements DockerConfigProvider
func (g *dockerConfigKeyProvider) Provide() credentialprovider.DockerConfig {
// Read the contents of the google-dockercfg metadata key and
// parse them as an alternate .dockercfg
if cfg, err := credentialprovider.ReadDockerConfigFileFromUrl(dockerConfigKey, g.Client, metadataHeader); err == nil {
return cfg
}
return credentialprovider.DockerConfig{}
}
// Provide implements DockerConfigProvider
func (g *dockerConfigUrlKeyProvider) Provide() credentialprovider.DockerConfig {
// Read the contents of the google-dockercfg-url key and load a .dockercfg from there
if url, err := credentialprovider.ReadUrl(dockerConfigUrlKey, g.Client, metadataHeader); err == nil {
if strings.HasPrefix(string(url), "http") {
if cfg, err := credentialprovider.ReadDockerConfigFileFromUrl(string(url), g.Client, nil); err == nil {
return cfg
}
} else {
// TODO(mattmoor): support reading alternate scheme URLs (e.g. gs:// or s3://)
glog.Errorf("Unsupported URL scheme: %s", string(url))
}
}
return credentialprovider.DockerConfig{}
}
// Enabled implements a special metadata-based check, which verifies the
// storage scope is available on the GCE VM.
func (g *containerRegistryProvider) Enabled() bool {
value, err := credentialprovider.ReadUrl(metadataScopes+"?alt=json", g.Client, metadataHeader)
if err != nil {
return false
}
var scopes []string
if err := json.Unmarshal([]byte(value), &scopes); err != nil {
return false
}
for _, v := range scopes {
if strings.HasPrefix(v, storageScopePrefix) {
return true
}
}
glog.Warningf("Google container registry is disabled, no storage scope is available: %s", value)
return false
}
// tokenBlob is used to decode the JSON blob containing an access token
// that is returned by GCE metadata.
type tokenBlob struct {
AccessToken string `json:"access_token"`
}
// Provide implements DockerConfigProvider
func (g *containerRegistryProvider) Provide() credentialprovider.DockerConfig {
cfg := credentialprovider.DockerConfig{}
tokenJsonBlob, err := credentialprovider.ReadUrl(metadataToken, g.Client, metadataHeader)
if err != nil {
return cfg
}
email, err := credentialprovider.ReadUrl(metadataEmail, g.Client, metadataHeader)
if err != nil {
return cfg
}
var parsedBlob tokenBlob
if err := json.Unmarshal([]byte(tokenJsonBlob), &parsedBlob); err != nil {
glog.Errorf("while parsing json blob %s: %v", tokenJsonBlob, err)
return cfg
}
entry := credentialprovider.DockerConfigEntry{
Username: "_token",
Password: parsedBlob.AccessToken,
Email: string(email),
}
// Add our entry for each of the supported container registry URLs
for _, k := range containerRegistryUrls {
cfg[k] = entry
}
return cfg
}

View File

@ -0,0 +1,293 @@
/*
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 gcp_credentials
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider"
)
func TestDockerKeyringFromGoogleDockerConfigMetadata(t *testing.T) {
registryUrl := "hello.kubernetes.io"
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, registryUrl, email, auth)
const probeEndpoint = "/computeMetadata/v1/"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only serve the one metadata key.
if probeEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
} else if strings.HasSuffix(dockerConfigKey, r.URL.Path) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, sampleDockerConfig)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
// Make a transport that reroutes all traffic to the example server
transport := &http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL + req.URL.Path)
},
}
keyring := &credentialprovider.BasicDockerKeyring{}
provider := &dockerConfigKeyProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
t.Errorf("Provider is unexpectedly disabled")
}
keyring.Add(provider.Provide())
val, ok := keyring.Lookup(registryUrl)
if !ok {
t.Errorf("Didn't find expected URL: %s", registryUrl)
}
if username != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
func TestDockerKeyringFromGoogleDockerConfigMetadataUrl(t *testing.T) {
registryUrl := "hello.kubernetes.io"
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, registryUrl, email, auth)
const probeEndpoint = "/computeMetadata/v1/"
const valueEndpoint = "/my/value"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only serve the URL key and the value endpoint
if probeEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
} else if valueEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, sampleDockerConfig)
} else if strings.HasSuffix(dockerConfigUrlKey, r.URL.Path) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/text")
fmt.Fprint(w, "http://foo.bar.com"+valueEndpoint)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
// Make a transport that reroutes all traffic to the example server
transport := &http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL + req.URL.Path)
},
}
keyring := &credentialprovider.BasicDockerKeyring{}
provider := &dockerConfigUrlKeyProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
t.Errorf("Provider is unexpectedly disabled")
}
keyring.Add(provider.Provide())
val, ok := keyring.Lookup(registryUrl)
if !ok {
t.Errorf("Didn't find expected URL: %s", registryUrl)
}
if username != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
func TestContainerRegistryBasics(t *testing.T) {
registryUrl := "container.cloud.google.com"
email := "1234@project.gserviceaccount.com"
token := &tokenBlob{AccessToken: "ya26.lots-of-indiscernible-garbage"}
const (
defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/"
scopeEndpoint = defaultEndpoint + "scopes"
emailEndpoint = defaultEndpoint + "email"
tokenEndpoint = defaultEndpoint + "token"
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only serve the URL key and the value endpoint
if scopeEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `["%s.read_write"]`, storageScopePrefix)
} else if emailEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, email)
} else if tokenEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
bytes, err := json.Marshal(token)
if err != nil {
t.Fatalf("unexpected error: %+v", err)
}
fmt.Fprintln(w, string(bytes))
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
// Make a transport that reroutes all traffic to the example server
transport := &http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL + req.URL.Path)
},
}
keyring := &credentialprovider.BasicDockerKeyring{}
provider := &containerRegistryProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
t.Errorf("Provider is unexpectedly disabled")
}
keyring.Add(provider.Provide())
val, ok := keyring.Lookup(registryUrl)
if !ok {
t.Errorf("Didn't find expected URL: %s", registryUrl)
}
if "_token" != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", "_token", val.Username)
}
if token.AccessToken != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", token.AccessToken, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
func TestContainerRegistryNoStorageScope(t *testing.T) {
const (
defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/"
scopeEndpoint = defaultEndpoint + "scopes"
)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only serve the URL key and the value endpoint
if scopeEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `["https://www.googleapis.com/auth/compute.read_write"]`)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
// Make a transport that reroutes all traffic to the example server
transport := &http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL + req.URL.Path)
},
}
provider := &containerRegistryProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
}
if provider.Enabled() {
t.Errorf("Provider is unexpectedly enabled")
}
}
func TestAllProvidersNoMetadata(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
// Make a transport that reroutes all traffic to the example server
transport := &http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(server.URL + req.URL.Path)
},
}
providers := []credentialprovider.DockerConfigProvider{
&dockerConfigKeyProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
},
&dockerConfigUrlKeyProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
},
&containerRegistryProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
},
}
for _, provider := range providers {
if provider.Enabled() {
t.Errorf("Provider %s is unexpectedly enabled", reflect.TypeOf(provider).String())
}
}
}

View File

@ -0,0 +1,134 @@
/*
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 credentialprovider
import (
"net/url"
"sort"
"strings"
docker "github.com/fsouza/go-dockerclient"
"github.com/golang/glog"
)
// 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 interface {
Lookup(image string) (docker.AuthConfiguration, bool)
}
// BasicDockerKeyring is a trivial map-backed implementation of DockerKeyring
type BasicDockerKeyring struct {
index []string
creds map[string]docker.AuthConfiguration
}
// lazyDockerKeyring is an implementation of DockerKeyring that lazily
// materializes its dockercfg based on a set of dockerConfigProviders.
type lazyDockerKeyring struct {
Providers []DockerConfigProvider
}
func (dk *BasicDockerKeyring) Add(cfg DockerConfig) {
if dk.index == nil {
dk.index = make([]string, 0)
dk.creds = make(map[string]docker.AuthConfiguration)
}
for loc, ident := range cfg {
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
}
registry := parsed.Host + parsed.Path
dk.creds[registry] = creds
dk.index = append(dk.index, registry)
}
// Update 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".
sort.Sort(sort.Reverse(sort.StringSlice(dk.index)))
}
const defaultRegistryHost = "index.docker.io/v1/"
// isDefaultRegistryMatch determines whether the given image will
// pull from the default registry (DockerHub) based on the
// characteristics of its name.
func isDefaultRegistryMatch(image string) bool {
parts := strings.SplitN(image, "/", 2)
if len(parts) == 1 {
// e.g. library/ubuntu
return true
}
// From: http://blog.docker.com/2013/07/how-to-use-your-own-registry/
// Docker looks for either a “.” (domain separator) or “:” (port separator)
// to learn that the first part of the repository name is a location and not
// a user name.
return !strings.ContainsAny(parts[0], ".:")
}
// Lookup implements the DockerKeyring method for fetching credentials
// based on image name.
func (dk *BasicDockerKeyring) 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 {
// NOTE: prefix is a sufficient check because while scheme is allowed,
// it is stripped as part of 'Add'
if !strings.HasPrefix(image, k) {
continue
}
return dk.creds[k], true
}
// Use credentials for the default registry if provided, and appropriate
if auth, ok := dk.creds[defaultRegistryHost]; ok && isDefaultRegistryMatch(image) {
return auth, true
}
return docker.AuthConfiguration{}, false
}
// Lookup implements the DockerKeyring method for fetching credentials
// based on image name.
func (dk *lazyDockerKeyring) Lookup(image string) (docker.AuthConfiguration, bool) {
keyring := &BasicDockerKeyring{}
for _, p := range dk.Providers {
keyring.Add(p.Provide())
}
return keyring.Lookup(image)
}

View File

@ -0,0 +1,261 @@
/*
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 credentialprovider
import (
"encoding/base64"
"fmt"
"testing"
)
func TestDockerKeyringFromBytes(t *testing.T) {
url := "hello.kubernetes.io"
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
}
val, ok := keyring.Lookup(url + "/foo/bar")
if !ok {
t.Errorf("Didn't find expected URL: %s", url)
}
if username != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
func TestKeyringMiss(t *testing.T) {
url := "hello.kubernetes.io"
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
}
val, ok := keyring.Lookup("world.mesos.org/foo/bar")
if ok {
t.Errorf("Found unexpected credential: %+v", val)
}
}
func TestKeyringMissWithDockerHubCredentials(t *testing.T) {
url := defaultRegistryHost
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
}
val, ok := keyring.Lookup("world.mesos.org/foo/bar")
if ok {
t.Errorf("Found unexpected credential: %+v", val)
}
}
func TestKeyringHitWithUnqualifiedDockerHub(t *testing.T) {
url := defaultRegistryHost
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
}
val, ok := keyring.Lookup("google/docker-registry")
if !ok {
t.Errorf("Didn't find expected URL: %s", url)
}
if username != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
func TestKeyringHitWithUnqualifiedLibraryDockerHub(t *testing.T) {
url := defaultRegistryHost
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
}
val, ok := keyring.Lookup("jenkins")
if !ok {
t.Errorf("Didn't find expected URL: %s", url)
}
if username != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
func TestKeyringHitWithQualifiedDockerHub(t *testing.T) {
url := defaultRegistryHost
email := "foo@bar.baz"
username := "foo"
password := "bar"
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password)))
sampleDockerConfig := fmt.Sprintf(`{
"https://%s": {
"email": %q,
"auth": %q
}
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
}
val, ok := keyring.Lookup(url + "/google/docker-registry")
if !ok {
t.Errorf("Didn't find expected URL: %s", url)
}
if username != val.Username {
t.Errorf("Unexpected username value, want: %s, got: %s", username, val.Username)
}
if password != val.Password {
t.Errorf("Unexpected password value, want: %s, got: %s", password, val.Password)
}
if email != val.Email {
t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email)
}
}
type testProvider struct {
Count int
}
// Enabled implements dockerConfigProvider
func (d *testProvider) Enabled() bool {
return true
}
// Provide implements dockerConfigProvider
func (d *testProvider) Provide() DockerConfig {
d.Count += 1
return DockerConfig{}
}
func TestLazyKeyring(t *testing.T) {
provider := &testProvider{
Count: 0,
}
lazy := &lazyDockerKeyring{
Providers: []DockerConfigProvider{
provider,
},
}
if provider.Count != 0 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
lazy.Lookup("foo")
if provider.Count != 1 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
lazy.Lookup("foo")
if provider.Count != 2 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
lazy.Lookup("foo")
if provider.Count != 3 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
}

View File

@ -0,0 +1,62 @@
/*
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 credentialprovider
import (
"sync"
"github.com/golang/glog"
)
// All registered credential providers.
var providersMutex sync.Mutex
var providers = make(map[string]DockerConfigProvider)
// RegisterCredentialProvider is called by provider implementations on
// initialization to register themselves, like so:
// func init() {
// RegisterCredentialProvider("name", &myProvider{...})
// }
func RegisterCredentialProvider(name string, provider DockerConfigProvider) {
providersMutex.Lock()
defer providersMutex.Unlock()
_, found := providers[name]
if found {
glog.Fatalf("Credential provider %q was registered twice", name)
}
glog.V(1).Infof("Registered credential provider %q", name)
providers[name] = provider
}
// NewDockerKeyring creates a DockerKeyring to use for resolving credentials,
// which lazily draws from the set of registered credential providers.
func NewDockerKeyring() DockerKeyring {
keyring := &lazyDockerKeyring{
Providers: make([]DockerConfigProvider, 0),
}
// TODO(mattmoor): iterating over the map is non-deterministic. We should
// introduce the notion of priorities for conflict resolution.
for name, provider := range providers {
if provider.Enabled() {
glog.Infof("Registering credential provider: %v", name)
keyring.Providers = append(keyring.Providers, provider)
}
}
return keyring
}

View File

@ -0,0 +1,95 @@
/*
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 credentialprovider
import (
"os"
"reflect"
"sync"
"time"
"github.com/golang/glog"
)
// DockerConfigProvider is the interface that registered extensions implement
// to materialize 'dockercfg' credentials.
type DockerConfigProvider interface {
Enabled() bool
Provide() DockerConfig
}
// A DockerConfigProvider that simply reads the .dockersfg file
type defaultDockerConfigProvider struct{}
// init registers our default provider, which simply reads the .dockercfg file.
func init() {
RegisterCredentialProvider(".dockercfg",
&CachingDockerConfigProvider{
Provider: &defaultDockerConfigProvider{},
Lifetime: 5 * time.Minute,
})
}
// CachingDockerConfigProvider implements DockerConfigProvider by composing
// with another DockerConfigProvider and caching the DockerConfig it provides
// for a pre-specified lifetime.
type CachingDockerConfigProvider struct {
Provider DockerConfigProvider
Lifetime time.Duration
// cache fields
cacheDockerConfig DockerConfig
expiration time.Time
mu sync.Mutex
}
// Enabled implements dockerConfigProvider
func (d *defaultDockerConfigProvider) Enabled() bool {
return true
}
// Provide implements dockerConfigProvider
func (d *defaultDockerConfigProvider) Provide() DockerConfig {
// Read the standard Docker credentials from .dockercfg
if cfg, err := ReadDockerConfigFile(); err == nil {
return cfg
} else if !os.IsNotExist(err) {
glog.V(1).Infof("Unable to parse Docker config file: %v", err)
}
return DockerConfig{}
}
// Enabled implements dockerConfigProvider
func (d *CachingDockerConfigProvider) Enabled() bool {
return d.Provider.Enabled()
}
// Provide implements dockerConfigProvider
func (d *CachingDockerConfigProvider) Provide() DockerConfig {
d.mu.Lock()
defer d.mu.Unlock()
// If the cache hasn't expired, return our cache
if time.Now().Before(d.expiration) {
return d.cacheDockerConfig
}
glog.Infof("Refreshing cache for provider: %v", reflect.TypeOf(d.Provider).String())
d.cacheDockerConfig = d.Provider.Provide()
d.expiration = time.Now().Add(d.Lifetime)
return d.cacheDockerConfig
}

View File

@ -0,0 +1,62 @@
/*
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 credentialprovider
import (
"testing"
"time"
)
func TestCachingProvider(t *testing.T) {
provider := &testProvider{
Count: 0,
}
cache := &CachingDockerConfigProvider{
Provider: provider,
Lifetime: 1 * time.Second,
}
if provider.Count != 0 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
cache.Provide()
cache.Provide()
cache.Provide()
cache.Provide()
if provider.Count != 1 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
time.Sleep(cache.Lifetime)
cache.Provide()
cache.Provide()
cache.Provide()
cache.Provide()
if provider.Count != 2 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
time.Sleep(cache.Lifetime)
cache.Provide()
cache.Provide()
cache.Provide()
cache.Provide()
if provider.Count != 3 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
}

View File

@ -25,15 +25,14 @@ import (
"io"
"io/ioutil"
"math/rand"
"os"
"os/exec"
"sort"
"strconv"
"strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
"github.com/fsouza/go-dockerclient"
docker "github.com/fsouza/go-dockerclient"
"github.com/golang/glog"
)
@ -65,7 +64,7 @@ type DockerPuller interface {
// dockerPuller is the default implementation of DockerPuller.
type dockerPuller struct {
client DockerInterface
keyring *dockerKeyring
keyring credentialprovider.DockerKeyring
}
type throttledDockerPuller struct {
@ -77,19 +76,9 @@ type throttledDockerPuller struct {
func NewDockerPuller(client DockerInterface, qps float32, burst int) DockerPuller {
dp := dockerPuller{
client: client,
keyring: newDockerKeyring(),
keyring: credentialprovider.NewDockerKeyring(),
}
cfg, err := readDockerConfigFile()
if err == nil {
cfg.addToKeyring(dp.keyring)
} else if !os.IsNotExist(err) {
glog.V(1).Infof("Unable to parse Docker config file: %v", err)
}
if dp.keyring.count() == 0 {
glog.V(1).Infof("Continuing with empty Docker keyring")
}
if qps == 0.0 {
return dp
}
@ -218,7 +207,7 @@ func (p dockerPuller) Pull(image string) error {
Tag: tag,
}
creds, ok := p.keyring.lookup(image)
creds, ok := p.keyring.Lookup(image)
if !ok {
glog.V(1).Infof("Pulling image %s without credentials", image)
}
@ -608,62 +597,3 @@ 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)))
}
const defaultRegistryHost = "index.docker.io/v1/"
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
}
// use credentials for the default registry if provided
if auth, ok := dk.creds[defaultRegistryHost]; ok {
return auth, true
}
return docker.AuthConfiguration{}, false
}
func (dk dockerKeyring) count() int {
return len(dk.creds)
}

View File

@ -23,7 +23,8 @@ import (
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/fsouza/go-dockerclient"
"github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider"
docker "github.com/fsouza/go-dockerclient"
)
func verifyCalls(t *testing.T, fakeDocker *FakeDockerClient, calls []string) {
@ -213,9 +214,19 @@ func TestDockerKeyringLookup(t *testing.T) {
Email: "grace@example.com",
}
dk := newDockerKeyring()
dk.add("bar.example.com/pong", grace)
dk.add("bar.example.com", ada)
dk := &credentialprovider.BasicDockerKeyring{}
dk.Add(credentialprovider.DockerConfig{
"bar.example.com/pong": credentialprovider.DockerConfigEntry{
Username: grace.Username,
Password: grace.Password,
Email: grace.Email,
},
"bar.example.com": credentialprovider.DockerConfigEntry{
Username: ada.Username,
Password: ada.Password,
Email: ada.Email,
},
})
tests := []struct {
image string
@ -243,7 +254,7 @@ func TestDockerKeyringLookup(t *testing.T) {
}
for i, tt := range tests {
match, ok := dk.lookup(tt.image)
match, ok := dk.Lookup(tt.image)
if tt.ok != ok {
t.Errorf("case %d: expected ok=%t, got %t", i, tt.ok, ok)
}

View File

@ -38,7 +38,7 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler"
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/factory"
"github.com/fsouza/go-dockerclient"
docker "github.com/fsouza/go-dockerclient"
"github.com/golang/glog"
)