mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-03 09:22:44 +00:00
credential provider config: validate duplicate names early and preserve provider order
Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com>
This commit is contained in:
parent
a1bbf17d73
commit
9a331bbf59
@ -18,10 +18,10 @@ package plugin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"os"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||||
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||||
@ -78,6 +78,7 @@ func validateCredentialProviderConfig(config *kubeletconfig.CredentialProviderCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
fieldPath := field.NewPath("providers")
|
fieldPath := field.NewPath("providers")
|
||||||
|
seenProviderNames := sets.NewString()
|
||||||
for _, provider := range config.Providers {
|
for _, provider := range config.Providers {
|
||||||
if strings.Contains(provider.Name, "/") {
|
if strings.Contains(provider.Name, "/") {
|
||||||
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot contain '/'"))
|
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot contain '/'"))
|
||||||
@ -95,14 +96,15 @@ func validateCredentialProviderConfig(config *kubeletconfig.CredentialProviderCo
|
|||||||
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot be '..'"))
|
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot be '..'"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if seenProviderNames.Has(provider.Name) {
|
||||||
|
allErrs = append(allErrs, field.Duplicate(fieldPath.Child("name"), provider.Name))
|
||||||
|
}
|
||||||
|
seenProviderNames.Insert(provider.Name)
|
||||||
|
|
||||||
if provider.APIVersion == "" {
|
if provider.APIVersion == "" {
|
||||||
allErrs = append(allErrs, field.Required(fieldPath.Child("apiVersion"), "apiVersion is required"))
|
allErrs = append(allErrs, field.Required(fieldPath.Child("apiVersion"), "apiVersion is required"))
|
||||||
} else if _, ok := apiVersions[provider.APIVersion]; !ok {
|
} else if _, ok := apiVersions[provider.APIVersion]; !ok {
|
||||||
validAPIVersions := []string{}
|
validAPIVersions := sets.StringKeySet(apiVersions).List()
|
||||||
for apiVersion := range apiVersions {
|
|
||||||
validAPIVersions = append(validAPIVersions, apiVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
allErrs = append(allErrs, field.NotSupported(fieldPath.Child("apiVersion"), provider.APIVersion, validAPIVersions))
|
allErrs = append(allErrs, field.NotSupported(fieldPath.Child("apiVersion"), provider.APIVersion, validAPIVersions))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/errors"
|
||||||
utiltesting "k8s.io/client-go/util/testing"
|
utiltesting "k8s.io/client-go/util/testing"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -371,12 +374,13 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
|
|||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
name string
|
name string
|
||||||
config *kubeletconfig.CredentialProviderConfig
|
config *kubeletconfig.CredentialProviderConfig
|
||||||
shouldErr bool
|
saTokenForCredentialProviders bool
|
||||||
|
expectErr string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no providers provided",
|
name: "no providers provided",
|
||||||
config: &kubeletconfig.CredentialProviderConfig{},
|
config: &kubeletconfig.CredentialProviderConfig{},
|
||||||
shouldErr: true,
|
expectErr: `providers: Required value: at least 1 item in plugins is required`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no matchImages provided",
|
name: "no matchImages provided",
|
||||||
@ -390,7 +394,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shouldErr: true,
|
expectErr: `providers.matchImages: Required value: at least 1 item in matchImages is required`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no default cache duration provided",
|
name: "no default cache duration provided",
|
||||||
@ -403,7 +407,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shouldErr: true,
|
expectErr: `providers.defaultCacheDuration: Required value: defaultCacheDuration is required`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "name contains '/'",
|
name: "name contains '/'",
|
||||||
@ -417,7 +421,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shouldErr: true,
|
expectErr: `providers.name: Invalid value: "foo/../bar": provider name cannot contain '/'`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "name is '.'",
|
name: "name is '.'",
|
||||||
@ -431,7 +435,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shouldErr: true,
|
expectErr: `providers.name: Invalid value: ".": provider name cannot be '.'`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "name is '..'",
|
name: "name is '..'",
|
||||||
@ -445,7 +449,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shouldErr: true,
|
expectErr: `providers.name: Invalid value: "..": provider name cannot be '..'`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "name contains spaces",
|
name: "name contains spaces",
|
||||||
@ -459,7 +463,27 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shouldErr: true,
|
expectErr: `providers.name: Invalid value: "foo bar": provider name cannot contain spaces`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate names",
|
||||||
|
config: &kubeletconfig.CredentialProviderConfig{
|
||||||
|
Providers: []kubeletconfig.CredentialProvider{
|
||||||
|
{
|
||||||
|
Name: "foobar",
|
||||||
|
MatchImages: []string{"foobar.registry.io"},
|
||||||
|
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||||
|
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "foobar",
|
||||||
|
MatchImages: []string{"bar.registry.io"},
|
||||||
|
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||||
|
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectErr: `providers.name: Duplicate value: "foobar"`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no apiVersion",
|
name: "no apiVersion",
|
||||||
@ -473,7 +497,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shouldErr: true,
|
expectErr: "providers.apiVersion: Required value: apiVersion is required",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid apiVersion",
|
name: "invalid apiVersion",
|
||||||
@ -487,7 +511,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shouldErr: true,
|
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"`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "negative default cache duration",
|
name: "negative default cache duration",
|
||||||
@ -501,7 +525,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shouldErr: true,
|
expectErr: "providers.defaultCacheDuration: Invalid value: -1m0s: defaultCacheDuration must be greater than or equal to 0",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid match image",
|
name: "invalid match image",
|
||||||
@ -515,7 +539,7 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shouldErr: true,
|
expectErr: `providers.matchImages: Invalid value: "%invalid%": match image is invalid: parse "https://%invalid%": invalid URL escape "%in"`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "valid config",
|
name: "valid config",
|
||||||
@ -529,19 +553,22 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
shouldErr: false,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, testcase := range testcases {
|
for _, testcase := range testcases {
|
||||||
t.Run(testcase.name, func(t *testing.T) {
|
t.Run(testcase.name, func(t *testing.T) {
|
||||||
errs := validateCredentialProviderConfig(testcase.config)
|
errs := validateCredentialProviderConfig(testcase.config).ToAggregate()
|
||||||
|
if d := cmp.Diff(testcase.expectErr, errString(errs)); d != "" {
|
||||||
if testcase.shouldErr && len(errs) == 0 {
|
t.Fatalf("CredentialProviderConfig validation mismatch (-want +got):\n%s", d)
|
||||||
t.Errorf("expected error but got none")
|
|
||||||
} else if !testcase.shouldErr && len(errs) > 0 {
|
|
||||||
t.Errorf("expected no error but received errors: %v", errs.ToAggregate())
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func errString(errs errors.Aggregate) string {
|
||||||
|
if errs != nil {
|
||||||
|
return errs.Error()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
@ -17,16 +17,21 @@ limitations under the License.
|
|||||||
package credentialprovider
|
package credentialprovider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type provider struct {
|
||||||
|
name string
|
||||||
|
impl DockerConfigProvider
|
||||||
|
}
|
||||||
|
|
||||||
// All registered credential providers.
|
// All registered credential providers.
|
||||||
var providersMutex sync.Mutex
|
var providersMutex sync.Mutex
|
||||||
var providers = make(map[string]DockerConfigProvider)
|
var providers = make([]provider, 0)
|
||||||
|
var seenProviderNames = sets.NewString()
|
||||||
|
|
||||||
// RegisterCredentialProvider is called by provider implementations on
|
// RegisterCredentialProvider is called by provider implementations on
|
||||||
// initialization to register themselves, like so:
|
// initialization to register themselves, like so:
|
||||||
@ -34,15 +39,17 @@ var providers = make(map[string]DockerConfigProvider)
|
|||||||
// func init() {
|
// func init() {
|
||||||
// RegisterCredentialProvider("name", &myProvider{...})
|
// RegisterCredentialProvider("name", &myProvider{...})
|
||||||
// }
|
// }
|
||||||
func RegisterCredentialProvider(name string, provider DockerConfigProvider) {
|
func RegisterCredentialProvider(name string, p DockerConfigProvider) {
|
||||||
providersMutex.Lock()
|
providersMutex.Lock()
|
||||||
defer providersMutex.Unlock()
|
defer providersMutex.Unlock()
|
||||||
_, found := providers[name]
|
|
||||||
if found {
|
if seenProviderNames.Has(name) {
|
||||||
klog.Fatalf("Credential provider %q was registered twice", name)
|
klog.Fatalf("Credential provider %q was registered twice", name)
|
||||||
}
|
}
|
||||||
|
seenProviderNames.Insert(name)
|
||||||
|
|
||||||
|
providers = append(providers, provider{name, p})
|
||||||
klog.V(4).Infof("Registered credential provider %q", name)
|
klog.V(4).Infof("Registered credential provider %q", name)
|
||||||
providers[name] = provider
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDockerKeyring creates a DockerKeyring to use for resolving credentials,
|
// NewDockerKeyring creates a DockerKeyring to use for resolving credentials,
|
||||||
@ -52,18 +59,10 @@ func NewDockerKeyring() DockerKeyring {
|
|||||||
Providers: make([]DockerConfigProvider, 0),
|
Providers: make([]DockerConfigProvider, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
keys := reflect.ValueOf(providers).MapKeys()
|
for _, p := range providers {
|
||||||
stringKeys := make([]string, len(keys))
|
if p.impl.Enabled() {
|
||||||
for ix := range keys {
|
klog.V(4).Infof("Registering credential provider: %v", p.name)
|
||||||
stringKeys[ix] = keys[ix].String()
|
keyring.Providers = append(keyring.Providers, p.impl)
|
||||||
}
|
|
||||||
sort.Strings(stringKeys)
|
|
||||||
|
|
||||||
for _, key := range stringKeys {
|
|
||||||
provider := providers[key]
|
|
||||||
if provider.Enabled() {
|
|
||||||
klog.V(4).Infof("Registering credential provider: %v", key)
|
|
||||||
keyring.Providers = append(keyring.Providers, provider)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user