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

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

View File

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

View File

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

View File

@@ -1,328 +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 (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"k8s.io/klog/v2"
)
const (
maxReadLength = 10 * 1 << 20 // 10MB
)
// DockerConfigJSON represents ~/.docker/config.json file info
// see https://github.com/docker/docker/pull/12009
type DockerConfigJSON struct {
Auths DockerConfig `json:"auths"`
// +optional
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
}
// DockerConfig 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 DockerConfig map[string]DockerConfigEntry
// DockerConfigEntry wraps a docker config as a entry
type DockerConfigEntry struct {
Username string
Password string
Email string
Provider DockerConfigProvider
}
var (
preferredPathLock sync.Mutex
preferredPath = ""
workingDirPath = ""
homeDirPath, _ = os.UserHomeDir()
rootDirPath = "/"
homeJSONDirPath = filepath.Join(homeDirPath, ".docker")
rootJSONDirPath = filepath.Join(rootDirPath, ".docker")
configFileName = ".dockercfg"
configJSONFileName = "config.json"
)
// SetPreferredDockercfgPath set preferred docker config path
func SetPreferredDockercfgPath(path string) {
preferredPathLock.Lock()
defer preferredPathLock.Unlock()
preferredPath = path
}
// GetPreferredDockercfgPath get preferred docker config path
func GetPreferredDockercfgPath() string {
preferredPathLock.Lock()
defer preferredPathLock.Unlock()
return preferredPath
}
//DefaultDockercfgPaths returns default search paths of .dockercfg
func DefaultDockercfgPaths() []string {
return []string{GetPreferredDockercfgPath(), workingDirPath, homeDirPath, rootDirPath}
}
//DefaultDockerConfigJSONPaths returns default search paths of .docker/config.json
func DefaultDockerConfigJSONPaths() []string {
return []string{GetPreferredDockercfgPath(), workingDirPath, homeJSONDirPath, rootJSONDirPath}
}
// ReadDockercfgFile attempts to read a legacy dockercfg file from the given paths.
// if searchPaths is empty, the default paths are used.
func ReadDockercfgFile(searchPaths []string) (cfg DockerConfig, err error) {
if len(searchPaths) == 0 {
searchPaths = DefaultDockercfgPaths()
}
for _, configPath := range searchPaths {
absDockerConfigFileLocation, err := filepath.Abs(filepath.Join(configPath, configFileName))
if err != nil {
klog.Errorf("while trying to canonicalize %s: %v", configPath, err)
continue
}
klog.V(4).Infof("looking for .dockercfg at %s", absDockerConfigFileLocation)
contents, err := ioutil.ReadFile(absDockerConfigFileLocation)
if os.IsNotExist(err) {
continue
}
if err != nil {
klog.V(4).Infof("while trying to read %s: %v", absDockerConfigFileLocation, err)
continue
}
cfg, err := readDockerConfigFileFromBytes(contents)
if err != nil {
klog.V(4).Infof("couldn't get the config from %q contents: %v", absDockerConfigFileLocation, err)
continue
}
klog.V(4).Infof("found .dockercfg at %s", absDockerConfigFileLocation)
return cfg, nil
}
return nil, fmt.Errorf("couldn't find valid .dockercfg after checking in %v", searchPaths)
}
// ReadDockerConfigJSONFile attempts to read a docker config.json file from the given paths.
// if searchPaths is empty, the default paths are used.
func ReadDockerConfigJSONFile(searchPaths []string) (cfg DockerConfig, err error) {
if len(searchPaths) == 0 {
searchPaths = DefaultDockerConfigJSONPaths()
}
for _, configPath := range searchPaths {
absDockerConfigFileLocation, err := filepath.Abs(filepath.Join(configPath, configJSONFileName))
if err != nil {
klog.Errorf("while trying to canonicalize %s: %v", configPath, err)
continue
}
klog.V(4).Infof("looking for %s at %s", configJSONFileName, absDockerConfigFileLocation)
cfg, err = ReadSpecificDockerConfigJSONFile(absDockerConfigFileLocation)
if err != nil {
if !os.IsNotExist(err) {
klog.V(4).Infof("while trying to read %s: %v", absDockerConfigFileLocation, err)
}
continue
}
klog.V(4).Infof("found valid %s at %s", configJSONFileName, absDockerConfigFileLocation)
return cfg, nil
}
return nil, fmt.Errorf("couldn't find valid %s after checking in %v", configJSONFileName, searchPaths)
}
//ReadSpecificDockerConfigJSONFile attempts to read docker configJSON from a given file path.
func ReadSpecificDockerConfigJSONFile(filePath string) (cfg DockerConfig, err error) {
var contents []byte
if contents, err = ioutil.ReadFile(filePath); err != nil {
return nil, err
}
return readDockerConfigJSONFileFromBytes(contents)
}
// ReadDockerConfigFile read a docker config file from default path
func ReadDockerConfigFile() (cfg DockerConfig, err error) {
if cfg, err := ReadDockerConfigJSONFile(nil); err == nil {
return cfg, nil
}
// Can't find latest config file so check for the old one
return ReadDockercfgFile(nil)
}
// 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 DockerConfig, err error) {
if contents, err := ReadURL(url, client, header); err == nil {
return readDockerConfigFileFromBytes(contents)
}
return nil, err
}
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")
}
return
}
func readDockerConfigJSONFileFromBytes(contents []byte) (cfg DockerConfig, err error) {
var cfgJSON DockerConfigJSON
if err = json.Unmarshal(contents, &cfgJSON); err != nil {
return nil, errors.New("error occurred while trying to unmarshal json")
}
cfg = cfgJSON.Auths
return
}
// dockerConfigEntryWithAuth is used solely for deserializing the Auth field
// into a dockerConfigEntry during JSON deserialization.
type dockerConfigEntryWithAuth 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"`
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (ident *DockerConfigEntry) UnmarshalJSON(data []byte) error {
var tmp dockerConfigEntryWithAuth
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 = decodeDockerConfigFieldAuth(tmp.Auth)
return err
}
// MarshalJSON implements the json.Marshaler interface.
func (ident DockerConfigEntry) MarshalJSON() ([]byte, error) {
toEncode := dockerConfigEntryWithAuth{ident.Username, ident.Password, ident.Email, ""}
toEncode.Auth = encodeDockerConfigFieldAuth(ident.Username, ident.Password)
return json.Marshal(toEncode)
}
// decodeDockerConfigFieldAuth deserializes the "auth" field from dockercfg into a
// username and a password. The format of the auth field is base64(<username>:<password>).
func decodeDockerConfigFieldAuth(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 encodeDockerConfigFieldAuth(username, password string) string {
fieldValue := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(fieldValue))
}

