Add RegistryConfig/RegistryConfigEntry.

This commit is contained in:
Kermit Alexander 2020-11-12 03:30:31 +00:00
parent acaea957ff
commit 0dcafb1f37
19 changed files with 875 additions and 369 deletions

View File

@ -59,8 +59,6 @@ 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"
@ -74,6 +72,7 @@ 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"
@ -1117,7 +1116,7 @@ func RunKubelet(kubeServer *options.KubeletServer, kubeDeps *kubelet.Dependencie
AllowPrivileged: true,
})
credentialconfig.SetPreferredDockercfgPath(kubeServer.RootDirectory)
credentialprovider.SetPreferredDockercfgPath(kubeServer.RootDirectory)
klog.V(2).Infof("Using root directory: %v", kubeServer.RootDirectory)
if kubeDeps.OSInterface == nil {

View File

@ -33,7 +33,6 @@ 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"
@ -55,7 +54,7 @@ type ecrProvider struct {
getterFactory tokenGetterFactory
}
var _ credentialconfig.DockerConfigProvider = &ecrProvider{}
var _ credentialprovider.DockerConfigProvider = &ecrProvider{}
func newECRProvider(getterFactory tokenGetterFactory) *ecrProvider {
return &ecrProvider{
@ -83,11 +82,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) credentialconfig.DockerConfig {
func (p *ecrProvider) Provide(image string) credentialprovider.DockerConfig {
parsed, err := parseRepoURL(image)
if err != nil {
klog.V(3).Info(err)
return credentialconfig.DockerConfig{}
return credentialprovider.DockerConfig{}
}
if cfg, exists := p.getFromCache(parsed); exists {
@ -99,15 +98,15 @@ func (p *ecrProvider) Provide(image string) credentialconfig.DockerConfig {
cfg, err := p.getFromECR(parsed)
if err != nil {
klog.Errorf("error getting credentials from ECR for %s %v", parsed.registry, err)
return credentialconfig.DockerConfig{}
return credentialprovider.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) (credentialconfig.DockerConfig, bool) {
cfg := credentialconfig.DockerConfig{}
func (p *ecrProvider) getFromCache(parsed *parsedURL) (credentialprovider.DockerConfig, bool) {
cfg := credentialprovider.DockerConfig{}
obj, exists, err := p.cache.GetByKey(parsed.registry)
if err != nil {
@ -125,8 +124,8 @@ func (p *ecrProvider) getFromCache(parsed *parsedURL) (credentialconfig.DockerCo
}
// getFromECR gets credentials from ECR since they are not in the cache
func (p *ecrProvider) getFromECR(parsed *parsedURL) (credentialconfig.DockerConfig, error) {
cfg := credentialconfig.DockerConfig{}
func (p *ecrProvider) getFromECR(parsed *parsedURL) (credentialprovider.DockerConfig, error) {
cfg := credentialprovider.DockerConfig{}
getter, err := p.getterFactory.GetTokenGetterForRegion(parsed.region)
if err != nil {
return cfg, err
@ -261,7 +260,7 @@ func (p *ecrTokenGetter) GetAuthorizationToken(input *ecr.GetAuthorizationTokenI
type cacheEntry struct {
expiresAt time.Time
credentials credentialconfig.DockerConfigEntry
credentials credentialprovider.DockerConfigEntry
registry string
}
@ -276,7 +275,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 := credentialconfig.DockerConfigEntry{
creds := credentialprovider.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,7 +35,6 @@ 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"
@ -66,7 +65,7 @@ func init() {
type cacheEntry struct {
expiresAt time.Time
credentials credentialconfig.DockerConfigEntry
credentials credentialprovider.DockerConfigEntry
registry string
}
@ -123,7 +122,7 @@ func (az *azRegistriesClient) List(ctx context.Context) ([]containerregistry.Reg
}
// NewACRProvider parses the specified configFile and returns a DockerConfigProvider
func NewACRProvider(configFile *string) credentialconfig.DockerConfigProvider {
func NewACRProvider(configFile *string) credentialprovider.DockerConfigProvider {
return &acrProvider{
file: configFile,
cache: cache.NewExpirationStore(stringKeyFunc, &acrExpirationPolicy{}),
@ -208,8 +207,8 @@ func (a *acrProvider) Enabled() bool {
}
// getFromCache attempts to get credentials from the cache
func (a *acrProvider) getFromCache(loginServer string) (credentialconfig.DockerConfig, bool) {
cfg := credentialconfig.DockerConfig{}
func (a *acrProvider) getFromCache(loginServer string) (credentialprovider.DockerConfig, bool) {
cfg := credentialprovider.DockerConfig{}
obj, exists, err := a.cache.GetByKey(loginServer)
if err != nil {
klog.Errorf("error getting ACR credentials from cache: %v", err)
@ -225,8 +224,8 @@ func (a *acrProvider) getFromCache(loginServer string) (credentialconfig.DockerC
}
// getFromACR gets credentials from ACR since they are not in the cache
func (a *acrProvider) getFromACR(loginServer string) (credentialconfig.DockerConfig, error) {
cfg := credentialconfig.DockerConfig{}
func (a *acrProvider) getFromACR(loginServer string) (credentialprovider.DockerConfig, error) {
cfg := credentialprovider.DockerConfig{}
cred, err := getACRDockerEntryFromARMToken(a, loginServer)
if err != nil {
return cfg, err
@ -244,14 +243,14 @@ func (a *acrProvider) getFromACR(loginServer string) (credentialconfig.DockerCon
return cfg, nil
}
func (a *acrProvider) Provide(image string) credentialconfig.DockerConfig {
func (a *acrProvider) Provide(image string) credentialprovider.DockerConfig {
loginServer := a.parseACRLoginServerFromImage(image)
if loginServer == "" {
klog.V(2).Infof("image(%s) is not from ACR, return empty authentication", image)
return credentialconfig.DockerConfig{}
return credentialprovider.DockerConfig{}
}
cfg := credentialconfig.DockerConfig{}
cfg := credentialprovider.DockerConfig{}
if a.config != nil && a.config.UseManagedIdentityExtension {
var exists bool
cfg, exists = a.getFromCache(loginServer)
@ -268,7 +267,7 @@ func (a *acrProvider) Provide(image string) credentialconfig.DockerConfig {
} else {
// Add our entry for each of the supported container registry URLs
for _, url := range containerRegistryUrls {
cred := &credentialconfig.DockerConfigEntry{
cred := &credentialprovider.DockerConfigEntry{
Username: a.config.AADClientID,
Password: a.config.AADClientSecret,
Email: dummyRegistryEmail,
@ -289,7 +288,7 @@ func (a *acrProvider) Provide(image string) credentialconfig.DockerConfig {
}
if !hasBeenAdded {
cred := &credentialconfig.DockerConfigEntry{
cred := &credentialprovider.DockerConfigEntry{
Username: a.config.AADClientID,
Password: a.config.AADClientSecret,
Email: dummyRegistryEmail,
@ -300,7 +299,7 @@ func (a *acrProvider) Provide(image string) credentialconfig.DockerConfig {
}
// add ACR anonymous repo support: use empty username and password for anonymous access
defaultConfigEntry := credentialconfig.DockerConfigEntry{
defaultConfigEntry := credentialprovider.DockerConfigEntry{
Username: "",
Password: "",
Email: dummyRegistryEmail,
@ -313,7 +312,7 @@ func getLoginServer(registry containerregistry.Registry) string {
return *(*registry.RegistryProperties).LoginServer
}
func getACRDockerEntryFromARMToken(a *acrProvider, loginServer string) (*credentialconfig.DockerConfigEntry, error) {
func getACRDockerEntryFromARMToken(a *acrProvider, loginServer string) (*credentialprovider.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)
@ -337,7 +336,7 @@ func getACRDockerEntryFromARMToken(a *acrProvider, loginServer string) (*credent
}
klog.V(4).Infof("adding ACR docker config entry for: %s", loginServer)
return &credentialconfig.DockerConfigEntry{
return &credentialprovider.DockerConfigEntry{
Username: dockerTokenLoginUsernameGUID,
Password: registryRefreshToken,
Email: dummyRegistryEmail,

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialconfig
package credentialprovider
import (
"encoding/base64"
@ -232,6 +232,7 @@ func ReadDockerConfigFileFromURL(url string, client *http.Client, header *http.H
return nil, err
}
// ReadDockerConfigFileFromBytes read a docker config file from the given bytes
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 credentialconfig
package credentialprovider
import (
"encoding/base64"

View File

@ -1,52 +0,0 @@
/*
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,15 +17,46 @@ 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/cloud-provider/credentialconfig"
"k8s.io/klog/v2"
"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 is the URL of the dockercfg metadata key used by DockerConfigKeyProvider.
DockerConfigKey = metadataAttributes + "google-dockercfg"
// DockerConfigURLKey is the URL of the dockercfg metadata key used by DockerConfigURLKeyProvider.
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 is the prefix checked by ContainerRegistryProvider.Enabled.
StorageScopePrefix = "https://www.googleapis.com/auth/devstorage"
cloudPlatformScopePrefix = "https://www.googleapis.com/auth/cloud-platform"
defaultServiceAccount = "default/"
)
// gceProductNameFile is the 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"
var metadataHeader = &http.Header{
"Metadata-Flavor": []string{"Google"},
}
// init registers the various means by which credentials may
// be resolved on GCP.
func init() {
@ -36,17 +67,17 @@ func init() {
Timeout: metadataHTTPClientTimeout,
}
credentialprovider.RegisterCredentialProvider("google-dockercfg",
&credentialconfig.CachingDockerConfigProvider{
Provider: &gcpcredential.DockerConfigKeyProvider{
gcpcredential.MetadataProvider{Client: httpClient},
&credentialprovider.CachingDockerConfigProvider{
Provider: &DockerConfigKeyProvider{
MetadataProvider: MetadataProvider{Client: httpClient},
},
Lifetime: 60 * time.Second,
})
credentialprovider.RegisterCredentialProvider("google-dockercfg-url",
&credentialconfig.CachingDockerConfigProvider{
Provider: &gcpcredential.DockerConfigURLKeyProvider{
gcpcredential.MetadataProvider{Client: httpClient},
&credentialprovider.CachingDockerConfigProvider{
Provider: &DockerConfigURLKeyProvider{
MetadataProvider: MetadataProvider{Client: httpClient},
},
Lifetime: 60 * time.Second,
})
@ -54,7 +85,169 @@ func init() {
credentialprovider.RegisterCredentialProvider("google-container-registry",
// Never cache this. The access token is already
// cached by the metadata service.
&gcpcredential.ContainerRegistryProvider{
gcpcredential.MetadataProvider{Client: httpClient},
&ContainerRegistryProvider{
MetadataProvider: MetadataProvider{Client: httpClient},
})
}
// MetadataProvider is a DockerConfigProvider that reads its configuration from Google
// Compute Engine metadata.
type MetadataProvider struct {
Client *http.Client
}
// DockerConfigKeyProvider is a DockerConfigProvider that reads its configuration from a specific
// Google Compute Engine metadata key: 'google-dockercfg'.
type DockerConfigKeyProvider struct {
MetadataProvider
}
// DockerConfigURLKeyProvider is 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
}
// ContainerRegistryProvider is 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) credentialprovider.DockerConfig {
return registryToDocker(gcpcredential.ProvideConfigKey(g.Client, image))
}
// Provide implements DockerConfigProvider
func (g *DockerConfigURLKeyProvider) Provide(image string) credentialprovider.DockerConfig {
return registryToDocker(gcpcredential.ProvideURLKey(g.Client, image))
}
// 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 := gcpcredential.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 := gcpcredential.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
}
// Provide implements DockerConfigProvider
func (g *ContainerRegistryProvider) Provide(image string) credentialprovider.DockerConfig {
return registryToDocker(gcpcredential.ProvideContainerRegistry(g.Client, image))
}
func registryToDocker(registryConfig credentialconfig.RegistryConfig) credentialprovider.DockerConfig {
dockerConfig := credentialprovider.DockerConfig{}
for k, v := range registryConfig {
dockerConfig[k] = credentialprovider.DockerConfigEntry{
Username: v.Username,
Password: v.Password,
Email: v.Email,
}
}
return dockerConfig
}

View File

@ -30,7 +30,6 @@ 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"
)
@ -43,7 +42,40 @@ func createProductNameFile() (string, error) {
return file.Name(), ioutil.WriteFile(file.Name(), []byte("Google"), 0600)
}
func TestDockerKeyringFromGoogleDockerConfigMetadata(t *testing.T) {
// The tests here are run in this fashion to ensure TestAllProvidersNoMetadata
// is run after the others, since that test currently relies upon the file
// referenced by gceProductNameFile being removed, which is the opposite of
// the other tests
func TestMetadata(t *testing.T) {
var err error
gceProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gceProductNameFile)
t.Run("productNameDependent", func(t *testing.T) {
t.Run("DockerKeyringFromGoogleDockerConfigMetadata",
DockerKeyringFromGoogleDockerConfigMetadata)
t.Run("DockerKeyringFromGoogleDockerConfigMetadataUrl",
DockerKeyringFromGoogleDockerConfigMetadataURL)
t.Run("ContainerRegistryNoServiceAccount",
ContainerRegistryNoServiceAccount)
t.Run("ContainerRegistryBasics",
ContainerRegistryBasics)
t.Run("ContainerRegistryNoStorageScope",
ContainerRegistryNoStorageScope)
t.Run("ComputePlatformScopeSubstitutesStorageScope",
ComputePlatformScopeSubstitutesStorageScope)
})
// We defer os.Remove in case of an unexpected exit, but this os.Remove call
// is the normal teardown call so AllProvidersNoMetadata executes properly
os.Remove(gceProductNameFile)
t.Run("AllProvidersNoMetadata",
AllProvidersNoMetadata)
}
func DockerKeyringFromGoogleDockerConfigMetadata(t *testing.T) {
t.Parallel()
registryURL := "hello.kubernetes.io"
email := "foo@bar.baz"
username := "foo"
@ -55,13 +87,6 @@ func TestDockerKeyringFromGoogleDockerConfigMetadata(t *testing.T) {
"auth": %q
}
}`, registryURL, email, auth)
var err error
gcpcredential.GCEProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
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.
@ -85,8 +110,8 @@ func TestDockerKeyringFromGoogleDockerConfigMetadata(t *testing.T) {
})
keyring := &credentialprovider.BasicDockerKeyring{}
provider := &gcpcredential.DockerConfigKeyProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
provider := &DockerConfigKeyProvider{
MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
@ -116,7 +141,8 @@ func TestDockerKeyringFromGoogleDockerConfigMetadata(t *testing.T) {
}
}
func TestDockerKeyringFromGoogleDockerConfigMetadataUrl(t *testing.T) {
func DockerKeyringFromGoogleDockerConfigMetadataURL(t *testing.T) {
t.Parallel()
registryURL := "hello.kubernetes.io"
email := "foo@bar.baz"
username := "foo"
@ -128,13 +154,6 @@ func TestDockerKeyringFromGoogleDockerConfigMetadataUrl(t *testing.T) {
"auth": %q
}
}`, registryURL, email, auth)
var err error
gcpcredential.GCEProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
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) {
@ -163,8 +182,8 @@ func TestDockerKeyringFromGoogleDockerConfigMetadataUrl(t *testing.T) {
})
keyring := &credentialprovider.BasicDockerKeyring{}
provider := &gcpcredential.DockerConfigURLKeyProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
provider := &DockerConfigURLKeyProvider{
MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
@ -194,7 +213,8 @@ func TestDockerKeyringFromGoogleDockerConfigMetadataUrl(t *testing.T) {
}
}
func TestContainerRegistryBasics(t *testing.T) {
func ContainerRegistryBasics(t *testing.T) {
t.Parallel()
registryURLs := []string{"container.cloud.google.com", "eu.gcr.io", "us-west2-docker.pkg.dev"}
for _, registryURL := range registryURLs {
t.Run(registryURL, func(t *testing.T) {
@ -208,12 +228,6 @@ func TestContainerRegistryBasics(t *testing.T) {
emailEndpoint = defaultEndpoint + "email"
tokenEndpoint = defaultEndpoint + "token"
)
var err error
gcpcredential.GCEProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
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
@ -249,8 +263,8 @@ func TestContainerRegistryBasics(t *testing.T) {
})
keyring := &credentialprovider.BasicDockerKeyring{}
provider := &gcpcredential.ContainerRegistryProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
provider := &ContainerRegistryProvider{
MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
@ -282,7 +296,7 @@ func TestContainerRegistryBasics(t *testing.T) {
}
}
func TestContainerRegistryNoServiceAccount(t *testing.T) {
func ContainerRegistryNoServiceAccount(t *testing.T) {
const (
serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/"
)
@ -302,13 +316,6 @@ func TestContainerRegistryNoServiceAccount(t *testing.T) {
}))
defer server.Close()
var err error
gcpcredential.GCEProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gcpcredential.GCEProductNameFile)
// Make a transport that reroutes all traffic to the example server
transport := utilnet.SetTransportDefaults(&http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
@ -316,8 +323,8 @@ func TestContainerRegistryNoServiceAccount(t *testing.T) {
},
})
provider := &gcpcredential.ContainerRegistryProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
provider := &ContainerRegistryProvider{
MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}},
}
if provider.Enabled() {
@ -325,7 +332,8 @@ func TestContainerRegistryNoServiceAccount(t *testing.T) {
}
}
func TestContainerRegistryNoStorageScope(t *testing.T) {
func ContainerRegistryNoStorageScope(t *testing.T) {
t.Parallel()
const (
serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/"
defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/"
@ -346,13 +354,6 @@ func TestContainerRegistryNoStorageScope(t *testing.T) {
}))
defer server.Close()
var err error
gcpcredential.GCEProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gcpcredential.GCEProductNameFile)
// Make a transport that reroutes all traffic to the example server
transport := utilnet.SetTransportDefaults(&http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
@ -360,8 +361,8 @@ func TestContainerRegistryNoStorageScope(t *testing.T) {
},
})
provider := &gcpcredential.ContainerRegistryProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
provider := &ContainerRegistryProvider{
MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}},
}
if provider.Enabled() {
@ -369,7 +370,8 @@ func TestContainerRegistryNoStorageScope(t *testing.T) {
}
}
func TestComputePlatformScopeSubstitutesStorageScope(t *testing.T) {
func ComputePlatformScopeSubstitutesStorageScope(t *testing.T) {
t.Parallel()
const (
serviceAccountsEndpoint = "/computeMetadata/v1/instance/service-accounts/"
defaultEndpoint = "/computeMetadata/v1/instance/service-accounts/default/"
@ -391,13 +393,6 @@ func TestComputePlatformScopeSubstitutesStorageScope(t *testing.T) {
}))
defer server.Close()
var err error
gcpcredential.GCEProductNameFile, err = createProductNameFile()
if err != nil {
t.Errorf("failed to create gce product name file: %v", err)
}
defer os.Remove(gcpcredential.GCEProductNameFile)
// Make a transport that reroutes all traffic to the example server
transport := utilnet.SetTransportDefaults(&http.Transport{
Proxy: func(req *http.Request) (*url.URL, error) {
@ -405,8 +400,8 @@ func TestComputePlatformScopeSubstitutesStorageScope(t *testing.T) {
},
})
provider := &gcpcredential.ContainerRegistryProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
provider := &ContainerRegistryProvider{
MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}},
}
if !provider.Enabled() {
@ -414,7 +409,7 @@ func TestComputePlatformScopeSubstitutesStorageScope(t *testing.T) {
}
}
func TestAllProvidersNoMetadata(t *testing.T) {
func AllProvidersNoMetadata(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "", http.StatusNotFound)
}))
@ -427,15 +422,15 @@ func TestAllProvidersNoMetadata(t *testing.T) {
},
})
providers := []credentialconfig.DockerConfigProvider{
&gcpcredential.DockerConfigKeyProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
providers := []credentialprovider.DockerConfigProvider{
&DockerConfigKeyProvider{
MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}},
},
&gcpcredential.DockerConfigURLKeyProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
&DockerConfigURLKeyProvider{
MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}},
},
&gcpcredential.ContainerRegistryProvider{
gcpcredential.MetadataProvider{Client: &http.Client{Transport: transport}},
&ContainerRegistryProvider{
MetadataProvider: MetadataProvider{Client: &http.Client{Transport: transport}},
},
}

View File

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

View File

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

View File

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

View File

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialconfig
package credentialprovider
import (
"os"
"reflect"
"sync"
"time"
@ -38,6 +39,18 @@ 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.
@ -55,6 +68,22 @@ 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(2).Infof("Docker config file not found: %v", err)
}
return DockerConfig{}
}
// Enabled implements dockerConfigProvider
func (d *CachingDockerConfigProvider) Enabled() bool {
return d.Provider.Enabled()

View File

@ -1,5 +1,5 @@
/*
Copyright 2014 The Kubernetes Authors.
Copyright 2020 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.
@ -14,13 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialconfig
package credentialprovider
import (
"testing"
"time"
)
type testProvider struct {
Count int
}
// Enabled implements dockerConfigProvider
func (d *testProvider) Enabled() bool {
return true
}
// Provide implements dockerConfigProvider
func (d *testProvider) Provide(image string) DockerConfig {
d.Count++
return DockerConfig{}
}
func TestCachingProvider(t *testing.T) {
provider := &testProvider{
Count: 0,

View File

@ -19,8 +19,7 @@ package secrets
import (
"encoding/json"
"k8s.io/api/core/v1"
"k8s.io/cloud-provider/credentialconfig"
v1 "k8s.io/api/core/v1"
"k8s.io/kubernetes/pkg/credentialprovider"
)
@ -28,17 +27,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 := []credentialconfig.DockerConfig{}
passedCredentials := []credentialprovider.DockerConfig{}
for _, passedSecret := range passedSecrets {
if dockerConfigJSONBytes, dockerConfigJSONExists := passedSecret.Data[v1.DockerConfigJsonKey]; (passedSecret.Type == v1.SecretTypeDockerConfigJson) && dockerConfigJSONExists && (len(dockerConfigJSONBytes) > 0) {
dockerConfigJSON := credentialconfig.DockerConfigJSON{}
dockerConfigJSON := credentialprovider.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 := credentialconfig.DockerConfig{}
dockercfg := credentialprovider.DockerConfig{}
if err := json.Unmarshal(dockercfgBytes, &dockercfg); err != nil {
return nil, err
}

View File

@ -0,0 +1,31 @@
/*
Copyright 2021 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 credentialconfig
// Code taken from /pkg/credentialprovider/config.go
// RegistryConfig 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 RegistryConfig map[string]RegistryConfigEntry
// RegistryConfigEntry wraps a docker config as a entry
type RegistryConfigEntry struct {
Username string
Password string
Email string
}

View File

@ -0,0 +1,113 @@
/*
Copyright 2021 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"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"k8s.io/cloud-provider/credentialconfig"
"k8s.io/klog/v2"
)
const (
maxReadLength = 10 * 1 << 20 // 10MB
)
// HTTPError wraps a non-StatusOK error code as an error.
type HTTPError struct {
StatusCode int
URL string
}
// Error implements error
func (he *HTTPError) Error() string {
return fmt.Sprintf("http status code: %d while fetching url %s",
he.StatusCode, he.URL)
}
// ReadURL read contents from given url
func ReadURL(url string, client *http.Client, header *http.Header) (body []byte, err error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
if header != nil {
req.Header = *header
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
klog.V(2).Infof("body of failing http response: %v", resp.Body)
return nil, &HTTPError{
StatusCode: resp.StatusCode,
URL: url,
}
}
limitedReader := &io.LimitedReader{R: resp.Body, N: maxReadLength}
contents, err := ioutil.ReadAll(limitedReader)
if err != nil {
return nil, err
}
if limitedReader.N <= 0 {
return nil, errors.New("the read limit is reached")
}
return contents, nil
}
// ReadDockerConfigFileFromURL read a docker config file from the given url
func ReadDockerConfigFileFromURL(url string, client *http.Client, header *http.Header) (cfg credentialconfig.RegistryConfig, err error) {
if contents, err := ReadURL(url, client, header); err == nil {
return ReadDockerConfigFileFromBytes(contents)
}
return nil, err
}
type internalRegistryConfig map[string]RegistryConfigEntry
// ReadDockerConfigFileFromBytes read a docker config file from the given bytes
func ReadDockerConfigFileFromBytes(contents []byte) (cfg credentialconfig.RegistryConfig, err error) {
serializableCfg := internalRegistryConfig{}
if err = json.Unmarshal(contents, &serializableCfg); err != nil {
return nil, errors.New("error occurred while trying to unmarshal json")
}
return convertToExternalConfig(serializableCfg), nil
}
func convertToExternalConfig(in internalRegistryConfig) (cfg credentialconfig.RegistryConfig) {
configMap := credentialconfig.RegistryConfig{}
for k, v := range in {
configMap[k] = credentialconfig.RegistryConfigEntry{
Username: v.Username,
Password: v.Password,
Email: v.Email,
}
}
return configMap
}

View File

@ -1,5 +1,5 @@
/*
Copyright 2014 The Kubernetes Authors.
Copyright 2020 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.
@ -18,32 +18,31 @@ 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"
metadataURL = "http://metadata.google.internal./computeMetadata/v1/"
metadataAttributes = metadataURL + "instance/attributes/"
// DockerConfigKey is the URL of the dockercfg metadata key used by DockerConfigKeyProvider.
DockerConfigKey = metadataAttributes + "google-dockercfg"
// DockerConfigURLKey is the URL of the dockercfg metadata key used by DockerConfigURLKeyProvider.
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 is the prefix checked by ContainerRegistryProvider.Enabled.
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.
// GCEProductNameFile is the 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"
@ -55,84 +54,27 @@ 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 {
// ProvideConfigKey implements a dockercfg-based authentication flow.
func ProvideConfigKey(client *http.Client, image string) credentialconfig.RegistryConfig {
// 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 {
if cfg, err := ReadDockerConfigFileFromURL(DockerConfigKey, client, metadataHeader); err != nil {
klog.Errorf("while reading 'google-dockercfg' metadata: %v", err)
} else {
return cfg
}
return credentialconfig.DockerConfig{}
return credentialconfig.RegistryConfig{}
}
// Provide implements DockerConfigProvider
func (g *DockerConfigURLKeyProvider) Provide(image string) credentialconfig.DockerConfig {
// ProvideURLKey implements a dockercfg-url-based authentication flow.
func ProvideURLKey(client *http.Client, image string) credentialconfig.RegistryConfig {
// 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 {
if url, err := ReadURL(DockerConfigURLKey, 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 {
if cfg, err := ReadDockerConfigFileFromURL(string(url), client, nil); err != nil {
klog.Errorf("while reading 'google-dockercfg-url'-specified url: %s, %v", string(url), err)
} else {
return cfg
@ -143,85 +85,7 @@ func (g *DockerConfigURLKeyProvider) Provide(image string) credentialconfig.Dock
}
}
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
return credentialconfig.RegistryConfig{}
}
// TokenBlob is used to decode the JSON blob containing an access token
@ -230,17 +94,17 @@ type TokenBlob struct {
AccessToken string `json:"access_token"`
}
// Provide implements DockerConfigProvider
func (g *ContainerRegistryProvider) Provide(image string) credentialconfig.DockerConfig {
cfg := credentialconfig.DockerConfig{}
// ProvideContainerRegistry implements a gcr.io-based authentication flow.
func ProvideContainerRegistry(client *http.Client, image string) credentialconfig.RegistryConfig {
cfg := credentialconfig.RegistryConfig{}
tokenJSONBlob, err := credentialconfig.ReadURL(metadataToken, g.Client, metadataHeader)
tokenJSONBlob, err := ReadURL(metadataToken, client, metadataHeader)
if err != nil {
klog.Errorf("while reading access token endpoint: %v", err)
return cfg
}
email, err := credentialconfig.ReadURL(metadataEmail, g.Client, metadataHeader)
email, err := ReadURL(metadataEmail, client, metadataHeader)
if err != nil {
klog.Errorf("while reading email endpoint: %v", err)
return cfg
@ -252,7 +116,7 @@ func (g *ContainerRegistryProvider) Provide(image string) credentialconfig.Docke
return cfg
}
entry := credentialconfig.DockerConfigEntry{
entry := credentialconfig.RegistryConfigEntry{
Username: "_token",
Password: parsedBlob.AccessToken,
Email: string(email),

View File

@ -0,0 +1,110 @@
/*
Copyright 2021 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/base64"
"encoding/json"
"fmt"
"strings"
"k8s.io/cloud-provider/credentialconfig"
)
// registryConfigEntryWithAuth is used solely for deserializing the Auth field
// into a dockerConfigEntry during JSON deserialization.
type registryConfigEntryWithAuth struct {
// +optional
Username string `json:"username,omitempty"`
// +optional
Password string `json:"password,omitempty"`
// +optional
Email string `json:"email,omitempty"`
// +optional
Auth string `json:"auth,omitempty"`
}
// RegistryConfigEntry is a serializable wrapper around credentialconfig.RegistryConfigEntry.
type RegistryConfigEntry struct {
credentialconfig.RegistryConfigEntry
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (ident *RegistryConfigEntry) UnmarshalJSON(data []byte) error {
var tmp registryConfigEntryWithAuth
err := json.Unmarshal(data, &tmp)
if err != nil {
return err
}
ident.Username = tmp.Username
ident.Password = tmp.Password
ident.Email = tmp.Email
if len(tmp.Auth) == 0 {
return nil
}
ident.Username, ident.Password, err = decodeRegistryConfigFieldAuth(tmp.Auth)
return err
}
// MarshalJSON implements the json.Marshaler interface.
func (ident RegistryConfigEntry) MarshalJSON() ([]byte, error) {
toEncode := registryConfigEntryWithAuth{ident.Username, ident.Password, ident.Email, ""}
toEncode.Auth = encodeRegistryConfigFieldAuth(ident.Username, ident.Password)
return json.Marshal(toEncode)
}
// decodeRegistryConfigFieldAuth deserializes the "auth" field from dockercfg into a
// username and a password. The format of the auth field is base64(<username>:<password>).
func decodeRegistryConfigFieldAuth(field string) (username, password string, err error) {
var decoded []byte
// StdEncoding can only decode padded string
// RawStdEncoding can only decode unpadded string
if strings.HasSuffix(strings.TrimSpace(field), "=") {
// decode padded data
decoded, err = base64.StdEncoding.DecodeString(field)
} else {
// decode unpadded data
decoded, err = base64.RawStdEncoding.DecodeString(field)
}
if err != nil {
return
}
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
err = fmt.Errorf("unable to parse auth field, must be formatted as base64(username:password)")
return
}
username = parts[0]
password = parts[1]
return
}
func encodeRegistryConfigFieldAuth(username, password string) string {
fieldValue := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(fieldValue))
}

View File

@ -0,0 +1,231 @@
/*
Copyright 2021 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/base64"
"encoding/json"
"k8s.io/cloud-provider/credentialconfig"
"reflect"
"testing"
)
// Code copied (and edited to replace DockerConfig* with RegistryConfig*) from:
// pkg/credentialprovider/config_test.go.
func TestRegistryConfigEntryJSONDecode(t *testing.T) {
tests := []struct {
input []byte
expect RegistryConfigEntry
fail bool
}{
// simple case, just decode the fields
{
// Fake values for testing.
input: []byte(`{"username": "foo", "password": "bar", "email": "foo@example.com"}`),
expect: RegistryConfigEntry{
credentialconfig.RegistryConfigEntry{
Username: "foo",
Password: "bar",
Email: "foo@example.com",
},
},
fail: false,
},
// auth field decodes to username & password
{
input: []byte(`{"auth": "Zm9vOmJhcg==", "email": "foo@example.com"}`),
expect: RegistryConfigEntry{
credentialconfig.RegistryConfigEntry{
Username: "foo",
Password: "bar",
Email: "foo@example.com",
},
},
fail: false,
},
// auth field overrides username & password
{
// Fake values for testing.
input: []byte(`{"username": "foo", "password": "bar", "auth": "cGluZzpwb25n", "email": "foo@example.com"}`),
expect: RegistryConfigEntry{
credentialconfig.RegistryConfigEntry{
Username: "ping",
Password: "pong",
Email: "foo@example.com",
},
},
fail: false,
},
// poorly-formatted auth causes failure
{
input: []byte(`{"auth": "pants", "email": "foo@example.com"}`),
expect: RegistryConfigEntry{
credentialconfig.RegistryConfigEntry{
Username: "",
Password: "",
Email: "foo@example.com",
},
},
fail: true,
},
// invalid JSON causes failure
{
input: []byte(`{"email": false}`),
expect: RegistryConfigEntry{
credentialconfig.RegistryConfigEntry{
Username: "",
Password: "",
Email: "",
},
},
fail: true,
},
}
for i, tt := range tests {
var output RegistryConfigEntry
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)
}
if !reflect.DeepEqual(tt.expect, output) {
t.Errorf("case %d: expected output %#v, got %#v", i, tt.expect, output)
}
}
}
func TestDecodeRegistryConfigFieldAuth(t *testing.T) {
tests := []struct {
input string
username string
password string
fail bool
}{
// auth field decodes to username & password
{
input: "Zm9vOmJhcg==",
username: "foo",
password: "bar",
},
// some test as before but with field not well padded
{
input: "Zm9vOmJhcg",
username: "foo",
password: "bar",
},
// some test as before but with new line characters
{
input: "Zm9vOm\nJhcg==\n",
username: "foo",
password: "bar",
},
// standard encoding (with padding)
{
input: base64.StdEncoding.EncodeToString([]byte("foo:bar")),
username: "foo",
password: "bar",
},
// raw encoding (without padding)
{
input: base64.RawStdEncoding.EncodeToString([]byte("foo:bar")),
username: "foo",
password: "bar",
},
// the input is encoded with encodeRegistryConfigFieldAuth (standard encoding)
{
input: encodeRegistryConfigFieldAuth("foo", "bar"),
username: "foo",
password: "bar",
},
// good base64 data, but no colon separating username & password
{
input: "cGFudHM=",
fail: true,
},
// only new line characters are ignored
{
input: "Zm9vOmJhcg== ",
fail: true,
},
// bad base64 data
{
input: "pants",
fail: true,
},
}
for i, tt := range tests {
username, password, err := decodeRegistryConfigFieldAuth(tt.input)
if (err != nil) != tt.fail {
t.Errorf("case %d: expected fail=%t, got err=%v", i, tt.fail, err)
}
if tt.username != username {
t.Errorf("case %d: expected username %q, got %q", i, tt.username, username)
}
if tt.password != password {
t.Errorf("case %d: expected password %q, got %q", i, tt.password, password)
}
}
}
func TestRegistryConfigEntryJSONCompatibleEncode(t *testing.T) {
tests := []struct {
input RegistryConfigEntry
expect []byte
}{
// simple case, just decode the fields
{
// Fake values for testing.
expect: []byte(`{"username":"foo","password":"bar","email":"foo@example.com","auth":"Zm9vOmJhcg=="}`),
input: RegistryConfigEntry{
credentialconfig.RegistryConfigEntry{
Username: "foo",
Password: "bar",
Email: "foo@example.com",
},
},
},
}
for i, tt := range tests {
actual, err := json.Marshal(tt.input)
if err != nil {
t.Errorf("case %d: unexpected error: %v", i, err)
}
if string(tt.expect) != string(actual) {
t.Errorf("case %d: expected %v, got %v", i, string(tt.expect), string(actual))
}
}
}