mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 11:21:47 +00:00
pkg/credentialprovider: add initial exec-based credential provider plugin
Signed-off-by: Andrew Sy Kim <kim.andrewsy@gmail.com>
This commit is contained in:
parent
2d0dd26252
commit
5344afd4fb
@ -46,6 +46,7 @@ filegroup(
|
||||
"//pkg/credentialprovider/aws:all-srcs",
|
||||
"//pkg/credentialprovider/azure:all-srcs",
|
||||
"//pkg/credentialprovider/gcp:all-srcs",
|
||||
"//pkg/credentialprovider/plugin:all-srcs",
|
||||
"//pkg/credentialprovider/secrets:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
|
58
pkg/credentialprovider/plugin/BUILD
Normal file
58
pkg/credentialprovider/plugin/BUILD
Normal file
@ -0,0 +1,58 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"config.go",
|
||||
"plugin.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/credentialprovider/plugin",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//pkg/credentialprovider:go_default_library",
|
||||
"//pkg/kubelet/apis/config:go_default_library",
|
||||
"//pkg/kubelet/apis/config/v1alpha1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/serializer/json:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
|
||||
"//staging/src/k8s.io/kubelet/pkg/apis/credentialprovider:go_default_library",
|
||||
"//staging/src/k8s.io/kubelet/pkg/apis/credentialprovider/install:go_default_library",
|
||||
"//staging/src/k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1:go_default_library",
|
||||
"//vendor/k8s.io/klog/v2:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"config_test.go",
|
||||
"plugin_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/credentialprovider:go_default_library",
|
||||
"//pkg/kubelet/apis/config:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
|
||||
"//staging/src/k8s.io/kubelet/pkg/apis/credentialprovider:go_default_library",
|
||||
"//staging/src/k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1:go_default_library",
|
||||
],
|
||||
)
|
128
pkg/credentialprovider/plugin/config.go
Normal file
128
pkg/credentialprovider/plugin/config.go
Normal file
@ -0,0 +1,128 @@
|
||||
/*
|
||||
Copyright 2020 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 (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
)
|
||||
|
||||
// readCredentialProviderConfigFile receives a path to a config file and decodes it
|
||||
// into the internal CredentialProviderConfig type.
|
||||
func readCredentialProviderConfigFile(configPath string) (*kubeletconfig.CredentialProviderConfig, error) {
|
||||
if configPath == "" {
|
||||
return nil, fmt.Errorf("credential provider config path is empty")
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read external registry credential provider configuration from %q: %v", configPath, err)
|
||||
}
|
||||
|
||||
config, err := decode(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding config %s: %v", configPath, err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// decode decodes data into the internal CredentialProviderConfig type.
|
||||
func decode(data []byte) (*kubeletconfig.CredentialProviderConfig, error) {
|
||||
obj, gvk, err := codecs.UniversalDecoder().Decode(data, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gvk.Kind != "CredentialProviderConfig" {
|
||||
return nil, fmt.Errorf("failed to decode %q (wrong Kind)", gvk.Kind)
|
||||
}
|
||||
|
||||
if gvk.Group != kubeletconfig.GroupName {
|
||||
return nil, fmt.Errorf("failed to decode CredentialProviderConfig, unexpected Group: %s", gvk.Group)
|
||||
}
|
||||
|
||||
if internalConfig, ok := obj.(*kubeletconfig.CredentialProviderConfig); ok {
|
||||
return internalConfig, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unable to convert %T to *CredentialProviderConfig", obj)
|
||||
}
|
||||
|
||||
// validateCredentialProviderConfig validates CredentialProviderConfig.
|
||||
func validateCredentialProviderConfig(config *kubeletconfig.CredentialProviderConfig) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
if len(config.Providers) == 0 {
|
||||
allErrs = append(allErrs, field.Required(field.NewPath("providers"), "at least 1 item in plugins is required"))
|
||||
}
|
||||
|
||||
fieldPath := field.NewPath("providers")
|
||||
for _, provider := range config.Providers {
|
||||
if strings.Contains(provider.Name, "/") {
|
||||
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot contain '/'"))
|
||||
}
|
||||
|
||||
if strings.Contains(provider.Name, " ") {
|
||||
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot contain spaces"))
|
||||
}
|
||||
|
||||
if provider.Name == "." {
|
||||
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot be '.'"))
|
||||
}
|
||||
|
||||
if provider.Name == ".." {
|
||||
allErrs = append(allErrs, field.Invalid(fieldPath.Child("name"), provider.Name, "provider name cannot be '..'"))
|
||||
}
|
||||
|
||||
if provider.APIVersion == "" {
|
||||
allErrs = append(allErrs, field.Required(fieldPath.Child("apiVersion"), "apiVersion is required"))
|
||||
} else if _, ok := apiVersions[provider.APIVersion]; !ok {
|
||||
validAPIVersions := []string{}
|
||||
for apiVersion := range apiVersions {
|
||||
validAPIVersions = append(validAPIVersions, apiVersion)
|
||||
}
|
||||
|
||||
allErrs = append(allErrs, field.NotSupported(fieldPath.Child("apiVersion"), provider.APIVersion, validAPIVersions))
|
||||
}
|
||||
|
||||
if len(provider.MatchImages) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fieldPath.Child("matchImages"), "at least 1 item in matchImages is required"))
|
||||
}
|
||||
|
||||
for _, matchImage := range provider.MatchImages {
|
||||
if _, err := credentialprovider.ParseSchemelessURL(matchImage); err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fieldPath.Child("matchImages"), matchImage, fmt.Sprintf("match image is invalid: %s", err.Error())))
|
||||
}
|
||||
}
|
||||
|
||||
if provider.DefaultCacheDuration == nil {
|
||||
allErrs = append(allErrs, field.Required(fieldPath.Child("defaultCacheDuration"), "defaultCacheDuration is required"))
|
||||
}
|
||||
|
||||
if provider.DefaultCacheDuration != nil && provider.DefaultCacheDuration.Duration < 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fieldPath.Child("defaultCacheDuration"), provider.DefaultCacheDuration.Duration, "defaultCacheDuration must be greater than or equal to 0"))
|
||||
}
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
435
pkg/credentialprovider/plugin/config_test.go
Normal file
435
pkg/credentialprovider/plugin/config_test.go
Normal file
@ -0,0 +1,435 @@
|
||||
/*
|
||||
Copyright 2020 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 (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
)
|
||||
|
||||
func Test_readCredentialProviderConfigFile(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
configData string
|
||||
config *kubeletconfig.CredentialProviderConfig
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "config with 1 plugin and 1 image matcher",
|
||||
configData: `---
|
||||
kind: CredentialProviderConfig
|
||||
apiVersion: kubelet.config.k8s.io/v1alpha1
|
||||
providers:
|
||||
- name: test
|
||||
matchImages:
|
||||
- "registry.io/foobar"
|
||||
defaultCacheDuration: 10m
|
||||
apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1
|
||||
args:
|
||||
- --v=5
|
||||
env:
|
||||
- name: FOO
|
||||
value: BAR`,
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "test",
|
||||
MatchImages: []string{"registry.io/foobar"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: 10 * time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||
Args: []string{"--v=5"},
|
||||
Env: []kubeletconfig.ExecEnvVar{
|
||||
{
|
||||
Name: "FOO",
|
||||
Value: "BAR",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with 1 plugin and a wildcard image match",
|
||||
configData: `---
|
||||
kind: CredentialProviderConfig
|
||||
apiVersion: kubelet.config.k8s.io/v1alpha1
|
||||
providers:
|
||||
- name: test
|
||||
matchImages:
|
||||
- "registry.io/*"
|
||||
defaultCacheDuration: 10m
|
||||
apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1
|
||||
args:
|
||||
- --v=5
|
||||
env:
|
||||
- name: FOO
|
||||
value: BAR`,
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "test",
|
||||
MatchImages: []string{"registry.io/*"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: 10 * time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||
Args: []string{"--v=5"},
|
||||
Env: []kubeletconfig.ExecEnvVar{
|
||||
{
|
||||
Name: "FOO",
|
||||
Value: "BAR",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with 1 plugin and multiple image matchers",
|
||||
configData: `---
|
||||
kind: CredentialProviderConfig
|
||||
apiVersion: kubelet.config.k8s.io/v1alpha1
|
||||
providers:
|
||||
- name: test
|
||||
matchImages:
|
||||
- "registry.io/*"
|
||||
- "foobar.registry.io/*"
|
||||
defaultCacheDuration: 10m
|
||||
apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1
|
||||
args:
|
||||
- --v=5
|
||||
env:
|
||||
- name: FOO
|
||||
value: BAR`,
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "test",
|
||||
MatchImages: []string{"registry.io/*", "foobar.registry.io/*"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: 10 * time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||
Args: []string{"--v=5"},
|
||||
Env: []kubeletconfig.ExecEnvVar{
|
||||
{
|
||||
Name: "FOO",
|
||||
Value: "BAR",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with multiple providers",
|
||||
configData: `---
|
||||
kind: CredentialProviderConfig
|
||||
apiVersion: kubelet.config.k8s.io/v1alpha1
|
||||
providers:
|
||||
- name: test1
|
||||
matchImages:
|
||||
- "registry.io/one"
|
||||
defaultCacheDuration: 10m
|
||||
apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1
|
||||
- name: test2
|
||||
matchImages:
|
||||
- "registry.io/two"
|
||||
defaultCacheDuration: 10m
|
||||
apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1
|
||||
args:
|
||||
- --v=5
|
||||
env:
|
||||
- name: FOO
|
||||
value: BAR`,
|
||||
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "test1",
|
||||
MatchImages: []string{"registry.io/one"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: 10 * time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
MatchImages: []string{"registry.io/two"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: 10 * time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||
Args: []string{"--v=5"},
|
||||
Env: []kubeletconfig.ExecEnvVar{
|
||||
{
|
||||
Name: "FOO",
|
||||
Value: "BAR",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with wrong Kind",
|
||||
configData: `---
|
||||
kind: WrongKind
|
||||
apiVersion: kubelet.config.k8s.io/v1alpha1
|
||||
providers:
|
||||
- name: test
|
||||
matchImages:
|
||||
- "registry.io/foobar"
|
||||
defaultCacheDuration: 10m
|
||||
apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1
|
||||
args:
|
||||
- --v=5
|
||||
env:
|
||||
- name: FOO
|
||||
value: BAR`,
|
||||
config: nil,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "config with wrong apiversion",
|
||||
configData: `---
|
||||
kind: CredentialProviderConfig
|
||||
apiVersion: foobar/v1alpha1
|
||||
providers:
|
||||
- name: test
|
||||
matchImages:
|
||||
- "registry.io/foobar"
|
||||
defaultCacheDuration: 10m
|
||||
apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1
|
||||
args:
|
||||
- --v=5
|
||||
env:
|
||||
- name: FOO
|
||||
value: BAR`,
|
||||
config: nil,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
file, err := ioutil.TempFile("", "config.yaml")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
_, err = file.WriteString(testcase.configData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
authConfig, err := readCredentialProviderConfigFile(file.Name())
|
||||
if err != nil && !testcase.expectErr {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err == nil && testcase.expectErr {
|
||||
t.Error("expected error but got none")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(authConfig, testcase.config) {
|
||||
t.Logf("actual auth config: %#v", authConfig)
|
||||
t.Logf("expected auth config: %#v", testcase.config)
|
||||
t.Error("credential provider config did not match")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_validateCredentialProviderConfig(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
config *kubeletconfig.CredentialProviderConfig
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
name: "no providers provided",
|
||||
config: &kubeletconfig.CredentialProviderConfig{},
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "no matchImages provided",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "no default cache duration provided",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "name contains '/'",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foo/../bar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "name is '.'",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: ".",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "name is '..'",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "..",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "name contains spaces",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foo bar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "no apiVersion",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid apiVersion",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha0",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "negative default cache duration",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: -1 * time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid match image",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"%invalid%"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "valid config",
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
shouldErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
errs := validateCredentialProviderConfig(testcase.config)
|
||||
|
||||
if testcase.shouldErr && len(errs) == 0 {
|
||||
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())
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
420
pkg/credentialprovider/plugin/plugin.go
Normal file
420
pkg/credentialprovider/plugin/plugin.go
Normal file
@ -0,0 +1,420 @@
|
||||
/*
|
||||
Copyright 2020 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/klog/v2"
|
||||
credentialproviderapi "k8s.io/kubelet/pkg/apis/credentialprovider"
|
||||
"k8s.io/kubelet/pkg/apis/credentialprovider/install"
|
||||
credentialproviderv1alpha1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
kubeletconfigv1alpha1 "k8s.io/kubernetes/pkg/kubelet/apis/config/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
globalCacheKey = "global"
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
codecs = serializer.NewCodecFactory(scheme)
|
||||
|
||||
apiVersions = map[string]schema.GroupVersion{
|
||||
credentialproviderv1alpha1.SchemeGroupVersion.String(): credentialproviderv1alpha1.SchemeGroupVersion,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
install.Install(scheme)
|
||||
kubeletconfig.AddToScheme(scheme)
|
||||
kubeletconfigv1alpha1.AddToScheme(scheme)
|
||||
}
|
||||
|
||||
// RegisterCredentialProviderPlugins is called from kubelet to register external credential provider
|
||||
// plugins according to the CredentialProviderConfig config file.
|
||||
func RegisterCredentialProviderPlugins(pluginConfigFile, pluginBinDir string) error {
|
||||
if _, err := os.Stat(pluginBinDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("plugin binary directory %s did not exist", pluginBinDir)
|
||||
}
|
||||
|
||||
return fmt.Errorf("error inspecting binary directory %s: %w", pluginBinDir, err)
|
||||
}
|
||||
|
||||
credentialProviderConfig, err := readCredentialProviderConfigFile(pluginConfigFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errs := validateCredentialProviderConfig(credentialProviderConfig)
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("failed to validate credential provider config: %v", errs.ToAggregate())
|
||||
}
|
||||
|
||||
for _, provider := range credentialProviderConfig.Providers {
|
||||
pluginBin := filepath.Join(pluginBinDir, provider.Name)
|
||||
if _, err := os.Stat(pluginBin); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("plugin binary executable %s did not exist", pluginBin)
|
||||
}
|
||||
|
||||
return fmt.Errorf("error inspecting binary executable %s: %w", pluginBin, err)
|
||||
}
|
||||
|
||||
plugin, err := newPluginProvider(pluginBinDir, provider)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error initializing plugin provider %s: %w", provider.Name, err)
|
||||
}
|
||||
|
||||
credentialprovider.RegisterCredentialProvider(provider.Name, plugin)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newPluginProvider returns a new pluginProvider based on the credential provider config.
|
||||
func newPluginProvider(pluginBinDir string, provider kubeletconfig.CredentialProvider) (*pluginProvider, error) {
|
||||
mediaType := "application/json"
|
||||
info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported media type %q", mediaType)
|
||||
}
|
||||
|
||||
gv, ok := apiVersions[provider.APIVersion]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid apiVersion: %q", provider.APIVersion)
|
||||
}
|
||||
|
||||
return &pluginProvider{
|
||||
matchImages: provider.MatchImages,
|
||||
cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{}),
|
||||
defaultCacheDuration: provider.DefaultCacheDuration.Duration,
|
||||
plugin: &execPlugin{
|
||||
name: provider.Name,
|
||||
apiVersion: provider.APIVersion,
|
||||
encoder: codecs.EncoderForVersion(info.Serializer, gv),
|
||||
pluginBinDir: pluginBinDir,
|
||||
args: provider.Args,
|
||||
envVars: provider.Env,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// pluginProvider is the plugin-based implementation of the DockerConfigProvider interface.
|
||||
type pluginProvider struct {
|
||||
sync.Mutex
|
||||
|
||||
// matchImages defines the matching image URLs this plugin should operate against.
|
||||
// The plugin provider will not return any credentials for images that do not match
|
||||
// against this list of match URLs.
|
||||
matchImages []string
|
||||
|
||||
// cache stores DockerConfig entries with an expiration time based on the cache duration
|
||||
// returned from the credential provider plugin.
|
||||
cache cache.Store
|
||||
// defaultCacheDuration is the default duration credentials are cached in-memory if the auth plugin
|
||||
// response did not provide a cache duration for credentials.
|
||||
defaultCacheDuration time.Duration
|
||||
|
||||
// plugin is the exec implementation of the credential providing plugin.
|
||||
plugin Plugin
|
||||
}
|
||||
|
||||
// cacheEntry is the cache object that will be stored in cache.Store.
|
||||
type cacheEntry struct {
|
||||
key string
|
||||
credentials credentialprovider.DockerConfig
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
// cacheKeyFunc extracts AuthEntry.MatchKey as the cache key function for the plugin provider.
|
||||
func cacheKeyFunc(obj interface{}) (string, error) {
|
||||
key := obj.(*cacheEntry).key
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// cacheExpirationPolicy defines implements cache.ExpirationPolicy, determining expiration based on the expiresAt timestamp.
|
||||
type cacheExpirationPolicy struct{}
|
||||
|
||||
// IsExpired returns true if the current time is after cacheEntry.expiresAt, which is determined by the
|
||||
// cache duration returned from the credential provider plugin response.
|
||||
func (c *cacheExpirationPolicy) IsExpired(entry *cache.TimestampedEntry) bool {
|
||||
return time.Now().After(entry.Obj.(*cacheEntry).expiresAt)
|
||||
}
|
||||
|
||||
// Provide returns a credentialprovider.DockerConfig based on the credentials returned
|
||||
// from cache or the exec plugin.
|
||||
func (p *pluginProvider) Provide(image string) credentialprovider.DockerConfig {
|
||||
if !p.isImageAllowed(image) {
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
cachedConfig, found, err := p.getCachedCredentials(image)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed to get cached docker config: %v", err)
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
|
||||
if found {
|
||||
return cachedConfig
|
||||
}
|
||||
|
||||
response, err := p.plugin.ExecPlugin(context.Background(), image)
|
||||
if err != nil {
|
||||
klog.Errorf("Failed getting credential from external registry credential provider: %v", err)
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
|
||||
var cacheKey string
|
||||
switch cacheKeyType := response.CacheKeyType; cacheKeyType {
|
||||
case credentialproviderapi.ImagePluginCacheKeyType:
|
||||
cacheKey = image
|
||||
case credentialproviderapi.RegistryPluginCacheKeyType:
|
||||
registry := parseRegistry(image)
|
||||
cacheKey = registry
|
||||
case credentialproviderapi.GlobalPluginCacheKeyType:
|
||||
cacheKey = globalCacheKey
|
||||
default:
|
||||
klog.Errorf("credential provider plugin did not return a valid cacheKeyType: %q", cacheKeyType)
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
|
||||
dockerConfig := make(credentialprovider.DockerConfig, len(response.Auth))
|
||||
for matchImage, authConfig := range response.Auth {
|
||||
dockerConfig[matchImage] = credentialprovider.DockerConfigEntry{
|
||||
Username: authConfig.Username,
|
||||
Password: authConfig.Password,
|
||||
}
|
||||
}
|
||||
|
||||
// cache duration was explicitly 0 so don't cache this response at all.
|
||||
if response.CacheDuration != nil && response.CacheDuration.Duration == 0 {
|
||||
return dockerConfig
|
||||
}
|
||||
|
||||
var expiresAt time.Time
|
||||
// nil cache duration means use the default cache duration
|
||||
if response.CacheDuration == nil {
|
||||
if p.defaultCacheDuration == 0 {
|
||||
return dockerConfig
|
||||
}
|
||||
|
||||
expiresAt = time.Now().Add(p.defaultCacheDuration)
|
||||
} else {
|
||||
expiresAt = time.Now().Add(response.CacheDuration.Duration)
|
||||
}
|
||||
|
||||
cachedEntry := &cacheEntry{
|
||||
key: cacheKey,
|
||||
credentials: dockerConfig,
|
||||
expiresAt: expiresAt,
|
||||
}
|
||||
|
||||
if err := p.cache.Add(cachedEntry); err != nil {
|
||||
klog.Errorf("Error adding auth entry to cache: %v", err)
|
||||
}
|
||||
|
||||
return dockerConfig
|
||||
}
|
||||
|
||||
// Enabled always returns true since registration of the plugin via kubelet implies it should be enabled.
|
||||
func (e *pluginProvider) Enabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// isImageAllowed returns true if the image matches against the list of allowed matches by the plugin.
|
||||
func (p *pluginProvider) isImageAllowed(image string) bool {
|
||||
for _, matchImage := range p.matchImages {
|
||||
if matched, _ := credentialprovider.URLsMatchStr(matchImage, image); matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// getCachedCredentials returns a credentialprovider.DockerConfig if cached from the plugin.
|
||||
func (p *pluginProvider) getCachedCredentials(image string) (credentialprovider.DockerConfig, bool, error) {
|
||||
obj, found, err := p.cache.GetByKey(image)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if found {
|
||||
return obj.(*cacheEntry).credentials, true, nil
|
||||
}
|
||||
|
||||
registry := parseRegistry(image)
|
||||
obj, found, err = p.cache.GetByKey(registry)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if found {
|
||||
return obj.(*cacheEntry).credentials, true, nil
|
||||
}
|
||||
|
||||
obj, found, err = p.cache.GetByKey(globalCacheKey)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if found {
|
||||
return obj.(*cacheEntry).credentials, true, nil
|
||||
}
|
||||
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// Plugin is the interface calling ExecPlugin. This is mainly for testability
|
||||
// so tests don't have to actually exec any processes.
|
||||
type Plugin interface {
|
||||
ExecPlugin(ctx context.Context, image string) (*credentialproviderapi.CredentialProviderResponse, error)
|
||||
}
|
||||
|
||||
// execPlugin is the implementation of the Plugin interface that execs a credential provider plugin based
|
||||
// on it's name provided in CredentialProviderConfig. It is assumed that the executable is available in the
|
||||
// plugin directory provided by the kubelet.
|
||||
type execPlugin struct {
|
||||
name string
|
||||
apiVersion string
|
||||
encoder runtime.Encoder
|
||||
args []string
|
||||
envVars []kubeletconfig.ExecEnvVar
|
||||
pluginBinDir string
|
||||
}
|
||||
|
||||
// ExecPlugin executes the plugin binary with arguments and environment variables specified in CredentialProviderConfig:
|
||||
//
|
||||
// $ ENV_NAME=ENV_VALUE <plugin-name> args[0] args[1] <<<request
|
||||
//
|
||||
// The plugin is expected to receive the CredentialProviderRequest API via stdin from the kubelet and
|
||||
// return CredentialProviderResponse via stdout.
|
||||
func (e *execPlugin) ExecPlugin(ctx context.Context, image string) (*credentialproviderapi.CredentialProviderResponse, error) {
|
||||
authRequest := &credentialproviderapi.CredentialProviderRequest{Image: image}
|
||||
data, err := e.encodeRequest(authRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode auth request: %v", err)
|
||||
}
|
||||
|
||||
stdout := &bytes.Buffer{}
|
||||
stderr := &bytes.Buffer{}
|
||||
stdin := bytes.NewBuffer(data)
|
||||
|
||||
// Use a catch-all timeout of 1 minute for all exec-based plugins, this should leave enough
|
||||
// head room in case a plugin needs to retry a failed request while ensuring an exec plugin
|
||||
// does not run forever. In the future we may want this timeout to be tweakable from the plugin
|
||||
// config file.
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, filepath.Join(e.pluginBinDir, e.name), e.args...)
|
||||
cmd.Stdout, cmd.Stderr, cmd.Stdin = stdout, stderr, stdin
|
||||
|
||||
cmd.Env = []string{}
|
||||
for _, envVar := range e.envVars {
|
||||
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", envVar.Name, envVar.Value))
|
||||
}
|
||||
|
||||
err = cmd.Run()
|
||||
if ctx.Err() != nil {
|
||||
return nil, fmt.Errorf("error execing credential provider plugin %s for image %s: %w", e.name, image, ctx.Err())
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
klog.V(2).Infof("Error execing credential provider plugin, stderr: %v", stderr.String())
|
||||
return nil, fmt.Errorf("error execing credential provider plugin %s for image %s: %w", e.name, image, err)
|
||||
}
|
||||
|
||||
data = stdout.Bytes()
|
||||
|
||||
// check that the response apiVersion matches what is expected
|
||||
gvk, err := json.DefaultMetaFactory.Interpret(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading GVK from response: %w", err)
|
||||
}
|
||||
|
||||
if gvk.GroupVersion().String() != e.apiVersion {
|
||||
return nil, errors.New("apiVersion from credential plugin response did not match")
|
||||
}
|
||||
|
||||
response, err := e.decodeResponse(stdout.Bytes())
|
||||
if err != nil {
|
||||
// err is explicitly not wrapped since it may contain credentials in the response.
|
||||
return nil, errors.New("error decoding credential provider plugin response from stdout")
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// encodeRequest encodes the internal CredentialProviderRequest type into the v1alpha1 version in json
|
||||
func (e *execPlugin) encodeRequest(request *credentialproviderapi.CredentialProviderRequest) ([]byte, error) {
|
||||
data, err := runtime.Encode(e.encoder, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error encoding request: %v", err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// decodeResponse decodes data into the internal CredentialProviderResponse type
|
||||
func (e *execPlugin) decodeResponse(data []byte) (*credentialproviderapi.CredentialProviderResponse, error) {
|
||||
obj, gvk, err := codecs.UniversalDecoder().Decode(data, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gvk.Kind != "CredentialProviderResponse" {
|
||||
return nil, fmt.Errorf("failed to decode CredentialProviderResponse, unexpected Kind: %q", gvk.Kind)
|
||||
}
|
||||
|
||||
if gvk.Group != credentialproviderapi.GroupName {
|
||||
return nil, fmt.Errorf("failed to decode CredentialProviderResponse, unexpected Group: %s", gvk.Group)
|
||||
}
|
||||
|
||||
if internalResponse, ok := obj.(*credentialproviderapi.CredentialProviderResponse); ok {
|
||||
return internalResponse, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unable to convert %T to *CredentialProviderResponse", obj)
|
||||
}
|
||||
|
||||
// parseRegistry extracts the registry hostname of an image (including port if specified).
|
||||
func parseRegistry(image string) string {
|
||||
imageParts := strings.Split(image, "/")
|
||||
return imageParts[0]
|
||||
}
|
505
pkg/credentialprovider/plugin/plugin_test.go
Normal file
505
pkg/credentialprovider/plugin/plugin_test.go
Normal file
@ -0,0 +1,505 @@
|
||||
/*
|
||||
Copyright 2020 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 (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
credentialproviderapi "k8s.io/kubelet/pkg/apis/credentialprovider"
|
||||
credentialproviderv1alpha1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
)
|
||||
|
||||
type fakeExecPlugin struct {
|
||||
cacheKeyType credentialproviderapi.PluginCacheKeyType
|
||||
cacheDuration time.Duration
|
||||
|
||||
auth map[string]credentialproviderapi.AuthConfig
|
||||
}
|
||||
|
||||
func (f *fakeExecPlugin) ExecPlugin(ctx context.Context, image string) (*credentialproviderapi.CredentialProviderResponse, error) {
|
||||
return &credentialproviderapi.CredentialProviderResponse{
|
||||
CacheKeyType: f.cacheKeyType,
|
||||
CacheDuration: &metav1.Duration{
|
||||
Duration: f.cacheDuration,
|
||||
},
|
||||
Auth: f.auth,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Test_Provide(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
pluginProvider *pluginProvider
|
||||
image string
|
||||
dockerconfig credentialprovider.DockerConfig
|
||||
}{
|
||||
{
|
||||
name: "exact image match, with Registry cache key",
|
||||
pluginProvider: &pluginProvider{
|
||||
matchImages: []string{"test.registry.io"},
|
||||
cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{}),
|
||||
plugin: &fakeExecPlugin{
|
||||
cacheKeyType: credentialproviderapi.RegistryPluginCacheKeyType,
|
||||
auth: map[string]credentialproviderapi.AuthConfig{
|
||||
"test.registry.io": {
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
image: "test.registry.io/foo/bar",
|
||||
dockerconfig: credentialprovider.DockerConfig{
|
||||
"test.registry.io": credentialprovider.DockerConfigEntry{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exact image match, with Image cache key",
|
||||
pluginProvider: &pluginProvider{
|
||||
matchImages: []string{"test.registry.io/foo/bar"},
|
||||
cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{}),
|
||||
plugin: &fakeExecPlugin{
|
||||
cacheKeyType: credentialproviderapi.ImagePluginCacheKeyType,
|
||||
auth: map[string]credentialproviderapi.AuthConfig{
|
||||
"test.registry.io/foo/bar": {
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
image: "test.registry.io/foo/bar",
|
||||
dockerconfig: credentialprovider.DockerConfig{
|
||||
"test.registry.io/foo/bar": credentialprovider.DockerConfigEntry{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exact image match, with Global cache key",
|
||||
pluginProvider: &pluginProvider{
|
||||
matchImages: []string{"test.registry.io"},
|
||||
cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{}),
|
||||
plugin: &fakeExecPlugin{
|
||||
cacheKeyType: credentialproviderapi.GlobalPluginCacheKeyType,
|
||||
auth: map[string]credentialproviderapi.AuthConfig{
|
||||
"test.registry.io": {
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
image: "test.registry.io",
|
||||
dockerconfig: credentialprovider.DockerConfig{
|
||||
"test.registry.io": credentialprovider.DockerConfigEntry{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wild card image match, with Registry cache key",
|
||||
pluginProvider: &pluginProvider{
|
||||
matchImages: []string{"*.registry.io:8080"},
|
||||
cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{}),
|
||||
plugin: &fakeExecPlugin{
|
||||
cacheKeyType: credentialproviderapi.RegistryPluginCacheKeyType,
|
||||
auth: map[string]credentialproviderapi.AuthConfig{
|
||||
"*.registry.io:8080": {
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
image: "test.registry.io:8080/foo",
|
||||
dockerconfig: credentialprovider.DockerConfig{
|
||||
"*.registry.io:8080": credentialprovider.DockerConfigEntry{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wild card image match, with Image cache key",
|
||||
pluginProvider: &pluginProvider{
|
||||
matchImages: []string{"*.*.registry.io"},
|
||||
cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{}),
|
||||
plugin: &fakeExecPlugin{
|
||||
cacheKeyType: credentialproviderapi.ImagePluginCacheKeyType,
|
||||
auth: map[string]credentialproviderapi.AuthConfig{
|
||||
"*.*.registry.io": {
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
image: "foo.bar.registry.io/foo/bar",
|
||||
dockerconfig: credentialprovider.DockerConfig{
|
||||
"*.*.registry.io": credentialprovider.DockerConfigEntry{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wild card image match, with Global cache key",
|
||||
pluginProvider: &pluginProvider{
|
||||
matchImages: []string{"*.registry.io"},
|
||||
cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{}),
|
||||
plugin: &fakeExecPlugin{
|
||||
cacheKeyType: credentialproviderapi.GlobalPluginCacheKeyType,
|
||||
auth: map[string]credentialproviderapi.AuthConfig{
|
||||
"*.registry.io": {
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
image: "test.registry.io",
|
||||
dockerconfig: credentialprovider.DockerConfig{
|
||||
"*.registry.io": credentialprovider.DockerConfigEntry{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
dockerconfig := testcase.pluginProvider.Provide(testcase.image)
|
||||
if !reflect.DeepEqual(dockerconfig, testcase.dockerconfig) {
|
||||
t.Logf("actual docker config: %v", dockerconfig)
|
||||
t.Logf("expected docker config: %v", testcase.dockerconfig)
|
||||
t.Error("unexpected docker config")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_encodeRequest(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
apiVersion string
|
||||
request *credentialproviderapi.CredentialProviderRequest
|
||||
expectedData []byte
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "successful",
|
||||
request: &credentialproviderapi.CredentialProviderRequest{
|
||||
Image: "test.registry.io/foobar",
|
||||
},
|
||||
expectedData: []byte(`{"kind":"CredentialProviderRequest","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","image":"test.registry.io/foobar"}
|
||||
`),
|
||||
expectedErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
mediaType := "application/json"
|
||||
info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType)
|
||||
if !ok {
|
||||
t.Fatalf("unsupported media type: %s", mediaType)
|
||||
}
|
||||
|
||||
e := &execPlugin{
|
||||
encoder: codecs.EncoderForVersion(info.Serializer, credentialproviderv1alpha1.SchemeGroupVersion),
|
||||
}
|
||||
|
||||
data, err := e.encodeRequest(testcase.request)
|
||||
if err != nil && !testcase.expectedErr {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if err == nil && testcase.expectedErr {
|
||||
t.Fatalf("expected error %v but got nil", testcase.expectedErr)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(data, testcase.expectedData) {
|
||||
t.Errorf("actual encoded data: %v", string(data))
|
||||
t.Errorf("expected encoded data: %v", string(testcase.expectedData))
|
||||
t.Errorf("unexpected encoded response")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_decodeResponse(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
expectedResponse *credentialproviderapi.CredentialProviderResponse
|
||||
expectedErr bool
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
data: []byte(`{"kind":"CredentialProviderResponse","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","cacheKeyType":"Registry","cacheDuration":"1m","auth":{"*.registry.io":{"username":"user","password":"password"}}}`),
|
||||
expectedResponse: &credentialproviderapi.CredentialProviderResponse{
|
||||
CacheKeyType: credentialproviderapi.RegistryPluginCacheKeyType,
|
||||
CacheDuration: &metav1.Duration{
|
||||
Duration: time.Minute,
|
||||
},
|
||||
Auth: map[string]credentialproviderapi.AuthConfig{
|
||||
"*.registry.io": {
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: false,
|
||||
},
|
||||
{
|
||||
name: "wrong Kind",
|
||||
data: []byte(`{"kind":"WrongKind","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","cacheKeyType":"Registry","cacheDuration":"1m","auth":{"*.registry.io":{"username":"user","password":"password"}}}`),
|
||||
expectedResponse: nil,
|
||||
expectedErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong Group",
|
||||
data: []byte(`{"kind":"CredentialProviderResponse","apiVersion":"foobar.kubelet.k8s.io/v1alpha1","cacheKeyType":"Registry","cacheDuration":"1m","auth":{"*.registry.io":{"username":"user","password":"password"}}}`),
|
||||
expectedResponse: nil,
|
||||
expectedErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
e := &execPlugin{}
|
||||
|
||||
decodedResponse, err := e.decodeResponse(testcase.data)
|
||||
if err != nil && !testcase.expectedErr {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if err == nil && testcase.expectedErr {
|
||||
t.Fatalf("expected error %v but not nil", testcase.expectedErr)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(decodedResponse, testcase.expectedResponse) {
|
||||
t.Logf("actual decoded response: %#v", decodedResponse)
|
||||
t.Logf("expected decoded response: %#v", testcase.expectedResponse)
|
||||
t.Errorf("unexpected decoded response")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_RegistryCacheKeyType(t *testing.T) {
|
||||
pluginProvider := &pluginProvider{
|
||||
matchImages: []string{"*.registry.io"},
|
||||
cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{}),
|
||||
plugin: &fakeExecPlugin{
|
||||
cacheKeyType: credentialproviderapi.RegistryPluginCacheKeyType,
|
||||
cacheDuration: time.Hour,
|
||||
auth: map[string]credentialproviderapi.AuthConfig{
|
||||
"*.registry.io": {
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedDockerConfig := credentialprovider.DockerConfig{
|
||||
"*.registry.io": credentialprovider.DockerConfigEntry{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
}
|
||||
|
||||
dockerConfig := pluginProvider.Provide("test.registry.io/foo/bar")
|
||||
if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) {
|
||||
t.Logf("actual docker config: %v", dockerConfig)
|
||||
t.Logf("expected docker config: %v", expectedDockerConfig)
|
||||
t.Fatal("unexpected docker config")
|
||||
}
|
||||
|
||||
expectedCacheKeys := []string{"test.registry.io"}
|
||||
cacheKeys := pluginProvider.cache.ListKeys()
|
||||
|
||||
if !reflect.DeepEqual(cacheKeys, expectedCacheKeys) {
|
||||
t.Logf("actual cache keys: %v", cacheKeys)
|
||||
t.Logf("expected cache keys: %v", expectedCacheKeys)
|
||||
t.Error("unexpected cache keys")
|
||||
}
|
||||
|
||||
// nil out the exec plugin, this will test whether credentialproviderapi are fetched
|
||||
// from cache, otherwise Provider should panic
|
||||
pluginProvider.plugin = nil
|
||||
dockerConfig = pluginProvider.Provide("test.registry.io/foo/bar")
|
||||
if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) {
|
||||
t.Logf("actual docker config: %v", dockerConfig)
|
||||
t.Logf("expected docker config: %v", expectedDockerConfig)
|
||||
t.Fatal("unexpected docker config")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ImageCacheKeyType(t *testing.T) {
|
||||
pluginProvider := &pluginProvider{
|
||||
matchImages: []string{"*.registry.io"},
|
||||
cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{}),
|
||||
plugin: &fakeExecPlugin{
|
||||
cacheKeyType: credentialproviderapi.ImagePluginCacheKeyType,
|
||||
cacheDuration: time.Hour,
|
||||
auth: map[string]credentialproviderapi.AuthConfig{
|
||||
"*.registry.io": {
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedDockerConfig := credentialprovider.DockerConfig{
|
||||
"*.registry.io": credentialprovider.DockerConfigEntry{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
}
|
||||
|
||||
dockerConfig := pluginProvider.Provide("test.registry.io/foo/bar")
|
||||
if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) {
|
||||
t.Logf("actual docker config: %v", dockerConfig)
|
||||
t.Logf("expected docker config: %v", expectedDockerConfig)
|
||||
t.Fatal("unexpected docker config")
|
||||
}
|
||||
|
||||
expectedCacheKeys := []string{"test.registry.io/foo/bar"}
|
||||
cacheKeys := pluginProvider.cache.ListKeys()
|
||||
|
||||
if !reflect.DeepEqual(cacheKeys, expectedCacheKeys) {
|
||||
t.Logf("actual cache keys: %v", cacheKeys)
|
||||
t.Logf("expected cache keys: %v", expectedCacheKeys)
|
||||
t.Error("unexpected cache keys")
|
||||
}
|
||||
|
||||
// nil out the exec plugin, this will test whether credentialproviderapi are fetched
|
||||
// from cache, otherwise Provider should panic
|
||||
pluginProvider.plugin = nil
|
||||
dockerConfig = pluginProvider.Provide("test.registry.io/foo/bar")
|
||||
if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) {
|
||||
t.Logf("actual docker config: %v", dockerConfig)
|
||||
t.Logf("expected docker config: %v", expectedDockerConfig)
|
||||
t.Fatal("unexpected docker config")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GlobalCacheKeyType(t *testing.T) {
|
||||
pluginProvider := &pluginProvider{
|
||||
matchImages: []string{"*.registry.io"},
|
||||
cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{}),
|
||||
plugin: &fakeExecPlugin{
|
||||
cacheKeyType: credentialproviderapi.GlobalPluginCacheKeyType,
|
||||
cacheDuration: time.Hour,
|
||||
auth: map[string]credentialproviderapi.AuthConfig{
|
||||
"*.registry.io": {
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedDockerConfig := credentialprovider.DockerConfig{
|
||||
"*.registry.io": credentialprovider.DockerConfigEntry{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
}
|
||||
|
||||
dockerConfig := pluginProvider.Provide("test.registry.io/foo/bar")
|
||||
if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) {
|
||||
t.Logf("actual docker config: %v", dockerConfig)
|
||||
t.Logf("expected docker config: %v", expectedDockerConfig)
|
||||
t.Fatal("unexpected docker config")
|
||||
}
|
||||
|
||||
expectedCacheKeys := []string{"global"}
|
||||
cacheKeys := pluginProvider.cache.ListKeys()
|
||||
|
||||
if !reflect.DeepEqual(cacheKeys, expectedCacheKeys) {
|
||||
t.Logf("actual cache keys: %v", cacheKeys)
|
||||
t.Logf("expected cache keys: %v", expectedCacheKeys)
|
||||
t.Error("unexpected cache keys")
|
||||
}
|
||||
|
||||
// nil out the exec plugin, this will test whether credentialproviderapi are fetched
|
||||
// from cache, otherwise Provider should panic
|
||||
pluginProvider.plugin = nil
|
||||
dockerConfig = pluginProvider.Provide("test.registry.io/foo/bar")
|
||||
if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) {
|
||||
t.Logf("actual docker config: %v", dockerConfig)
|
||||
t.Logf("expected docker config: %v", expectedDockerConfig)
|
||||
t.Fatal("unexpected docker config")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NoCacheResponse(t *testing.T) {
|
||||
pluginProvider := &pluginProvider{
|
||||
matchImages: []string{"*.registry.io"},
|
||||
cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{}),
|
||||
plugin: &fakeExecPlugin{
|
||||
cacheKeyType: credentialproviderapi.GlobalPluginCacheKeyType,
|
||||
cacheDuration: 0, // no cache
|
||||
auth: map[string]credentialproviderapi.AuthConfig{
|
||||
"*.registry.io": {
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedDockerConfig := credentialprovider.DockerConfig{
|
||||
"*.registry.io": credentialprovider.DockerConfigEntry{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
},
|
||||
}
|
||||
|
||||
dockerConfig := pluginProvider.Provide("test.registry.io/foo/bar")
|
||||
if !reflect.DeepEqual(dockerConfig, expectedDockerConfig) {
|
||||
t.Logf("actual docker config: %v", dockerConfig)
|
||||
t.Logf("expected docker config: %v", expectedDockerConfig)
|
||||
t.Fatal("unexpected docker config")
|
||||
}
|
||||
|
||||
expectedCacheKeys := []string{}
|
||||
cacheKeys := pluginProvider.cache.ListKeys()
|
||||
if !reflect.DeepEqual(cacheKeys, expectedCacheKeys) {
|
||||
t.Logf("actual cache keys: %v", cacheKeys)
|
||||
t.Logf("expected cache keys: %v", expectedCacheKeys)
|
||||
t.Error("unexpected cache keys")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user