Skip to content

Commit b480e31

Browse files
authored
Merge pull request kubernetes#129669 from aramase/aramase/f/credential_provider_config_dup_validation
credential provider config: validate duplicate names early and preserve provider order
2 parents 5d478a6 + 92e35e7 commit b480e31

File tree

8 files changed

+89
-57
lines changed

8 files changed

+89
-57
lines changed

pkg/credentialprovider/plugin/config.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ package plugin
1818

1919
import (
2020
"fmt"
21-
"strings"
22-
2321
"os"
22+
"strings"
2423

24+
"k8s.io/apimachinery/pkg/util/sets"
2525
"k8s.io/apimachinery/pkg/util/validation/field"
2626
"k8s.io/kubernetes/pkg/credentialprovider"
2727
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
@@ -78,6 +78,7 @@ func validateCredentialProviderConfig(config *kubeletconfig.CredentialProviderCo
7878
}
7979

8080
fieldPath := field.NewPath("providers")
81+
seenProviderNames := sets.NewString()
8182
for _, provider := range config.Providers {
8283
if strings.Contains(provider.Name, "/") {
8384
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot contain '/'"))
@@ -95,14 +96,15 @@ func validateCredentialProviderConfig(config *kubeletconfig.CredentialProviderCo
9596
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot be '..'"))
9697
}
9798

99+
if seenProviderNames.Has(provider.Name) {
100+
allErrs = append(allErrs, field.Duplicate(fieldPath.Child("name"), provider.Name))
101+
}
102+
seenProviderNames.Insert(provider.Name)
103+
98104
if provider.APIVersion == "" {
99105
allErrs = append(allErrs, field.Required(fieldPath.Child("apiVersion"), "apiVersion is required"))
100106
} else if _, ok := apiVersions[provider.APIVersion]; !ok {
101-
validAPIVersions := []string{}
102-
for apiVersion := range apiVersions {
103-
validAPIVersions = append(validAPIVersions, apiVersion)
104-
}
105-
107+
validAPIVersions := sets.StringKeySet(apiVersions).List()
106108
allErrs = append(allErrs, field.NotSupported(fieldPath.Child("apiVersion"), provider.APIVersion, validAPIVersions))
107109
}
108110

pkg/credentialprovider/plugin/config_test.go

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import (
2222
"testing"
2323
"time"
2424

25+
"github.com/google/go-cmp/cmp"
26+
27+
"k8s.io/apimachinery/pkg/util/errors"
2528
utiltesting "k8s.io/client-go/util/testing"
2629

2730
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -369,14 +372,15 @@ providers:
369372

370373
func Test_validateCredentialProviderConfig(t *testing.T) {
371374
testcases := []struct {
372-
name string
373-
config *kubeletconfig.CredentialProviderConfig
374-
shouldErr bool
375+
name string
376+
config *kubeletconfig.CredentialProviderConfig
377+
saTokenForCredentialProviders bool
378+
expectErr string
375379
}{
376380
{
377381
name: "no providers provided",
378382
config: &kubeletconfig.CredentialProviderConfig{},
379-
shouldErr: true,
383+
expectErr: `providers: Required value: at least 1 item in plugins is required`,
380384
},
381385
{
382386
name: "no matchImages provided",
@@ -390,7 +394,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
390394
},
391395
},
392396
},
393-
shouldErr: true,
397+
expectErr: `providers.matchImages: Required value: at least 1 item in matchImages is required`,
394398
},
395399
{
396400
name: "no default cache duration provided",
@@ -403,7 +407,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
403407
},
404408
},
405409
},
406-
shouldErr: true,
410+
expectErr: `providers.defaultCacheDuration: Required value: defaultCacheDuration is required`,
407411
},
408412
{
409413
name: "name contains '/'",
@@ -417,7 +421,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
417421
},
418422
},
419423
},
420-
shouldErr: true,
424+
expectErr: `providers.name: Invalid value: "foo/../bar": provider name cannot contain '/'`,
421425
},
422426
{
423427
name: "name is '.'",
@@ -431,7 +435,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
431435
},
432436
},
433437
},
434-
shouldErr: true,
438+
expectErr: `providers.name: Invalid value: ".": provider name cannot be '.'`,
435439
},
436440
{
437441
name: "name is '..'",
@@ -445,7 +449,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
445449
},
446450
},
447451
},
448-
shouldErr: true,
452+
expectErr: `providers.name: Invalid value: "..": provider name cannot be '..'`,
449453
},
450454
{
451455
name: "name contains spaces",
@@ -459,7 +463,27 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
459463
},
460464
},
461465
},
462-
shouldErr: true,
466+
expectErr: `providers.name: Invalid value: "foo bar": provider name cannot contain spaces`,
467+
},
468+
{
469+
name: "duplicate names",
470+
config: &kubeletconfig.CredentialProviderConfig{
471+
Providers: []kubeletconfig.CredentialProvider{
472+
{
473+
Name: "foobar",
474+
MatchImages: []string{"foobar.registry.io"},
475+
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
476+
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
477+
},
478+
{
479+
Name: "foobar",
480+
MatchImages: []string{"bar.registry.io"},
481+
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
482+
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
483+
},
484+
},
485+
},
486+
expectErr: `providers.name: Duplicate value: "foobar"`,
463487
},
464488
{
465489
name: "no apiVersion",
@@ -473,7 +497,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
473497
},
474498
},
475499
},
476-
shouldErr: true,
500+
expectErr: "providers.apiVersion: Required value: apiVersion is required",
477501
},
478502
{
479503
name: "invalid apiVersion",
@@ -487,7 +511,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
487511
},
488512
},
489513
},
490-
shouldErr: true,
514+
expectErr: `providers.apiVersion: Unsupported value: "credentialprovider.kubelet.k8s.io/v1alpha0": supported values: "credentialprovider.kubelet.k8s.io/v1", "credentialprovider.kubelet.k8s.io/v1alpha1", "credentialprovider.kubelet.k8s.io/v1beta1"`,
491515
},
492516
{
493517
name: "negative default cache duration",
@@ -501,7 +525,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
501525
},
502526
},
503527
},
504-
shouldErr: true,
528+
expectErr: "providers.defaultCacheDuration: Invalid value: -1m0s: defaultCacheDuration must be greater than or equal to 0",
505529
},
506530
{
507531
name: "invalid match image",
@@ -515,7 +539,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
515539
},
516540
},
517541
},
518-
shouldErr: true,
542+
expectErr: `providers.matchImages: Invalid value: "%invalid%": match image is invalid: parse "https://%invalid%": invalid URL escape "%in"`,
519543
},
520544
{
521545
name: "valid config",
@@ -529,19 +553,22 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
529553
},
530554
},
531555
},
532-
shouldErr: false,
533556
},
534557
}
535558

