mirror of https://github.com/k3s-io/k3s
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
parent
931cd3a2df
commit
0c5d9ed0d2
|
@ -31,7 +31,7 @@ MASTER_TAG="${INSTANCE_PREFIX}-master"
|
||||||
MINION_TAG="${INSTANCE_PREFIX}-minion"
|
MINION_TAG="${INSTANCE_PREFIX}-minion"
|
||||||
MINION_NAMES=($(eval echo ${INSTANCE_PREFIX}-minion-{1..${NUM_MINIONS}}))
|
MINION_NAMES=($(eval echo ${INSTANCE_PREFIX}-minion-{1..${NUM_MINIONS}}))
|
||||||
MINION_IP_RANGES=($(eval echo "10.244.{1..${NUM_MINIONS}}.0/24"))
|
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.
|
# Increase the sleep interval value if concerned about API rate limits. 3, in seconds, is the default.
|
||||||
POLL_SLEEP_INTERVAL=3
|
POLL_SLEEP_INTERVAL=3
|
||||||
PORTAL_NET="10.0.0.0/16"
|
PORTAL_NET="10.0.0.0/16"
|
||||||
|
|
|
@ -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"
|
||||||
|
)
|
|
@ -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
|
|
@ -14,26 +14,38 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package dockertools
|
package credentialprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/url"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fsouza/go-dockerclient"
|
|
||||||
"github.com/golang/glog"
|
"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 (
|
const (
|
||||||
dockerConfigFileLocation = ".dockercfg"
|
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)
|
absDockerConfigFileLocation, err := filepath.Abs(dockerConfigFileLocation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("while trying to canonicalize %s: %v", dockerConfigFileLocation, err)
|
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)
|
glog.Errorf("while trying to read %s: %v", absDockerConfigFileLocation, err)
|
||||||
return nil, 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 {
|
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 nil, err
|
||||||
}
|
}
|
||||||
return
|
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
|
// dockerConfigEntryWithAuth is used solely for deserializing the Auth field
|
||||||
// into a dockerConfigEntry during JSON deserialization.
|
// into a dockerConfigEntry during JSON deserialization.
|
||||||
type dockerConfigEntryWithAuth struct {
|
type dockerConfigEntryWithAuth struct {
|
||||||
|
@ -90,7 +118,7 @@ type dockerConfigEntryWithAuth struct {
|
||||||
Auth string
|
Auth string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ident *dockerConfigEntry) UnmarshalJSON(data []byte) error {
|
func (ident *DockerConfigEntry) UnmarshalJSON(data []byte) error {
|
||||||
var tmp dockerConfigEntryWithAuth
|
var tmp dockerConfigEntryWithAuth
|
||||||
err := json.Unmarshal(data, &tmp)
|
err := json.Unmarshal(data, &tmp)
|
||||||
if err != nil {
|
if err != nil {
|
|
@ -14,20 +14,18 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package dockertools
|
package credentialprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/fsouza/go-dockerclient"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDockerConfigJSONDecode(t *testing.T) {
|
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"}}`)
|
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": {
|
"http://foo.example.com": {
|
||||||
Username: "foo",
|
Username: "foo",
|
||||||
Password: "bar",
|
Password: "bar",
|
||||||
|
@ -40,7 +38,7 @@ func TestDockerConfigJSONDecode(t *testing.T) {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
var output dockerConfig
|
var output DockerConfig
|
||||||
err := json.Unmarshal(input, &output)
|
err := json.Unmarshal(input, &output)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Received unexpected error: %v", err)
|
t.Errorf("Received unexpected error: %v", err)
|
||||||
|
@ -54,13 +52,13 @@ func TestDockerConfigJSONDecode(t *testing.T) {
|
||||||
func TestDockerConfigEntryJSONDecode(t *testing.T) {
|
func TestDockerConfigEntryJSONDecode(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
input []byte
|
input []byte
|
||||||
expect dockerConfigEntry
|
expect DockerConfigEntry
|
||||||
fail bool
|
fail bool
|
||||||
}{
|
}{
|
||||||
// simple case, just decode the fields
|
// simple case, just decode the fields
|
||||||
{
|
{
|
||||||
input: []byte(`{"username": "foo", "password": "bar", "email": "foo@example.com"}`),
|
input: []byte(`{"username": "foo", "password": "bar", "email": "foo@example.com"}`),
|
||||||
expect: dockerConfigEntry{
|
expect: DockerConfigEntry{
|
||||||
Username: "foo",
|
Username: "foo",
|
||||||
Password: "bar",
|
Password: "bar",
|
||||||
Email: "foo@example.com",
|
Email: "foo@example.com",
|
||||||
|
@ -71,7 +69,7 @@ func TestDockerConfigEntryJSONDecode(t *testing.T) {
|
||||||
// auth field decodes to username & password
|
// auth field decodes to username & password
|
||||||
{
|
{
|
||||||
input: []byte(`{"auth": "Zm9vOmJhcg==", "email": "foo@example.com"}`),
|
input: []byte(`{"auth": "Zm9vOmJhcg==", "email": "foo@example.com"}`),
|
||||||
expect: dockerConfigEntry{
|
expect: DockerConfigEntry{
|
||||||
Username: "foo",
|
Username: "foo",
|
||||||
Password: "bar",
|
Password: "bar",
|
||||||
Email: "foo@example.com",
|
Email: "foo@example.com",
|
||||||
|
@ -82,7 +80,7 @@ func TestDockerConfigEntryJSONDecode(t *testing.T) {
|
||||||
// auth field overrides username & password
|
// auth field overrides username & password
|
||||||
{
|
{
|
||||||
input: []byte(`{"username": "foo", "password": "bar", "auth": "cGluZzpwb25n", "email": "foo@example.com"}`),
|
input: []byte(`{"username": "foo", "password": "bar", "auth": "cGluZzpwb25n", "email": "foo@example.com"}`),
|
||||||
expect: dockerConfigEntry{
|
expect: DockerConfigEntry{
|
||||||
Username: "ping",
|
Username: "ping",
|
||||||
Password: "pong",
|
Password: "pong",
|
||||||
Email: "foo@example.com",
|
Email: "foo@example.com",
|
||||||
|
@ -93,7 +91,7 @@ func TestDockerConfigEntryJSONDecode(t *testing.T) {
|
||||||
// poorly-formatted auth causes failure
|
// poorly-formatted auth causes failure
|
||||||
{
|
{
|
||||||
input: []byte(`{"auth": "pants", "email": "foo@example.com"}`),
|
input: []byte(`{"auth": "pants", "email": "foo@example.com"}`),
|
||||||
expect: dockerConfigEntry{
|
expect: DockerConfigEntry{
|
||||||
Username: "",
|
Username: "",
|
||||||
Password: "",
|
Password: "",
|
||||||
Email: "foo@example.com",
|
Email: "foo@example.com",
|
||||||
|
@ -104,7 +102,7 @@ func TestDockerConfigEntryJSONDecode(t *testing.T) {
|
||||||
// invalid JSON causes failure
|
// invalid JSON causes failure
|
||||||
{
|
{
|
||||||
input: []byte(`{"email": false}`),
|
input: []byte(`{"email": false}`),
|
||||||
expect: dockerConfigEntry{
|
expect: DockerConfigEntry{
|
||||||
Username: "",
|
Username: "",
|
||||||
Password: "",
|
Password: "",
|
||||||
Email: "",
|
Email: "",
|
||||||
|
@ -114,7 +112,7 @@ func TestDockerConfigEntryJSONDecode(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
var output dockerConfigEntry
|
var output DockerConfigEntry
|
||||||
err := json.Unmarshal(tt.input, &output)
|
err := json.Unmarshal(tt.input, &output)
|
||||||
if (err != nil) != tt.fail {
|
if (err != nil) != tt.fail {
|
||||||
t.Errorf("case %d: expected fail=%t, got err=%v", i, tt.fail, err)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,15 +25,14 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
"github.com/fsouza/go-dockerclient"
|
docker "github.com/fsouza/go-dockerclient"
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,7 +64,7 @@ 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
|
keyring credentialprovider.DockerKeyring
|
||||||
}
|
}
|
||||||
|
|
||||||
type throttledDockerPuller struct {
|
type throttledDockerPuller struct {
|
||||||
|
@ -77,19 +76,9 @@ type throttledDockerPuller struct {
|
||||||
func NewDockerPuller(client DockerInterface, qps float32, burst int) DockerPuller {
|
func NewDockerPuller(client DockerInterface, qps float32, burst int) DockerPuller {
|
||||||
dp := dockerPuller{
|
dp := dockerPuller{
|
||||||
client: client,
|
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 {
|
if qps == 0.0 {
|
||||||
return dp
|
return dp
|
||||||
}
|
}
|
||||||
|
@ -218,7 +207,7 @@ func (p dockerPuller) Pull(image string) error {
|
||||||
Tag: tag,
|
Tag: tag,
|
||||||
}
|
}
|
||||||
|
|
||||||
creds, ok := p.keyring.lookup(image)
|
creds, ok := p.keyring.Lookup(image)
|
||||||
if !ok {
|
if !ok {
|
||||||
glog.V(1).Infof("Pulling image %s without credentials", image)
|
glog.V(1).Infof("Pulling image %s without credentials", image)
|
||||||
}
|
}
|
||||||
|
@ -608,62 +597,3 @@ 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)))
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -23,7 +23,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"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) {
|
func verifyCalls(t *testing.T, fakeDocker *FakeDockerClient, calls []string) {
|
||||||
|
@ -213,9 +214,19 @@ func TestDockerKeyringLookup(t *testing.T) {
|
||||||
Email: "grace@example.com",
|
Email: "grace@example.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
dk := newDockerKeyring()
|
dk := &credentialprovider.BasicDockerKeyring{}
|
||||||
dk.add("bar.example.com/pong", grace)
|
dk.Add(credentialprovider.DockerConfig{
|
||||||
dk.add("bar.example.com", ada)
|
"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 {
|
tests := []struct {
|
||||||
image string
|
image string
|
||||||
|
@ -243,7 +254,7 @@ func TestDockerKeyringLookup(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, tt := range tests {
|
for i, tt := range tests {
|
||||||
match, ok := dk.lookup(tt.image)
|
match, ok := dk.Lookup(tt.image)
|
||||||
if tt.ok != ok {
|
if tt.ok != ok {
|
||||||
t.Errorf("case %d: expected ok=%t, got %t", i, tt.ok, ok)
|
t.Errorf("case %d: expected ok=%t, got %t", i, tt.ok, ok)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ import (
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler"
|
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/factory"
|
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/scheduler/factory"
|
||||||
|
|
||||||
"github.com/fsouza/go-dockerclient"
|
docker "github.com/fsouza/go-dockerclient"
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue