Move config and provider code out of pkg/credentialprovider and into staging.

This commit is contained in:
Kermit Alexander 2020-11-12 01:19:48 +00:00
parent 0ced9d2854
commit 42fb89eb89
15 changed files with 429 additions and 370 deletions

View File

@ -59,6 +59,8 @@ import (
"k8s.io/client-go/util/connrotation"
"k8s.io/client-go/util/keyutil"
cloudprovider "k8s.io/cloud-provider"
"k8s.io/cloud-provider/credentialconfig"
"k8s.io/component-base/cli/flag"
cliflag "k8s.io/component-base/cli/flag"
"k8s.io/component-base/configz"
"k8s.io/component-base/featuregate"
@ -72,7 +74,6 @@ import (
"k8s.io/kubernetes/pkg/api/legacyscheme"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/capabilities"
"k8s.io/kubernetes/pkg/credentialprovider"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/kubelet"
kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config"
@ -1116,7 +1117,7 @@ func RunKubelet(kubeServer *options.KubeletServer, kubeDeps *kubelet.Dependencie
AllowPrivileged: true,
})
credentialprovider.SetPreferredDockercfgPath(kubeServer.RootDirectory)
credentialconfig.SetPreferredDockercfgPath(kubeServer.RootDirectory)
klog.V(2).Infof("Using root directory: %v", kubeServer.RootDirectory)
if kubeDeps.OSInterface == nil {

View File

@ -33,6 +33,7 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"k8s.io/cloud-provider/credentialconfig"
"k8s.io/component-base/version"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/credentialprovider"
@ -54,7 +55,7 @@ type ecrProvider struct {
getterFactory tokenGetterFactory
}
var _ credentialprovider.DockerConfigProvider = &ecrProvider{}
var _ credentialconfig.DockerConfigProvider = &ecrProvider{}
func newECRProvider(getterFactory tokenGetterFactory) *ecrProvider {
return &ecrProvider{
@ -82,11 +83,11 @@ func (p *ecrProvider) Enabled() bool {
// Provide returns a DockerConfig with credentials from the cache if they are
// found, or from ECR
func (p *ecrProvider) Provide(image string) credentialprovider.DockerConfig {
func (p *ecrProvider) Provide(image string) credentialconfig.DockerConfig {
parsed, err := parseRepoURL(image)
if err != nil {
klog.V(3).Info(err)
return credentialprovider.DockerConfig{}
return credentialconfig.DockerConfig{}
}
if cfg, exists := p.getFromCache(parsed); exists {
@ -98,15 +99,15 @@ func (p *ecrProvider) Provide(image string) credentialprovider.DockerConfig {
cfg, err := p.getFromECR(parsed)
if err != nil {
klog.Errorf("error getting credentials from ECR for %s %v", parsed.registry, err)
return credentialprovider.DockerConfig{}
return credentialconfig.DockerConfig{}
}
klog.V(3).Infof("Got ECR credentials from ECR API for %s", parsed.registry)
return cfg
}
// getFromCache attempts to get credentials from the cache
func (p *ecrProvider) getFromCache(parsed *parsedURL) (credentialprovider.DockerConfig, bool) {
cfg := credentialprovider.DockerConfig{}
func (p *ecrProvider) getFromCache(parsed *parsedURL) (credentialconfig.DockerConfig, bool) {
cfg := credentialconfig.DockerConfig{}
obj, exists, err := p.cache.GetByKey(parsed.registry)
if err != nil {
@ -124,8 +125,8 @@ func (p *ecrProvider) getFromCache(parsed *parsedURL) (credentialprovider.Docker
}
// getFromECR gets credentials from ECR since they are not in the cache
func (p *ecrProvider) getFromECR(parsed *parsedURL) (credentialprovider.DockerConfig, error) {
cfg := credentialprovider.DockerConfig{}
func (p *ecrProvider) getFromECR(parsed *parsedURL) (credentialconfig.DockerConfig, error) {
cfg := credentialconfig.DockerConfig{}
getter, err := p.getterFactory.GetTokenGetterForRegion(parsed.region)
if err != nil {
return cfg, err
@ -260,7 +261,7 @@ func (p *ecrTokenGetter) GetAuthorizationToken(input *ecr.GetAuthorizationTokenI
type cacheEntry struct {
expiresAt time.Time
credentials credentialprovider.DockerConfigEntry
credentials credentialconfig.DockerConfigEntry
registry string
}
@ -275,7 +276,7 @@ func makeCacheEntry(data *ecr.AuthorizationData, registry string) (*cacheEntry,
if len(parts) < 2 {
return nil, errors.New("error getting username and password from authorization token")
}
creds := credentialprovider.DockerConfigEntry{
creds := credentialconfig.DockerConfigEntry{
Username: parts[0],
Password: parts[1],
Email: "not@val.id", // ECR doesn't care and Docker is about to obsolete it

View File

@ -35,6 +35,7 @@ import (
"github.com/spf13/pflag"
"k8s.io/client-go/tools/cache"
"k8s.io/cloud-provider/credentialconfig"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/credentialprovider"
"k8s.io/legacy-cloud-providers/azure/auth"
@ -65,7 +66,7 @@ func init() {
type cacheEntry struct {
expiresAt time.Time
credentials credentialprovider.DockerConfigEntry
credentials credentialconfig.DockerConfigEntry
registry string
}
@ -122,7 +123,7 @@ func (az *azRegistriesClient) List(ctx context.Context) ([]containerregistry.Reg
}
// NewACRProvider parses the specified configFile and returns a DockerConfigProvider
func NewACRProvider(configFile *string) credentialprovider.DockerConfigProvider {
func NewACRProvider(configFile *string) credentialconfig.DockerConfigProvider {
return &acrProvider{
file: configFile,
cache: cache.NewExpirationStore(stringKeyFunc, &acrExpirationPolicy{}),
@ -207,8 +208,8 @@ func (a *acrProvider) Enabled() bool {
}
// getFromCache attempts to get credentials from the cache
func (a *acrProvider) getFromCache(loginServer string) (credentialprovider.DockerConfig, bool) {
cfg := credentialprovider.DockerConfig{}
func (a *acrProvider) getFromCache(loginServer string) (credentialconfig.DockerConfig, bool) {
cfg := credentialconfig.DockerConfig{}
obj, exists, err := a.cache.GetByKey(loginServer)
if err != nil {
klog.Errorf("error getting ACR credentials from cache: %v", err)
@ -224,8 +225,8 @@ func (a *acrProvider) getFromCache(loginServer string) (credentialprovider.Docke
}
// getFromACR gets credentials from ACR since they are not in the cache
func (a *acrProvider) getFromACR(loginServer string) (credentialprovider.DockerConfig, error) {
cfg := credentialprovider.DockerConfig{}
func (a *acrProvider) getFromACR(loginServer string) (credentialconfig.DockerConfig, error) {
cfg := credentialconfig.DockerConfig{}
cred, err := getACRDockerEntryFromARMToken(a, loginServer)
if err != nil {
return cfg, err
@ -243,14 +244,14 @@ func (a *acrProvider) getFromACR(loginServer string) (credentialprovider.DockerC
return cfg, nil
}
func (a *acrProvider) Provide(image string) credentialprovider.DockerConfig {
func (a *acrProvider) Provide(image string) credentialconfig.DockerConfig {
loginServer := a.parseACRLoginServerFromImage(image)
if loginServer == "" {
klog.V(2).Infof("image(%s) is not from ACR, return empty authentication", image)
return credentialprovider.DockerConfig{}
return credentialconfig.DockerConfig{}
}
cfg := credentialprovider.DockerConfig{}
cfg := credentialconfig.DockerConfig{}
if a.config != nil && a.config.UseManagedIdentityExtension {
var exists bool
cfg, exists = a.getFromCache(loginServer)
@ -267,7 +268,7 @@ func (a *acrProvider) Provide(image string) credentialprovider.DockerConfig {
} else {
// Add our entry for each of the supported container registry URLs
for _, url := range containerRegistryUrls {
cred := &credentialprovider.DockerConfigEntry{
cred := &credentialconfig.DockerConfigEntry{
Username: a.config.AADClientID,
Password: a.config.AADClientSecret,
Email: dummyRegistryEmail,
@ -288,7 +289,7 @@ func (a *acrProvider) Provide(image string) credentialprovider.DockerConfig {
}
if !hasBeenAdded {
cred := &credentialprovider.DockerConfigEntry{
cred := &credentialconfig.DockerConfigEntry{
Username: a.config.AADClientID,
Password: a.config.AADClientSecret,
Email: dummyRegistryEmail,
@ -299,7 +300,7 @@ func (a *acrProvider) Provide(image string) credentialprovider.DockerConfig {
}
// add ACR anonymous repo support: use empty username and password for anonymous access
defaultConfigEntry := credentialprovider.DockerConfigEntry{
defaultConfigEntry := credentialconfig.DockerConfigEntry{
Username: "",
Password: "",
Email: dummyRegistryEmail,
@ -312,7 +313,7 @@ func getLoginServer(registry containerregistry.Registry) string {
return *(*registry.RegistryProperties).LoginServer
}
func getACRDockerEntryFromARMToken(a *acrProvider, loginServer string) (*credentialprovider.DockerConfigEntry, error) {
func getACRDockerEntryFromARMToken(a *acrProvider, loginServer string) (*credentialconfig.DockerConfigEntry, error) {
// Run EnsureFresh to make sure the token is valid and does not expire
if err := a.servicePrincipalToken.EnsureFresh(); err != nil {
klog.Errorf("Failed to ensure fresh service principal token: %v", err)
@ -336,7 +337,7 @@ func getACRDockerEntryFromARMToken(a *acrProvider, loginServer string) (*credent
}
klog.V(4).Infof("adding ACR docker config entry for: %s", loginServer)
return &credentialprovider.DockerConfigEntry{
return &credentialconfig.DockerConfigEntry{
Username: dockerTokenLoginUsernameGUID,
Password: registryRefreshToken,
Email: dummyRegistryEmail,

View File

@ -0,0 +1,52 @@
/*
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 credentialprovider
import (
"k8s.io/cloud-provider/credentialconfig"
"k8s.io/klog/v2"
"os"
"time"
)
// A DockerConfigProvider that simply reads the .dockercfg file
type defaultDockerConfigProvider struct{}
// init registers our default provider, which simply reads the .dockercfg file.
func init() {
RegisterCredentialProvider(".dockercfg",
&credentialconfig.CachingDockerConfigProvider{
Provider: &defaultDockerConfigProvider{},
Lifetime: 5 * time.Minute,
})
}
// Enabled implements dockerConfigProvider
func (d *defaultDockerConfigProvider) Enabled() bool {
return true
}
// Provide implements dockerConfigProvider
func (d *defaultDockerConfigProvider) Provide(image string) credentialconfig.DockerConfig {
// Read the standard Docker credentials from .dockercfg
if cfg, err := credentialconfig.ReadDockerConfigFile(); err == nil {
return cfg
} else if !os.IsNotExist(err) {
klog.V(4).Infof("Unable to parse Docker config file: %v", err)
}
return credentialconfig.DockerConfig{}
}

View File

@ -17,70 +17,15 @@ 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/v2"
"k8s.io/cloud-provider/credentialconfig"
"k8s.io/kubernetes/pkg/credentialprovider"
"k8s.io/legacy-cloud-providers/gce/gcpcredential"
)
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() {
@ -91,17 +36,17 @@ func init() {
Timeout: metadataHTTPClientTimeout,
}
credentialprovider.RegisterCredentialProvider("google-dockercfg",
&credentialprovider.CachingDockerConfigProvider{
Provider: &dockerConfigKeyProvider{
metadataProvider{Client: httpClient},
&credentialconfig.CachingDockerConfigProvider{
Provider: &gcpcredential.DockerConfigKeyProvider{
gcpcredential.MetadataProvider{Client: httpClient},
},
Lifetime: 60 * time.Second,
})
credentialprovider.RegisterCredentialProvider("google-dockercfg-url",
&credentialprovider.CachingDockerConfigProvider{
Provider: &dockerConfigURLKeyProvider{
metadataProvider{Client: httpClient},
&credentialconfig.CachingDockerConfigProvider{
Provider: &gcpcredential.DockerConfigURLKeyProvider{
gcpcredential.MetadataProvider{Client: httpClient},
},
Lifetime: 60 * time.Second,
})
@ -109,192 +54,7 @@ func init() {
credentialprovider.RegisterCredentialProvider("google-container-registry",
// Never cache this. The access token is already
// cached by the metadata service.
&containerRegistryProvider{
metadataProvider{Client: httpClient},
&gcpcredential.ContainerRegistryProvider{
gcpcredential.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
}

View File

@ -30,7 +30,9 @@ import (
"testing"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/cloud-provider/credentialconfig"
"k8s.io/kubernetes/pkg/credentialprovider"
"k8s.io/legacy-cloud-providers/gce/gcpcredential"
)
func createProductNameFile() (string, error) {
@ -55,17 +57,17 @@ func TestDockerKeyringFromGoogleDockerConfigMetadata(t *testing.T) {
}`, registryURL, email, auth)
var err error
gceProductNameFile, err = createProductNameFile()
gcpcredential.GCEProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gceProductNameFile)
defer os.Remove(gcpcredential.GCEProductNameFile)
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) {
} else if strings.HasSuffix(gcpcredential.DockerConfigKey, r.URL.Path) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, sampleDockerConfig)
@ -83,8 +85,8 @@ func TestDockerKeyringFromGoogleDockerConfigMetadata(t *testing.T) {
})
keyring := &credentialprovider.BasicDockerKeyring{}
provider := &dockerConfigKeyProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
provider := &gcpcredential.DockerConfigKeyProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
@ -128,11 +130,11 @@ func TestDockerKeyringFromGoogleDockerConfigMetadataUrl(t *testing.T) {
}`, registryURL, email, auth)
var err error
gceProductNameFile, err = createProductNameFile()
gcpcredential.GCEProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gceProductNameFile)
defer os.Remove(gcpcredential.GCEProductNameFile)
const probeEndpoint = "/computeMetadata/v1/"
const valueEndpoint = "/my/value"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -143,7 +145,7 @@ func TestDockerKeyringFromGoogleDockerConfigMetadataUrl(t *testing.T) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, sampleDockerConfig)
} else if strings.HasSuffix(dockerConfigURLKey, r.URL.Path) {
} else if strings.HasSuffix(gcpcredential.DockerConfigURLKey, r.URL.Path) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/text")
fmt.Fprint(w, "http://foo.bar.com"+valueEndpoint)
@ -161,8 +163,8 @@ func TestDockerKeyringFromGoogleDockerConfigMetadataUrl(t *testing.T) {
})
keyring := &credentialprovider.BasicDockerKeyring{}
provider := &dockerConfigURLKeyProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
provider := &gcpcredential.DockerConfigURLKeyProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
@ -197,7 +199,7 @@ func TestContainerRegistryBasics(t *testing.T) {
for _, registryURL := range registryURLs {
t.Run(registryURL, func(t *testing.T) {
email := "1234@project.gserviceaccount.com"
token := &tokenBlob{AccessToken: "ya26.lots-of-indiscernible-garbage"} // Fake value for testing.
token := &gcpcredential.TokenBlob{AccessToken: "ya26.lots-of-indiscernible-garbage"} // Fake value for testing.
const (
serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/"
@ -207,18 +209,18 @@ func TestContainerRegistryBasics(t *testing.T) {
tokenEndpoint = defaultEndpoint + "token"
)
var err error
gceProductNameFile, err = createProductNameFile()
gcpcredential.GCEProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gceProductNameFile)
defer os.Remove(gcpcredential.GCEProductNameFile)
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)
fmt.Fprintf(w, `["%s.read_write"]`, gcpcredential.StorageScopePrefix)
} else if emailEndpoint == r.URL.Path {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, email)
@ -247,8 +249,8 @@ func TestContainerRegistryBasics(t *testing.T) {
})
keyring := &credentialprovider.BasicDockerKeyring{}
provider := &containerRegistryProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
provider := &gcpcredential.ContainerRegistryProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
@ -301,11 +303,11 @@ func TestContainerRegistryNoServiceAccount(t *testing.T) {
defer server.Close()
var err error
gceProductNameFile, err = createProductNameFile()
gcpcredential.GCEProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gceProductNameFile)
defer os.Remove(gcpcredential.GCEProductNameFile)
// Make a transport that reroutes all traffic to the example server
transport := utilnet.SetTransportDefaults(&http.Transport{
@ -314,8 +316,8 @@ func TestContainerRegistryNoServiceAccount(t *testing.T) {
},
})
provider := &containerRegistryProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
provider := &gcpcredential.ContainerRegistryProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
}
if provider.Enabled() {
@ -345,11 +347,11 @@ func TestContainerRegistryNoStorageScope(t *testing.T) {
defer server.Close()
var err error
gceProductNameFile, err = createProductNameFile()
gcpcredential.GCEProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gceProductNameFile)
defer os.Remove(gcpcredential.GCEProductNameFile)
// Make a transport that reroutes all traffic to the example server
transport := utilnet.SetTransportDefaults(&http.Transport{
@ -358,8 +360,8 @@ func TestContainerRegistryNoStorageScope(t *testing.T) {
},
})
provider := &containerRegistryProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
provider := &gcpcredential.ContainerRegistryProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
}
if provider.Enabled() {
@ -390,11 +392,11 @@ func TestComputePlatformScopeSubstitutesStorageScope(t *testing.T) {
defer server.Close()
var err error
gceProductNameFile, err = createProductNameFile()
gcpcredential.GCEProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gceProductNameFile)
defer os.Remove(gcpcredential.GCEProductNameFile)
// Make a transport that reroutes all traffic to the example server
transport := utilnet.SetTransportDefaults(&http.Transport{
@ -403,8 +405,8 @@ func TestComputePlatformScopeSubstitutesStorageScope(t *testing.T) {
},
})
provider := &containerRegistryProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
provider := &gcpcredential.ContainerRegistryProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
@ -425,15 +427,15 @@ func TestAllProvidersNoMetadata(t *testing.T) {
},
})
providers := []credentialprovider.DockerConfigProvider{
&dockerConfigKeyProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
providers := []credentialconfig.DockerConfigProvider{
&gcpcredential.DockerConfigKeyProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
},
&dockerConfigURLKeyProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
&gcpcredential.DockerConfigURLKeyProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
},
&containerRegistryProvider{
metadataProvider{Client: &http.Client{Transport: transport}},
&gcpcredential.ContainerRegistryProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
},
}

View File

@ -26,6 +26,7 @@ import (
"k8s.io/klog/v2"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/cloud-provider/credentialconfig"
)
// DockerKeyring tracks a set of docker registry credentials, maintaining a
@ -48,7 +49,7 @@ type BasicDockerKeyring struct {
// providersDockerKeyring is an implementation of DockerKeyring that
// materializes its dockercfg based on a set of dockerConfigProviders.
type providersDockerKeyring struct {
Providers []DockerConfigProvider
Providers []credentialconfig.DockerConfigProvider
}
// AuthConfig contains authorization information for connecting to a Registry
@ -74,7 +75,7 @@ type AuthConfig struct {
}
// Add add some docker config in basic docker keyring
func (dk *BasicDockerKeyring) Add(cfg DockerConfig) {
func (dk *BasicDockerKeyring) Add(cfg credentialconfig.DockerConfig) {
if dk.index == nil {
dk.index = make([]string, 0)
dk.creds = make(map[string][]AuthConfig)

View File

@ -21,6 +21,8 @@ import (
"fmt"
"reflect"
"testing"
"k8s.io/cloud-provider/credentialconfig"
)
func TestURLsMatch(t *testing.T) {
@ -203,7 +205,7 @@ func TestDockerKeyringForGlob(t *testing.T) {
}`, test.globURL, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
if cfg, err := credentialconfig.ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
@ -271,7 +273,7 @@ func TestKeyringMiss(t *testing.T) {
}`, test.globURL, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
if cfg, err := credentialconfig.ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
@ -299,7 +301,7 @@ func TestKeyringMissWithDockerHubCredentials(t *testing.T) {
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
if cfg, err := credentialconfig.ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
@ -325,7 +327,7 @@ func TestKeyringHitWithUnqualifiedDockerHub(t *testing.T) {
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
if cfg, err := credentialconfig.ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
@ -366,7 +368,7 @@ func TestKeyringHitWithUnqualifiedLibraryDockerHub(t *testing.T) {
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
if cfg, err := credentialconfig.ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
@ -407,7 +409,7 @@ func TestKeyringHitWithQualifiedDockerHub(t *testing.T) {
}`, url, email, auth)
keyring := &BasicDockerKeyring{}
if cfg, err := readDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
if cfg, err := credentialconfig.ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else {
keyring.Add(cfg)
@ -464,9 +466,9 @@ func (d *testProvider) Enabled() bool {
}
// Provide implements dockerConfigProvider
func (d *testProvider) Provide(image string) DockerConfig {
func (d *testProvider) Provide(image string) credentialconfig.DockerConfig {
d.Count++
return DockerConfig{}
return credentialconfig.DockerConfig{}
}
func TestProvidersDockerKeyring(t *testing.T) {
@ -474,7 +476,7 @@ func TestProvidersDockerKeyring(t *testing.T) {
Count: 0,
}
keyring := &providersDockerKeyring{
Providers: []DockerConfigProvider{
Providers: []credentialconfig.DockerConfigProvider{
provider,
},
}
@ -510,13 +512,13 @@ func TestDockerKeyringLookup(t *testing.T) {
}
dk := &BasicDockerKeyring{}
dk.Add(DockerConfig{
"bar.example.com/pong": DockerConfigEntry{
dk.Add(credentialconfig.DockerConfig{
"bar.example.com/pong": credentialconfig.DockerConfigEntry{
Username: grace.Username,
Password: grace.Password,
Email: grace.Email,
},
"bar.example.com": DockerConfigEntry{
"bar.example.com": credentialconfig.DockerConfigEntry{
Username: ada.Username,
Password: ada.Password,
Email: ada.Email,
@ -571,8 +573,8 @@ func TestIssue3797(t *testing.T) {
}
dk := &BasicDockerKeyring{}
dk.Add(DockerConfig{
"https://quay.io/v1/": DockerConfigEntry{
dk.Add(credentialconfig.DockerConfig{
"https://quay.io/v1/": credentialconfig.DockerConfigEntry{
Username: rex.Username,
Password: rex.Password,
Email: rex.Email,

View File

@ -21,19 +21,20 @@ import (
"sort"
"sync"
"k8s.io/cloud-provider/credentialconfig"
"k8s.io/klog/v2"
)
// All registered credential providers.
var providersMutex sync.Mutex
var providers = make(map[string]DockerConfigProvider)
var providers = make(map[string]credentialconfig.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) {
func RegisterCredentialProvider(name string, provider credentialconfig.DockerConfigProvider) {
providersMutex.Lock()
defer providersMutex.Unlock()
_, found := providers[name]
@ -48,7 +49,7 @@ func RegisterCredentialProvider(name string, provider DockerConfigProvider) {
// which draws from the set of registered credential providers.
func NewDockerKeyring() DockerKeyring {
keyring := &providersDockerKeyring{
Providers: make([]DockerConfigProvider, 0),
Providers: make([]credentialconfig.DockerConfigProvider, 0),
}
keys := reflect.ValueOf(providers).MapKeys()

View File

@ -20,6 +20,7 @@ import (
"encoding/json"
"k8s.io/api/core/v1"
"k8s.io/cloud-provider/credentialconfig"
"k8s.io/kubernetes/pkg/credentialprovider"
)
@ -27,17 +28,17 @@ import (
// then a DockerKeyring is built based on every hit and unioned with the defaultKeyring.
// If they do not, then the default keyring is returned
func MakeDockerKeyring(passedSecrets []v1.Secret, defaultKeyring credentialprovider.DockerKeyring) (credentialprovider.DockerKeyring, error) {
passedCredentials := []credentialprovider.DockerConfig{}
passedCredentials := []credentialconfig.DockerConfig{}
for _, passedSecret := range passedSecrets {
if dockerConfigJSONBytes, dockerConfigJSONExists := passedSecret.Data[v1.DockerConfigJsonKey]; (passedSecret.Type == v1.SecretTypeDockerConfigJson) && dockerConfigJSONExists && (len(dockerConfigJSONBytes) > 0) {
dockerConfigJSON := credentialprovider.DockerConfigJSON{}
dockerConfigJSON := credentialconfig.DockerConfigJSON{}
if err := json.Unmarshal(dockerConfigJSONBytes, &dockerConfigJSON); err != nil {
return nil, err
}
passedCredentials = append(passedCredentials, dockerConfigJSON.Auths)
} else if dockercfgBytes, dockercfgExists := passedSecret.Data[v1.DockerConfigKey]; (passedSecret.Type == v1.SecretTypeDockercfg) && dockercfgExists && (len(dockercfgBytes) > 0) {
dockercfg := credentialprovider.DockerConfig{}
dockercfg := credentialconfig.DockerConfig{}
if err := json.Unmarshal(dockercfgBytes, &dockercfg); err != nil {
return nil, err
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialprovider
package credentialconfig
import (
"encoding/base64"
@ -116,7 +116,7 @@ func ReadDockercfgFile(searchPaths []string) (cfg DockerConfig, err error) {
klog.V(4).Infof("while trying to read %s: %v", absDockerConfigFileLocation, err)
continue
}
cfg, err := readDockerConfigFileFromBytes(contents)
cfg, err := ReadDockerConfigFileFromBytes(contents)
if err != nil {
klog.V(4).Infof("couldn't get the config from %q contents: %v", absDockerConfigFileLocation, err)
continue
@ -226,13 +226,13 @@ func ReadURL(url string, client *http.Client, header *http.Header) (body []byte,
// ReadDockerConfigFileFromURL read a docker config file from the given url
func ReadDockerConfigFileFromURL(url string, client *http.Client, header *http.Header) (cfg DockerConfig, err error) {
if contents, err := ReadURL(url, client, header); err == nil {
return readDockerConfigFileFromBytes(contents)
return ReadDockerConfigFileFromBytes(contents)
}
return nil, err
}
func readDockerConfigFileFromBytes(contents []byte) (cfg DockerConfig, err error) {
func ReadDockerConfigFileFromBytes(contents []byte) (cfg DockerConfig, err error) {
if err = json.Unmarshal(contents, &cfg); err != nil {
return nil, errors.New("error occurred while trying to unmarshal json")
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialprovider
package credentialconfig
import (
"encoding/base64"
@ -338,7 +338,7 @@ func TestReadDockerConfigFileFromBytes(t *testing.T) {
}
for _, tc := range testCases {
cfg, err := readDockerConfigFileFromBytes(tc.input)
cfg, err := ReadDockerConfigFileFromBytes(tc.input)
if err != nil && !tc.errorExpected {
t.Fatalf("Error was not expected: %v", err)
}

View File

@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialprovider
package credentialconfig
import (
"os"
"reflect"
"sync"
"time"
@ -39,18 +38,6 @@ type DockerConfigProvider interface {
Provide(image string) DockerConfig
}
// A DockerConfigProvider that simply reads the .dockercfg 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.
@ -68,22 +55,6 @@ type CachingDockerConfigProvider struct {
mu sync.Mutex
}
// Enabled implements dockerConfigProvider
func (d *defaultDockerConfigProvider) Enabled() bool {
return true
}
// Provide implements dockerConfigProvider
func (d *defaultDockerConfigProvider) Provide(image string) DockerConfig {
// Read the standard Docker credentials from .dockercfg
if cfg, err := ReadDockerConfigFile(); err == nil {
return cfg
} else if !os.IsNotExist(err) {
klog.V(4).Infof("Unable to parse Docker config file: %v", err)
}
return DockerConfig{}
}
// Enabled implements dockerConfigProvider
func (d *CachingDockerConfigProvider) Enabled() bool {
return d.Provider.Enabled()

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialprovider
package credentialconfig
import (
"testing"

View File

@ -0,0 +1,266 @@
/*
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 gcpcredential
import (
"encoding/json"
"io/ioutil"
"net/http"
"os/exec"
"runtime"
"strings"
"time"
"k8s.io/cloud-provider/credentialconfig"
"k8s.io/klog/v2"
)
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
}
// 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) credentialconfig.DockerConfig {
// Read the contents of the google-dockercfg metadata key and
// parse them as an alternate .dockercfg
if cfg, err := credentialconfig.ReadDockerConfigFileFromURL(DockerConfigKey, g.Client, metadataHeader); err != nil {
klog.Errorf("while reading 'google-dockercfg' metadata: %v", err)
} else {
return cfg
}
return credentialconfig.DockerConfig{}
}
// Provide implements DockerConfigProvider
func (g *DockerConfigURLKeyProvider) Provide(image string) credentialconfig.DockerConfig {
// Read the contents of the google-dockercfg-url key and load a .dockercfg from there
if url, err := credentialconfig.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 := credentialconfig.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 credentialconfig.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 := credentialconfig.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 := credentialconfig.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) credentialconfig.DockerConfig {
cfg := credentialconfig.DockerConfig{}
tokenJSONBlob, err := credentialconfig.ReadURL(metadataToken, g.Client, metadataHeader)
if err != nil {
klog.Errorf("while reading access token endpoint: %v", err)
return cfg
}
email, err := credentialconfig.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 := credentialconfig.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
}