From 675d82cd935b1fec03c097e1a9b3ef2a2ed0f78f Mon Sep 17 00:00:00 2001 From: Matt Moore Date: Sat, 15 Nov 2014 05:50:59 -0800 Subject: [PATCH 1/7] 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. (cherry picked from commit 0c5d9ed0d21f0ebace1b5bd437077bc6f6e070f2) --- cluster/gce/config-default.sh | 2 +- cmd/kubelet/plugins.go | 24 ++ hack/e2e-suite/private.sh | 86 +++++ .../config.go | 98 +++--- .../config_test.go | 60 +--- pkg/credentialprovider/doc.go | 19 ++ pkg/credentialprovider/gcp/doc.go | 19 ++ pkg/credentialprovider/gcp/metadata.go | 190 ++++++++++++ pkg/credentialprovider/gcp/metadata_test.go | 293 ++++++++++++++++++ pkg/credentialprovider/keyring.go | 134 ++++++++ pkg/credentialprovider/keyring_test.go | 261 ++++++++++++++++ pkg/credentialprovider/plugins.go | 62 ++++ pkg/credentialprovider/provider.go | 95 ++++++ pkg/credentialprovider/provider_test.go | 62 ++++ pkg/kubelet/dockertools/docker.go | 80 +---- pkg/kubelet/dockertools/docker_test.go | 21 +- pkg/standalone/standalone.go | 2 +- 17 files changed, 1341 insertions(+), 167 deletions(-) create mode 100644 cmd/kubelet/plugins.go create mode 100755 hack/e2e-suite/private.sh rename pkg/{kubelet/dockertools => credentialprovider}/config.go (60%) rename pkg/{kubelet/dockertools => credentialprovider}/config_test.go (76%) create mode 100644 pkg/credentialprovider/doc.go create mode 100644 pkg/credentialprovider/gcp/doc.go create mode 100644 pkg/credentialprovider/gcp/metadata.go create mode 100644 pkg/credentialprovider/gcp/metadata_test.go create mode 100644 pkg/credentialprovider/keyring.go create mode 100644 pkg/credentialprovider/keyring_test.go create mode 100644 pkg/credentialprovider/plugins.go create mode 100644 pkg/credentialprovider/provider.go create mode 100644 pkg/credentialprovider/provider_test.go diff --git a/cluster/gce/config-default.sh b/cluster/gce/config-default.sh index 15a581ea643..7ff2c4e139f 100755 --- a/cluster/gce/config-default.sh +++ b/cluster/gce/config-default.sh @@ -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" diff --git a/cmd/kubelet/plugins.go b/cmd/kubelet/plugins.go new file mode 100644 index 00000000000..de10a7d65ca --- /dev/null +++ b/cmd/kubelet/plugins.go @@ -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" +) diff --git a/hack/e2e-suite/private.sh b/hack/e2e-suite/private.sh new file mode 100755 index 00000000000..1401ed69d5e --- /dev/null +++ b/hack/e2e-suite/private.sh @@ -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 diff --git a/pkg/kubelet/dockertools/config.go b/pkg/credentialprovider/config.go similarity index 60% rename from pkg/kubelet/dockertools/config.go rename to pkg/credentialprovider/config.go index f52351cb6f5..f1880291872 100644 --- a/pkg/kubelet/dockertools/config.go +++ b/pkg/credentialprovider/config.go @@ -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 { diff --git a/pkg/kubelet/dockertools/config_test.go b/pkg/credentialprovider/config_test.go similarity index 76% rename from pkg/kubelet/dockertools/config_test.go rename to pkg/credentialprovider/config_test.go index 9b1a75e48bf..2508552e16f 100644 --- a/pkg/kubelet/dockertools/config_test.go +++ b/pkg/credentialprovider/config_test.go @@ -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) - } -} diff --git a/pkg/credentialprovider/doc.go b/pkg/credentialprovider/doc.go new file mode 100644 index 00000000000..22bb33e0bda --- /dev/null +++ b/pkg/credentialprovider/doc.go @@ -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 diff --git a/pkg/credentialprovider/gcp/doc.go b/pkg/credentialprovider/gcp/doc.go new file mode 100644 index 00000000000..4f5c7d5ebd5 --- /dev/null +++ b/pkg/credentialprovider/gcp/doc.go @@ -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 diff --git a/pkg/credentialprovider/gcp/metadata.go b/pkg/credentialprovider/gcp/metadata.go new file mode 100644 index 00000000000..dd7149c42a9 --- /dev/null +++ b/pkg/credentialprovider/gcp/metadata.go @@ -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 +} diff --git a/pkg/credentialprovider/gcp/metadata_test.go b/pkg/credentialprovider/gcp/metadata_test.go new file mode 100644 index 00000000000..e363e53a0df --- /dev/null +++ b/pkg/credentialprovider/gcp/metadata_test.go @@ -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()) + } + } +} diff --git a/pkg/credentialprovider/keyring.go b/pkg/credentialprovider/keyring.go new file mode 100644 index 00000000000..518cacb4aef --- /dev/null +++ b/pkg/credentialprovider/keyring.go @@ -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) +} diff --git a/pkg/credentialprovider/keyring_test.go b/pkg/credentialprovider/keyring_test.go new file mode 100644 index 00000000000..e4835bc6d50 --- /dev/null +++ b/pkg/credentialprovider/keyring_test.go @@ -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) + } +} diff --git a/pkg/credentialprovider/plugins.go b/pkg/credentialprovider/plugins.go new file mode 100644 index 00000000000..d92570ca8ea --- /dev/null +++ b/pkg/credentialprovider/plugins.go @@ -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 +} diff --git a/pkg/credentialprovider/provider.go b/pkg/credentialprovider/provider.go new file mode 100644 index 00000000000..9ce5e93a553 --- /dev/null +++ b/pkg/credentialprovider/provider.go @@ -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 +} diff --git a/pkg/credentialprovider/provider_test.go b/pkg/credentialprovider/provider_test.go new file mode 100644 index 00000000000..0c279232caa --- /dev/null +++ b/pkg/credentialprovider/provider_test.go @@ -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) + } +} diff --git a/pkg/kubelet/dockertools/docker.go b/pkg/kubelet/dockertools/docker.go index 481ee7096e9..f14cb616168 100644 --- a/pkg/kubelet/dockertools/docker.go +++ b/pkg/kubelet/dockertools/docker.go @@ -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) -} diff --git a/pkg/kubelet/dockertools/docker_test.go b/pkg/kubelet/dockertools/docker_test.go index f7a814dce10..dda9ae34bd5 100644 --- a/pkg/kubelet/dockertools/docker_test.go +++ b/pkg/kubelet/dockertools/docker_test.go @@ -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) } diff --git a/pkg/standalone/standalone.go b/pkg/standalone/standalone.go index bf8bf85c7a6..839276ed158 100644 --- a/pkg/standalone/standalone.go +++ b/pkg/standalone/standalone.go @@ -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" ) From 618a367dbb48dbaa9c3b50e877858d854322fd0b Mon Sep 17 00:00:00 2001 From: Eric Tune Date: Thu, 13 Nov 2014 15:45:34 -0800 Subject: [PATCH 2/7] Kubelet talks securely to apiserver. Configure apiserver to serve Securely on port 6443. Generate token for kubelets during master VM startup. Put token into file apiserver can get and another file the kubelets can get. Added e2e test. (cherry picked from commit 46dcacfa93bfa3a73c7a5b3740c29dfa9440a375) --- .../templates/create-dynamic-salt-files.sh | 16 +++++ cluster/saltbase/salt/kube-apiserver/default | 4 +- cluster/saltbase/salt/kube-apiserver/init.sls | 7 ++ cluster/saltbase/salt/kubelet/default | 10 ++- cluster/saltbase/salt/kubelet/init.sls | 9 +++ cmd/e2e/e2e.go | 70 +++++++++++++++++++ 6 files changed, 114 insertions(+), 2 deletions(-) diff --git a/cluster/gce/templates/create-dynamic-salt-files.sh b/cluster/gce/templates/create-dynamic-salt-files.sh index 0183e286007..b059aab6ac6 100644 --- a/cluster/gce/templates/create-dynamic-salt-files.sh +++ b/cluster/gce/templates/create-dynamic-salt-files.sh @@ -28,3 +28,19 @@ EOF mkdir -p /srv/salt-overlay/salt/nginx echo $MASTER_HTPASSWD > /srv/salt-overlay/salt/nginx/htpasswd + +# TODO: do aws. + +# Generate and distribute a shared secret (bearer token) to +# apiserver and kubelet so that kubelet can authenticate to +# apiserver to send events. +# This works on CoreOS, so it should work on a lot of distros. +kubelet_token=$(cat /dev/urandom | base64 | tr -d "=+/" | dd bs=32 count=1 2> /dev/null) + +mkdir -p /srv/salt-overlay/salt/kube-apiserver +known_tokens_file="/srv/salt-overlay/salt/kube-apiserver/known_tokens.csv" +(umask u=rw,go= ; echo "$kubelet_token,kubelet,kubelet" > $known_tokens_file) + +mkdir -p /srv/salt-overlay/salt/kubelet +kubelet_auth_file="/srv/salt-overlay/salt/kubelet/kubernetes_auth" +(umask u=rw,go= ; echo "{\"BearerToken\": \"$kubelet_token\", \"Insecure\": true }" > $kubelet_auth_file) diff --git a/cluster/saltbase/salt/kube-apiserver/default b/cluster/saltbase/salt/kube-apiserver/default index e39024faf2f..04c55a310d8 100644 --- a/cluster/saltbase/salt/kube-apiserver/default +++ b/cluster/saltbase/salt/kube-apiserver/default @@ -29,5 +29,7 @@ {% set cert_file = "-tls_cert_file=/srv/kubernetes/server.cert" %} {% set key_file = "-tls_private_key_file=/srv/kubernetes/server.key" %} +{% set secure_port = "-secure_port=6443" %} +{% set token_auth_file = "-token_auth_file=/srv/kubernetes/known_tokens.csv" %} -DAEMON_ARGS="{{daemon_args}} {{address}} {{etcd_servers}} {{ cloud_provider }} --allow_privileged={{pillar['allow_privileged']}} {{portal_net}} {{cert_file}} {{key_file}}" +DAEMON_ARGS="{{daemon_args}} {{address}} {{etcd_servers}} {{ cloud_provider }} --allow_privileged={{pillar['allow_privileged']}} {{portal_net}} {{cert_file}} {{key_file}} {{secure_port}} {{token_auth_file}}" diff --git a/cluster/saltbase/salt/kube-apiserver/init.sls b/cluster/saltbase/salt/kube-apiserver/init.sls index 11427bf1911..fab9e7367a3 100644 --- a/cluster/saltbase/salt/kube-apiserver/init.sls +++ b/cluster/saltbase/salt/kube-apiserver/init.sls @@ -38,6 +38,13 @@ {% endif %} +/srv/kubernetes/known_tokens.csv: + file.managed: + - source: salt://kube-apiserver/known_tokens.csv + - user: kube-apiserver + - group: kube-apiserver + - mode: 400 + kube-apiserver: group.present: - system: True diff --git a/cluster/saltbase/salt/kubelet/default b/cluster/saltbase/salt/kubelet/default index 812cf47b5b2..d50172d40ba 100644 --- a/cluster/saltbase/salt/kubelet/default +++ b/cluster/saltbase/salt/kubelet/default @@ -9,6 +9,13 @@ {% set etcd_servers = "-etcd_servers=http://" + ips[0][0] + ":4001" %} {% endif %} +{% if grains.apiservers is defined %} + {% set apiservers = "-api_servers=https://" + grains.apiservers + ":6443" %} +{% else %} + {% set ips = salt['mine.get']('roles:kubernetes-master', 'network.ip_addrs', 'grain').values() %} + {% set apiservers = "-api_servers=https://" + ips[0][0] + ":6443" %} +{% endif %} + {% set address = "-address=0.0.0.0" %} {% set config = "-config=/etc/kubernetes/manifests" %} {% set hostname_override = "" %} @@ -16,5 +23,6 @@ {% set hostname_override = " -hostname_override=" + grains.minion_ip %} {% endif %} +{% set auth_path = "-auth_path=/var/lib/kubelet/kubernetes_auth" %} -DAEMON_ARGS="{{daemon_args}} {{etcd_servers}} {{hostname_override}} {{address}} {{config}} --allow_privileged={{pillar['allow_privileged']}}" +DAEMON_ARGS="{{daemon_args}} {{etcd_servers}} {{apiservers}} {{auth_path}} {{hostname_override}} {{address}} {{config}} --allow_privileged={{pillar['allow_privileged']}}" diff --git a/cluster/saltbase/salt/kubelet/init.sls b/cluster/saltbase/salt/kubelet/init.sls index bd71bb61cef..e7493af8e0f 100644 --- a/cluster/saltbase/salt/kubelet/init.sls +++ b/cluster/saltbase/salt/kubelet/init.sls @@ -38,6 +38,14 @@ {% endif %} +# Kubelet will run without this file but will not be able to send events to the apiserver. +/var/lib/kubelet/kubernetes_auth: + file.managed: + - source: salt://kubelet/kubernetes_auth + - user: root + - group: root + - mode: 400 + kubelet: group.present: - system: True @@ -57,4 +65,5 @@ kubelet: {% if grains['os_family'] != 'RedHat' %} - file: /etc/init.d/kubelet {% endif %} + - file: /var/lib/kubelet/kubernetes_auth diff --git a/cmd/e2e/e2e.go b/cmd/e2e/e2e.go index 031997f77f1..505c39f568e 100644 --- a/cmd/e2e/e2e.go +++ b/cmd/e2e/e2e.go @@ -169,6 +169,75 @@ func TestPodUpdate(c *client.Client) bool { return true } +// TestKubeletSendsEvent checks that kubelets and scheduler send events about pods scheduling and running. +func TestKubeletSendsEvent(c *client.Client) bool { + podClient := c.Pods(api.NamespaceDefault) + + pod := loadPodOrDie("./api/examples/pod.json") + value := strconv.Itoa(time.Now().Nanosecond()) + pod.Labels["time"] = value + + _, err := podClient.Create(pod) + if err != nil { + glog.Errorf("Failed to create pod: %v", err) + return false + } + defer podClient.Delete(pod.Name) + waitForPodRunning(c, pod.Name) + pods, err := podClient.List(labels.SelectorFromSet(labels.Set(map[string]string{"time": value}))) + if len(pods.Items) != 1 { + glog.Errorf("Failed to find the correct pod") + return false + } + + _, err = podClient.Get(pod.Name) + if err != nil { + glog.Errorf("Failed to get pod: %v", err) + return false + } + + // Check for scheduler event about the pod. + events, err := c.Events(api.NamespaceDefault).List( + labels.Everything(), + labels.Set{ + "involvedObject.name": pod.Name, + "involvedObject.kind": "Pod", + "involvedObject.namespace": api.NamespaceDefault, + "source": "scheduler", + }.AsSelector(), + ) + if err != nil { + glog.Error("Error while listing events:", err) + return false + } + if len(events.Items) == 0 { + glog.Error("Didn't see any scheduler events even though pod was running.") + return false + } + glog.Info("Saw scheduler event for our pod.") + + // Check for kubelet event about the pod. + events, err = c.Events(api.NamespaceDefault).List( + labels.Everything(), + labels.Set{ + "involvedObject.name": pod.Name, + "involvedObject.kind": "BoundPod", + "involvedObject.namespace": api.NamespaceDefault, + "source": "kubelet", + }.AsSelector(), + ) + if err != nil { + glog.Error("Error while listing events:", err) + return false + } + if len(events.Items) == 0 { + glog.Error("Didn't see any kubelet events even though pod was running.") + return false + } + glog.Info("Saw kubelet event for our pod.") + return true +} + func main() { flag.Parse() runtime.GOMAXPROCS(runtime.NumCPU()) @@ -186,6 +255,7 @@ func main() { tests := []func(c *client.Client) bool{ TestKubernetesROService, + TestKubeletSendsEvent, // TODO(brendandburns): fix this test and re-add it: TestPodUpdate, } From f27bf783f5166c00777214b5d91a0b7bf9e8246d Mon Sep 17 00:00:00 2001 From: Eric Tune Date: Mon, 17 Nov 2014 23:33:07 -0800 Subject: [PATCH 3/7] Don't reference non-existent token file if not GCE. (cherry picked from commit 67adf6473eb4ff576e743987cccb3158363db2c8) --- cluster/gce/templates/create-dynamic-salt-files.sh | 2 -- cluster/saltbase/salt/kube-apiserver/default | 10 +++++++++- cluster/saltbase/salt/kube-apiserver/init.sls | 6 ++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/cluster/gce/templates/create-dynamic-salt-files.sh b/cluster/gce/templates/create-dynamic-salt-files.sh index b059aab6ac6..c02ff5d7e63 100644 --- a/cluster/gce/templates/create-dynamic-salt-files.sh +++ b/cluster/gce/templates/create-dynamic-salt-files.sh @@ -29,8 +29,6 @@ EOF mkdir -p /srv/salt-overlay/salt/nginx echo $MASTER_HTPASSWD > /srv/salt-overlay/salt/nginx/htpasswd -# TODO: do aws. - # Generate and distribute a shared secret (bearer token) to # apiserver and kubelet so that kubelet can authenticate to # apiserver to send events. diff --git a/cluster/saltbase/salt/kube-apiserver/default b/cluster/saltbase/salt/kube-apiserver/default index 04c55a310d8..4e6e51f8595 100644 --- a/cluster/saltbase/salt/kube-apiserver/default +++ b/cluster/saltbase/salt/kube-apiserver/default @@ -29,7 +29,15 @@ {% set cert_file = "-tls_cert_file=/srv/kubernetes/server.cert" %} {% set key_file = "-tls_private_key_file=/srv/kubernetes/server.key" %} + {% set secure_port = "-secure_port=6443" %} -{% set token_auth_file = "-token_auth_file=/srv/kubernetes/known_tokens.csv" %} +{% set token_auth_file = "-token_auth_file=/dev/null" %} + +{% if grains.cloud is defined %} +{% if grains.cloud == 'gce' %} + # TODO: generate and distribute tokens for other cloud providers. + {% set token_auth_file = "-token_auth_file=/srv/kubernetes/known_tokens.csv" %} +{% endif %} +{% endif %} DAEMON_ARGS="{{daemon_args}} {{address}} {{etcd_servers}} {{ cloud_provider }} --allow_privileged={{pillar['allow_privileged']}} {{portal_net}} {{cert_file}} {{key_file}} {{secure_port}} {{token_auth_file}}" diff --git a/cluster/saltbase/salt/kube-apiserver/init.sls b/cluster/saltbase/salt/kube-apiserver/init.sls index fab9e7367a3..ba21f2482d5 100644 --- a/cluster/saltbase/salt/kube-apiserver/init.sls +++ b/cluster/saltbase/salt/kube-apiserver/init.sls @@ -38,6 +38,9 @@ {% endif %} +{% if grains.cloud is defined %} +{% if grains.cloud == 'gce' %} +# TODO: generate and distribute tokens on other cloud providers. /srv/kubernetes/known_tokens.csv: file.managed: - source: salt://kube-apiserver/known_tokens.csv @@ -45,6 +48,9 @@ - group: kube-apiserver - mode: 400 +{% endif %} +{% endif %} + kube-apiserver: group.present: - system: True From 4df8c62c9a99fad78ccf4cf5ac36ad2f857223e3 Mon Sep 17 00:00:00 2001 From: Eric Tune Date: Tue, 18 Nov 2014 00:22:01 -0800 Subject: [PATCH 4/7] Only test events on GCE. (cherry picked from commit 4dbdfd79357e4a656e59ae074201ae335b80a6d0) --- cmd/e2e/e2e.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cmd/e2e/e2e.go b/cmd/e2e/e2e.go index 505c39f568e..1d0765fab2f 100644 --- a/cmd/e2e/e2e.go +++ b/cmd/e2e/e2e.go @@ -171,6 +171,16 @@ func TestPodUpdate(c *client.Client) bool { // TestKubeletSendsEvent checks that kubelets and scheduler send events about pods scheduling and running. func TestKubeletSendsEvent(c *client.Client) bool { + provider := os.Getenv("KUBERNETES_PROVIDER") + if provider == "" { + glog.Errorf("unable to detect cloud type.") + return false + } + if provider != "gce" { + glog.Infof("skipping TestKubeletSendsEvent on cloud provider %s", provider) + return true + } + podClient := c.Pods(api.NamespaceDefault) pod := loadPodOrDie("./api/examples/pod.json") @@ -204,6 +214,7 @@ func TestKubeletSendsEvent(c *client.Client) bool { "involvedObject.kind": "Pod", "involvedObject.namespace": api.NamespaceDefault, "source": "scheduler", + "time": value, }.AsSelector(), ) if err != nil { @@ -265,6 +276,8 @@ func main() { if !testPassed { passed = false } + // TODO: clean up objects created during a test after the test, so cases + // are independent. } if !passed { glog.Fatalf("Tests failed") From a26f65f4a34d1fcc869ca1590d170f5be4bd819b Mon Sep 17 00:00:00 2001 From: Eric Tune Date: Tue, 18 Nov 2014 10:13:45 -0800 Subject: [PATCH 5/7] Treat unset KUBERNETES_PROVIDER as gce. Should fix jenkins failure. (cherry picked from commit 057d78e47108e81b6842ffac0ba22f2adb0f664c) --- cmd/e2e/e2e.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd/e2e/e2e.go b/cmd/e2e/e2e.go index 1d0765fab2f..9099bfac06f 100644 --- a/cmd/e2e/e2e.go +++ b/cmd/e2e/e2e.go @@ -172,14 +172,13 @@ func TestPodUpdate(c *client.Client) bool { // TestKubeletSendsEvent checks that kubelets and scheduler send events about pods scheduling and running. func TestKubeletSendsEvent(c *client.Client) bool { provider := os.Getenv("KUBERNETES_PROVIDER") - if provider == "" { - glog.Errorf("unable to detect cloud type.") - return false - } if provider != "gce" { glog.Infof("skipping TestKubeletSendsEvent on cloud provider %s", provider) return true } + if provider == "" { + glog.Info("KUBERNETES_PROVIDER is unset assuming \"gce\"") + } podClient := c.Pods(api.NamespaceDefault) From 3c409cdf2636ff225682d8f3aaff44fbfa5abb21 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Tue, 18 Nov 2014 12:32:02 -0800 Subject: [PATCH 6/7] Add the 0.5.1 release --- pkg/version/base.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/version/base.go b/pkg/version/base.go index 831bd78d1f8..a809b9304b2 100644 --- a/pkg/version/base.go +++ b/pkg/version/base.go @@ -37,7 +37,7 @@ var ( // TODO: Deprecate gitMajor and gitMinor, use only gitVersion instead. gitMajor string = "0" // major version, always numeric gitMinor string = "5" // minor version, numeric possibly followed by "+" - gitVersion string = "v0.5" // version from git, output of $(git describe) + gitVersion string = "v0.5.1" // version from git, output of $(git describe) gitCommit string = "" // sha1 from git, output of $(git rev-parse HEAD) gitTreeState string = "not a git tree" // state of git tree, either "clean" or "dirty" ) From facaf4f8972fcea55e4cc2f2797afc61c27b1493 Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Tue, 18 Nov 2014 12:32:28 -0800 Subject: [PATCH 7/7] Add the 0.5.1-dev --- pkg/version/base.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/version/base.go b/pkg/version/base.go index a809b9304b2..0406a11871e 100644 --- a/pkg/version/base.go +++ b/pkg/version/base.go @@ -36,8 +36,8 @@ package version var ( // TODO: Deprecate gitMajor and gitMinor, use only gitVersion instead. gitMajor string = "0" // major version, always numeric - gitMinor string = "5" // minor version, numeric possibly followed by "+" - gitVersion string = "v0.5.1" // version from git, output of $(git describe) + gitMinor string = "5+" // minor version, numeric possibly followed by "+" + gitVersion string = "v0.5.1-dev" // version from git, output of $(git describe) gitCommit string = "" // sha1 from git, output of $(git rev-parse HEAD) gitTreeState string = "not a git tree" // state of git tree, either "clean" or "dirty" )