credentialprovider: track kube secrets as creds sources in DockerKeyrings

This commit is contained in:
Stanislav Láznička 2024-10-09 14:13:34 +02:00
parent e549eeb796
commit 09284d926c
No known key found for this signature in database
GPG Key ID: F8D8054395A1D157
5 changed files with 293 additions and 126 deletions

View File

@ -17,6 +17,9 @@ limitations under the License.
package credentialprovider package credentialprovider
import ( import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net" "net"
"net/url" "net/url"
"path/filepath" "path/filepath"
@ -24,7 +27,9 @@ import (
"strings" "strings"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/features"
) )
// DockerKeyring tracks a set of docker registry credentials, maintaining a // DockerKeyring tracks a set of docker registry credentials, maintaining a
@ -35,13 +40,13 @@ import (
// most specific match for a given image // most specific match for a given image
// - iterating a map does not yield predictable results // - iterating a map does not yield predictable results
type DockerKeyring interface { type DockerKeyring interface {
Lookup(image string) ([]AuthConfig, bool) Lookup(image string) ([]TrackedAuthConfig, bool)
} }
// BasicDockerKeyring is a trivial map-backed implementation of DockerKeyring // BasicDockerKeyring is a trivial map-backed implementation of DockerKeyring
type BasicDockerKeyring struct { type BasicDockerKeyring struct {
index []string index []string
creds map[string][]AuthConfig creds map[string][]TrackedAuthConfig
} }
// providersDockerKeyring is an implementation of DockerKeyring that // providersDockerKeyring is an implementation of DockerKeyring that
@ -50,6 +55,47 @@ type providersDockerKeyring struct {
Providers []DockerConfigProvider Providers []DockerConfigProvider
} }
// TrackedAuthConfig wraps the AuthConfig and adds information about the source
// of the credentials.
type TrackedAuthConfig struct {
AuthConfig
AuthConfigHash string
Source *CredentialSource
}
// NewTrackedAuthConfig initializes the TrackedAuthConfig structure by adding
// the source information to the supplied AuthConfig. It also counts a hash of the
// AuthConfig and keeps it in the returned structure.
//
// The supplied CredentialSource is only used when the "KubeletEnsureSecretPulledImages"
// is enabled, the same applies for counting the hash.
func NewTrackedAuthConfig(c *AuthConfig, src *CredentialSource) *TrackedAuthConfig {
if c == nil {
panic("cannot construct TrackedAuthConfig with a nil AuthConfig")
}
authConfig := &TrackedAuthConfig{
AuthConfig: *c,
}
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletEnsureSecretPulledImages) {
authConfig.Source = src
authConfig.AuthConfigHash = hashAuthConfig(c)
}
return authConfig
}
type CredentialSource struct {
Secret SecretCoordinates
}
type SecretCoordinates struct {
UID string
Namespace string
Name string
}
// AuthConfig contains authorization information for connecting to a Registry // AuthConfig contains authorization information for connecting to a Registry
// This type mirrors "github.com/docker/docker/api/types.AuthConfig" // This type mirrors "github.com/docker/docker/api/types.AuthConfig"
type AuthConfig struct { type AuthConfig struct {
@ -72,11 +118,13 @@ type AuthConfig struct {
RegistryToken string `json:"registrytoken,omitempty"` RegistryToken string `json:"registrytoken,omitempty"`
} }
// Add add some docker config in basic docker keyring // Add inserts the docker config `cfg` into the basic docker keyring. It attaches
func (dk *BasicDockerKeyring) Add(cfg DockerConfig) { // the `src` information that describes where the docker config `cfg` comes from.
// `src` is nil if the docker config is globally available on the node.
func (dk *BasicDockerKeyring) Add(src *CredentialSource, cfg DockerConfig) {
if dk.index == nil { if dk.index == nil {
dk.index = make([]string, 0) dk.index = make([]string, 0)
dk.creds = make(map[string][]AuthConfig) dk.creds = make(map[string][]TrackedAuthConfig)
} }
for loc, ident := range cfg { for loc, ident := range cfg {
creds := AuthConfig{ creds := AuthConfig{
@ -111,7 +159,9 @@ func (dk *BasicDockerKeyring) Add(cfg DockerConfig) {
} else { } else {
key = parsed.Host key = parsed.Host
} }
dk.creds[key] = append(dk.creds[key], creds) trackedCreds := NewTrackedAuthConfig(&creds, src)
dk.creds[key] = append(dk.creds[key], *trackedCreds)
dk.index = append(dk.index, key) dk.index = append(dk.index, key)
} }
@ -235,9 +285,9 @@ func URLsMatch(globURL *url.URL, targetURL *url.URL) (bool, error) {
// Lookup implements the DockerKeyring method for fetching credentials based on image name. // Lookup implements the DockerKeyring method for fetching credentials based on image name.
// Multiple credentials may be returned if there are multiple potentially valid credentials // Multiple credentials may be returned if there are multiple potentially valid credentials
// available. This allows for rotation. // available. This allows for rotation.
func (dk *BasicDockerKeyring) Lookup(image string) ([]AuthConfig, bool) { func (dk *BasicDockerKeyring) Lookup(image string) ([]TrackedAuthConfig, bool) {
// range over the index as iterating over a map does not provide a predictable ordering // range over the index as iterating over a map does not provide a predictable ordering
ret := []AuthConfig{} ret := []TrackedAuthConfig{}
for _, k := range dk.index { for _, k := range dk.index {
// both k and image are schemeless URLs because even though schemes are allowed // both k and image are schemeless URLs because even though schemes are allowed
// in the credential configurations, we remove them in Add. // in the credential configurations, we remove them in Add.
@ -257,16 +307,18 @@ func (dk *BasicDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
} }
} }
return []AuthConfig{}, false return []TrackedAuthConfig{}, false
} }
// Lookup implements the DockerKeyring method for fetching credentials // Lookup implements the DockerKeyring method for fetching credentials
// based on image name. // based on image name.
func (dk *providersDockerKeyring) Lookup(image string) ([]AuthConfig, bool) { func (dk *providersDockerKeyring) Lookup(image string) ([]TrackedAuthConfig, bool) {
keyring := &BasicDockerKeyring{} keyring := &BasicDockerKeyring{}
for _, p := range dk.Providers { for _, p := range dk.Providers {
keyring.Add(p.Provide(image)) // TODO: the source should probably change once we depend on service accounts (KEP-4412).
// Perhaps `Provide()` should return the source modified to accommodate this?
keyring.Add(nil, p.Provide(image))
} }
return keyring.Lookup(image) return keyring.Lookup(image)
@ -274,13 +326,13 @@ func (dk *providersDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
// FakeKeyring a fake config credentials // FakeKeyring a fake config credentials
type FakeKeyring struct { type FakeKeyring struct {
auth []AuthConfig auth []TrackedAuthConfig
ok bool ok bool
} }
// Lookup implements the DockerKeyring method for fetching credentials based on image name // Lookup implements the DockerKeyring method for fetching credentials based on image name
// return fake auth and ok // return fake auth and ok
func (f *FakeKeyring) Lookup(image string) ([]AuthConfig, bool) { func (f *FakeKeyring) Lookup(image string) ([]TrackedAuthConfig, bool) {
return f.auth, f.ok return f.auth, f.ok
} }
@ -289,8 +341,8 @@ type UnionDockerKeyring []DockerKeyring
// Lookup implements the DockerKeyring method for fetching credentials based on image name. // Lookup implements the DockerKeyring method for fetching credentials based on image name.
// return each credentials // return each credentials
func (k UnionDockerKeyring) Lookup(image string) ([]AuthConfig, bool) { func (k UnionDockerKeyring) Lookup(image string) ([]TrackedAuthConfig, bool) {
authConfigs := []AuthConfig{} authConfigs := []TrackedAuthConfig{}
for _, subKeyring := range k { for _, subKeyring := range k {
if subKeyring == nil { if subKeyring == nil {
continue continue
@ -302,3 +354,14 @@ func (k UnionDockerKeyring) Lookup(image string) ([]AuthConfig, bool) {
return authConfigs, (len(authConfigs) > 0) return authConfigs, (len(authConfigs) > 0)
} }
func hashAuthConfig(creds *AuthConfig) string {
credBytes, err := json.Marshal(creds)
if err != nil {
return ""
}
hash := sha256.New()
hash.Write([]byte(credBytes))
return hex.EncodeToString(hash.Sum(nil))
}

View File

@ -21,6 +21,10 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"testing" "testing"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/pkg/features"
) )
func TestURLsMatch(t *testing.T) { func TestURLsMatch(t *testing.T) {
@ -222,7 +226,7 @@ func TestDockerKeyringForGlob(t *testing.T) {
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil { if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err) t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else { } else {
keyring.Add(cfg) keyring.Add(nil, cfg)
} }
creds, ok := keyring.Lookup(test.targetURL + "/foo/bar") creds, ok := keyring.Lookup(test.targetURL + "/foo/bar")
@ -290,7 +294,7 @@ func TestKeyringMiss(t *testing.T) {
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil { if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err) t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else { } else {
keyring.Add(cfg) keyring.Add(nil, cfg)
} }
_, ok := keyring.Lookup(test.lookupURL + "/foo/bar") _, ok := keyring.Lookup(test.lookupURL + "/foo/bar")
@ -318,7 +322,7 @@ func TestKeyringMissWithDockerHubCredentials(t *testing.T) {
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil { if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err) t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else { } else {
keyring.Add(cfg) keyring.Add(nil, cfg)
} }
val, ok := keyring.Lookup("world.mesos.org/foo/bar") val, ok := keyring.Lookup("world.mesos.org/foo/bar")
@ -344,7 +348,7 @@ func TestKeyringHitWithUnqualifiedDockerHub(t *testing.T) {
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil { if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err) t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else { } else {
keyring.Add(cfg) keyring.Add(nil, cfg)
} }
creds, ok := keyring.Lookup("google/docker-registry") creds, ok := keyring.Lookup("google/docker-registry")
@ -353,7 +357,7 @@ func TestKeyringHitWithUnqualifiedDockerHub(t *testing.T) {
return return
} }
if len(creds) > 1 { if len(creds) > 1 {
t.Errorf("Got more hits than expected: %s", creds) t.Errorf("Got more hits than expected: %v", creds)
} }
val := creds[0] val := creds[0]
@ -385,7 +389,7 @@ func TestKeyringHitWithUnqualifiedLibraryDockerHub(t *testing.T) {
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil { if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err) t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else { } else {
keyring.Add(cfg) keyring.Add(nil, cfg)
} }
creds, ok := keyring.Lookup("jenkins") creds, ok := keyring.Lookup("jenkins")
@ -394,7 +398,7 @@ func TestKeyringHitWithUnqualifiedLibraryDockerHub(t *testing.T) {
return return
} }
if len(creds) > 1 { if len(creds) > 1 {
t.Errorf("Got more hits than expected: %s", creds) t.Errorf("Got more hits than expected: %v", creds)
} }
val := creds[0] val := creds[0]
@ -426,7 +430,7 @@ func TestKeyringHitWithQualifiedDockerHub(t *testing.T) {
if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil { if cfg, err := ReadDockerConfigFileFromBytes([]byte(sampleDockerConfig)); err != nil {
t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err) t.Errorf("Error processing json blob %q, %v", sampleDockerConfig, err)
} else { } else {
keyring.Add(cfg) keyring.Add(nil, cfg)
} }
creds, ok := keyring.Lookup(url + "/google/docker-registry") creds, ok := keyring.Lookup(url + "/google/docker-registry")
@ -435,7 +439,7 @@ func TestKeyringHitWithQualifiedDockerHub(t *testing.T) {
return return
} }
if len(creds) > 2 { if len(creds) > 2 {
t.Errorf("Got more hits than expected: %s", creds) t.Errorf("Got more hits than expected: %v", creds)
} }
val := creds[0] val := creds[0]
@ -498,20 +502,24 @@ func TestProvidersDockerKeyring(t *testing.T) {
} }
func TestDockerKeyringLookup(t *testing.T) { func TestDockerKeyringLookup(t *testing.T) {
// turn on the ensure secret pulled images feature to get the hashes with the creds
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletEnsureSecretPulledImages, true)
ada := AuthConfig{ ada := AuthConfig{
Username: "ada", Username: "ada",
Password: "smash", // Fake value for testing. Password: "smash", // Fake value for testing.
Email: "ada@example.com", Email: "ada@example.com",
} }
adaHash := "353258b53f5e9a57b059eab3f05312fc35bbeb874f08ce101e7bf0bf46977423"
grace := AuthConfig{ grace := AuthConfig{
Username: "grace", Username: "grace",
Password: "squash", // Fake value for testing. Password: "squash", // Fake value for testing.
Email: "grace@example.com", Email: "grace@example.com",
} }
graceHash := "f949b3837a1eb733a951b6aeda0b3327c09ec50c917de9ca35818e8fbf567e29"
dk := &BasicDockerKeyring{} dk := &BasicDockerKeyring{}
dk.Add(DockerConfig{ dk.Add(nil, DockerConfig{
"bar.example.com/pong": DockerConfigEntry{ "bar.example.com/pong": DockerConfigEntry{
Username: grace.Username, Username: grace.Username,
Password: grace.Password, Password: grace.Password,
@ -526,27 +534,27 @@ func TestDockerKeyringLookup(t *testing.T) {
tests := []struct { tests := []struct {
image string image string
match []AuthConfig match []TrackedAuthConfig
ok bool ok bool
}{ }{
// direct match // direct match
{"bar.example.com", []AuthConfig{ada}, true}, {"bar.example.com", []TrackedAuthConfig{{AuthConfig: ada, AuthConfigHash: adaHash}}, true},
// direct match deeper than other possible matches // direct match deeper than other possible matches
{"bar.example.com/pong", []AuthConfig{grace, ada}, true}, {"bar.example.com/pong", []TrackedAuthConfig{{AuthConfig: grace, AuthConfigHash: graceHash}, {AuthConfig: ada, AuthConfigHash: adaHash}}, true},
// no direct match, deeper path ignored // no direct match, deeper path ignored
{"bar.example.com/ping", []AuthConfig{ada}, true}, {"bar.example.com/ping", []TrackedAuthConfig{{AuthConfig: ada, AuthConfigHash: adaHash}}, true},
// match first part of path token // match first part of path token
{"bar.example.com/pongz", []AuthConfig{grace, ada}, true}, {"bar.example.com/pongz", []TrackedAuthConfig{{AuthConfig: grace, AuthConfigHash: graceHash}, {AuthConfig: ada, AuthConfigHash: adaHash}}, true},
// match regardless of sub-path // match regardless of sub-path
{"bar.example.com/pong/pang", []AuthConfig{grace, ada}, true}, {"bar.example.com/pong/pang", []TrackedAuthConfig{{AuthConfig: grace, AuthConfigHash: graceHash}, {AuthConfig: ada, AuthConfigHash: adaHash}}, true},
// no host match // no host match
{"example.com", []AuthConfig{}, false}, {"example.com", []TrackedAuthConfig{}, false},
{"foo.example.com", []AuthConfig{}, false}, {"foo.example.com", []TrackedAuthConfig{}, false},
} }
for i, tt := range tests { for i, tt := range tests {
@ -565,14 +573,17 @@ func TestDockerKeyringLookup(t *testing.T) {
// by images that only match the hostname. // by images that only match the hostname.
// NOTE: the above covers the case of a more specific match trumping just hostname. // NOTE: the above covers the case of a more specific match trumping just hostname.
func TestIssue3797(t *testing.T) { func TestIssue3797(t *testing.T) {
// turn on the ensure secret pulled images feature to get the hashes with the creds
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletEnsureSecretPulledImages, true)
rex := AuthConfig{ rex := AuthConfig{
Username: "rex", Username: "rex",
Password: "tiny arms", // Fake value for testing. Password: "tiny arms", // Fake value for testing.
Email: "rex@example.com", Email: "rex@example.com",
} }
rexHash := "899748fec74c8dd761845fca727f4249b05be275ff24026676fcd4351f656363"
dk := &BasicDockerKeyring{} dk := &BasicDockerKeyring{}
dk.Add(DockerConfig{ dk.Add(nil, DockerConfig{
"https://quay.io/v1/": DockerConfigEntry{ "https://quay.io/v1/": DockerConfigEntry{
Username: rex.Username, Username: rex.Username,
Password: rex.Password, Password: rex.Password,
@ -582,15 +593,15 @@ func TestIssue3797(t *testing.T) {
tests := []struct { tests := []struct {
image string image string
match []AuthConfig match []TrackedAuthConfig
ok bool ok bool
}{ }{
// direct match // direct match
{"quay.io", []AuthConfig{rex}, true}, {"quay.io", []TrackedAuthConfig{{AuthConfig: rex, AuthConfigHash: rexHash}}, true},
// partial matches // partial matches
{"quay.io/foo", []AuthConfig{rex}, true}, {"quay.io/foo", []TrackedAuthConfig{{AuthConfig: rex, AuthConfigHash: rexHash}}, true},
{"quay.io/foo/bar", []AuthConfig{rex}, true}, {"quay.io/foo/bar", []TrackedAuthConfig{{AuthConfig: rex, AuthConfigHash: rexHash}}, true},
} }
for i, tt := range tests { for i, tt := range tests {

View File

@ -27,32 +27,52 @@ import (
// then a DockerKeyring is built based on every hit and unioned with the defaultKeyring. // then a DockerKeyring is built based on every hit and unioned with the defaultKeyring.
// If they do not, then the default keyring is returned // If they do not, then the default keyring is returned
func MakeDockerKeyring(passedSecrets []v1.Secret, defaultKeyring credentialprovider.DockerKeyring) (credentialprovider.DockerKeyring, error) { func MakeDockerKeyring(passedSecrets []v1.Secret, defaultKeyring credentialprovider.DockerKeyring) (credentialprovider.DockerKeyring, error) {
passedCredentials := []credentialprovider.DockerConfig{} providerFromSecrets, err := secretsToTrackedDockerConfigs(passedSecrets)
for _, passedSecret := range passedSecrets { if err != nil {
return nil, err
}
if providerFromSecrets == nil {
return defaultKeyring, nil
}
return credentialprovider.UnionDockerKeyring{providerFromSecrets, defaultKeyring}, nil
}
func secretsToTrackedDockerConfigs(secrets []v1.Secret) (credentialprovider.DockerKeyring, error) {
provider := &credentialprovider.BasicDockerKeyring{}
validSecretsFound := 0
for _, passedSecret := range secrets {
if dockerConfigJSONBytes, dockerConfigJSONExists := passedSecret.Data[v1.DockerConfigJsonKey]; (passedSecret.Type == v1.SecretTypeDockerConfigJson) && dockerConfigJSONExists && (len(dockerConfigJSONBytes) > 0) { if dockerConfigJSONBytes, dockerConfigJSONExists := passedSecret.Data[v1.DockerConfigJsonKey]; (passedSecret.Type == v1.SecretTypeDockerConfigJson) && dockerConfigJSONExists && (len(dockerConfigJSONBytes) > 0) {
dockerConfigJSON := credentialprovider.DockerConfigJSON{} dockerConfigJSON := credentialprovider.DockerConfigJSON{}
if err := json.Unmarshal(dockerConfigJSONBytes, &dockerConfigJSON); err != nil { if err := json.Unmarshal(dockerConfigJSONBytes, &dockerConfigJSON); err != nil {
return nil, err return nil, err
} }
passedCredentials = append(passedCredentials, dockerConfigJSON.Auths) coords := credentialprovider.SecretCoordinates{
UID: string(passedSecret.UID),
Namespace: passedSecret.Namespace,
Name: passedSecret.Name}
provider.Add(&credentialprovider.CredentialSource{Secret: coords}, dockerConfigJSON.Auths)
validSecretsFound++
} else if dockercfgBytes, dockercfgExists := passedSecret.Data[v1.DockerConfigKey]; (passedSecret.Type == v1.SecretTypeDockercfg) && dockercfgExists && (len(dockercfgBytes) > 0) { } else if dockercfgBytes, dockercfgExists := passedSecret.Data[v1.DockerConfigKey]; (passedSecret.Type == v1.SecretTypeDockercfg) && dockercfgExists && (len(dockercfgBytes) > 0) {
dockercfg := credentialprovider.DockerConfig{} dockercfg := credentialprovider.DockerConfig{}
if err := json.Unmarshal(dockercfgBytes, &dockercfg); err != nil { if err := json.Unmarshal(dockercfgBytes, &dockercfg); err != nil {
return nil, err return nil, err
} }
passedCredentials = append(passedCredentials, dockercfg) coords := credentialprovider.SecretCoordinates{
UID: string(passedSecret.UID),
Namespace: passedSecret.Namespace,
Name: passedSecret.Name}
provider.Add(&credentialprovider.CredentialSource{Secret: coords}, dockercfg)
validSecretsFound++
} }
} }
if len(passedCredentials) > 0 { if validSecretsFound == 0 {
basicKeyring := &credentialprovider.BasicDockerKeyring{} return nil, nil
for _, currCredentials := range passedCredentials {
basicKeyring.Add(currCredentials)
} }
return credentialprovider.UnionDockerKeyring{basicKeyring, defaultKeyring}, nil return provider, nil
}
return defaultKeyring, nil
} }

View File

@ -20,243 +20,316 @@ import (
"reflect" "reflect"
"testing" "testing"
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/pkg/credentialprovider" "k8s.io/kubernetes/pkg/credentialprovider"
"k8s.io/kubernetes/pkg/features"
) )
// fakeKeyring is a fake docker auth config keyring // FakeKeyring a fake config credentials
type fakeKeyring struct { type FakeKeyring struct {
auth []credentialprovider.AuthConfig auth []credentialprovider.TrackedAuthConfig
ok bool ok bool
} }
// Lookup implements the DockerKeyring method for fetching credentials based on image name. // Lookup implements the DockerKeyring method for fetching credentials based on image name
// Returns fake results based on the auth and ok fields in fakeKeyring // return fake auth and ok
func (f *fakeKeyring) Lookup(image string) ([]credentialprovider.AuthConfig, bool) { func (f *FakeKeyring) Lookup(image string) ([]credentialprovider.TrackedAuthConfig, bool) {
return f.auth, f.ok return f.auth, f.ok
} }
func Test_MakeDockerKeyring(t *testing.T) { func Test_MakeDockerKeyring(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletEnsureSecretPulledImages, true)
testcases := []struct { testcases := []struct {
name string name string
image string image string
defaultKeyring credentialprovider.DockerKeyring defaultKeyring credentialprovider.DockerKeyring
pullSecrets []v1.Secret pullSecrets []v1.Secret
authConfigs []credentialprovider.AuthConfig expectedAuthConfigs []credentialprovider.TrackedAuthConfig
found bool found bool
}{ }{
{ {
name: "with .dockerconfigjson and auth field", name: "with .dockerconfigjson and auth field",
image: "test.registry.io", image: "test.registry.io",
defaultKeyring: &fakeKeyring{},
pullSecrets: []v1.Secret{ pullSecrets: []v1.Secret{
{ {
ObjectMeta: metav1.ObjectMeta{
Name: "s1", Namespace: "ns1", UID: "uid1"},
Type: v1.SecretTypeDockerConfigJson, Type: v1.SecretTypeDockerConfigJson,
Data: map[string][]byte{ Data: map[string][]byte{
v1.DockerConfigJsonKey: []byte(`{"auths": {"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}}`), v1.DockerConfigJsonKey: []byte(`{"auths": {"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}}`),
}, },
}, },
}, },
authConfigs: []credentialprovider.AuthConfig{ expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
{ {
Source: &credentialprovider.CredentialSource{
Secret: credentialprovider.SecretCoordinates{
Name: "s1", Namespace: "ns1", UID: "uid1"},
},
AuthConfig: credentialprovider.AuthConfig{
Username: "user", Username: "user",
Password: "password", Password: "password",
}, },
AuthConfigHash: "a55436fc140d516560d072c5fe8700385ce9f41629abf65c1edcbcb39fac691d",
},
}, },
found: true, found: true,
}, },
{ {
name: "with .dockerconfig and auth field", name: "with .dockerconfig and auth field",
image: "test.registry.io", image: "test.registry.io",
defaultKeyring: &fakeKeyring{},
pullSecrets: []v1.Secret{ pullSecrets: []v1.Secret{
{ {
ObjectMeta: metav1.ObjectMeta{
Name: "s1", Namespace: "ns1", UID: "uid1"},
Type: v1.SecretTypeDockercfg, Type: v1.SecretTypeDockercfg,
Data: map[string][]byte{ Data: map[string][]byte{
v1.DockerConfigKey: []byte(`{"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}`), v1.DockerConfigKey: []byte(`{"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}`),
}, },
}, },
}, },
authConfigs: []credentialprovider.AuthConfig{ expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
{ {
Source: &credentialprovider.CredentialSource{
Secret: credentialprovider.SecretCoordinates{
Name: "s1", Namespace: "ns1", UID: "uid1"},
},
AuthConfig: credentialprovider.AuthConfig{
Username: "user", Username: "user",
Password: "password", Password: "password",
}, },
AuthConfigHash: "a55436fc140d516560d072c5fe8700385ce9f41629abf65c1edcbcb39fac691d",
},
}, },
found: true, found: true,
}, },
{ {
name: "with .dockerconfigjson and username/password fields", name: "with .dockerconfigjson and username/password fields",
image: "test.registry.io", image: "test.registry.io",
defaultKeyring: &fakeKeyring{},
pullSecrets: []v1.Secret{ pullSecrets: []v1.Secret{
{ {
ObjectMeta: metav1.ObjectMeta{
Name: "s1", Namespace: "ns1", UID: "uid1"},
Type: v1.SecretTypeDockerConfigJson, Type: v1.SecretTypeDockerConfigJson,
Data: map[string][]byte{ Data: map[string][]byte{
v1.DockerConfigJsonKey: []byte(`{"auths": {"test.registry.io": {"username": "user", "password": "password"}}}`), v1.DockerConfigJsonKey: []byte(`{"auths": {"test.registry.io": {"username": "user", "password": "password"}}}`),
}, },
}, },
}, },
authConfigs: []credentialprovider.AuthConfig{ expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
{ {
Source: &credentialprovider.CredentialSource{
Secret: credentialprovider.SecretCoordinates{
Name: "s1", Namespace: "ns1", UID: "uid1"},
},
AuthConfig: credentialprovider.AuthConfig{
Username: "user", Username: "user",
Password: "password", Password: "password",
}, },
AuthConfigHash: "a55436fc140d516560d072c5fe8700385ce9f41629abf65c1edcbcb39fac691d",
},
}, },
found: true, found: true,
}, },
{ {
name: "with .dockerconfig and username/password fields", name: "with .dockerconfig and username/password fields",
image: "test.registry.io", image: "test.registry.io",
defaultKeyring: &fakeKeyring{},
pullSecrets: []v1.Secret{ pullSecrets: []v1.Secret{
{ {
ObjectMeta: metav1.ObjectMeta{
Name: "s1", Namespace: "ns1", UID: "uid1"},
Type: v1.SecretTypeDockercfg, Type: v1.SecretTypeDockercfg,
Data: map[string][]byte{ Data: map[string][]byte{
v1.DockerConfigKey: []byte(`{"test.registry.io": {"username": "user", "password": "password"}}`), v1.DockerConfigKey: []byte(`{"test.registry.io": {"username": "user", "password": "password"}}`),
}, },
}, },
}, },
authConfigs: []credentialprovider.AuthConfig{ expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
{ {
Source: &credentialprovider.CredentialSource{
Secret: credentialprovider.SecretCoordinates{
Name: "s1", Namespace: "ns1", UID: "uid1"},
},
AuthConfig: credentialprovider.AuthConfig{
Username: "user", Username: "user",
Password: "password", Password: "password",
}, },
AuthConfigHash: "a55436fc140d516560d072c5fe8700385ce9f41629abf65c1edcbcb39fac691d",
},
}, },
found: true, found: true,
}, },
{ {
name: "with .dockerconfigjson but with wrong Secret Type", name: "with .dockerconfigjson but with wrong Secret Type",
image: "test.registry.io", image: "test.registry.io",
defaultKeyring: &fakeKeyring{}, defaultKeyring: &FakeKeyring{},
pullSecrets: []v1.Secret{ pullSecrets: []v1.Secret{
{ {
ObjectMeta: metav1.ObjectMeta{
Name: "s1", Namespace: "ns1", UID: "uid1"},
Type: v1.SecretTypeDockercfg, Type: v1.SecretTypeDockercfg,
Data: map[string][]byte{ Data: map[string][]byte{
v1.DockerConfigJsonKey: []byte(`{"auths": {"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}}`), v1.DockerConfigJsonKey: []byte(`{"auths": {"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}}`),
}, },
}, },
}, },
authConfigs: nil,
found: false, found: false,
}, },
{ {
name: "with .dockerconfig but with wrong Secret Type", name: "with .dockerconfig but with wrong Secret Type",
image: "test.registry.io", image: "test.registry.io",
defaultKeyring: &fakeKeyring{}, defaultKeyring: &FakeKeyring{},
pullSecrets: []v1.Secret{ pullSecrets: []v1.Secret{
{ {
ObjectMeta: metav1.ObjectMeta{
Name: "s1", Namespace: "ns1", UID: "uid1"},
Type: v1.SecretTypeDockerConfigJson, Type: v1.SecretTypeDockerConfigJson,
Data: map[string][]byte{ Data: map[string][]byte{
v1.DockerConfigKey: []byte(`{"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}`), v1.DockerConfigKey: []byte(`{"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}`),
}, },
}, },
}, },
authConfigs: nil,
found: false, found: false,
}, },
{ {
name: "with not matcing .dockerconfigjson and default keyring", name: "with not matcing .dockerconfigjson and default keyring",
image: "test.registry.io", image: "test.registry.io",
defaultKeyring: &fakeKeyring{ defaultKeyring: &FakeKeyring{
auth: []credentialprovider.AuthConfig{ auth: []credentialprovider.TrackedAuthConfig{
{ {
AuthConfig: credentialprovider.AuthConfig{
Username: "default-user", Username: "default-user",
Password: "default-password", Password: "default-password",
}, },
}, },
}, },
},
pullSecrets: []v1.Secret{ pullSecrets: []v1.Secret{
{ {
ObjectMeta: metav1.ObjectMeta{
Name: "s1", Namespace: "ns1", UID: "uid1"},
Type: v1.SecretTypeDockerConfigJson, Type: v1.SecretTypeDockerConfigJson,
Data: map[string][]byte{ Data: map[string][]byte{
v1.DockerConfigJsonKey: []byte(`{"auths": {"foobar.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}}`), v1.DockerConfigJsonKey: []byte(`{"auths": {"foobar.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}}`),
}, },
}, },
}, },
authConfigs: []credentialprovider.AuthConfig{ expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
{ {
AuthConfig: credentialprovider.AuthConfig{
Username: "default-user", Username: "default-user",
Password: "default-password", Password: "default-password",
}, },
AuthConfigHash: "",
},
}, },
found: true, found: true,
}, },
{ {
name: "with not matching .dockerconfig and default keyring", name: "with not matching .dockerconfig and default keyring",
image: "test.registry.io", image: "test.registry.io",
defaultKeyring: &fakeKeyring{ defaultKeyring: &FakeKeyring{
auth: []credentialprovider.AuthConfig{ auth: []credentialprovider.TrackedAuthConfig{
{ {
AuthConfig: credentialprovider.AuthConfig{
Username: "default-user", Username: "default-user",
Password: "default-password", Password: "default-password",
}, },
}, },
}, },
},
pullSecrets: []v1.Secret{ pullSecrets: []v1.Secret{
{ {
ObjectMeta: metav1.ObjectMeta{
Name: "s1", Namespace: "ns1", UID: "uid1"},
Type: v1.SecretTypeDockercfg, Type: v1.SecretTypeDockercfg,
Data: map[string][]byte{ Data: map[string][]byte{
v1.DockerConfigKey: []byte(`{"foobar.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}`), v1.DockerConfigKey: []byte(`{"foobar.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}`),
}, },
}, },
}, },
authConfigs: []credentialprovider.AuthConfig{ expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
{ {
AuthConfig: credentialprovider.AuthConfig{
Username: "default-user", Username: "default-user",
Password: "default-password", Password: "default-password",
}, },
AuthConfigHash: "",
},
}, },
found: true, found: true,
}, },
{ {
name: "with no pull secrets but has default keyring", name: "with no pull secrets but has default keyring",
image: "test.registry.io", image: "test.registry.io",
defaultKeyring: &fakeKeyring{ defaultKeyring: &FakeKeyring{
auth: []credentialprovider.AuthConfig{ auth: []credentialprovider.TrackedAuthConfig{
{ {
AuthConfig: credentialprovider.AuthConfig{
Username: "default-user", Username: "default-user",
Password: "default-password", Password: "default-password",
}, },
}, },
}, },
},
pullSecrets: []v1.Secret{}, pullSecrets: []v1.Secret{},
authConfigs: []credentialprovider.AuthConfig{ expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
{ {
AuthConfig: credentialprovider.AuthConfig{
Username: "default-user", Username: "default-user",
Password: "default-password", Password: "default-password",
}, },
AuthConfigHash: "",
},
}, },
found: false, found: false,
}, },
{ {
name: "with pull secrets and has default keyring", name: "with pull secrets and has default keyring",
image: "test.registry.io", image: "test.registry.io",
defaultKeyring: &fakeKeyring{ defaultKeyring: &FakeKeyring{
auth: []credentialprovider.AuthConfig{ auth: []credentialprovider.TrackedAuthConfig{
{ {
AuthConfig: credentialprovider.AuthConfig{
Username: "default-user", Username: "default-user",
Password: "default-password", Password: "default-password",
}, },
}, },
}, },
},
pullSecrets: []v1.Secret{ pullSecrets: []v1.Secret{
{ {
ObjectMeta: metav1.ObjectMeta{
Name: "s1", Namespace: "ns1", UID: "uid1"},
Type: v1.SecretTypeDockerConfigJson, Type: v1.SecretTypeDockerConfigJson,
Data: map[string][]byte{ Data: map[string][]byte{
v1.DockerConfigJsonKey: []byte(`{"auths": {"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}}`), v1.DockerConfigJsonKey: []byte(`{"auths": {"test.registry.io": {"auth": "dXNlcjpwYXNzd29yZA=="}}}`),
}, },
}, },
}, },
authConfigs: []credentialprovider.AuthConfig{ expectedAuthConfigs: []credentialprovider.TrackedAuthConfig{
{ {
Source: &credentialprovider.CredentialSource{
Secret: credentialprovider.SecretCoordinates{
Name: "s1", Namespace: "ns1", UID: "uid1"},
},
AuthConfig: credentialprovider.AuthConfig{
Username: "user", Username: "user",
Password: "password", Password: "password",
}, },
AuthConfigHash: "a55436fc140d516560d072c5fe8700385ce9f41629abf65c1edcbcb39fac691d",
},
{ {
AuthConfig: credentialprovider.AuthConfig{
Username: "default-user", Username: "default-user",
Password: "default-password", Password: "default-password",
}, },
AuthConfigHash: "",
},
}, },
found: true, found: true,
}, },
@ -276,9 +349,9 @@ func Test_MakeDockerKeyring(t *testing.T) {
t.Errorf("unexpected lookup for image: %s", testcase.image) t.Errorf("unexpected lookup for image: %s", testcase.image)
} }
if !reflect.DeepEqual(authConfigs, testcase.authConfigs) { if !reflect.DeepEqual(authConfigs, testcase.expectedAuthConfigs) { // TODO: we may need better comparison as the result is unordered
t.Logf("actual auth configs: %#v", authConfigs) t.Logf("actual auth configs: %#v", authConfigs)
t.Logf("expected auth configs: %#v", testcase.authConfigs) t.Logf("expected auth configs: %#v", testcase.expectedAuthConfigs)
t.Error("auth configs did not match") t.Error("auth configs did not match")
} }
}) })

View File

@ -320,7 +320,7 @@ func TestPullWithSecrets(t *testing.T) {
} }
for description, test := range tests { for description, test := range tests {
builtInKeyRing := &credentialprovider.BasicDockerKeyring{} builtInKeyRing := &credentialprovider.BasicDockerKeyring{}
builtInKeyRing.Add(test.builtInDockerConfig) builtInKeyRing.Add(nil, test.builtInDockerConfig)
_, fakeImageService, fakeManager, err := customTestRuntimeManager(builtInKeyRing) _, fakeImageService, fakeManager, err := customTestRuntimeManager(builtInKeyRing)
require.NoError(t, err) require.NoError(t, err)