Files
kubernetes/pkg/credentialprovider/plugin/plugins_test.go
2025-07-18 16:38:23 -05:00

518 lines
16 KiB
Go

/*
Copyright 2025 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 plugin
import (
"sync"
"testing"
"time"
"k8s.io/apimachinery/pkg/util/sets"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/tools/cache"
featuregatetesting "k8s.io/component-base/featuregate/testing"
credentialproviderapi "k8s.io/kubelet/pkg/apis/credentialprovider"
"k8s.io/kubernetes/pkg/credentialprovider"
"k8s.io/kubernetes/pkg/features"
testingclock "k8s.io/utils/clock/testing"
)
// fakedockerConfigProviderWithCoordinates implements dockerConfigProviderWithCoordinates for testing
type fakedockerConfigProviderWithCoordinates struct {
dockerConfig credentialprovider.DockerConfig
serviceAccountCoords *credentialprovider.ServiceAccountCoordinates
callCount int
lastImageRequested string
mu sync.Mutex
}
func (f *fakedockerConfigProviderWithCoordinates) provideWithCoordinates(image string) (credentialprovider.DockerConfig, *credentialprovider.ServiceAccountCoordinates) {
f.mu.Lock()
defer f.mu.Unlock()
f.callCount++
f.lastImageRequested = image
return f.dockerConfig, f.serviceAccountCoords
}
func (f *fakedockerConfigProviderWithCoordinates) getCallCount() int {
f.mu.Lock()
defer f.mu.Unlock()
return f.callCount
}
func (f *fakedockerConfigProviderWithCoordinates) getLastImageRequested() string {
f.mu.Lock()
defer f.mu.Unlock()
return f.lastImageRequested
}
// createTestPluginProvider creates a pluginProvider for testing
func createTestPluginProvider(dockerConfig credentialprovider.DockerConfig) *pluginProvider {
testClock := testingclock.NewFakeClock(time.Now())
authConfigMap := make(map[string]credentialproviderapi.AuthConfig)
for registry, config := range dockerConfig {
authConfigMap[registry] = credentialproviderapi.AuthConfig{
Username: config.Username,
Password: config.Password,
}
}
mockPlugin := &fakeExecPlugin{
cacheKeyType: credentialproviderapi.ImagePluginCacheKeyType,
cacheDuration: time.Hour,
auth: authConfigMap,
}
provider := &pluginProvider{
clock: testClock,
matchImages: []string{"*"},
cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: testClock}),
defaultCacheDuration: time.Hour,
lastCachePurge: testClock.Now(),
plugin: mockPlugin,
}
return provider
}
// setupTestProviders sets up test providers and cleans up after the test
func setupTestProviders(t *testing.T) {
t.Helper()
// Save original state
providersMutex.Lock()
originalProviders := providers
originalSeenProviderNames := seenProviderNames
// Reset to clean state
providers = make([]provider, 0)
seenProviderNames = sets.NewString()
providersMutex.Unlock()
t.Cleanup(func() {
// Restore original state
providersMutex.Lock()
providers = originalProviders
seenProviderNames = originalSeenProviderNames
providersMutex.Unlock()
})
}
func TestRegisterCredentialProviderPlugin(t *testing.T) {
testCases := []struct {
name string
firstProvider string
secondProvider string // empty means no second provider
shouldPanic bool
}{
{
name: "successful registration",
firstProvider: "test-provider",
shouldPanic: false,
},
{
name: "duplicate registration should panic",
firstProvider: "duplicate-provider",
secondProvider: "duplicate-provider",
shouldPanic: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setupTestProviders(t)
mockProvider1 := createTestPluginProvider(credentialprovider.DockerConfig{
"test.registry.io": credentialprovider.DockerConfigEntry{
Username: "user",
Password: "pass",
},
})
// Register first provider
registerCredentialProviderPlugin(tc.firstProvider, mockProvider1)
// Verify first registration
providersMutex.RLock()
if len(providers) != 1 {
t.Errorf("Expected 1 provider after first registration, got %d", len(providers))
}
if providers[0].name != tc.firstProvider {
t.Errorf("Expected provider name '%s', got %s", tc.firstProvider, providers[0].name)
}
if providers[0].impl != mockProvider1 {
t.Errorf("Expected provider implementation to match")
}
if !seenProviderNames.Has(tc.firstProvider) {
t.Errorf("Expected '%s' to be in seenProviderNames", tc.firstProvider)
}
providersMutex.RUnlock()
// If we have a second provider to test, register it
if tc.secondProvider != "" {
mockProvider2 := createTestPluginProvider(credentialprovider.DockerConfig{})
if tc.shouldPanic {
// Test that registering duplicate provider name panics
defer func() {
if r := recover(); r == nil {
t.Errorf("Expected panic for duplicate provider registration")
}
}()
}
registerCredentialProviderPlugin(tc.secondProvider, mockProvider2)
if !tc.shouldPanic {
// If we didn't expect a panic, verify the second provider was registered
providersMutex.RLock()
if len(providers) != 2 {
t.Errorf("Expected 2 providers after second registration, got %d", len(providers))
}
providersMutex.RUnlock()
}
}
})
}
}
func TestNewExternalCredentialProviderDockerKeyring(t *testing.T) {
testCases := []struct {
name string
setupProviders func()
featureGateEnabled bool
expectedProviderCount int
expectedPodNamespace string
expectedPodName string
expectedPodUID string
expectedServiceAccountName string
validatePerPodProviderFields bool
}{
{
name: "no providers registered",
setupProviders: func() {},
expectedProviderCount: 0,
},
{
name: "multiple providers registered",
setupProviders: func() {
mockProvider1 := createTestPluginProvider(credentialprovider.DockerConfig{})
mockProvider2 := createTestPluginProvider(credentialprovider.DockerConfig{})
registerCredentialProviderPlugin("enabled-provider-1", mockProvider1)
registerCredentialProviderPlugin("enabled-provider-2", mockProvider2)
},
expectedProviderCount: 2,
},
{
name: "feature gate enabled - pod information should be set",
setupProviders: func() {
mockProvider := createTestPluginProvider(credentialprovider.DockerConfig{})
registerCredentialProviderPlugin("test-provider", mockProvider)
},
featureGateEnabled: true,
expectedProviderCount: 1,
expectedPodNamespace: "test-namespace",
expectedPodName: "test-pod",
expectedPodUID: "test-uid",
expectedServiceAccountName: "test-sa",
validatePerPodProviderFields: true,
},
{
name: "feature gate disabled - pod information should be empty",
setupProviders: func() {
mockProvider := createTestPluginProvider(credentialprovider.DockerConfig{})
registerCredentialProviderPlugin("test-provider", mockProvider)
},
featureGateEnabled: false,
expectedProviderCount: 1,
expectedPodNamespace: "",
expectedPodName: "",
expectedPodUID: "",
expectedServiceAccountName: "",
validatePerPodProviderFields: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
setupTestProviders(t)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletServiceAccountTokenForCredentialProviders, tc.featureGateEnabled)
tc.setupProviders()
keyring := NewExternalCredentialProviderDockerKeyring("test-namespace", "test-pod", "test-uid", "test-sa")
externalKeyring, ok := keyring.(*externalCredentialProviderKeyring)
if !ok {
t.Fatalf("Expected externalCredentialProviderKeyring, got %T", keyring)
}
if len(externalKeyring.providers) != tc.expectedProviderCount {
t.Errorf("Expected %d providers, got %d", tc.expectedProviderCount, len(externalKeyring.providers))
}
if tc.validatePerPodProviderFields && len(externalKeyring.providers) > 0 {
perPodProvider, ok := externalKeyring.providers[0].(*perPodPluginProvider)
if !ok {
t.Fatalf("Expected perPodPluginProvider, got %T", externalKeyring.providers[0])
}
if perPodProvider.podNamespace != tc.expectedPodNamespace {
t.Errorf("Expected podNamespace '%s', got %s", tc.expectedPodNamespace, perPodProvider.podNamespace)
}
if perPodProvider.podName != tc.expectedPodName {
t.Errorf("Expected podName '%s', got %s", tc.expectedPodName, perPodProvider.podName)
}
if string(perPodProvider.podUID) != tc.expectedPodUID {
t.Errorf("Expected podUID '%s', got %s", tc.expectedPodUID, string(perPodProvider.podUID))
}
if perPodProvider.serviceAccountName != tc.expectedServiceAccountName {
t.Errorf("Expected serviceAccountName '%s', got %s", tc.expectedServiceAccountName, perPodProvider.serviceAccountName)
}
}
})
}
}
func TestExternalCredentialProviderKeyringLookupNoProviders(t *testing.T) {
keyring := &externalCredentialProviderKeyring{
providers: []dockerConfigProviderWithCoordinates{},
}
configs, found := keyring.Lookup("test.registry.io/image:tag")
if found {
t.Errorf("Expected not found, got found=true")
}
if len(configs) != 0 {
t.Errorf("Expected 0 configs, got %d", len(configs))
}
}
func TestExternalCredentialProviderKeyringLookupWithProviders(t *testing.T) {
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.KubeletEnsureSecretPulledImages, true)
testCases := []struct {
name string
image string
providers []dockerConfigProviderWithCoordinates
expectedConfigCount int
expectedFound bool
expectedServiceAccountOK bool
}{
{
name: "single provider with config",
image: "test.registry.io/image:tag",
providers: []dockerConfigProviderWithCoordinates{
&fakedockerConfigProviderWithCoordinates{
dockerConfig: credentialprovider.DockerConfig{
"test.registry.io": credentialprovider.DockerConfigEntry{
Username: "user1",
Password: "pass1",
},
},
serviceAccountCoords: nil,
},
},
expectedConfigCount: 1,
expectedFound: true,
expectedServiceAccountOK: false,
},
{
name: "single provider with service account coordinates",
image: "test.registry.io/image:tag",
providers: []dockerConfigProviderWithCoordinates{
&fakedockerConfigProviderWithCoordinates{
dockerConfig: credentialprovider.DockerConfig{
"test.registry.io": credentialprovider.DockerConfigEntry{
Username: "user1",
Password: "pass1",
},
},
serviceAccountCoords: &credentialprovider.ServiceAccountCoordinates{
Namespace: "test-namespace",
Name: "test-sa",
UID: "test-uid",
},
},
},
expectedConfigCount: 1,
expectedFound: true,
expectedServiceAccountOK: true,
},
{
name: "multiple providers",
image: "test.registry.io/image:tag",
providers: []dockerConfigProviderWithCoordinates{
&fakedockerConfigProviderWithCoordinates{
dockerConfig: credentialprovider.DockerConfig{
"test.registry.io": credentialprovider.DockerConfigEntry{
Username: "user1",
Password: "pass1",
},
},
serviceAccountCoords: &credentialprovider.ServiceAccountCoordinates{
Namespace: "test-namespace",
Name: "test-sa-1",
UID: "test-uid-1",
},
},
&fakedockerConfigProviderWithCoordinates{
dockerConfig: credentialprovider.DockerConfig{
"test.registry.io": credentialprovider.DockerConfigEntry{
Username: "user2",
Password: "pass2",
},
},
serviceAccountCoords: &credentialprovider.ServiceAccountCoordinates{
Namespace: "test-namespace",
Name: "test-sa-2",
UID: "test-uid-2",
},
},
},
expectedConfigCount: 2,
expectedFound: true,
expectedServiceAccountOK: true,
},
{
name: "provider with empty config",
image: "test.registry.io/image:tag",
providers: []dockerConfigProviderWithCoordinates{
&fakedockerConfigProviderWithCoordinates{
dockerConfig: credentialprovider.DockerConfig{},
serviceAccountCoords: nil,
},
},
expectedConfigCount: 0,
expectedFound: false,
expectedServiceAccountOK: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
keyring := &externalCredentialProviderKeyring{
providers: tc.providers,
}
configs, found := keyring.Lookup(tc.image)
if found != tc.expectedFound {
t.Errorf("Expected found=%v, got found=%v", tc.expectedFound, found)
}
if len(configs) != tc.expectedConfigCount {
t.Errorf("Expected %d configs, got %d", tc.expectedConfigCount, len(configs))
}
// Verify that each provider was called with the correct image
for i, provider := range tc.providers {
mockProvider := provider.(*fakedockerConfigProviderWithCoordinates)
if mockProvider.getCallCount() != 1 {
t.Errorf("Provider %d expected 1 call, got %d", i, mockProvider.getCallCount())
}
if mockProvider.getLastImageRequested() != tc.image {
t.Errorf("Provider %d expected image %s, got %s", i, tc.image, mockProvider.getLastImageRequested())
}
}
// Verify service account coordinates in TrackedAuthConfig
if tc.expectedServiceAccountOK && len(configs) > 0 {
foundServiceAccountCoords := false
for _, config := range configs {
if config.Source != nil && config.Source.ServiceAccount != nil {
foundServiceAccountCoords = true
break
}
}
if !foundServiceAccountCoords {
t.Errorf("Expected to find service account coordinates in TrackedAuthConfig")
}
}
})
}
}
func TestExternalCredentialProviderKeyringLookupConcurrency(t *testing.T) {
// Test concurrent access to the keyring
mockProvider := &fakedockerConfigProviderWithCoordinates{
dockerConfig: credentialprovider.DockerConfig{
"test.registry.io": credentialprovider.DockerConfigEntry{
Username: "user1",
Password: "pass1",
},
},
serviceAccountCoords: &credentialprovider.ServiceAccountCoordinates{
Namespace: "test-namespace",
Name: "test-sa",
UID: "test-uid",
},
}
keyring := &externalCredentialProviderKeyring{
providers: []dockerConfigProviderWithCoordinates{mockProvider},
}
const numGoroutines = 10
const numCallsPerGoroutine = 5
var wg sync.WaitGroup
var errorOccurred bool
var mu sync.Mutex
wg.Add(numGoroutines)
for i := range numGoroutines {
go func(goroutineID int) {
defer wg.Done()
for j := range numCallsPerGoroutine {
image := "test.registry.io/image:tag"
configs, found := keyring.Lookup(image)
if !found {
mu.Lock()
errorOccurred = true
t.Errorf("Goroutine %d call %d: Expected found=true, got found=false", goroutineID, j)
mu.Unlock()
return
}
if len(configs) != 1 {
mu.Lock()
errorOccurred = true
t.Errorf("Goroutine %d call %d: Expected 1 config, got %d", goroutineID, j, len(configs))
mu.Unlock()
return
}
}
}(i)
}
wg.Wait()
if !errorOccurred {
expectedTotalCalls := numGoroutines * numCallsPerGoroutine
actualCalls := mockProvider.getCallCount()
if actualCalls != expectedTotalCalls {
t.Errorf("Expected %d total calls, got %d", expectedTotalCalls, actualCalls)
}
}
}