mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 15:05:27 +00:00
Add RegistryConfig/RegistryConfigEntry.
This commit is contained in:
parent
acaea957ff
commit
0dcafb1f37
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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")
|
@ -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"
|
@ -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{}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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}},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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()
|
@ -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,
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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),
|
||||
|
@ -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))
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user