8e5b17cf13
Signed-off-by: Mrunal Patel <mrunalp@gmail.com>
298 lines
10 KiB
Go
298 lines
10 KiB
Go
/*
|
|
Copyright 2014 The Kubernetes Authors.
|
|
|
|
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"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang/glog"
|
|
utilnet "k8s.io/apimachinery/pkg/util/net"
|
|
"k8s.io/kubernetes/pkg/credentialprovider"
|
|
)
|
|
|
|
const (
|
|
metadataUrl = "http://metadata.google.internal./computeMetadata/v1/"
|
|
metadataAttributes = metadataUrl + "instance/attributes/"
|
|
dockerConfigKey = metadataAttributes + "google-dockercfg"
|
|
dockerConfigUrlKey = metadataAttributes + "google-dockercfg-url"
|
|
serviceAccounts = metadataUrl + "instance/service-accounts/"
|
|
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"
|
|
cloudPlatformScopePrefix = "https://www.googleapis.com/auth/cloud-platform"
|
|
googleProductName = "Google"
|
|
defaultServiceAccount = "default/"
|
|
)
|
|
|
|
// Product file path that contains the cloud service name.
|
|
// This is a variable instead of a const to enable testing.
|
|
var gceProductNameFile = "/sys/class/dmi/id/product_name"
|
|
|
|
// For these urls, the parts of the host name can be glob, for example '*.gcr.io" will match
|
|
// "foo.gcr.io" and "bar.gcr.io".
|
|
var containerRegistryUrls = []string{"container.cloud.google.com", "gcr.io", "*.gcr.io"}
|
|
|
|
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() {
|
|
tr := utilnet.SetTransportDefaults(&http.Transport{})
|
|
metadataHTTPClientTimeout := time.Second * 10
|
|
httpClient := &http.Client{
|
|
Transport: tr,
|
|
Timeout: metadataHTTPClientTimeout,
|
|
}
|
|
credentialprovider.RegisterCredentialProvider("google-dockercfg",
|
|
&credentialprovider.CachingDockerConfigProvider{
|
|
Provider: &dockerConfigKeyProvider{
|
|
metadataProvider{Client: httpClient},
|
|
},
|
|
Lifetime: 60 * time.Second,
|
|
})
|
|
|
|
credentialprovider.RegisterCredentialProvider("google-dockercfg-url",
|
|
&credentialprovider.CachingDockerConfigProvider{
|
|
Provider: &dockerConfigUrlKeyProvider{
|
|
metadataProvider{Client: httpClient},
|
|
},
|
|
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: httpClient},
|
|
})
|
|
}
|
|
|
|
// Returns true if it finds a local GCE VM.
|
|
// Looks at a product file that is an undocumented API.
|
|
func onGCEVM() bool {
|
|
data, err := ioutil.ReadFile(gceProductNameFile)
|
|
if err != nil {
|
|
glog.V(2).Infof("Error while reading product_name: %v", err)
|
|
return false
|
|
}
|
|
return strings.Contains(string(data), googleProductName)
|
|
}
|
|
|
|
// Enabled implements DockerConfigProvider for all of the Google implementations.
|
|
func (g *metadataProvider) Enabled() bool {
|
|
return onGCEVM()
|
|
}
|
|
|
|
// LazyProvide implements DockerConfigProvider. Should never be called.
|
|
func (g *dockerConfigKeyProvider) LazyProvide() *credentialprovider.DockerConfigEntry {
|
|
return 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 {
|
|
glog.Errorf("while reading 'google-dockercfg' metadata: %v", err)
|
|
} else {
|
|
return cfg
|
|
}
|
|
|
|
return credentialprovider.DockerConfig{}
|
|
}
|
|
|
|
// LazyProvide implements DockerConfigProvider. Should never be called.
|
|
func (g *dockerConfigUrlKeyProvider) LazyProvide() *credentialprovider.DockerConfigEntry {
|
|
return nil
|
|
}
|
|
|
|
// 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 {
|
|
glog.Errorf("while reading 'google-dockercfg-url' metadata: %v", err)
|
|
} else {
|
|
if strings.HasPrefix(string(url), "http") {
|
|
if cfg, err := credentialprovider.ReadDockerConfigFileFromUrl(string(url), g.Client, nil); err != nil {
|
|
glog.Errorf("while reading 'google-dockercfg-url'-specified url: %s, %v", string(url), err)
|
|
} else {
|
|
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{}
|
|
}
|
|
|
|
// runcWithBackoff runs input function `f` with an exponential backoff.
|
|
// Note that this method can block indefinitely.
|
|
func runWithBackoff(f func() ([]byte, error)) []byte {
|
|
var backoff = 100 * time.Millisecond
|
|
const maxBackoff = time.Minute
|
|
for {
|
|
value, err := f()
|
|
if err == nil {
|
|
return value
|
|
}
|
|
time.Sleep(backoff)
|
|
backoff = backoff * 2
|
|
if backoff > maxBackoff {
|
|
backoff = maxBackoff
|
|
}
|
|
}
|
|
}
|
|
|
|
// Enabled implements a special metadata-based check, which verifies the
|
|
// storage scope is available on the GCE VM.
|
|
// If running on a GCE VM, check if 'default' service account exists.
|
|
// If it does not exist, assume that registry is not enabled.
|
|
// If default service account exists, check if relevant scopes exist in the default service account.
|
|
// The metadata service can become temporarily inaccesible. Hence all requests to the metadata
|
|
// service will be retried until the metadata server returns a `200`.
|
|
// It is expected that "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/" will return a `200`
|
|
// and "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/default/scopes" will also return `200`.
|
|
// More information on metadata service can be found here - https://cloud.google.com/compute/docs/storing-retrieving-metadata
|
|
func (g *containerRegistryProvider) Enabled() bool {
|
|
if !onGCEVM() {
|
|
return false
|
|
}
|
|
// Given that we are on GCE, we should keep retrying until the metadata server responds.
|
|
value := runWithBackoff(func() ([]byte, error) {
|
|
value, err := credentialprovider.ReadUrl(serviceAccounts, g.Client, metadataHeader)
|
|
if err != nil {
|
|
glog.V(2).Infof("Failed to Get service accounts from gce metadata server: %v", err)
|
|
}
|
|
return value, err
|
|
})
|
|
// We expect the service account to return a list of account directories separated by newlines, e.g.,
|
|
// sv-account-name1/
|
|
// sv-account-name2/
|
|
// ref: https://cloud.google.com/compute/docs/storing-retrieving-metadata
|
|
defaultServiceAccountExists := false
|
|
for _, sa := range strings.Split(string(value), "\n") {
|
|
if strings.TrimSpace(sa) == defaultServiceAccount {
|
|
defaultServiceAccountExists = true
|
|
break
|
|
}
|
|
}
|
|
if !defaultServiceAccountExists {
|
|
glog.V(2).Infof("'default' service account does not exist. Found following service accounts: %q", string(value))
|
|
return false
|
|
}
|
|
url := metadataScopes + "?alt=json"
|
|
value = runWithBackoff(func() ([]byte, error) {
|
|
value, err := credentialprovider.ReadUrl(url, g.Client, metadataHeader)
|
|
if err != nil {
|
|
glog.V(2).Infof("Failed to Get scopes in default service account from gce metadata server: %v", err)
|
|
}
|
|
return value, err
|
|
})
|
|
var scopes []string
|
|
if err := json.Unmarshal(value, &scopes); err != nil {
|
|
glog.Errorf("Failed to unmarshal scopes: %v", err)
|
|
return false
|
|
}
|
|
for _, v := range scopes {
|
|
// cloudPlatformScope implies storage scope.
|
|
if strings.HasPrefix(v, storageScopePrefix) || strings.HasPrefix(v, cloudPlatformScopePrefix) {
|
|
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"`
|
|
}
|
|
|
|
// LazyProvide implements DockerConfigProvider. Should never be called.
|
|
func (g *containerRegistryProvider) LazyProvide() *credentialprovider.DockerConfigEntry {
|
|
return nil
|
|
}
|
|
|
|
// Provide implements DockerConfigProvider
|
|
func (g *containerRegistryProvider) Provide() credentialprovider.DockerConfig {
|
|
cfg := credentialprovider.DockerConfig{}
|
|
|
|
tokenJsonBlob, err := credentialprovider.ReadUrl(metadataToken, g.Client, metadataHeader)
|
|
if err != nil {
|
|
glog.Errorf("while reading access token endpoint: %v", err)
|
|
return cfg
|
|
}
|
|
|
|
email, err := credentialprovider.ReadUrl(metadataEmail, g.Client, metadataHeader)
|
|
if err != nil {
|
|
glog.Errorf("while reading email endpoint: %v", err)
|
|
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
|
|
}
|