536559
for _, testcase := range testcases {
537560
t.Run(testcase.name, func(t *testing.T) {
538-
errs := validateCredentialProviderConfig(testcase.config)
539-
540-
if testcase.shouldErr && len(errs) == 0 {
541-
t.Errorf("expected error but got none")
542-
} else if !testcase.shouldErr && len(errs) > 0 {
543-
t.Errorf("expected no error but received errors: %v", errs.ToAggregate())
561+
errs := validateCredentialProviderConfig(testcase.config).ToAggregate()
562+
if d := cmp.Diff(testcase.expectErr, errString(errs)); d != "" {
563+
t.Fatalf("CredentialProviderConfig validation mismatch (-want +got):\n%s", d)
544564
}
545565
})
546566
}
547567
}
568+
569+
func errString(errs errors.Aggregate) string {
570+
if errs != nil {
571+
return errs.Error()
572+
}
573+
return ""
574+
}

pkg/credentialprovider/plugins.go

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,39 @@ limitations under the License.
1717
package credentialprovider
1818

1919
import (
20-
"reflect"
21-
"sort"
2220
"sync"
2321

22+
"k8s.io/apimachinery/pkg/util/sets"
2423
"k8s.io/klog/v2"
2524
)
2625

26+
type provider struct {
27+
name string
28+
impl DockerConfigProvider
29+
}
30+
2731
// All registered credential providers.
2832
var providersMutex sync.Mutex
29-
var providers = make(map[string]DockerConfigProvider)
33+
var providers = make([]provider, 0)
34+
var seenProviderNames = sets.NewString()
3035