View File

@@ -1,404 +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 (
"encoding/base64"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
)
func TestReadDockerConfigFile(t *testing.T) {
configJSONFileName := "config.json"
var fileInfo *os.File
//test dockerconfig json
inputDockerconfigJSONFile := "{ \"auths\": { \"http://foo.example.com\":{\"auth\":\"Zm9vOmJhcgo=\",\"email\":\"foo@example.com\"}}}"
preferredPath, err := ioutil.TempDir("", "test_foo_bar_dockerconfigjson_")
if err != nil {
t.Fatalf("Creating tmp dir fail: %v", err)
return
}
defer os.RemoveAll(preferredPath)
absDockerConfigFileLocation, err := filepath.Abs(filepath.Join(preferredPath, configJSONFileName))
if err != nil {
t.Fatalf("While trying to canonicalize %s: %v", preferredPath, err)
}
if _, err := os.Stat(absDockerConfigFileLocation); os.IsNotExist(err) {
//create test cfg file
fileInfo, err = os.OpenFile(absDockerConfigFileLocation, os.O_CREATE|os.O_RDWR, 0664)
if err != nil {
t.Fatalf("While trying to create file %s: %v", absDockerConfigFileLocation, err)
}
defer fileInfo.Close()
}
fileInfo.WriteString(inputDockerconfigJSONFile)
orgPreferredPath := GetPreferredDockercfgPath()
SetPreferredDockercfgPath(preferredPath)
defer SetPreferredDockercfgPath(orgPreferredPath)
if _, err := ReadDockerConfigFile(); err != nil {
t.Errorf("Getting docker config file fail : %v preferredPath : %q", err, preferredPath)
}
}
func TestDockerConfigJsonJSONDecode(t *testing.T) {
// Fake values for testing.
input := []byte(`{"auths": {"http://foo.example.com":{"username": "foo", "password": "bar", "email": "foo@example.com"}, "http://bar.example.com":{"username": "bar", "password": "baz", "email": "bar@example.com"}}}`)
expect := DockerConfigJSON{
Auths: DockerConfig(map[string]DockerConfigEntry{
"http://foo.example.com": {
Username: "foo",
Password: "bar",
Email: "foo@example.com",
},
"http://bar.example.com": {
Username: "bar",
Password: "baz",
Email: "bar@example.com",
},
}),
}
var output DockerConfigJSON
err := json.Unmarshal(input, &output)
if err != nil {
t.Errorf("Received unexpected error: %v", err)
}
if !reflect.DeepEqual(expect, output) {
t.Errorf("Received unexpected output. Expected %#v, got %#v", expect, output)
}
}
func TestDockerConfigJSONDecode(t *testing.T) {
// Fake values for testing.
input := []byte(`{"http://foo.example.com":{"username": "foo", "password": "bar", "email": "foo@example.com"}, "http://bar.example.com":{"username": "bar", "password": "baz", "email": "bar@example.com"}}`)
expect := DockerConfig(map[string]DockerConfigEntry{
"http://foo.example.com": {
Username: "foo",
Password: "bar",
Email: "foo@example.com",
},
"http://bar.example.com": {
Username: "bar",
Password: "baz",
Email: "bar@example.com",
},
})
var output DockerConfig
err := json.Unmarshal(input, &output)
if err != nil {
t.Errorf("Received unexpected error: %v", err)
}
if !reflect.DeepEqual(expect, output) {
t.Errorf("Received unexpected output. Expected %#v, got %#v", expect, output)
}
}
func TestDockerConfigEntryJSONDecode(t *testing.T) {
tests := []struct {
input []byte
expect DockerConfigEntry
fail bool
}{
// simple case, just decode the fields
{
// Fake values for testing.
input: []byte(`{"username": "foo", "password": "bar", "email": "foo@example.com"}`),
expect: DockerConfigEntry{
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: DockerConfigEntry{
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: DockerConfigEntry{
Username: "ping",
Password: "pong",
Email: "foo@example.com",
},
fail: false,
},
// poorly-formatted auth causes failure
{
input: []byte(`{"auth": "pants", "email": "foo@example.com"}`),
expect: DockerConfigEntry{
Username: "",
Password: "",
Email: "foo@example.com",
},
fail: true,
},
// invalid JSON causes failure
{
input: []byte(`{"email": false}`),
expect: DockerConfigEntry{
Username: "",
Password: "",
Email: "",
},
fail: true,
},
}
for i, tt := range tests {
var output DockerConfigEntry
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 TestDecodeDockerConfigFieldAuth(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 encodeDockerConfigFieldAuth (standard encoding)
{
input: encodeDockerConfigFieldAuth("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 := decodeDockerConfigFieldAuth(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 TestDockerConfigEntryJSONCompatibleEncode(t *testing.T) {
tests := []struct {
input DockerConfigEntry
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: DockerConfigEntry{
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))
}
}
}
func TestReadDockerConfigFileFromBytes(t *testing.T) {
testCases := []struct {
id string
input []byte
expectedCfg DockerConfig
errorExpected bool
expectedErrorMsg string
}{
{
id: "valid input, no error expected",
input: []byte(`{"http://foo.example.com":{"username": "foo", "password": "bar", "email": "foo@example.com"}}`),
expectedCfg: DockerConfig(map[string]DockerConfigEntry{
"http://foo.example.com": {
Username: "foo",
Password: "bar",
Email: "foo@example.com",
},
}),
},
{
id: "invalid input, error expected",
input: []byte(`{"http://foo.example.com":{"username": "foo", "password": "bar", "email": "foo@example.com"`),
errorExpected: true,
expectedErrorMsg: "error occurred while trying to unmarshal json",
},
}
for _, tc := range testCases {
cfg, err := readDockerConfigFileFromBytes(tc.input)
if err != nil && !tc.errorExpected {
t.Fatalf("Error was not expected: %v", err)
}
if err != nil && tc.errorExpected {
if !reflect.DeepEqual(err.Error(), tc.expectedErrorMsg) {
t.Fatalf("Expected error message: `%s` got `%s`", tc.expectedErrorMsg, err.Error())
}
} else {
if !reflect.DeepEqual(cfg, tc.expectedCfg) {
t.Fatalf("expected: %v got %v", tc.expectedCfg, cfg)
}
}
}
}
func TestReadDockerConfigJSONFileFromBytes(t *testing.T) {
testCases := []struct {
id string
input []byte
expectedCfg DockerConfig
errorExpected bool
expectedErrorMsg string
}{
{
id: "valid input, no error expected",
input: []byte(`{"auths": {"http://foo.example.com":{"username": "foo", "password": "bar", "email": "foo@example.com"}, "http://bar.example.com":{"username": "bar", "password": "baz", "email": "bar@example.com"}}}`),
expectedCfg: DockerConfig(map[string]DockerConfigEntry{
"http://foo.example.com": {
Username: "foo",
Password: "bar",
Email: "foo@example.com",
},
"http://bar.example.com": {
Username: "bar",
Password: "baz",
Email: "bar@example.com",
},
}),
},
{
id: "invalid input, error expected",
input: []byte(`{"auths": {"http://foo.example.com":{"username": "foo", "password": "bar", "email": "foo@example.com"}, "http://bar.example.com":{"username": "bar", "password": "baz", "email": "bar@example.com"`),
errorExpected: true,
expectedErrorMsg: "error occurred while trying to unmarshal json",
},
}
for _, tc := range testCases {
cfg, err := readDockerConfigJSONFileFromBytes(tc.input)
if err != nil && !tc.errorExpected {
t.Fatalf("Error was not expected: %v", err)
}
if err != nil && tc.errorExpected {
if !reflect.DeepEqual(err.Error(), tc.expectedErrorMsg) {
t.Fatalf("Expected error message: `%s` got `%s`", tc.expectedErrorMsg, err.Error())
}
} else {
if !reflect.DeepEqual(cfg, tc.expectedCfg) {
t.Fatalf("expected: %v got %v", tc.expectedCfg, cfg)
}
}
}
}

View File

@@ -0,0 +1,52 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package credentialprovider
import (
"k8s.io/cloud-provider/credentialconfig"
"k8s.io/klog/v2"
"os"
"time"
)
// A DockerConfigProvider that simply reads the .dockercfg file
type defaultDockerConfigProvider struct{}
// init registers our default provider, which simply reads the .dockercfg file.
func init() {
RegisterCredentialProvider(".dockercfg",
&credentialconfig.CachingDockerConfigProvider{
Provider: &defaultDockerConfigProvider{},
Lifetime: 5 * time.Minute,
})
}
// Enabled implements dockerConfigProvider
func (d *defaultDockerConfigProvider) Enabled() bool {
return true
}
// Provide implements dockerConfigProvider
func (d *defaultDockerConfigProvider) Provide(image string) credentialconfig.DockerConfig {
// Read the standard Docker credentials from .dockercfg
if cfg, err := credentialconfig.ReadDockerConfigFile(); err == nil {
return cfg
} else if !os.IsNotExist(err) {
klog.V(4).Infof("Unable to parse Docker config file: %v", err)
}
return credentialconfig.DockerConfig{}
}

View File

@@ -17,70 +17,15 @@ limitations under the License.
package gcp
import (
"encoding/json"
"io/ioutil"
"net/http"
"os/exec"
"runtime"
"strings"
"time"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/klog/v2"
"k8s.io/cloud-provider/credentialconfig"
"k8s.io/kubernetes/pkg/credentialprovider"
"k8s.io/legacy-cloud-providers/gce/gcpcredential"
)
const (
metadataURL = "http://metadata.google.internal./computeMetadata/v1/"
metadataAttributes = metadataURL + "instance/attributes/"
dockerConfigKey = metadataAttributes + "google-dockercfg"
dockerConfigURLKey = metadataAttributes + "google-dockercfg-url"
serviceAccounts = metadataURL + "instance/service-accounts/"
metadataScopes = metadataURL + "instance/service-accounts/default/scopes"
metadataToken = metadataURL + "instance/service-accounts/default/token"
metadataEmail = metadataURL + "instance/service-accounts/default/email"
storageScopePrefix = "https://www.googleapis.com/auth/devstorage"
cloudPlatformScopePrefix = "https://www.googleapis.com/auth/cloud-platform"
defaultServiceAccount = "default/"
)
// Product file path that contains the cloud service name.
// This is a variable instead of a const to enable testing.
var gceProductNameFile = "/sys/class/dmi/id/product_name"
// For these urls, the parts of the host name can be glob, for example '*.gcr.io" will match
// "foo.gcr.io" and "bar.gcr.io".
var containerRegistryUrls = []string{"container.cloud.google.com", "gcr.io", "*.gcr.io", "*.pkg.dev"}
var metadataHeader = &http.Header{
"Metadata-Flavor": []string{"Google"},
}
// A DockerConfigProvider that reads its configuration from Google
// Compute Engine metadata.
type metadataProvider struct {
Client *http.Client
}
// A DockerConfigProvider that reads its configuration from a specific
// Google Compute Engine metadata key: 'google-dockercfg'.
type dockerConfigKeyProvider struct {
metadataProvider
}
// A DockerConfigProvider that reads its configuration from a URL read from
// a specific Google Compute Engine metadata key: 'google-dockercfg-url'.
type dockerConfigURLKeyProvider struct {
metadataProvider
}
// A DockerConfigProvider that provides a dockercfg with:
// Username: "_token"
// Password: "{access token from metadata}"
type containerRegistryProvider struct {
metadataProvider
}
// init registers the various means by which credentials may
// be resolved on GCP.
func init() {
@@ -91,17 +36,17 @@ func init() {
Timeout: metadataHTTPClientTimeout,
}
credentialprovider.RegisterCredentialProvider("google-dockercfg",
&credentialprovider.CachingDockerConfigProvider{
Provider: &dockerConfigKeyProvider{
metadataProvider{Client: httpClient},
&credentialconfig.CachingDockerConfigProvider{
Provider: &gcpcredential.DockerConfigKeyProvider{
gcpcredential.MetadataProvider{Client: httpClient},
},
Lifetime: 60 * time.Second,
})
credentialprovider.RegisterCredentialProvider("google-dockercfg-url",
&credentialprovider.CachingDockerConfigProvider{
Provider: &dockerConfigURLKeyProvider{
metadataProvider{Client: httpClient},
&credentialconfig.CachingDockerConfigProvider{
Provider: &gcpcredential.DockerConfigURLKeyProvider{
gcpcredential.MetadataProvider{Client: httpClient},
},
Lifetime: 60 * time.Second,
})
@@ -109,192 +54,7 @@ func init() {
credentialprovider.RegisterCredentialProvider("google-container-registry",
// Never cache this. The access token is already
// cached by the metadata service.
&containerRegistryProvider{
metadataProvider{Client: httpClient},
&gcpcredential.ContainerRegistryProvider{
gcpcredential.MetadataProvider{Client: httpClient},
})
}
// Returns true if it finds a local GCE VM.
// Looks at a product file that is an undocumented API.
func onGCEVM() bool {
var name string
if runtime.GOOS == "windows" {
data, err := exec.Command("wmic", "computersystem", "get", "model").Output()
if err != nil {
return false
}
fields := strings.Split(strings.TrimSpace(string(data)), "\r\n")
if len(fields) != 2 {
klog.V(2).Infof("Received unexpected value retrieving system model: %q", string(data))
return false
}
name = fields[1]
} else {
data, err := ioutil.ReadFile(gceProductNameFile)
if err != nil {
klog.V(2).Infof("Error while reading product_name: %v", err)
return false
}
name = strings.TrimSpace(string(data))
}
return name == "Google" || name == "Google Compute Engine"
}
// Enabled implements DockerConfigProvider for all of the Google implementations.
func (g *metadataProvider) Enabled() bool {
return onGCEVM()
}
// Provide implements DockerConfigProvider
func (g *dockerConfigKeyProvider) Provide(image string) credentialprovider.DockerConfig {
// Read the contents of the google-dockercfg metadata key and
// parse them as an alternate .dockercfg
if cfg, err := credentialprovider.ReadDockerConfigFileFromURL(dockerConfigKey, g.Client, metadataHeader); err != nil {
klog.Errorf("while reading 'google-dockercfg' metadata: %v", err)
} else {
return cfg
}
return credentialprovider.DockerConfig{}
}
// Provide implements DockerConfigProvider
func (g *dockerConfigURLKeyProvider) Provide(image string) credentialprovider.DockerConfig {
// Read the contents of the google-dockercfg-url key and load a .dockercfg from there
if url, err := credentialprovider.ReadURL(dockerConfigURLKey, g.Client, metadataHeader); err != nil {
klog.Errorf("while reading 'google-dockercfg-url' metadata: %v", err)
} else {
if strings.HasPrefix(string(url), "http") {
if cfg, err := credentialprovider.ReadDockerConfigFileFromURL(string(url), g.Client, nil); err != nil {
klog.Errorf("while reading 'google-dockercfg-url'-specified url: %s, %v", string(url), err)
} else {
return cfg
}
} else {
// TODO(mattmoor): support reading alternate scheme URLs (e.g. gs:// or s3://)
klog.Errorf("Unsupported URL scheme: %s", string(url))
}
}
return credentialprovider.DockerConfig{}
}
// runWithBackoff runs input function `f` with an exponential backoff.
// Note that this method can block indefinitely.
func runWithBackoff(f func() ([]byte, error)) []byte {
var backoff = 100 * time.Millisecond
const maxBackoff = time.Minute
for {
value, err := f()
if err == nil {
return value
}
time.Sleep(backoff)
backoff = backoff * 2
if backoff > maxBackoff {
backoff = maxBackoff
}
}
}
// Enabled implements a special metadata-based check, which verifies the
// storage scope is available on the GCE VM.
// If running on a GCE VM, check if 'default' service account exists.
// If it does not exist, assume that registry is not enabled.
// If default service account exists, check if relevant scopes exist in the default service account.
// The metadata service can become temporarily inaccesible. Hence all requests to the metadata
// service will be retried until the metadata server returns a `200`.
// It is expected that "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/" will return a `200`
// and "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/default/scopes" will also return `200`.
// More information on metadata service can be found here - https://cloud.google.com/compute/docs/storing-retrieving-metadata
func (g *containerRegistryProvider) Enabled() bool {
if !onGCEVM() {
return false
}
// Given that we are on GCE, we should keep retrying until the metadata server responds.
value := runWithBackoff(func() ([]byte, error) {
value, err := credentialprovider.ReadURL(serviceAccounts, g.Client, metadataHeader)
if err != nil {
klog.V(2).Infof("Failed to Get service accounts from gce metadata server: %v", err)
}
return value, err
})
// We expect the service account to return a list of account directories separated by newlines, e.g.,
// sv-account-name1/
// sv-account-name2/
// ref: https://cloud.google.com/compute/docs/storing-retrieving-metadata
defaultServiceAccountExists := false
for _, sa := range strings.Split(string(value), "\n") {
if strings.TrimSpace(sa) == defaultServiceAccount {
defaultServiceAccountExists = true
break
}
}
if !defaultServiceAccountExists {
klog.V(2).Infof("'default' service account does not exist. Found following service accounts: %q", string(value))
return false
}
url := metadataScopes + "?alt=json"
value = runWithBackoff(func() ([]byte, error) {
value, err := credentialprovider.ReadURL(url, g.Client, metadataHeader)
if err != nil {
klog.V(2).Infof("Failed to Get scopes in default service account from gce metadata server: %v", err)
}
return value, err
})
var scopes []string
if err := json.Unmarshal(value, &scopes); err != nil {
klog.Errorf("Failed to unmarshal scopes: %v", err)
return false
}
for _, v := range scopes {
// cloudPlatformScope implies storage scope.
if strings.HasPrefix(v, storageScopePrefix) || strings.HasPrefix(v, cloudPlatformScopePrefix) {
return true
}
}
klog.Warningf("Google container registry is disabled, no storage scope is available: %s", value)
return false
}
// tokenBlob is used to decode the JSON blob containing an access token
// that is returned by GCE metadata.
type tokenBlob struct {
AccessToken string `json:"access_token"`
}
// Provide implements DockerConfigProvider
func (g *containerRegistryProvider) Provide(image string) credentialprovider.DockerConfig {
cfg := credentialprovider.DockerConfig{}
tokenJSONBlob, err := credentialprovider.ReadURL(metadataToken, g.Client, metadataHeader)
if err != nil {
klog.Errorf("while reading access token endpoint: %v", err)
return cfg
}
email, err := credentialprovider.ReadURL(metadataEmail, g.Client, metadataHeader)
if err != nil {
klog.Errorf("while reading email endpoint: %v", err)
return cfg
}
var parsedBlob tokenBlob
if err := json.Unmarshal([]byte(tokenJSONBlob), &parsedBlob); err != nil {
klog.Errorf("while parsing json blob %s: %v", tokenJSONBlob, err)
return cfg
}
entry := credentialprovider.DockerConfigEntry{
Username: "_token",
Password: parsedBlob.AccessToken,
Email: string(email),
}
// Add our entry for each of the supported container registry URLs
for _, k := range containerRegistryUrls {
cfg[k] = entry
}
return cfg
}

View File

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

View File

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

View File

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

View File

@@ -21,19 +21,20 @@ import (
"sort"
"sync"
"k8s.io/cloud-provider/credentialconfig"
"k8s.io/klog/v2"
)
// All registered credential providers.
var providersMutex sync.Mutex
var providers = make(map[string]DockerConfigProvider)
var providers = make(map[string]credentialconfig.DockerConfigProvider)
// RegisterCredentialProvider is called by provider implementations on
// initialization to register themselves, like so:
// func init() {
// RegisterCredentialProvider("name", &myProvider{...})
// }
func RegisterCredentialProvider(name string, provider DockerConfigProvider) {
func RegisterCredentialProvider(name string, provider credentialconfig.DockerConfigProvider) {
providersMutex.Lock()
defer providersMutex.Unlock()
_, found := providers[name]
@@ -48,7 +49,7 @@ func RegisterCredentialProvider(name string, provider DockerConfigProvider) {
// which draws from the set of registered credential providers.
func NewDockerKeyring() DockerKeyring {
keyring := &providersDockerKeyring{
Providers: make([]DockerConfigProvider, 0),
Providers: make([]credentialconfig.DockerConfigProvider, 0),
}
keys := reflect.ValueOf(providers).MapKeys()

View File

@@ -1,109 +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 (
"os"
"reflect"
"sync"
"time"
"k8s.io/klog/v2"
)
// DockerConfigProvider is the interface that registered extensions implement
// to materialize 'dockercfg' credentials.
type DockerConfigProvider interface {
// Enabled returns true if the config provider is enabled.
// Implementations can be blocking - e.g. metadata server unavailable.
Enabled() bool
// Provide returns docker configuration.
// Implementations can be blocking - e.g. metadata server unavailable.
// The image is passed in as context in the event that the
// implementation depends on information in the image name to return
// credentials; implementations are safe to ignore the image.
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.
type CachingDockerConfigProvider struct {
Provider DockerConfigProvider
Lifetime time.Duration
// ShouldCache is an optional function that returns true if the specific config should be cached.
// If nil, all configs are treated as cacheable.
ShouldCache func(DockerConfig) bool
// cache fields
cacheDockerConfig DockerConfig
expiration time.Time
mu sync.Mutex
}
// Enabled implements dockerConfigProvider
func (d *defaultDockerConfigProvider) Enabled() bool {
return true
}
// Provide implements dockerConfigProvider
func (d *defaultDockerConfigProvider) Provide(image string) DockerConfig {
// Read the standard Docker credentials from .dockercfg
if cfg, err := ReadDockerConfigFile(); err == nil {
return cfg
} else if !os.IsNotExist(err) {
klog.V(4).Infof("Unable to parse Docker config file: %v", err)
}
return DockerConfig{}
}
// Enabled implements dockerConfigProvider
func (d *CachingDockerConfigProvider) Enabled() bool {
return d.Provider.Enabled()
}
// Provide implements dockerConfigProvider
func (d *CachingDockerConfigProvider) Provide(image string) DockerConfig {
d.mu.Lock()
defer d.mu.Unlock()
// If the cache hasn't expired, return our cache
if time.Now().Before(d.expiration) {
return d.cacheDockerConfig
}
klog.V(2).Infof("Refreshing cache for provider: %v", reflect.TypeOf(d.Provider).String())
config := d.Provider.Provide(image)
if d.ShouldCache == nil || d.ShouldCache(config) {
d.cacheDockerConfig = config
d.expiration = time.Now().Add(d.Lifetime)
}
return config
}

View File

@@ -1,64 +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 (
"testing"
"time"
)
func TestCachingProvider(t *testing.T) {
provider := &testProvider{
Count: 0,
}
cache := &CachingDockerConfigProvider{
Provider: provider,
Lifetime: 1 * time.Second,
}
image := "image"
if provider.Count != 0 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
cache.Provide(image)
cache.Provide(image)
cache.Provide(image)
cache.Provide(image)
if provider.Count != 1 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
time.Sleep(cache.Lifetime)
cache.Provide(image)
cache.Provide(image)
cache.Provide(image)
cache.Provide(image)
if provider.Count != 2 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
time.Sleep(cache.Lifetime)
cache.Provide(image)
cache.Provide(image)
cache.Provide(image)
cache.Provide(image)
if provider.Count != 3 {
t.Errorf("Unexpected number of Provide calls: %v", provider.Count)
}
}

View File

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