mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-11-03 23:40:03 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			301 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			301 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
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/json"
 | 
						|
	"io/ioutil"
 | 
						|
	"net/http"
 | 
						|
	"os/exec"
 | 
						|
	"runtime"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	utilnet "k8s.io/apimachinery/pkg/util/net"
 | 
						|
	"k8s.io/klog"
 | 
						|
	"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"
 | 
						|
	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", "*.pkg.dev"}
 | 
						|
 | 
						|
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 {
 | 
						|
	var name string
 | 
						|
 | 
						|
	if runtime.GOOS == "windows" {
 | 
						|
		data, err := exec.Command("wmic", "computersystem", "get", "model").Output()
 | 
						|
		if err != nil {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
		fields := strings.Split(strings.TrimSpace(string(data)), "\r\n")
 | 
						|
		if len(fields) != 2 {
 | 
						|
			klog.V(2).Infof("Received unexpected value retrieving system model: %q", string(data))
 | 
						|
			return false
 | 
						|
		}
 | 
						|
		name = fields[1]
 | 
						|
	} else {
 | 
						|
		data, err := ioutil.ReadFile(gceProductNameFile)
 | 
						|
		if err != nil {
 | 
						|
			klog.V(2).Infof("Error while reading product_name: %v", err)
 | 
						|
			return false
 | 
						|
		}
 | 
						|
		name = strings.TrimSpace(string(data))
 | 
						|
	}
 | 
						|
	return name == "Google" || name == "Google Compute Engine"
 | 
						|
}
 | 
						|
 | 
						|
// Enabled implements DockerConfigProvider for all of the Google implementations.
 | 
						|
func (g *metadataProvider) Enabled() bool {
 | 
						|
	return onGCEVM()
 | 
						|
}
 | 
						|
 | 
						|
// Provide implements DockerConfigProvider
 | 
						|
func (g *dockerConfigKeyProvider) Provide(image string) 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 {
 | 
						|
		klog.Errorf("while reading 'google-dockercfg' metadata: %v", err)
 | 
						|
	} else {
 | 
						|
		return cfg
 | 
						|
	}
 | 
						|
 | 
						|
	return credentialprovider.DockerConfig{}
 | 
						|
}
 | 
						|
 | 
						|
// Provide implements DockerConfigProvider
 | 
						|
func (g *dockerConfigURLKeyProvider) Provide(image string) 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 {
 | 
						|
		klog.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 {
 | 
						|
				klog.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://)
 | 
						|
			klog.Errorf("Unsupported URL scheme: %s", string(url))
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return credentialprovider.DockerConfig{}
 | 
						|
}
 | 
						|
 | 
						|
// runWithBackoff 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 {
 | 
						|
			klog.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 {
 | 
						|
		klog.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 {
 | 
						|
			klog.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 {
 | 
						|
		klog.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
 | 
						|
		}
 | 
						|
	}
 | 
						|
	klog.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(image string) credentialprovider.DockerConfig {
 | 
						|
	cfg := credentialprovider.DockerConfig{}
 | 
						|
 | 
						|
	tokenJSONBlob, err := credentialprovider.ReadUrl(metadataToken, g.Client, metadataHeader)
 | 
						|
	if err != nil {
 | 
						|
		klog.Errorf("while reading access token endpoint: %v", err)
 | 
						|
		return cfg
 | 
						|
	}
 | 
						|
 | 
						|
	email, err := credentialprovider.ReadUrl(metadataEmail, g.Client, metadataHeader)
 | 
						|
	if err != nil {
 | 
						|
		klog.Errorf("while reading email endpoint: %v", err)
 | 
						|
		return cfg
 | 
						|
	}
 | 
						|
 | 
						|
	var parsedBlob tokenBlob
 | 
						|
	if err := json.Unmarshal([]byte(tokenJSONBlob), &parsedBlob); err != nil {
 | 
						|
		klog.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
 | 
						|
}
 |