3136
// RegisterCredentialProvider is called by provider implementations on
3237
// initialization to register themselves, like so:
3338
//
3439
// func init() {
3540
// RegisterCredentialProvider("name", &myProvider{...})
3641
// }
37-
func RegisterCredentialProvider(name string, provider DockerConfigProvider) {
42+
func RegisterCredentialProvider(name string, p DockerConfigProvider) {
3843
providersMutex.Lock()
3944
defer providersMutex.Unlock()
40-
_, found := providers[name]
41-
if found {
45+
46+
if seenProviderNames.Has(name) {
4247
klog.Fatalf("Credential provider %q was registered twice", name)
4348
}
49+
seenProviderNames.Insert(name)
50+
51+
providers = append(providers, provider{name, p})
4452
klog.V(4).Infof("Registered credential provider %q", name)
45-
providers[name] = provider
4653
}
4754

4855
// NewDockerKeyring creates a DockerKeyring to use for resolving credentials,
@@ -52,18 +59,10 @@ func NewDockerKeyring() DockerKeyring {
5259
Providers: make([]DockerConfigProvider, 0),
5360
}
5461

55-
keys := reflect.ValueOf(providers).MapKeys()
56-
stringKeys := make([]string, len(keys))
57-
for ix := range keys {
58-
stringKeys[ix] = keys[ix].String()
59-
}
60-
sort.Strings(stringKeys)
61-
62-
for _, key := range stringKeys {
63-
provider := providers[key]
64-
if provider.Enabled() {
65-
klog.V(4).Infof("Registering credential provider: %v", key)
66-
keyring.Providers = append(keyring.Providers, provider)
62+
for _, p := range providers {
63+
if p.impl.Enabled() {
64+
klog.V(4).Infof("Registering credential provider: %v", p.name)
65+
keyring.Providers = append(keyring.Providers, p.impl)
6766
}
6867
}
6968

pkg/generated/openapi/zz_generated.openapi.go

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/kubelet/apis/config/types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ type CredentialProviderConfig struct {
604604
// Multiple providers may match against a single image, in which case credentials
605605
// from all providers will be returned to the kubelet. If multiple providers are called
606606
// for a single image, the results are combined. If providers return overlapping
607-
// auth keys, the value from the provider earlier in this list is used.
607+
// auth keys, the value from the provider earlier in this list is attempted first.
608608
Providers []CredentialProvider
609609
}
610610

@@ -614,6 +614,7 @@ type CredentialProvider struct {
614614
// name is the required name of the credential provider. It must match the name of the
615615
// provider executable as seen by the kubelet. The executable must be in the kubelet's
616616
// bin directory (set by the --credential-provider-bin-dir flag).
617+
// Required to be unique across all providers.
617618
Name string
618619

619620
// matchImages is a required list of strings used to match against images in order to

staging/src/k8s.io/kubelet/config/v1/types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type CredentialProviderConfig struct {
3232
// Multiple providers may match against a single image, in which case credentials
3333
// from all providers will be returned to the kubelet. If multiple providers are called
3434
// for a single image, the results are combined. If providers return overlapping
35-
// auth keys, the value from the provider earlier in this list is used.
35+
// auth keys, the value from the provider earlier in this list is attempted first.
3636
Providers []CredentialProvider `json:"providers"`
3737
}
3838

@@ -42,6 +42,7 @@ type CredentialProvider struct {
4242
// name is the required name of the credential provider. It must match the name of the
4343
// provider executable as seen by the kubelet. The executable must be in the kubelet's
4444
// bin directory (set by the --image-credential-provider-bin-dir flag).
45+
// Required to be unique across all providers.
4546
Name string `json:"name"`
4647

4748
// matchImages is a required list of strings used to match against images in order to

staging/src/k8s.io/kubelet/config/v1alpha1/types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type CredentialProviderConfig struct {
3232
// Multiple providers may match against a single image, in which case credentials
3333
// from all providers will be returned to the kubelet. If multiple providers are called
3434
// for a single image, the results are combined. If providers return overlapping
35-
// auth keys, the value from the provider earlier in this list is used.
35+
// auth keys, the value from the provider earlier in this list is attempted first.
3636
Providers []CredentialProvider `json:"providers"`
3737
}
3838

@@ -42,6 +42,7 @@ type CredentialProvider struct {
4242
// name is the required name of the credential provider. It must match the name of the
4343
// provider executable as seen by the kubelet. The executable must be in the kubelet's
4444
// bin directory (set by the --image-credential-provider-bin-dir flag).
45+
// Required to be unique across all providers.
4546
Name string `json:"name"`
4647

4748
// matchImages is a required list of strings used to match against images in order to

0 commit comments

Comments
 (0)