mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 23:37:01 +00:00
Merge pull request #128372 from aramase/aramase/f/kep_4412_alpha_impl
KSA token for Kubelet image credential providers alpha
This commit is contained in:
commit
e0ab1a16ad
@ -22,7 +22,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
credentialproviderv1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
)
|
||||
@ -70,7 +72,7 @@ func decode(data []byte) (*kubeletconfig.CredentialProviderConfig, error) {
|
||||
}
|
||||
|
||||
// validateCredentialProviderConfig validates CredentialProviderConfig.
|
||||
func validateCredentialProviderConfig(config *kubeletconfig.CredentialProviderConfig) field.ErrorList {
|
||||
func validateCredentialProviderConfig(config *kubeletconfig.CredentialProviderConfig, saTokenForCredentialProviders bool) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
if len(config.Providers) == 0 {
|
||||
@ -125,7 +127,56 @@ func validateCredentialProviderConfig(config *kubeletconfig.CredentialProviderCo
|
||||
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"))
|
||||
}
|
||||
|
||||
if provider.TokenAttributes != nil {
|
||||
fldPath := fieldPath.Child("tokenAttributes")
|
||||
if !saTokenForCredentialProviders {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath, "tokenAttributes is not supported when KubeletServiceAccountTokenForCredentialProviders feature gate is disabled"))
|
||||
}
|
||||
if len(provider.TokenAttributes.ServiceAccountTokenAudience) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("serviceAccountTokenAudience"), "serviceAccountTokenAudience is required"))
|
||||
}
|
||||
if provider.TokenAttributes.RequireServiceAccount == nil {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("requireServiceAccount"), "requireServiceAccount is required"))
|
||||
}
|
||||
if provider.APIVersion != credentialproviderv1.SchemeGroupVersion.String() {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath, fmt.Sprintf("tokenAttributes is only supported for %s API version", credentialproviderv1.SchemeGroupVersion.String())))
|
||||
}
|
||||
|
||||
if provider.TokenAttributes.RequireServiceAccount != nil && !*provider.TokenAttributes.RequireServiceAccount && len(provider.TokenAttributes.RequiredServiceAccountAnnotationKeys) > 0 {
|
||||
allErrs = append(allErrs, field.Forbidden(fldPath.Child("requiredServiceAccountAnnotationKeys"), "requireServiceAccount cannot be false when requiredServiceAccountAnnotationKeys is set"))
|
||||
}
|
||||
|
||||
allErrs = append(allErrs, validateServiceAccountAnnotationKeys(fldPath.Child("requiredServiceAccountAnnotationKeys"), provider.TokenAttributes.RequiredServiceAccountAnnotationKeys)...)
|
||||
allErrs = append(allErrs, validateServiceAccountAnnotationKeys(fldPath.Child("optionalServiceAccountAnnotationKeys"), provider.TokenAttributes.OptionalServiceAccountAnnotationKeys)...)
|
||||
|
||||
requiredServiceAccountAnnotationKeys := sets.New[string](provider.TokenAttributes.RequiredServiceAccountAnnotationKeys...)
|
||||
optionalServiceAccountAnnotationKeys := sets.New[string](provider.TokenAttributes.OptionalServiceAccountAnnotationKeys...)
|
||||
duplicateAnnotationKeys := requiredServiceAccountAnnotationKeys.Intersection(optionalServiceAccountAnnotationKeys)
|
||||
if duplicateAnnotationKeys.Len() > 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, sets.List(duplicateAnnotationKeys), "annotation keys cannot be both required and optional"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// validateServiceAccountAnnotationKeys validates the service account annotation keys.
|
||||
func validateServiceAccountAnnotationKeys(fldPath *field.Path, keys []string) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
|
||||
seenAnnotationKeys := sets.New[string]()
|
||||
// Using the validation logic for keys from https://github.com/kubernetes/kubernetes/blob/69dbc74417304328a9fd3c161643dc4f0a057f41/staging/src/k8s.io/apimachinery/pkg/api/validation/objectmeta.go#L46-L51
|
||||
for _, k := range keys {
|
||||
// The rule is QualifiedName except that case doesn't matter, so convert to lowercase before checking.
|
||||
for _, msg := range validation.IsQualifiedName(strings.ToLower(k)) {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, k, msg))
|
||||
}
|
||||
if seenAnnotationKeys.Has(k) {
|
||||
allErrs = append(allErrs, field.Duplicate(fldPath, k))
|
||||
}
|
||||
seenAnnotationKeys.Insert(k)
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
@ -19,16 +19,17 @@ package plugin
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/errors"
|
||||
utiltesting "k8s.io/client-go/util/testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func Test_readCredentialProviderConfigFile(t *testing.T) {
|
||||
@ -337,6 +338,48 @@ providers:
|
||||
config: nil,
|
||||
expectErr: `strict decoding error: unknown field "providers[0].unknownField"`,
|
||||
},
|
||||
{
|
||||
name: "v1alpha1 config with token attributes should fail",
|
||||
configData: `---
|
||||
kind: CredentialProviderConfig
|
||||
apiVersion: kubelet.config.k8s.io/v1alpha1
|
||||
providers:
|
||||
- name: test
|
||||
matchImages:
|
||||
- "registry.io/foobar"
|
||||
defaultCacheDuration: 10m
|
||||
apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1
|
||||
tokenAttributes:
|
||||
serviceAccountTokenAudience: audience
|
||||
args:
|
||||
- --v=5
|
||||
env:
|
||||
- name: FOO
|
||||
value: BAR`,
|
||||
config: nil,
|
||||
expectErr: `strict decoding error: unknown field "providers[0].tokenAttributes"`,
|
||||
},
|
||||
{
|
||||
name: "v1beta1 config with token attributes should fail",
|
||||
configData: `---
|
||||
kind: CredentialProviderConfig
|
||||
apiVersion: kubelet.config.k8s.io/v1beta1
|
||||
providers:
|
||||
- name: test
|
||||
matchImages:
|
||||
- "registry.io/foobar"
|
||||
defaultCacheDuration: 10m
|
||||
apiVersion: credentialprovider.kubelet.k8s.io/v1beta1
|
||||
tokenAttributes:
|
||||
serviceAccountTokenAudience: audience
|
||||
args:
|
||||
- --v=5
|
||||
env:
|
||||
- name: FOO
|
||||
value: BAR`,
|
||||
config: nil,
|
||||
expectErr: `strict decoding error: unknown field "providers[0].tokenAttributes"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
@ -347,17 +390,19 @@ providers:
|
||||
}
|
||||
defer utiltesting.CloseAndRemove(t, file)
|
||||
|
||||
_, err = file.WriteString(testcase.configData)
|
||||
if err != nil {
|
||||
if _, err = file.WriteString(testcase.configData); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
authConfig, err := readCredentialProviderConfigFile(file.Name())
|
||||
if err != nil && len(testcase.expectErr) == 0 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err == nil && len(testcase.expectErr) > 0 {
|
||||
if err != nil {
|
||||
if len(testcase.expectErr) == 0 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), testcase.expectErr) {
|
||||
t.Fatalf("expected error %q but got %q", testcase.expectErr, err.Error())
|
||||
}
|
||||
} else if len(testcase.expectErr) > 0 {
|
||||
t.Fatalf("expected error %q but got none", testcase.expectErr)
|
||||
}
|
||||
|
||||
@ -554,11 +599,248 @@ func Test_validateCredentialProviderConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "token attributes set without KubeletServiceAccountTokenForCredentialProviders feature gate enabled",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1",
|
||||
TokenAttributes: &kubeletconfig.ServiceAccountTokenAttributes{
|
||||
ServiceAccountTokenAudience: "audience",
|
||||
RequireServiceAccount: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectErr: `providers.tokenAttributes: Forbidden: tokenAttributes is not supported when KubeletServiceAccountTokenForCredentialProviders feature gate is disabled`,
|
||||
},
|
||||
{
|
||||
name: "token attributes not nil but empty ServiceAccountTokenAudience",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1",
|
||||
TokenAttributes: &kubeletconfig.ServiceAccountTokenAttributes{
|
||||
RequiredServiceAccountAnnotationKeys: []string{"prefix.io/annotation-1", "prefix.io/annotation-2"},
|
||||
RequireServiceAccount: ptr.To(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
saTokenForCredentialProviders: true,
|
||||
expectErr: `providers.tokenAttributes.serviceAccountTokenAudience: Required value: serviceAccountTokenAudience is required`,
|
||||
},
|
||||
{
|
||||
name: "token attributes not nil but empty ServiceAccountTokenRequired",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1",
|
||||
TokenAttributes: &kubeletconfig.ServiceAccountTokenAttributes{
|
||||
ServiceAccountTokenAudience: "audience",
|
||||
RequiredServiceAccountAnnotationKeys: []string{"prefix.io/annotation-1", "prefix.io/annotation-2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
saTokenForCredentialProviders: true,
|
||||
expectErr: `providers.tokenAttributes.requireServiceAccount: Required value: requireServiceAccount is required`,
|
||||
},
|
||||
{
|
||||
name: "required service account annotation keys not qualified name (same validation as metav1.ObjectMeta)",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1",
|
||||
TokenAttributes: &kubeletconfig.ServiceAccountTokenAttributes{
|
||||
ServiceAccountTokenAudience: "audience",
|
||||
RequireServiceAccount: ptr.To(true),
|
||||
RequiredServiceAccountAnnotationKeys: []string{"cantendwithadash-", "now-with-dashes/simple"}, // first key is invalid
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
saTokenForCredentialProviders: true,
|
||||
expectErr: `providers.tokenAttributes.requiredServiceAccountAnnotationKeys: Invalid value: "cantendwithadash-": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`,
|
||||
},
|
||||
{
|
||||
name: "optional service account annotation keys not qualified name (same validation as metav1.ObjectMeta)",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1",
|
||||
TokenAttributes: &kubeletconfig.ServiceAccountTokenAttributes{
|
||||
ServiceAccountTokenAudience: "audience",
|
||||
RequireServiceAccount: ptr.To(true),
|
||||
OptionalServiceAccountAnnotationKeys: []string{"cantendwithadash-", "now-with-dashes/simple"}, // first key is invalid
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
saTokenForCredentialProviders: true,
|
||||
expectErr: `providers.tokenAttributes.optionalServiceAccountAnnotationKeys: Invalid value: "cantendwithadash-": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`,
|
||||
},
|
||||
{
|
||||
name: "duplicate required service account annotation keys",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1",
|
||||
TokenAttributes: &kubeletconfig.ServiceAccountTokenAttributes{
|
||||
ServiceAccountTokenAudience: "audience",
|
||||
RequireServiceAccount: ptr.To(true),
|
||||
RequiredServiceAccountAnnotationKeys: []string{"now-with-dashes/simple", "now-with-dashes/simple"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
saTokenForCredentialProviders: true,
|
||||
expectErr: `providers.tokenAttributes.requiredServiceAccountAnnotationKeys: Duplicate value: "now-with-dashes/simple"`,
|
||||
},
|
||||
{
|
||||
name: "duplicate optional service account annotation keys",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1",
|
||||
TokenAttributes: &kubeletconfig.ServiceAccountTokenAttributes{
|
||||
ServiceAccountTokenAudience: "audience",
|
||||
RequireServiceAccount: ptr.To(true),
|
||||
OptionalServiceAccountAnnotationKeys: []string{"now-with-dashes/simple", "now-with-dashes/simple"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
saTokenForCredentialProviders: true,
|
||||
expectErr: `providers.tokenAttributes.optionalServiceAccountAnnotationKeys: Duplicate value: "now-with-dashes/simple"`,
|
||||
},
|
||||
{
|
||||
name: "annotation key in required and optional keys",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1",
|
||||
TokenAttributes: &kubeletconfig.ServiceAccountTokenAttributes{
|
||||
ServiceAccountTokenAudience: "audience",
|
||||
RequireServiceAccount: ptr.To(true),
|
||||
RequiredServiceAccountAnnotationKeys: []string{"now-with-dashes/simple-1", "now-with-dashes/simple-2"},
|
||||
OptionalServiceAccountAnnotationKeys: []string{"now-with-dashes/simple-2", "now-with-dashes/simple-3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
saTokenForCredentialProviders: true,
|
||||
expectErr: `providers.tokenAttributes: Invalid value: []string{"now-with-dashes/simple-2"}: annotation keys cannot be both required and optional`,
|
||||
},
|
||||
{
|
||||
name: "required annotation keys set when requireServiceAccount is false",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1",
|
||||
TokenAttributes: &kubeletconfig.ServiceAccountTokenAttributes{
|
||||
ServiceAccountTokenAudience: "audience",
|
||||
RequireServiceAccount: ptr.To(false),
|
||||
RequiredServiceAccountAnnotationKeys: []string{"now-with-dashes/simple-1", "now-with-dashes/simple-2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
saTokenForCredentialProviders: true,
|
||||
expectErr: `providers.tokenAttributes.requiredServiceAccountAnnotationKeys: Forbidden: requireServiceAccount cannot be false when requiredServiceAccountAnnotationKeys is set`,
|
||||
},
|
||||
{
|
||||
name: "valid config with KubeletServiceAccountTokenForCredentialProviders feature gate enabled",
|
||||
config: &kubeletconfig.CredentialProviderConfig{
|
||||
Providers: []kubeletconfig.CredentialProvider{
|
||||
{
|
||||
Name: "foobar",
|
||||
MatchImages: []string{"foobar.registry.io"},
|
||||
DefaultCacheDuration: &metav1.Duration{Duration: time.Minute},
|
||||
APIVersion: "credentialprovider.kubelet.k8s.io/v1",
|
||||
TokenAttributes: &kubeletconfig.ServiceAccountTokenAttributes{
|
||||
ServiceAccountTokenAudience: "audience",
|
||||
RequireServiceAccount: ptr.To(true),
|
||||
RequiredServiceAccountAnnotationKeys: []string{"now-with-dashes/simple-1", "now-with-dashes/simple-2"},
|
||||
OptionalServiceAccountAnnotationKeys: []string{"now-with-dashes/simple-3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
saTokenForCredentialProviders: true,
|
||||
},
|
||||
{
|
||||
name: "tokenAttributes set with credentialprovider.kubelet.k8s.io/v1alpha1 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/v1alpha1",
|
||||
TokenAttributes: &kubeletconfig.ServiceAccountTokenAttributes{
|
||||
ServiceAccountTokenAudience: "audience",
|
||||
RequireServiceAccount: ptr.To(true),
|
||||
RequiredServiceAccountAnnotationKeys: []string{"now-with-dashes/simple"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
saTokenForCredentialProviders: true,
|
||||
expectErr: `providers.tokenAttributes: Forbidden: tokenAttributes is only supported for credentialprovider.kubelet.k8s.io/v1 API version`,
|
||||
},
|
||||
{
|
||||
name: "tokenAttributes set with credentialprovider.kubelet.k8s.io/v1beta1 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/v1beta1",
|
||||
TokenAttributes: &kubeletconfig.ServiceAccountTokenAttributes{
|
||||
ServiceAccountTokenAudience: "audience",
|
||||
RequireServiceAccount: ptr.To(true),
|
||||
RequiredServiceAccountAnnotationKeys: []string{"now-with-dashes/simple"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
saTokenForCredentialProviders: true,
|
||||
expectErr: `providers.tokenAttributes: Forbidden: tokenAttributes is only supported for credentialprovider.kubelet.k8s.io/v1 API version`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
t.Run(testcase.name, func(t *testing.T) {
|
||||
errs := validateCredentialProviderConfig(testcase.config).ToAggregate()
|
||||
errs := validateCredentialProviderConfig(testcase.config, testcase.saTokenForCredentialProviders).ToAggregate()
|
||||
if d := cmp.Diff(testcase.expectErr, errString(errs)); d != "" {
|
||||
t.Fatalf("CredentialProviderConfig validation mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package plugin
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@ -28,12 +29,18 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"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/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/klog/v2"
|
||||
credentialproviderapi "k8s.io/kubelet/pkg/apis/credentialprovider"
|
||||
@ -42,6 +49,7 @@ import (
|
||||
credentialproviderv1alpha1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1"
|
||||
credentialproviderv1beta1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1beta1"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
kubeletconfigv1 "k8s.io/kubernetes/pkg/kubelet/apis/config/v1"
|
||||
kubeletconfigv1alpha1 "k8s.io/kubernetes/pkg/kubelet/apis/config/v1alpha1"
|
||||
@ -75,7 +83,10 @@ func init() {
|
||||
|
||||
// RegisterCredentialProviderPlugins is called from kubelet to register external credential provider
|
||||
// plugins according to the CredentialProviderConfig config file.
|
||||
func RegisterCredentialProviderPlugins(pluginConfigFile, pluginBinDir string) error {
|
||||
func RegisterCredentialProviderPlugins(pluginConfigFile, pluginBinDir string,
|
||||
getServiceAccountToken func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error),
|
||||
getServiceAccount func(namespace, name string) (*v1.ServiceAccount, error),
|
||||
) error {
|
||||
if _, err := os.Stat(pluginBinDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("plugin binary directory %s did not exist", pluginBinDir)
|
||||
@ -89,8 +100,8 @@ func RegisterCredentialProviderPlugins(pluginConfigFile, pluginBinDir string) er
|
||||
return err
|
||||
}
|
||||
|
||||
errs := validateCredentialProviderConfig(credentialProviderConfig)
|
||||
if len(errs) > 0 {
|
||||
saTokenForCredentialProvidersFeatureEnabled := utilfeature.DefaultFeatureGate.Enabled(features.KubeletServiceAccountTokenForCredentialProviders)
|
||||
if errs := validateCredentialProviderConfig(credentialProviderConfig, saTokenForCredentialProvidersFeatureEnabled); len(errs) > 0 {
|
||||
return fmt.Errorf("failed to validate credential provider config: %v", errs.ToAggregate())
|
||||
}
|
||||
|
||||
@ -109,19 +120,22 @@ func RegisterCredentialProviderPlugins(pluginConfigFile, pluginBinDir string) er
|
||||
return fmt.Errorf("error inspecting binary executable %s: %w", pluginBin, err)
|
||||
}
|
||||
|
||||
plugin, err := newPluginProvider(pluginBinDir, provider)
|
||||
plugin, err := newPluginProvider(pluginBinDir, provider, getServiceAccountToken, getServiceAccount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error initializing plugin provider %s: %w", provider.Name, err)
|
||||
}
|
||||
|
||||
credentialprovider.RegisterCredentialProvider(provider.Name, plugin)
|
||||
registerCredentialProviderPlugin(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) {
|
||||
func newPluginProvider(pluginBinDir string, provider kubeletconfig.CredentialProvider,
|
||||
getServiceAccountToken func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error),
|
||||
getServiceAccount func(namespace, name string) (*v1.ServiceAccount, error),
|
||||
) (*pluginProvider, error) {
|
||||
mediaType := "application/json"
|
||||
info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType)
|
||||
if !ok {
|
||||
@ -134,7 +148,6 @@ func newPluginProvider(pluginBinDir string, provider kubeletconfig.CredentialPro
|
||||
}
|
||||
|
||||
clock := clock.RealClock{}
|
||||
|
||||
return &pluginProvider{
|
||||
clock: clock,
|
||||
matchImages: provider.MatchImages,
|
||||
@ -150,6 +163,7 @@ func newPluginProvider(pluginBinDir string, provider kubeletconfig.CredentialPro
|
||||
envVars: provider.Env,
|
||||
environ: os.Environ,
|
||||
},
|
||||
serviceAccountProvider: newServiceAccountProvider(provider, getServiceAccount, getServiceAccountToken),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -178,6 +192,101 @@ type pluginProvider struct {
|
||||
|
||||
// lastCachePurge is the last time cache is cleaned for expired entries.
|
||||
lastCachePurge time.Time
|
||||
|
||||
// serviceAccountProvider holds the logic for handling service account tokens when needed.
|
||||
serviceAccountProvider *serviceAccountProvider
|
||||
}
|
||||
|
||||
type serviceAccountProvider struct {
|
||||
audience string
|
||||
requireServiceAccount bool
|
||||
getServiceAccountFunc func(namespace, name string) (*v1.ServiceAccount, error)
|
||||
getServiceAccountTokenFunc func(podNamespace, serviceAccountName string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error)
|
||||
requiredServiceAccountAnnotationKeys []string
|
||||
optionalServiceAccountAnnotationKeys []string
|
||||
}
|
||||
|
||||
func newServiceAccountProvider(
|
||||
provider kubeletconfig.CredentialProvider,
|
||||
getServiceAccount func(namespace, name string) (*v1.ServiceAccount, error),
|
||||
getServiceAccountToken func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error),
|
||||
) *serviceAccountProvider {
|
||||
featureGateEnabled := utilfeature.DefaultFeatureGate.Enabled(features.KubeletServiceAccountTokenForCredentialProviders)
|
||||
serviceAccountTokenAudienceSet := provider.TokenAttributes != nil && len(provider.TokenAttributes.ServiceAccountTokenAudience) > 0
|
||||
|
||||
if !featureGateEnabled || !serviceAccountTokenAudienceSet {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &serviceAccountProvider{
|
||||
audience: provider.TokenAttributes.ServiceAccountTokenAudience,
|
||||
requireServiceAccount: *provider.TokenAttributes.RequireServiceAccount,
|
||||
getServiceAccountFunc: getServiceAccount,
|
||||
getServiceAccountTokenFunc: getServiceAccountToken,
|
||||
requiredServiceAccountAnnotationKeys: provider.TokenAttributes.RequiredServiceAccountAnnotationKeys,
|
||||
optionalServiceAccountAnnotationKeys: provider.TokenAttributes.OptionalServiceAccountAnnotationKeys,
|
||||
}
|
||||
}
|
||||
|
||||
type requiredAnnotationNotFoundError string
|
||||
|
||||
func (e requiredAnnotationNotFoundError) Error() string {
|
||||
return fmt.Sprintf("required annotation %s not found", string(e))
|
||||
}
|
||||
|
||||
// getServiceAccountData returns the service account UID and required annotations for the service account.
|
||||
// If the service account does not exist, an error is returned.
|
||||
// saAnnotations is a map of annotation keys and values that the plugin requires to generate credentials
|
||||
// that's defined in the tokenAttributes in the credential provider config.
|
||||
// requiredServiceAccountAnnotationKeys are the keys that are required to be present in the service account.
|
||||
// If any of the keys defined in this list are not present in the service account, kubelet will not invoke the plugin
|
||||
// and will return an error.
|
||||
// optionalServiceAccountAnnotationKeys are the keys that are optional to be present in the service account.
|
||||
// If present, they will be added to the saAnnotations map.
|
||||
func (s *serviceAccountProvider) getServiceAccountData(namespace, name string) (types.UID, map[string]string, error) {
|
||||
sa, err := s.getServiceAccountFunc(namespace, name)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
saAnnotations := make(map[string]string, len(s.requiredServiceAccountAnnotationKeys)+len(s.optionalServiceAccountAnnotationKeys))
|
||||
for _, k := range s.requiredServiceAccountAnnotationKeys {
|
||||
val, ok := sa.Annotations[k]
|
||||
if !ok {
|
||||
return "", nil, requiredAnnotationNotFoundError(k)
|
||||
}
|
||||
saAnnotations[k] = val
|
||||
}
|
||||
|
||||
for _, k := range s.optionalServiceAccountAnnotationKeys {
|
||||
if val, ok := sa.Annotations[k]; ok {
|
||||
saAnnotations[k] = val
|
||||
}
|
||||
}
|
||||
|
||||
return sa.UID, saAnnotations, nil
|
||||
}
|
||||
|
||||
// getServiceAccountToken returns a service account token for the service account.
|
||||
func (s *serviceAccountProvider) getServiceAccountToken(podNamespace, podName, serviceAccountName string, podUID types.UID) (string, error) {
|
||||
tr, err := s.getServiceAccountTokenFunc(podNamespace, serviceAccountName, &authenticationv1.TokenRequest{
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
Audiences: []string{s.audience},
|
||||
// expirationSeconds is not set explicitly here. It has the same default value of "ExpirationSeconds" in the TokenRequestSpec.
|
||||
BoundObjectRef: &authenticationv1.BoundObjectReference{
|
||||
APIVersion: "v1",
|
||||
Kind: "Pod",
|
||||
Name: podName,
|
||||
UID: podUID,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tr.Status.Token, nil
|
||||
}
|
||||
|
||||
// cacheEntry is the cache object that will be stored in cache.Store.
|
||||
@ -204,15 +313,86 @@ func (c *cacheExpirationPolicy) IsExpired(entry *cache.TimestampedEntry) bool {
|
||||
return c.clock.Now().After(entry.Obj.(*cacheEntry).expiresAt)
|
||||
}
|
||||
|
||||
// Provide returns a credentialprovider.DockerConfig based on the credentials returned
|
||||
// perPluginProvider holds the shared pluginProvider and the per-request information
|
||||
// like podName, podNamespace, podUID and serviceAccountName.
|
||||
// This is used to provide the per-request information to the pluginProvider.provide method, so
|
||||
// that the plugin can use this information to get the pod's service account and generate bound service account tokens
|
||||
// for plugins running in service account token mode.
|
||||
type perPodPluginProvider struct {
|
||||
name string
|
||||
|
||||
provider *pluginProvider
|
||||
|
||||
podNamespace string
|
||||
podName string
|
||||
podUID types.UID
|
||||
|
||||
serviceAccountName string
|
||||
}
|
||||
|
||||
// Enabled always returns true since registration of the plugin via kubelet implies it should be enabled.
|
||||
func (p *perPodPluginProvider) Enabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *perPodPluginProvider) Provide(image string) credentialprovider.DockerConfig {
|
||||
return p.provider.provide(image, p.podNamespace, p.podName, p.podUID, p.serviceAccountName)
|
||||
}
|
||||
|
||||
// provide returns a credentialprovider.DockerConfig based on the credentials returned
|
||||
// from cache or the exec plugin.
|
||||
func (p *pluginProvider) Provide(image string) credentialprovider.DockerConfig {
|
||||
func (p *pluginProvider) provide(image, podNamespace, podName string, podUID types.UID, serviceAccountName string) credentialprovider.DockerConfig {
|
||||
if !p.isImageAllowed(image) {
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
|
||||
cachedConfig, found, err := p.getCachedCredentials(image)
|
||||
if err != nil {
|
||||
var serviceAccountUID types.UID
|
||||
var serviceAccountToken string
|
||||
var saAnnotations map[string]string
|
||||
var err error
|
||||
var serviceAccountCacheKey string
|
||||
|
||||
if p.serviceAccountProvider != nil {
|
||||
if len(serviceAccountName) == 0 && p.serviceAccountProvider.requireServiceAccount {
|
||||
klog.V(5).Infof("Service account name is empty for pod %s/%s", podNamespace, podName)
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
|
||||
// If the service account name is empty and the plugin has indicated that invoking the plugin
|
||||
// without a service account is allowed, we will continue without generating a service account token.
|
||||
// This is useful for plugins that are running in service account token mode and are also used
|
||||
// to pull images for pods without service accounts (e.g., static pods).
|
||||
if len(serviceAccountName) > 0 {
|
||||
if serviceAccountUID, saAnnotations, err = p.serviceAccountProvider.getServiceAccountData(podNamespace, serviceAccountName); err != nil {
|
||||
var requiredAnnotationNotFoundErr requiredAnnotationNotFoundError
|
||||
if errors.As(err, &requiredAnnotationNotFoundErr) {
|
||||
// The required annotation could be a mechanism for individual workloads to opt in to using service account tokens
|
||||
// for image pull. If any of the required annotation is missing, we will not invoke the plugin. We will log the error
|
||||
// at higher verbosity level as it could be noisy.
|
||||
klog.V(5).Infof("Failed to get service account data %s/%s: %v", podNamespace, serviceAccountName, err)
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
|
||||
klog.Errorf("Failed to get service account %s/%s: %v", podNamespace, serviceAccountName, err)
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
|
||||
if serviceAccountToken, err = p.serviceAccountProvider.getServiceAccountToken(podNamespace, podName, serviceAccountName, podUID); err != nil {
|
||||
klog.Errorf("Error getting service account token %s/%s: %v", podNamespace, serviceAccountName, err)
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
|
||||
serviceAccountCacheKey, err = generateServiceAccountCacheKey(podNamespace, serviceAccountName, serviceAccountUID, saAnnotations)
|
||||
if err != nil {
|
||||
klog.Errorf("Error generating service account cache key: %v", err)
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the credentials are cached and return them if found.
|
||||
cachedConfig, found, errCache := p.getCachedCredentials(image, serviceAccountCacheKey)
|
||||
if errCache != nil {
|
||||
klog.Errorf("Failed to get cached docker config: %v", err)
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
@ -227,8 +407,23 @@ func (p *pluginProvider) Provide(image string) credentialprovider.DockerConfig {
|
||||
// foo.bar.registry
|
||||
// foo.bar.registry/image1
|
||||
// foo.bar.registry/image2
|
||||
res, err, _ := p.group.Do(image, func() (interface{}, error) {
|
||||
return p.plugin.ExecPlugin(context.Background(), image)
|
||||
// When the plugin is operating in the service account token mode, the singleflight key is the image plus the serviceAccountCacheKey
|
||||
// which is generated from the service account namespace, name, uid and the annotations passed to the plugin.
|
||||
singleFlightKey := image
|
||||
if p.serviceAccountProvider != nil && len(serviceAccountName) > 0 {
|
||||
// When the plugin is operating in the service account token mode, the singleflight key is the
|
||||
// image + sa annotations + sa token.
|
||||
// This does mean the singleflight key is different for each image pull request (even if the image is the same)
|
||||
// and the workload is using the same service account.
|
||||
// In the future, when we support caching of the service account token for pod-sa pairs, this will be singleflighted
|
||||
// for different containers in the same pod using the same image.
|
||||
if singleFlightKey, err = generateSingleFlightKey(image, getHashIfNotEmpty(serviceAccountToken), saAnnotations); err != nil {
|
||||
klog.Errorf("Error generating singleflight key: %v", err)
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
}
|
||||
res, err, _ := p.group.Do(singleFlightKey, func() (interface{}, error) {
|
||||
return p.plugin.ExecPlugin(context.Background(), image, serviceAccountToken, saAnnotations)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@ -280,6 +475,12 @@ func (p *pluginProvider) Provide(image string) credentialprovider.DockerConfig {
|
||||
expiresAt = p.clock.Now().Add(response.CacheDuration.Duration)
|
||||
}
|
||||
|
||||
cacheKey, err = generateCacheKey(cacheKey, serviceAccountCacheKey)
|
||||
if err != nil {
|
||||
klog.Errorf("Error generating cache key: %v", err)
|
||||
return credentialprovider.DockerConfig{}
|
||||
}
|
||||
|
||||
cachedEntry := &cacheEntry{
|
||||
key: cacheKey,
|
||||
credentials: dockerConfig,
|
||||
@ -310,7 +511,7 @@ func (p *pluginProvider) isImageAllowed(image string) bool {
|
||||
}
|
||||
|
||||
// getCachedCredentials returns a credentialprovider.DockerConfig if cached from the plugin.
|
||||
func (p *pluginProvider) getCachedCredentials(image string) (credentialprovider.DockerConfig, bool, error) {
|
||||
func (p *pluginProvider) getCachedCredentials(image, serviceAccountCacheKey string) (credentialprovider.DockerConfig, bool, error) {
|
||||
p.Lock()
|
||||
if p.clock.Now().After(p.lastCachePurge.Add(cachePurgeInterval)) {
|
||||
// NewExpirationCache purges expired entries when List() is called
|
||||
@ -321,7 +522,12 @@ func (p *pluginProvider) getCachedCredentials(image string) (credentialprovider.
|
||||
}
|
||||
p.Unlock()
|
||||
|
||||
obj, found, err := p.cache.GetByKey(image)
|
||||
cacheKey, err := generateCacheKey(image, serviceAccountCacheKey)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error generating cache key: %w", err)
|
||||
}
|
||||
|
||||
obj, found, err := p.cache.GetByKey(cacheKey)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@ -331,7 +537,13 @@ func (p *pluginProvider) getCachedCredentials(image string) (credentialprovider.
|
||||
}
|
||||
|
||||
registry := parseRegistry(image)
|
||||
obj, found, err = p.cache.GetByKey(registry)
|
||||
|
||||
cacheKey, err = generateCacheKey(registry, serviceAccountCacheKey)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error generating cache key: %w", err)
|
||||
}
|
||||
|
||||
obj, found, err = p.cache.GetByKey(cacheKey)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@ -340,7 +552,12 @@ func (p *pluginProvider) getCachedCredentials(image string) (credentialprovider.
|
||||
return obj.(*cacheEntry).credentials, true, nil
|
||||
}
|
||||
|
||||
obj, found, err = p.cache.GetByKey(globalCacheKey)
|
||||
cacheKey, err = generateCacheKey(globalCacheKey, serviceAccountCacheKey)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error generating cache key: %w", err)
|
||||
}
|
||||
|
||||
obj, found, err = p.cache.GetByKey(cacheKey)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@ -355,7 +572,7 @@ func (p *pluginProvider) getCachedCredentials(image string) (credentialprovider.
|
||||
// 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(ctx context.Context, image, serviceAccountToken string, serviceAccountAnnotations map[string]string) (*credentialproviderapi.CredentialProviderResponse, error)
|
||||
}
|
||||
|
||||
// execPlugin is the implementation of the Plugin interface that execs a credential provider plugin based
|
||||
@ -377,10 +594,10 @@ type execPlugin struct {
|
||||
//
|
||||
// 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) {
|
||||
func (e *execPlugin) ExecPlugin(ctx context.Context, image, serviceAccountToken string, serviceAccountAnnotations map[string]string) (*credentialproviderapi.CredentialProviderResponse, error) {
|
||||
klog.V(5).Infof("Getting image %s credentials from external exec plugin %s", image, e.name)
|
||||
|
||||
authRequest := &credentialproviderapi.CredentialProviderRequest{Image: image}
|
||||
authRequest := &credentialproviderapi.CredentialProviderRequest{Image: image, ServiceAccountToken: serviceAccountToken, ServiceAccountAnnotations: serviceAccountAnnotations}
|
||||
data, err := e.encodeRequest(authRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode auth request: %w", err)
|
||||
@ -499,3 +716,96 @@ func mergeEnvVars(sysEnvVars, credProviderVars []string) []string {
|
||||
mergedEnvVars = append(mergedEnvVars, credProviderVars...)
|
||||
return mergedEnvVars
|
||||
}
|
||||
|
||||
// generateServiceAccountCacheKey generates the serviceaccount cache key to be used for
|
||||
// 1. constructing the cache key for the service account token based plugin in addition to the actual cache key (image, registry, global).
|
||||
// 2. the unique key to use singleflight for the plugin in addition to the image.
|
||||
func generateServiceAccountCacheKey(serviceAccountNamespace, serviceAccountName string, serviceAccountUID types.UID, saAnnotations map[string]string) (string, error) {
|
||||
b := cryptobyte.NewBuilder(nil)
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes([]byte(serviceAccountNamespace))
|
||||
})
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes([]byte(serviceAccountName))
|
||||
})
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes([]byte(serviceAccountUID))
|
||||
})
|
||||
|
||||
// add the length of annotations to the cache key
|
||||
b.AddUint32(uint32(len(saAnnotations)))
|
||||
|
||||
// Sort the annotations by key to ensure the cache key is deterministic
|
||||
keys := sets.StringKeySet(saAnnotations).List()
|
||||
for _, k := range keys {
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes([]byte(k))
|
||||
})
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes([]byte(saAnnotations[k]))
|
||||
})
|
||||
}
|
||||
|
||||
keyBytes, err := b.Bytes()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(keyBytes), nil
|
||||
}
|
||||
|
||||
func generateCacheKey(baseKey, serviceAccountCacheKey string) (string, error) {
|
||||
b := cryptobyte.NewBuilder(nil)
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes([]byte(baseKey))
|
||||
})
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes([]byte(serviceAccountCacheKey))
|
||||
})
|
||||
|
||||
keyBytes, err := b.Bytes()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(keyBytes), nil
|
||||
}
|
||||
|
||||
func generateSingleFlightKey(image, saTokenHash string, saAnnotations map[string]string) (string, error) {
|
||||
b := cryptobyte.NewBuilder(nil)
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes([]byte(image))
|
||||
})
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes([]byte(saTokenHash))
|
||||
})
|
||||
|
||||
// add the length of annotations to the cache key
|
||||
b.AddUint32(uint32(len(saAnnotations)))
|
||||
|
||||
// Sort the annotations by key to ensure the cache key is deterministic
|
||||
keys := sets.StringKeySet(saAnnotations).List()
|
||||
for _, k := range keys {
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes([]byte(k))
|
||||
})
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes([]byte(saAnnotations[k]))
|
||||
})
|
||||
}
|
||||
|
||||
keyBytes, err := b.Bytes()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(keyBytes), nil
|
||||
}
|
||||
|
||||
// getHashIfNotEmpty returns the sha256 hash of the data if it is not empty.
|
||||
func getHashIfNotEmpty(data string) string {
|
||||
if len(data) > 0 {
|
||||
return fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(data)))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
98
pkg/credentialprovider/plugin/plugins.go
Normal file
98
pkg/credentialprovider/plugin/plugins.go
Normal file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright 2024 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"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
name string
|
||||
impl *pluginProvider
|
||||
}
|
||||
|
||||
var providersMutex sync.RWMutex
|
||||
var providers = make([]provider, 0)
|
||||
var seenProviderNames = sets.NewString()
|
||||
|
||||
func registerCredentialProviderPlugin(name string, p *pluginProvider) {
|
||||
providersMutex.Lock()
|
||||
defer providersMutex.Unlock()
|
||||
|
||||
if seenProviderNames.Has(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)
|
||||
}
|
||||
|
||||
type externalCredentialProviderKeyring struct {
|
||||
providers []credentialprovider.DockerConfigProvider
|
||||
}
|
||||
|
||||
func NewExternalCredentialProviderDockerKeyring(podNamespace, podName, podUID, serviceAccountName string) credentialprovider.DockerKeyring {
|
||||
providersMutex.RLock()
|
||||
defer providersMutex.RUnlock()
|
||||
|
||||
keyring := &externalCredentialProviderKeyring{
|
||||
providers: make([]credentialprovider.DockerConfigProvider, 0, len(providers)),
|
||||
}
|
||||
|
||||
for _, p := range providers {
|
||||
if !p.impl.Enabled() {
|
||||
continue
|
||||
}
|
||||
|
||||
pp := &perPodPluginProvider{
|
||||
name: p.name,
|
||||
provider: p.impl,
|
||||
}
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletServiceAccountTokenForCredentialProviders) {
|
||||
klog.V(4).InfoS("Generating per pod credential provider", "provider", p.name, "podName", podName, "podNamespace", podNamespace, "podUID", podUID, "serviceAccountName", serviceAccountName)
|
||||
|
||||
pp.podNamespace = podNamespace
|
||||
pp.podName = podName
|
||||
pp.podUID = types.UID(podUID)
|
||||
pp.serviceAccountName = serviceAccountName
|
||||
} else {
|
||||
klog.V(4).InfoS("Generating credential provider", "provider", p.name)
|
||||
}
|
||||
|
||||
keyring.providers = append(keyring.providers, pp)
|
||||
}
|
||||
|
||||
return keyring
|
||||
}
|
||||
|
||||
func (k *externalCredentialProviderKeyring) Lookup(image string) ([]credentialprovider.AuthConfig, bool) {
|
||||
keyring := &credentialprovider.BasicDockerKeyring{}
|
||||
|
||||
for _, p := range k.providers {
|
||||
keyring.Add(p.Provide(image))
|
||||
}
|
||||
|
||||
return keyring.Lookup(image)
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 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 credentialprovider
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type provider struct {
|
||||
name string
|
||||
impl DockerConfigProvider
|
||||
}
|
||||
|
||||
// All registered credential providers.
|
||||
var providersMutex sync.Mutex
|
||||
var providers = make([]provider, 0)
|
||||
var seenProviderNames = sets.NewString()
|
||||
|
||||
// RegisterCredentialProvider is called by provider implementations on
|
||||
// initialization to register themselves, like so:
|
||||
//
|
||||
// func init() {
|
||||
// RegisterCredentialProvider("name", &myProvider{...})
|
||||
// }
|
||||
func RegisterCredentialProvider(name string, p DockerConfigProvider) {
|
||||
providersMutex.Lock()
|
||||
defer providersMutex.Unlock()
|
||||
|
||||
if seenProviderNames.Has(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)
|
||||
}
|
||||
|
||||
// NewDockerKeyring creates a DockerKeyring to use for resolving credentials,
|
||||
// which draws from the set of registered credential providers.
|
||||
func NewDockerKeyring() DockerKeyring {
|
||||
keyring := &providersDockerKeyring{
|
||||
Providers: make([]DockerConfigProvider, 0),
|
||||
}
|
||||
|
||||
for _, p := range providers {
|
||||
if p.impl.Enabled() {
|
||||
klog.V(4).Infof("Registering credential provider: %v", p.name)
|
||||
keyring.Providers = append(keyring.Providers, p.impl)
|
||||
}
|
||||
}
|
||||
|
||||
return keyring
|
||||
}
|
@ -42,15 +42,6 @@ type DockerConfigProvider interface {
|
||||
// A DockerConfigProvider that simply reads the .dockercfg file
|
||||
type defaultDockerConfigProvider struct{}
|
||||
|
||||
// init registers our default provider, which simply reads the .dockercfg file.
|
||||
func init() {
|
||||
RegisterCredentialProvider(".dockercfg",
|
||||
&CachingDockerConfigProvider{
|
||||
Provider: &defaultDockerConfigProvider{},
|
||||
Lifetime: 5 * time.Minute,
|
||||
})
|
||||
}
|
||||
|
||||
// CachingDockerConfigProvider implements DockerConfigProvider by composing
|
||||
// with another DockerConfigProvider and caching the DockerConfig it provides
|
||||
// for a pre-specified lifetime.
|
||||
@ -107,3 +98,16 @@ func (d *CachingDockerConfigProvider) Provide(image string) DockerConfig {
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
// NewDefaultDockerKeyring creates a DockerKeyring to use for resolving credentials,
|
||||
// which returns the default credentials from the .dockercfg file.
|
||||
func NewDefaultDockerKeyring() DockerKeyring {
|
||||
return &providersDockerKeyring{
|
||||
Providers: []DockerConfigProvider{
|
||||
&CachingDockerConfigProvider{
|
||||
Provider: &defaultDockerConfigProvider{},
|
||||
Lifetime: 5 * time.Minute,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -377,6 +377,13 @@ const (
|
||||
// separate filesystems.
|
||||
KubeletSeparateDiskGC featuregate.Feature = "KubeletSeparateDiskGC"
|
||||
|
||||
// owner: @aramase
|
||||
// kep: http://kep.k8s.io/4412
|
||||
//
|
||||
// Enable kubelet to send the service account token bound to the pod for which the image
|
||||
// is being pulled to the credential provider plugin.
|
||||
KubeletServiceAccountTokenForCredentialProviders featuregate.Feature = "KubeletServiceAccountTokenForCredentialProviders"
|
||||
|
||||
// owner: @sallyom
|
||||
// kep: https://kep.k8s.io/2832
|
||||
//
|
||||
|
@ -456,6 +456,10 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate
|
||||
{Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta},
|
||||
},
|
||||
|
||||
KubeletServiceAccountTokenForCredentialProviders: {
|
||||
{Version: version.MustParse("1.33"), Default: false, PreRelease: featuregate.Alpha},
|
||||
},
|
||||
|
||||
KubeletTracing: {
|
||||
{Version: version.MustParse("1.25"), Default: false, PreRelease: featuregate.Alpha},
|
||||
{Version: version.MustParse("1.27"), Default: true, PreRelease: featuregate.Beta},
|
||||
|
78
pkg/generated/openapi/zz_generated.openapi.go
generated
78
pkg/generated/openapi/zz_generated.openapi.go
generated
@ -1253,6 +1253,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
"k8s.io/kubelet/config/v1.CredentialProvider": schema_k8sio_kubelet_config_v1_CredentialProvider(ref),
|
||||
"k8s.io/kubelet/config/v1.CredentialProviderConfig": schema_k8sio_kubelet_config_v1_CredentialProviderConfig(ref),
|
||||
"k8s.io/kubelet/config/v1.ExecEnvVar": schema_k8sio_kubelet_config_v1_ExecEnvVar(ref),
|
||||
"k8s.io/kubelet/config/v1.ServiceAccountTokenAttributes": schema_k8sio_kubelet_config_v1_ServiceAccountTokenAttributes(ref),
|
||||
"k8s.io/kubelet/config/v1alpha1.CredentialProvider": schema_k8sio_kubelet_config_v1alpha1_CredentialProvider(ref),
|
||||
"k8s.io/kubelet/config/v1alpha1.CredentialProviderConfig": schema_k8sio_kubelet_config_v1alpha1_CredentialProviderConfig(ref),
|
||||
"k8s.io/kubelet/config/v1alpha1.ExecEnvVar": schema_k8sio_kubelet_config_v1alpha1_ExecEnvVar(ref),
|
||||
@ -64169,12 +64170,18 @@ func schema_k8sio_kubelet_config_v1_CredentialProvider(ref common.ReferenceCallb
|
||||
},
|
||||
},
|
||||
},
|
||||
"tokenAttributes": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "tokenAttributes is the configuration for the service account token that will be passed to the plugin. The credential provider opts in to using service account tokens for image pull by setting this field. When this field is set, kubelet will generate a service account token bound to the pod for which the image is being pulled and pass to the plugin as part of CredentialProviderRequest along with other attributes required by the plugin.\n\nThe service account metadata and token attributes will be used as a dimension to cache the credentials in kubelet. The cache key is generated by combining the service account metadata (namespace, name, UID, and annotations key+value for the keys defined in serviceAccountTokenAttribute.requiredServiceAccountAnnotationKeys and serviceAccountTokenAttribute.optionalServiceAccountAnnotationKeys). The pod metadata (namespace, name, UID) that are in the service account token are not used as a dimension to cache the credentials in kubelet. This means workloads that are using the same service account could end up using the same credentials for image pull. For plugins that don't want this behavior, or plugins that operate in pass-through mode; i.e., they return the service account token as-is, they can set the credentialProviderResponse.cacheDuration to 0. This will disable the caching of credentials in kubelet and the plugin will be invoked for every image pull. This does result in token generation overhead for every image pull, but it is the only way to ensure that the credentials are not shared across pods (even if they are using the same service account).",
|
||||
Ref: ref("k8s.io/kubelet/config/v1.ServiceAccountTokenAttributes"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"name", "matchImages", "defaultCacheDuration", "apiVersion"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "k8s.io/kubelet/config/v1.ExecEnvVar"},
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1.Duration", "k8s.io/kubelet/config/v1.ExecEnvVar", "k8s.io/kubelet/config/v1.ServiceAccountTokenAttributes"},
|
||||
}
|
||||
}
|
||||
|
||||
@ -64250,6 +64257,75 @@ func schema_k8sio_kubelet_config_v1_ExecEnvVar(ref common.ReferenceCallback) com
|
||||
}
|
||||
}
|
||||
|
||||
func schema_k8sio_kubelet_config_v1_ServiceAccountTokenAttributes(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "ServiceAccountTokenAttributes is the configuration for the service account token that will be passed to the plugin.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"serviceAccountTokenAudience": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "serviceAccountTokenAudience is the intended audience for the projected service account token.",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"requireServiceAccount": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "requireServiceAccount indicates whether the plugin requires the pod to have a service account. If set to true, kubelet will only invoke the plugin if the pod has a service account. If set to false, kubelet will invoke the plugin even if the pod does not have a service account and will not include a token in the CredentialProviderRequest in that scenario. This is useful for plugins that are used to pull images for pods without service accounts (e.g., static pods).",
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"requiredServiceAccountAnnotationKeys": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "set",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "requiredServiceAccountAnnotationKeys is the list of annotation keys that the plugin is interested in and that are required to be present in the service account. The keys defined in this list will be extracted from the corresponding service account and passed to the plugin as part of the CredentialProviderRequest. If any of the keys defined in this list are not present in the service account, kubelet will not invoke the plugin and will return an error. This field is optional and may be empty. Plugins may use this field to extract additional information required to fetch credentials or allow workloads to opt in to using service account tokens for image pull. If non-empty, requireServiceAccount must be set to true.",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"optionalServiceAccountAnnotationKeys": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "set",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "optionalServiceAccountAnnotationKeys is the list of annotation keys that the plugin is interested in and that are optional to be present in the service account. The keys defined in this list will be extracted from the corresponding service account and passed to the plugin as part of the CredentialProviderRequest. The plugin is responsible for validating the existence of annotations and their values. This field is optional and may be empty. Plugins may use this field to extract additional information required to fetch credentials.",
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"serviceAccountTokenAudience", "requireServiceAccount"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_k8sio_kubelet_config_v1alpha1_CredentialProvider(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
|
@ -127,5 +127,11 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} {
|
||||
"AllBeta": true,
|
||||
}
|
||||
},
|
||||
|
||||
// tokenAttributes field is only supported in v1 CredentialProvider
|
||||
func(obj *kubeletconfig.CredentialProvider, c randfill.Continue) {
|
||||
c.FillNoCustom(obj)
|
||||
obj.TokenAttributes = nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -670,6 +670,64 @@ type CredentialProvider struct {
|
||||
// to pass argument to the plugin.
|
||||
// +optional
|
||||
Env []ExecEnvVar
|
||||
|
||||
// tokenAttributes is the configuration for the service account token that will be passed to the plugin.
|
||||
// The credential provider opts in to using service account tokens for image pull by setting this field.
|
||||
// When this field is set, kubelet will generate a service account token bound to the pod for which the
|
||||
// image is being pulled and pass to the plugin as part of CredentialProviderRequest along with other
|
||||
// attributes required by the plugin.
|
||||
//
|
||||
// The service account metadata and token attributes will be used as a dimension to cache
|
||||
// the credentials in kubelet. The cache key is generated by combining the service account metadata
|
||||
// (namespace, name, UID, and annotations key+value for the keys defined in
|
||||
// serviceAccountTokenAttribute.requiredServiceAccountAnnotationKeys and serviceAccountTokenAttribute.optionalServiceAccountAnnotationKeys).
|
||||
// The pod metadata (namespace, name, UID) that are in the service account token are not used as a dimension
|
||||
// to cache the credentials in kubelet. This means workloads that are using the same service account
|
||||
// could end up using the same credentials for image pull. For plugins that don't want this behavior, or
|
||||
// plugins that operate in pass-through mode; i.e., they return the service account token as-is, they
|
||||
// can set the credentialProviderResponse.cacheDuration to 0. This will disable the caching of
|
||||
// credentials in kubelet and the plugin will be invoked for every image pull. This does result in
|
||||
// token generation overhead for every image pull, but it is the only way to ensure that the
|
||||
// credentials are not shared across pods (even if they are using the same service account).
|
||||
// +optional
|
||||
TokenAttributes *ServiceAccountTokenAttributes
|
||||
}
|
||||
|
||||
// ServiceAccountTokenAttributes is the configuration for the service account token that will be passed to the plugin.
|
||||
type ServiceAccountTokenAttributes struct {
|
||||
// serviceAccountTokenAudience is the intended audience for the projected service account token.
|
||||
// +required
|
||||
ServiceAccountTokenAudience string
|
||||
|
||||
// requireServiceAccount indicates whether the plugin requires the pod to have a service account.
|
||||
// If set to true, kubelet will only invoke the plugin if the pod has a service account.
|
||||
// If set to false, kubelet will invoke the plugin even if the pod does not have a service account
|
||||
// and will not include a token in the CredentialProviderRequest in that scenario. This is useful for plugins that
|
||||
// are used to pull images for pods without service accounts (e.g., static pods).
|
||||
// +required
|
||||
RequireServiceAccount *bool
|
||||
|
||||
// requiredServiceAccountAnnotationKeys is the list of annotation keys that the plugin is interested in
|
||||
// and that are required to be present in the service account.
|
||||
// The keys defined in this list will be extracted from the corresponding service account and passed
|
||||
// to the plugin as part of the CredentialProviderRequest. If any of the keys defined in this list
|
||||
// are not present in the service account, kubelet will not invoke the plugin and will return an error.
|
||||
// This field is optional and may be empty. Plugins may use this field to extract
|
||||
// additional information required to fetch credentials or allow workloads to opt in to
|
||||
// using service account tokens for image pull.
|
||||
// If non-empty, requireServiceAccount must be set to true.
|
||||
// +optional
|
||||
RequiredServiceAccountAnnotationKeys []string
|
||||
|
||||
// optionalServiceAccountAnnotationKeys is the list of annotation keys that the plugin is interested in
|
||||
// and that are optional to be present in the service account.
|
||||
// The keys defined in this list will be extracted from the corresponding service account and passed
|
||||
// to the plugin as part of the CredentialProviderRequest. The plugin is responsible for validating
|
||||
// the existence of annotations and their values.
|
||||
// This field is optional and may be empty. Plugins may use this field to extract
|
||||
// additional information required to fetch credentials.
|
||||
// +optional
|
||||
OptionalServiceAccountAnnotationKeys []string
|
||||
}
|
||||
|
||||
// ExecEnvVar is used for setting environment variables when executing an exec-based
|
||||
|
@ -68,6 +68,16 @@ func RegisterConversions(s *runtime.Scheme) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*configv1.ServiceAccountTokenAttributes)(nil), (*config.ServiceAccountTokenAttributes)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1_ServiceAccountTokenAttributes_To_config_ServiceAccountTokenAttributes(a.(*configv1.ServiceAccountTokenAttributes), b.(*config.ServiceAccountTokenAttributes), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*config.ServiceAccountTokenAttributes)(nil), (*configv1.ServiceAccountTokenAttributes)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_ServiceAccountTokenAttributes_To_v1_ServiceAccountTokenAttributes(a.(*config.ServiceAccountTokenAttributes), b.(*configv1.ServiceAccountTokenAttributes), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -78,6 +88,7 @@ func autoConvert_v1_CredentialProvider_To_config_CredentialProvider(in *configv1
|
||||
out.APIVersion = in.APIVersion
|
||||
out.Args = *(*[]string)(unsafe.Pointer(&in.Args))
|
||||
out.Env = *(*[]config.ExecEnvVar)(unsafe.Pointer(&in.Env))
|
||||
out.TokenAttributes = (*config.ServiceAccountTokenAttributes)(unsafe.Pointer(in.TokenAttributes))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -93,6 +104,7 @@ func autoConvert_config_CredentialProvider_To_v1_CredentialProvider(in *config.C
|
||||
out.APIVersion = in.APIVersion
|
||||
out.Args = *(*[]string)(unsafe.Pointer(&in.Args))
|
||||
out.Env = *(*[]configv1.ExecEnvVar)(unsafe.Pointer(&in.Env))
|
||||
out.TokenAttributes = (*configv1.ServiceAccountTokenAttributes)(unsafe.Pointer(in.TokenAttributes))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -142,3 +154,29 @@ func autoConvert_config_ExecEnvVar_To_v1_ExecEnvVar(in *config.ExecEnvVar, out *
|
||||
func Convert_config_ExecEnvVar_To_v1_ExecEnvVar(in *config.ExecEnvVar, out *configv1.ExecEnvVar, s conversion.Scope) error {
|
||||
return autoConvert_config_ExecEnvVar_To_v1_ExecEnvVar(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1_ServiceAccountTokenAttributes_To_config_ServiceAccountTokenAttributes(in *configv1.ServiceAccountTokenAttributes, out *config.ServiceAccountTokenAttributes, s conversion.Scope) error {
|
||||
out.ServiceAccountTokenAudience = in.ServiceAccountTokenAudience
|
||||
out.RequireServiceAccount = (*bool)(unsafe.Pointer(in.RequireServiceAccount))
|
||||
out.RequiredServiceAccountAnnotationKeys = *(*[]string)(unsafe.Pointer(&in.RequiredServiceAccountAnnotationKeys))
|
||||
out.OptionalServiceAccountAnnotationKeys = *(*[]string)(unsafe.Pointer(&in.OptionalServiceAccountAnnotationKeys))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1_ServiceAccountTokenAttributes_To_config_ServiceAccountTokenAttributes is an autogenerated conversion function.
|
||||
func Convert_v1_ServiceAccountTokenAttributes_To_config_ServiceAccountTokenAttributes(in *configv1.ServiceAccountTokenAttributes, out *config.ServiceAccountTokenAttributes, s conversion.Scope) error {
|
||||
return autoConvert_v1_ServiceAccountTokenAttributes_To_config_ServiceAccountTokenAttributes(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_config_ServiceAccountTokenAttributes_To_v1_ServiceAccountTokenAttributes(in *config.ServiceAccountTokenAttributes, out *configv1.ServiceAccountTokenAttributes, s conversion.Scope) error {
|
||||
out.ServiceAccountTokenAudience = in.ServiceAccountTokenAudience
|
||||
out.RequireServiceAccount = (*bool)(unsafe.Pointer(in.RequireServiceAccount))
|
||||
out.RequiredServiceAccountAnnotationKeys = *(*[]string)(unsafe.Pointer(&in.RequiredServiceAccountAnnotationKeys))
|
||||
out.OptionalServiceAccountAnnotationKeys = *(*[]string)(unsafe.Pointer(&in.OptionalServiceAccountAnnotationKeys))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_config_ServiceAccountTokenAttributes_To_v1_ServiceAccountTokenAttributes is an autogenerated conversion function.
|
||||
func Convert_config_ServiceAccountTokenAttributes_To_v1_ServiceAccountTokenAttributes(in *config.ServiceAccountTokenAttributes, out *configv1.ServiceAccountTokenAttributes, s conversion.Scope) error {
|
||||
return autoConvert_config_ServiceAccountTokenAttributes_To_v1_ServiceAccountTokenAttributes(in, out, s)
|
||||
}
|
||||
|
28
pkg/kubelet/apis/config/v1alpha1/conversion.go
Normal file
28
pkg/kubelet/apis/config/v1alpha1/conversion.go
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
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 v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
configv1alpha1 "k8s.io/kubelet/config/v1alpha1"
|
||||
"k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
)
|
||||
|
||||
func Convert_config_CredentialProvider_To_v1alpha1_CredentialProvider(in *config.CredentialProvider, out *configv1alpha1.CredentialProvider, s conversion.Scope) error {
|
||||
// This conversion intentionally omits the tokenAttributes field which is only supported in v1 CredentialProvider.
|
||||
return autoConvert_config_CredentialProvider_To_v1alpha1_CredentialProvider(in, out, s)
|
||||
}
|
@ -43,11 +43,6 @@ func RegisterConversions(s *runtime.Scheme) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*config.CredentialProvider)(nil), (*configv1alpha1.CredentialProvider)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_CredentialProvider_To_v1alpha1_CredentialProvider(a.(*config.CredentialProvider), b.(*configv1alpha1.CredentialProvider), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*configv1alpha1.CredentialProviderConfig)(nil), (*config.CredentialProviderConfig)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_CredentialProviderConfig_To_config_CredentialProviderConfig(a.(*configv1alpha1.CredentialProviderConfig), b.(*config.CredentialProviderConfig), scope)
|
||||
}); err != nil {
|
||||
@ -68,6 +63,11 @@ func RegisterConversions(s *runtime.Scheme) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*config.CredentialProvider)(nil), (*configv1alpha1.CredentialProvider)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_CredentialProvider_To_v1alpha1_CredentialProvider(a.(*config.CredentialProvider), b.(*configv1alpha1.CredentialProvider), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -93,16 +93,22 @@ func autoConvert_config_CredentialProvider_To_v1alpha1_CredentialProvider(in *co
|
||||
out.APIVersion = in.APIVersion
|
||||
out.Args = *(*[]string)(unsafe.Pointer(&in.Args))
|
||||
out.Env = *(*[]configv1alpha1.ExecEnvVar)(unsafe.Pointer(&in.Env))
|
||||
// WARNING: in.TokenAttributes requires manual conversion: does not exist in peer-type
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_config_CredentialProvider_To_v1alpha1_CredentialProvider is an autogenerated conversion function.
|
||||
func Convert_config_CredentialProvider_To_v1alpha1_CredentialProvider(in *config.CredentialProvider, out *configv1alpha1.CredentialProvider, s conversion.Scope) error {
|
||||
return autoConvert_config_CredentialProvider_To_v1alpha1_CredentialProvider(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_CredentialProviderConfig_To_config_CredentialProviderConfig(in *configv1alpha1.CredentialProviderConfig, out *config.CredentialProviderConfig, s conversion.Scope) error {
|
||||
out.Providers = *(*[]config.CredentialProvider)(unsafe.Pointer(&in.Providers))
|
||||
if in.Providers != nil {
|
||||
in, out := &in.Providers, &out.Providers
|
||||
*out = make([]config.CredentialProvider, len(*in))
|
||||
for i := range *in {
|
||||
if err := Convert_v1alpha1_CredentialProvider_To_config_CredentialProvider(&(*in)[i], &(*out)[i], s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.Providers = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -112,7 +118,17 @@ func Convert_v1alpha1_CredentialProviderConfig_To_config_CredentialProviderConfi
|
||||
}
|
||||
|
||||
func autoConvert_config_CredentialProviderConfig_To_v1alpha1_CredentialProviderConfig(in *config.CredentialProviderConfig, out *configv1alpha1.CredentialProviderConfig, s conversion.Scope) error {
|
||||
out.Providers = *(*[]configv1alpha1.CredentialProvider)(unsafe.Pointer(&in.Providers))
|
||||
if in.Providers != nil {
|
||||
in, out := &in.Providers, &out.Providers
|
||||
*out = make([]configv1alpha1.CredentialProvider, len(*in))
|
||||
for i := range *in {
|
||||
if err := Convert_config_CredentialProvider_To_v1alpha1_CredentialProvider(&(*in)[i], &(*out)[i], s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.Providers = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
28
pkg/kubelet/apis/config/v1beta1/conversion.go
Normal file
28
pkg/kubelet/apis/config/v1beta1/conversion.go
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
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 v1beta1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
configv1beta1 "k8s.io/kubelet/config/v1beta1"
|
||||
"k8s.io/kubernetes/pkg/kubelet/apis/config"
|
||||
)
|
||||
|
||||
func Convert_config_CredentialProvider_To_v1beta1_CredentialProvider(in *config.CredentialProvider, out *configv1beta1.CredentialProvider, s conversion.Scope) error {
|
||||
// This conversion intentionally omits the tokenAttributes field which is only supported in v1 CredentialProvider.
|
||||
return autoConvert_config_CredentialProvider_To_v1beta1_CredentialProvider(in, out, s)
|
||||
}
|
@ -55,11 +55,6 @@ func RegisterConversions(s *runtime.Scheme) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*config.CredentialProvider)(nil), (*configv1beta1.CredentialProvider)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_CredentialProvider_To_v1beta1_CredentialProvider(a.(*config.CredentialProvider), b.(*configv1beta1.CredentialProvider), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*configv1beta1.CredentialProviderConfig)(nil), (*config.CredentialProviderConfig)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1beta1_CredentialProviderConfig_To_config_CredentialProviderConfig(a.(*configv1beta1.CredentialProviderConfig), b.(*config.CredentialProviderConfig), scope)
|
||||
}); err != nil {
|
||||
@ -190,6 +185,11 @@ func RegisterConversions(s *runtime.Scheme) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*config.CredentialProvider)(nil), (*configv1beta1.CredentialProvider)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_CredentialProvider_To_v1beta1_CredentialProvider(a.(*config.CredentialProvider), b.(*configv1beta1.CredentialProvider), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -235,16 +235,22 @@ func autoConvert_config_CredentialProvider_To_v1beta1_CredentialProvider(in *con
|
||||
out.APIVersion = in.APIVersion
|
||||
out.Args = *(*[]string)(unsafe.Pointer(&in.Args))
|
||||
out.Env = *(*[]configv1beta1.ExecEnvVar)(unsafe.Pointer(&in.Env))
|
||||
// WARNING: in.TokenAttributes requires manual conversion: does not exist in peer-type
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_config_CredentialProvider_To_v1beta1_CredentialProvider is an autogenerated conversion function.
|
||||
func Convert_config_CredentialProvider_To_v1beta1_CredentialProvider(in *config.CredentialProvider, out *configv1beta1.CredentialProvider, s conversion.Scope) error {
|
||||
return autoConvert_config_CredentialProvider_To_v1beta1_CredentialProvider(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1beta1_CredentialProviderConfig_To_config_CredentialProviderConfig(in *configv1beta1.CredentialProviderConfig, out *config.CredentialProviderConfig, s conversion.Scope) error {
|
||||
out.Providers = *(*[]config.CredentialProvider)(unsafe.Pointer(&in.Providers))
|
||||
if in.Providers != nil {
|
||||
in, out := &in.Providers, &out.Providers
|
||||
*out = make([]config.CredentialProvider, len(*in))
|
||||
for i := range *in {
|
||||
if err := Convert_v1beta1_CredentialProvider_To_config_CredentialProvider(&(*in)[i], &(*out)[i], s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.Providers = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -254,7 +260,17 @@ func Convert_v1beta1_CredentialProviderConfig_To_config_CredentialProviderConfig
|
||||
}
|
||||
|
||||
func autoConvert_config_CredentialProviderConfig_To_v1beta1_CredentialProviderConfig(in *config.CredentialProviderConfig, out *configv1beta1.CredentialProviderConfig, s conversion.Scope) error {
|
||||
out.Providers = *(*[]configv1beta1.CredentialProvider)(unsafe.Pointer(&in.Providers))
|
||||
if in.Providers != nil {
|
||||
in, out := &in.Providers, &out.Providers
|
||||
*out = make([]configv1beta1.CredentialProvider, len(*in))
|
||||
for i := range *in {
|
||||
if err := Convert_config_CredentialProvider_To_v1beta1_CredentialProvider(&(*in)[i], &(*out)[i], s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.Providers = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
36
pkg/kubelet/apis/config/zz_generated.deepcopy.go
generated
36
pkg/kubelet/apis/config/zz_generated.deepcopy.go
generated
@ -72,6 +72,11 @@ func (in *CredentialProvider) DeepCopyInto(out *CredentialProvider) {
|
||||
*out = make([]ExecEnvVar, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.TokenAttributes != nil {
|
||||
in, out := &in.TokenAttributes, &out.TokenAttributes
|
||||
*out = new(ServiceAccountTokenAttributes)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -491,6 +496,37 @@ func (in *SerializedNodeConfigSource) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ServiceAccountTokenAttributes) DeepCopyInto(out *ServiceAccountTokenAttributes) {
|
||||
*out = *in
|
||||
if in.RequireServiceAccount != nil {
|
||||
in, out := &in.RequireServiceAccount, &out.RequireServiceAccount
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.RequiredServiceAccountAnnotationKeys != nil {
|
||||
in, out := &in.RequiredServiceAccountAnnotationKeys, &out.RequiredServiceAccountAnnotationKeys
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.OptionalServiceAccountAnnotationKeys != nil {
|
||||
in, out := &in.OptionalServiceAccountAnnotationKeys, &out.OptionalServiceAccountAnnotationKeys
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountTokenAttributes.
|
||||
func (in *ServiceAccountTokenAttributes) DeepCopy() *ServiceAccountTokenAttributes {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ServiceAccountTokenAttributes)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ShutdownGracePeriodByPodPriority) DeepCopyInto(out *ShutdownGracePeriodByPodPriority) {
|
||||
*out = *in
|
||||
|
@ -152,7 +152,7 @@ type StreamingRuntime interface {
|
||||
type ImageService interface {
|
||||
// PullImage pulls an image from the network to local storage using the supplied
|
||||
// secrets if necessary. It returns a reference (digest or ID) to the pulled image.
|
||||
PullImage(ctx context.Context, image ImageSpec, pullSecrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, error)
|
||||
PullImage(ctx context.Context, image ImageSpec, pullSecrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig, serviceAccountName string) (string, error)
|
||||
// GetImageRef gets the reference (digest or ID) of the image which has already been in
|
||||
// the local storage. It returns ("", nil) if the image isn't in the local storage.
|
||||
GetImageRef(ctx context.Context, image ImageSpec) (string, error)
|
||||
|
@ -308,7 +308,7 @@ func (f *FakeRuntime) GetContainerLogs(_ context.Context, pod *v1.Pod, container
|
||||
return f.Err
|
||||
}
|
||||
|
||||
func (f *FakeRuntime) PullImage(ctx context.Context, image kubecontainer.ImageSpec, pullSecrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, error) {
|
||||
func (f *FakeRuntime) PullImage(ctx context.Context, image kubecontainer.ImageSpec, pullSecrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig, serviceAccountName string) (string, error) {
|
||||
f.Lock()
|
||||
f.CalledFunctions = append(f.CalledFunctions, "PullImage")
|
||||
if f.Err == nil {
|
||||
|
@ -990,9 +990,9 @@ func (_c *MockRuntime_ListPodSandboxMetrics_Call) RunAndReturn(run func(context.
|
||||
return _c
|
||||
}
|
||||
|
||||
// PullImage provides a mock function with given fields: ctx, image, pullSecrets, podSandboxConfig
|
||||
func (_m *MockRuntime) PullImage(ctx context.Context, image container.ImageSpec, pullSecrets []corev1.Secret, podSandboxConfig *v1.PodSandboxConfig) (string, error) {
|
||||
ret := _m.Called(ctx, image, pullSecrets, podSandboxConfig)
|
||||
// PullImage provides a mock function with given fields: ctx, image, pullSecrets, podSandboxConfig, serviceAccountName
|
||||
func (_m *MockRuntime) PullImage(ctx context.Context, image container.ImageSpec, pullSecrets []corev1.Secret, podSandboxConfig *v1.PodSandboxConfig, serviceAccountName string) (string, error) {
|
||||
ret := _m.Called(ctx, image, pullSecrets, podSandboxConfig, serviceAccountName)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for PullImage")
|
||||
@ -1000,17 +1000,17 @@ func (_m *MockRuntime) PullImage(ctx context.Context, image container.ImageSpec,
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, container.ImageSpec, []corev1.Secret, *v1.PodSandboxConfig) (string, error)); ok {
|
||||
return rf(ctx, image, pullSecrets, podSandboxConfig)
|
||||
if rf, ok := ret.Get(0).(func(context.Context, container.ImageSpec, []corev1.Secret, *v1.PodSandboxConfig, string) (string, error)); ok {
|
||||
return rf(ctx, image, pullSecrets, podSandboxConfig, serviceAccountName)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, container.ImageSpec, []corev1.Secret, *v1.PodSandboxConfig) string); ok {
|
||||
r0 = rf(ctx, image, pullSecrets, podSandboxConfig)
|
||||
if rf, ok := ret.Get(0).(func(context.Context, container.ImageSpec, []corev1.Secret, *v1.PodSandboxConfig, string) string); ok {
|
||||
r0 = rf(ctx, image, pullSecrets, podSandboxConfig, serviceAccountName)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, container.ImageSpec, []corev1.Secret, *v1.PodSandboxConfig) error); ok {
|
||||
r1 = rf(ctx, image, pullSecrets, podSandboxConfig)
|
||||
if rf, ok := ret.Get(1).(func(context.Context, container.ImageSpec, []corev1.Secret, *v1.PodSandboxConfig, string) error); ok {
|
||||
r1 = rf(ctx, image, pullSecrets, podSandboxConfig, serviceAccountName)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
@ -1028,13 +1028,14 @@ type MockRuntime_PullImage_Call struct {
|
||||
// - image container.ImageSpec
|
||||
// - pullSecrets []corev1.Secret
|
||||
// - podSandboxConfig *v1.PodSandboxConfig
|
||||
func (_e *MockRuntime_Expecter) PullImage(ctx interface{}, image interface{}, pullSecrets interface{}, podSandboxConfig interface{}) *MockRuntime_PullImage_Call {
|
||||
return &MockRuntime_PullImage_Call{Call: _e.mock.On("PullImage", ctx, image, pullSecrets, podSandboxConfig)}
|
||||
// - serviceAccountName string
|
||||
func (_e *MockRuntime_Expecter) PullImage(ctx interface{}, image interface{}, pullSecrets interface{}, podSandboxConfig interface{}, serviceAccountName interface{}) *MockRuntime_PullImage_Call {
|
||||
return &MockRuntime_PullImage_Call{Call: _e.mock.On("PullImage", ctx, image, pullSecrets, podSandboxConfig, serviceAccountName)}
|
||||
}
|
||||
|
||||
func (_c *MockRuntime_PullImage_Call) Run(run func(ctx context.Context, image container.ImageSpec, pullSecrets []corev1.Secret, podSandboxConfig *v1.PodSandboxConfig)) *MockRuntime_PullImage_Call {
|
||||
func (_c *MockRuntime_PullImage_Call) Run(run func(ctx context.Context, image container.ImageSpec, pullSecrets []corev1.Secret, podSandboxConfig *v1.PodSandboxConfig, serviceAccountName string)) *MockRuntime_PullImage_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(container.ImageSpec), args[2].([]corev1.Secret), args[3].(*v1.PodSandboxConfig))
|
||||
run(args[0].(context.Context), args[1].(container.ImageSpec), args[2].([]corev1.Secret), args[3].(*v1.PodSandboxConfig), args[4].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
@ -1044,7 +1045,7 @@ func (_c *MockRuntime_PullImage_Call) Return(_a0 string, _a1 error) *MockRuntime
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockRuntime_PullImage_Call) RunAndReturn(run func(context.Context, container.ImageSpec, []corev1.Secret, *v1.PodSandboxConfig) (string, error)) *MockRuntime_PullImage_Call {
|
||||
func (_c *MockRuntime_PullImage_Call) RunAndReturn(run func(context.Context, container.ImageSpec, []corev1.Secret, *v1.PodSandboxConfig, string) (string, error)) *MockRuntime_PullImage_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
@ -44,9 +44,9 @@ type throttledImageService struct {
|
||||
limiter flowcontrol.RateLimiter
|
||||
}
|
||||
|
||||
func (ts throttledImageService) PullImage(ctx context.Context, image kubecontainer.ImageSpec, secrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, error) {
|
||||
func (ts throttledImageService) PullImage(ctx context.Context, image kubecontainer.ImageSpec, secrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig, serviceAccountName string) (string, error) {
|
||||
if ts.limiter.TryAccept() {
|
||||
return ts.ImageService.PullImage(ctx, image, secrets, podSandboxConfig)
|
||||
return ts.ImageService.PullImage(ctx, image, secrets, podSandboxConfig, serviceAccountName)
|
||||
}
|
||||
return "", fmt.Errorf("pull QPS exceeded")
|
||||
}
|
||||
|
@ -175,7 +175,7 @@ func (m *imageManager) EnsureImageExists(ctx context.Context, objRef *v1.ObjectR
|
||||
m.logIt(objRef, v1.EventTypeNormal, events.PullingImage, logPrefix, fmt.Sprintf("Pulling image %q", imgRef), klog.Info)
|
||||
startTime := time.Now()
|
||||
pullChan := make(chan pullResult)
|
||||
m.puller.pullImage(ctx, spec, pullSecrets, pullChan, podSandboxConfig)
|
||||
m.puller.pullImage(ctx, spec, pullSecrets, pullChan, podSandboxConfig, pod.Spec.ServiceAccountName)
|
||||
imagePullResult := <-pullChan
|
||||
if imagePullResult.err != nil {
|
||||
m.logIt(objRef, v1.EventTypeWarning, events.FailedToPullImage, logPrefix, fmt.Sprintf("Failed to pull image %q: %v", imgRef, imagePullResult.err), klog.Warning)
|
||||
|
@ -34,7 +34,7 @@ type pullResult struct {
|
||||
}
|
||||
|
||||
type imagePuller interface {
|
||||
pullImage(context.Context, kubecontainer.ImageSpec, []v1.Secret, chan<- pullResult, *runtimeapi.PodSandboxConfig)
|
||||
pullImage(context.Context, kubecontainer.ImageSpec, []v1.Secret, chan<- pullResult, *runtimeapi.PodSandboxConfig, string)
|
||||
}
|
||||
|
||||
var _, _ imagePuller = ¶llelImagePuller{}, &serialImagePuller{}
|
||||
@ -51,14 +51,14 @@ func newParallelImagePuller(imageService kubecontainer.ImageService, maxParallel
|
||||
return ¶llelImagePuller{imageService, make(chan struct{}, *maxParallelImagePulls)}
|
||||
}
|
||||
|
||||
func (pip *parallelImagePuller) pullImage(ctx context.Context, spec kubecontainer.ImageSpec, pullSecrets []v1.Secret, pullChan chan<- pullResult, podSandboxConfig *runtimeapi.PodSandboxConfig) {
|
||||
func (pip *parallelImagePuller) pullImage(ctx context.Context, spec kubecontainer.ImageSpec, pullSecrets []v1.Secret, pullChan chan<- pullResult, podSandboxConfig *runtimeapi.PodSandboxConfig, serviceAccountName string) {
|
||||
go func() {
|
||||
if pip.tokens != nil {
|
||||
pip.tokens <- struct{}{}
|
||||
defer func() { <-pip.tokens }()
|
||||
}
|
||||
startTime := time.Now()
|
||||
imageRef, err := pip.imageService.PullImage(ctx, spec, pullSecrets, podSandboxConfig)
|
||||
imageRef, err := pip.imageService.PullImage(ctx, spec, pullSecrets, podSandboxConfig, serviceAccountName)
|
||||
var size uint64
|
||||
if err == nil && imageRef != "" {
|
||||
// Getting the image size with best effort, ignoring the error.
|
||||
@ -88,27 +88,29 @@ func newSerialImagePuller(imageService kubecontainer.ImageService) imagePuller {
|
||||
}
|
||||
|
||||
type imagePullRequest struct {
|
||||
ctx context.Context
|
||||
spec kubecontainer.ImageSpec
|
||||
pullSecrets []v1.Secret
|
||||
pullChan chan<- pullResult
|
||||
podSandboxConfig *runtimeapi.PodSandboxConfig
|
||||
ctx context.Context
|
||||
spec kubecontainer.ImageSpec
|
||||
pullSecrets []v1.Secret
|
||||
pullChan chan<- pullResult
|
||||
podSandboxConfig *runtimeapi.PodSandboxConfig
|
||||
serviceAccountName string
|
||||
}
|
||||
|
||||
func (sip *serialImagePuller) pullImage(ctx context.Context, spec kubecontainer.ImageSpec, pullSecrets []v1.Secret, pullChan chan<- pullResult, podSandboxConfig *runtimeapi.PodSandboxConfig) {
|
||||
func (sip *serialImagePuller) pullImage(ctx context.Context, spec kubecontainer.ImageSpec, pullSecrets []v1.Secret, pullChan chan<- pullResult, podSandboxConfig *runtimeapi.PodSandboxConfig, serviceAccountName string) {
|
||||
sip.pullRequests <- &imagePullRequest{
|
||||
ctx: ctx,
|
||||
spec: spec,
|
||||
pullSecrets: pullSecrets,
|
||||
pullChan: pullChan,
|
||||
podSandboxConfig: podSandboxConfig,
|
||||
ctx: ctx,
|
||||
spec: spec,
|
||||
pullSecrets: pullSecrets,
|
||||
pullChan: pullChan,
|
||||
podSandboxConfig: podSandboxConfig,
|
||||
serviceAccountName: serviceAccountName,
|
||||
}
|
||||
}
|
||||
|
||||
func (sip *serialImagePuller) processImagePullRequests() {
|
||||
for pullRequest := range sip.pullRequests {
|
||||
startTime := time.Now()
|
||||
imageRef, err := sip.imageService.PullImage(pullRequest.ctx, pullRequest.spec, pullRequest.pullSecrets, pullRequest.podSandboxConfig)
|
||||
imageRef, err := sip.imageService.PullImage(pullRequest.ctx, pullRequest.spec, pullRequest.pullSecrets, pullRequest.podSandboxConfig, pullRequest.serviceAccountName)
|
||||
var size uint64
|
||||
if err == nil && imageRef != "" {
|
||||
// Getting the image size with best effort, ignoring the error.
|
||||
|
@ -41,6 +41,7 @@ import (
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/mount-utils"
|
||||
|
||||
@ -713,6 +714,19 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
tokenManager := token.NewManager(kubeDeps.KubeClient)
|
||||
getServiceAccount := func(namespace, name string) (*v1.ServiceAccount, error) {
|
||||
return nil, fmt.Errorf("get service account is not implemented")
|
||||
}
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletServiceAccountTokenForCredentialProviders) {
|
||||
getServiceAccount = func(namespace, name string) (*v1.ServiceAccount, error) {
|
||||
if klet.kubeClient == nil {
|
||||
return nil, errors.New("cannot get ServiceAccounts when kubelet is in standalone mode")
|
||||
}
|
||||
return klet.kubeClient.CoreV1().ServiceAccounts(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
}
|
||||
}
|
||||
|
||||
runtime, err := kuberuntime.NewKubeGenericRuntimeManager(
|
||||
kubecontainer.FilterEventRecorder(kubeDeps.Recorder),
|
||||
klet.livenessManager,
|
||||
@ -747,6 +761,8 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
|
||||
*kubeCfg.MemoryThrottlingFactor,
|
||||
kubeDeps.PodStartupLatencyTracker,
|
||||
kubeDeps.TracerProvider,
|
||||
tokenManager,
|
||||
getServiceAccount,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -876,8 +892,6 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
|
||||
kubeDeps.Recorder)
|
||||
}
|
||||
|
||||
tokenManager := token.NewManager(kubeDeps.KubeClient)
|
||||
|
||||
var clusterTrustBundleManager clustertrustbundle.Manager = &clustertrustbundle.NoopManager{}
|
||||
if kubeDeps.KubeClient != nil && utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundleProjection) {
|
||||
clusterTrustBundleManager = clustertrustbundle.NewLazyInformerManager(ctx, kubeDeps.KubeClient, 2*int(kubeCfg.MaxPods))
|
||||
|
@ -40,6 +40,7 @@ import (
|
||||
cadvisorapiv2 "github.com/google/cadvisor/info/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
core "k8s.io/client-go/testing"
|
||||
"k8s.io/mount-utils"
|
||||
|
||||
@ -3424,6 +3425,8 @@ func TestSyncPodSpans(t *testing.T) {
|
||||
*kubeCfg.MemoryThrottlingFactor,
|
||||
kubeletutil.NewPodStartupLatencyTracker(),
|
||||
tp,
|
||||
token.NewManager(kubelet.kubeClient),
|
||||
func(string, string) (*v1.ServiceAccount, error) { return nil, nil },
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -24,6 +24,8 @@ import (
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
credentialproviderplugin "k8s.io/kubernetes/pkg/credentialprovider/plugin"
|
||||
credentialprovidersecrets "k8s.io/kubernetes/pkg/credentialprovider/secrets"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||
@ -32,14 +34,26 @@ import (
|
||||
|
||||
// PullImage pulls an image from the network to local storage using the supplied
|
||||
// secrets if necessary.
|
||||
func (m *kubeGenericRuntimeManager) PullImage(ctx context.Context, image kubecontainer.ImageSpec, pullSecrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, error) {
|
||||
func (m *kubeGenericRuntimeManager) PullImage(ctx context.Context, image kubecontainer.ImageSpec, pullSecrets []v1.Secret, podSandboxConfig *runtimeapi.PodSandboxConfig, serviceAccountName string) (string, error) {
|
||||
img := image.Image
|
||||
repoToPull, _, _, err := parsers.ParseImageName(img)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
keyring, err := credentialprovidersecrets.MakeDockerKeyring(pullSecrets, m.keyring)
|
||||
// construct the dynamic keyring using the providers we have in the kubelet
|
||||
var podName, podNamespace, podUID string
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletServiceAccountTokenForCredentialProviders) {
|
||||
sandboxMetadata := podSandboxConfig.GetMetadata()
|
||||
|
||||
podName = sandboxMetadata.Name
|
||||
podNamespace = sandboxMetadata.Namespace
|
||||
podUID = sandboxMetadata.Uid
|
||||
}
|
||||
|
||||
externalCredentialProviderKeyring := credentialproviderplugin.NewExternalCredentialProviderDockerKeyring(podNamespace, podName, podUID, serviceAccountName)
|
||||
|
||||
keyring, err := credentialprovidersecrets.MakeDockerKeyring(pullSecrets, credentialprovider.UnionDockerKeyring{m.keyring, externalCredentialProviderKeyring})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ func TestPullImage(t *testing.T) {
|
||||
_, _, fakeManager, err := createTestRuntimeManager()
|
||||
assert.NoError(t, err)
|
||||
|
||||
imageRef, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil)
|
||||
imageRef, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "busybox", imageRef)
|
||||
|
||||
@ -53,12 +53,12 @@ func TestPullImageWithError(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// trying to pull an image with an invalid name should return an error
|
||||
imageRef, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: ":invalid"}, nil, nil)
|
||||
imageRef, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: ":invalid"}, nil, nil, "")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", imageRef)
|
||||
|
||||
fakeImageService.InjectError("PullImage", fmt.Errorf("test-error"))
|
||||
imageRef, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil)
|
||||
imageRef, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil, "")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", imageRef)
|
||||
|
||||
@ -75,7 +75,7 @@ func TestPullImageWithInvalidImageName(t *testing.T) {
|
||||
fakeImageService.SetFakeImages(imageList)
|
||||
for _, val := range imageList {
|
||||
ctx := context.Background()
|
||||
imageRef, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: val}, nil, nil)
|
||||
imageRef, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: val}, nil, nil, "")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", imageRef)
|
||||
|
||||
@ -196,7 +196,7 @@ func TestRemoveImage(t *testing.T) {
|
||||
_, fakeImageService, fakeManager, err := createTestRuntimeManager()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil)
|
||||
_, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fakeImageService.Images, 1)
|
||||
|
||||
@ -219,7 +219,7 @@ func TestRemoveImageWithError(t *testing.T) {
|
||||
_, fakeImageService, fakeManager, err := createTestRuntimeManager()
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil)
|
||||
_, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: "busybox"}, nil, nil, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fakeImageService.Images, 1)
|
||||
|
||||
@ -324,7 +324,7 @@ func TestPullWithSecrets(t *testing.T) {
|
||||
_, fakeImageService, fakeManager, err := customTestRuntimeManager(builtInKeyRing)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: test.imageName}, test.passedSecrets, nil)
|
||||
_, err = fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: test.imageName}, test.passedSecrets, nil, "")
|
||||
require.NoError(t, err)
|
||||
fakeImageService.AssertImagePulledWithAuth(t, &runtimeapi.ImageSpec{Image: test.imageName, Annotations: make(map[string]string)}, test.expectedAuth, description)
|
||||
}
|
||||
@ -375,7 +375,7 @@ func TestPullWithSecretsWithError(t *testing.T) {
|
||||
fakeImageService.InjectError("PullImage", fmt.Errorf("test-error"))
|
||||
}
|
||||
|
||||
imageRef, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: test.imageName}, test.passedSecrets, nil)
|
||||
imageRef, err := fakeManager.PullImage(ctx, kubecontainer.ImageSpec{Image: test.imageName}, test.passedSecrets, nil, "")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "", imageRef)
|
||||
|
||||
@ -398,7 +398,7 @@ func TestPullThenListWithAnnotations(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
_, err = fakeManager.PullImage(ctx, imageSpec, nil, nil)
|
||||
_, err = fakeManager.PullImage(ctx, imageSpec, nil, nil, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
images, err := fakeManager.ListImages(ctx)
|
||||
|
@ -28,8 +28,6 @@ import (
|
||||
cadvisorapi "github.com/google/cadvisor/info/v1"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
grpcstatus "google.golang.org/grpc/status"
|
||||
crierror "k8s.io/cri-api/pkg/errors"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
@ -44,7 +42,8 @@ import (
|
||||
"k8s.io/component-base/logs/logreduction"
|
||||
internalapi "k8s.io/cri-api/pkg/apis"
|
||||
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
|
||||
|
||||
crierror "k8s.io/cri-api/pkg/errors"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
|
||||
"k8s.io/kubernetes/pkg/credentialprovider"
|
||||
@ -62,6 +61,7 @@ import (
|
||||
proberesults "k8s.io/kubernetes/pkg/kubelet/prober/results"
|
||||
"k8s.io/kubernetes/pkg/kubelet/runtimeclass"
|
||||
"k8s.io/kubernetes/pkg/kubelet/sysctl"
|
||||
"k8s.io/kubernetes/pkg/kubelet/token"
|
||||
"k8s.io/kubernetes/pkg/kubelet/types"
|
||||
"k8s.io/kubernetes/pkg/kubelet/util/cache"
|
||||
"k8s.io/kubernetes/pkg/kubelet/util/format"
|
||||
@ -223,6 +223,8 @@ func NewKubeGenericRuntimeManager(
|
||||
memoryThrottlingFactor float64,
|
||||
podPullingTimeRecorder images.ImagePodPullingTimeRecorder,
|
||||
tracerProvider trace.TracerProvider,
|
||||
tokenManager *token.Manager,
|
||||
getServiceAccount func(string, string) (*v1.ServiceAccount, error),
|
||||
) (KubeGenericRuntime, error) {
|
||||
ctx := context.Background()
|
||||
runtimeService = newInstrumentedRuntimeService(runtimeService)
|
||||
@ -277,12 +279,12 @@ func NewKubeGenericRuntimeManager(
|
||||
"apiVersion", typedVersion.RuntimeApiVersion)
|
||||
|
||||
if imageCredentialProviderConfigFile != "" || imageCredentialProviderBinDir != "" {
|
||||
if err := plugin.RegisterCredentialProviderPlugins(imageCredentialProviderConfigFile, imageCredentialProviderBinDir); err != nil {
|
||||
if err := plugin.RegisterCredentialProviderPlugins(imageCredentialProviderConfigFile, imageCredentialProviderBinDir, tokenManager.GetServiceAccountToken, getServiceAccount); err != nil {
|
||||
klog.ErrorS(err, "Failed to register CRI auth plugins")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
kubeRuntimeManager.keyring = credentialprovider.NewDockerKeyring()
|
||||
kubeRuntimeManager.keyring = credentialprovider.NewDefaultDockerKeyring()
|
||||
|
||||
kubeRuntimeManager.imagePuller = images.NewImageManager(
|
||||
kubecontainer.FilterEventRecorder(recorder),
|
||||
|
@ -27,14 +27,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
cadvisorapi "github.com/google/cadvisor/info/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
noopoteltrace "go.opentelemetry.io/otel/trace/noop"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
|
@ -135,7 +135,7 @@ func (r *NodeAuthorizer) Authorize(ctx context.Context, attrs authorizer.Attribu
|
||||
case vaResource:
|
||||
return r.authorizeGet(nodeName, vaVertexType, attrs)
|
||||
case svcAcctResource:
|
||||
return r.authorizeCreateToken(nodeName, serviceAccountVertexType, attrs)
|
||||
return r.authorizeServiceAccount(nodeName, attrs)
|
||||
case leaseResource:
|
||||
return r.authorizeLease(nodeName, attrs)
|
||||
case csiNodeResource:
|
||||
@ -196,7 +196,7 @@ func (r *NodeAuthorizer) authorizeGet(nodeName string, startingType vertexType,
|
||||
func (r *NodeAuthorizer) authorizeReadNamespacedObject(nodeName string, startingType vertexType, attrs authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
switch attrs.GetVerb() {
|
||||
case "get", "list", "watch":
|
||||
//ok
|
||||
// ok
|
||||
default:
|
||||
klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs)
|
||||
return authorizer.DecisionNoOpinion, "can only read resources of this type", nil
|
||||
@ -231,6 +231,23 @@ func (r *NodeAuthorizer) authorize(nodeName string, startingType vertexType, att
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
// authorizeServiceAccount authorizes
|
||||
// - "get" requests to serviceaccounts when KubeletServiceAccountTokenForCredentialProviders feature is enabled
|
||||
// - "create" requests to serviceaccounts 'token' subresource of pods running on a node
|
||||
func (r *NodeAuthorizer) authorizeServiceAccount(nodeName string, attrs authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
verb := attrs.GetVerb()
|
||||
|
||||
if verb == "get" && attrs.GetSubresource() == "" {
|
||||
if !r.features.Enabled(features.KubeletServiceAccountTokenForCredentialProviders) {
|
||||
klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs)
|
||||
return authorizer.DecisionNoOpinion, "not allowed to get service accounts", nil
|
||||
}
|
||||
return r.authorizeReadNamespacedObject(nodeName, serviceAccountVertexType, attrs)
|
||||
}
|
||||
|
||||
return r.authorizeCreateToken(nodeName, serviceAccountVertexType, attrs)
|
||||
}
|
||||
|
||||
// authorizeCreateToken authorizes "create" requests to serviceaccounts 'token'
|
||||
// subresource of pods running on a node
|
||||
func (r *NodeAuthorizer) authorizeCreateToken(nodeName string, startingType vertexType, attrs authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
@ -262,7 +279,7 @@ func (r *NodeAuthorizer) authorizeLease(nodeName string, attrs authorizer.Attrib
|
||||
verb := attrs.GetVerb()
|
||||
switch verb {
|
||||
case "get", "create", "update", "patch", "delete":
|
||||
//ok
|
||||
// ok
|
||||
default:
|
||||
klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs)
|
||||
return authorizer.DecisionNoOpinion, "can only get, create, update, patch, or delete a node lease", nil
|
||||
@ -291,7 +308,7 @@ func (r *NodeAuthorizer) authorizeCSINode(nodeName string, attrs authorizer.Attr
|
||||
verb := attrs.GetVerb()
|
||||
switch verb {
|
||||
case "get", "create", "update", "patch", "delete":
|
||||
//ok
|
||||
// ok
|
||||
default:
|
||||
klog.V(2).Infof("NODE DENY: '%s' %#v", nodeName, attrs)
|
||||
return authorizer.DecisionNoOpinion, "can only get, create, update, patch, or delete a CSINode", nil
|
||||
|
@ -80,6 +80,12 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, selectorAuthzEnabled, genericfeatures.AuthorizeWithSelectors, true)
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, selectorAuthzEnabled, features.AuthorizeNodeWithSelectors, true)
|
||||
|
||||
serviceAccountTokenForCredentialProvidersDisabled := utilfeature.DefaultFeatureGate.DeepCopy()
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, serviceAccountTokenForCredentialProvidersDisabled, features.KubeletServiceAccountTokenForCredentialProviders, false)
|
||||
|
||||
serviceAccountTokenForCredentialProvidersEnabled := utilfeature.DefaultFeatureGate.DeepCopy()
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, serviceAccountTokenForCredentialProvidersEnabled, features.KubeletServiceAccountTokenForCredentialProviders, true)
|
||||
|
||||
featureVariants := []struct {
|
||||
suffix string
|
||||
features featuregate.FeatureGate
|
||||
@ -89,10 +95,11 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
attrs authorizer.AttributesRecord
|
||||
expect authorizer.Decision
|
||||
features featuregate.FeatureGate
|
||||
name string
|
||||
attrs authorizer.AttributesRecord
|
||||
expect authorizer.Decision
|
||||
expectReason string
|
||||
features featuregate.FeatureGate
|
||||
}{
|
||||
{
|
||||
name: "allowed configmap",
|
||||
@ -115,19 +122,22 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
expect: authorizer.DecisionAllow,
|
||||
},
|
||||
{
|
||||
name: "disallowed list many secrets",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "secrets", Name: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
name: "disallowed list many secrets",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "secrets", Name: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
expectReason: "No Object name found,",
|
||||
},
|
||||
{
|
||||
name: "disallowed watch many secrets",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "secrets", Name: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
name: "disallowed watch many secrets",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "secrets", Name: "", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
expectReason: "No Object name found,",
|
||||
},
|
||||
{
|
||||
name: "disallowed list secrets from all namespaces with name",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "secrets", Name: "secret0-pod0-node0", Namespace: ""},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
name: "disallowed list secrets from all namespaces with name",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "secrets", Name: "secret0-pod0-node0", Namespace: ""},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
expectReason: "can only read namespaced object of this type",
|
||||
},
|
||||
{
|
||||
name: "allowed shared secret via pod",
|
||||
@ -219,6 +229,33 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node0", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
},
|
||||
{
|
||||
name: "get allowed svcacct via pod - feature enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "serviceaccounts", Name: "svcacct0-node0", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionAllow,
|
||||
features: serviceAccountTokenForCredentialProvidersEnabled,
|
||||
},
|
||||
{
|
||||
name: "disallowed get svcacct via pod - feature disabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "serviceaccounts", Name: "svcacct0-node0", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
features: serviceAccountTokenForCredentialProvidersDisabled,
|
||||
expectReason: "not allowed to get service accounts",
|
||||
},
|
||||
{
|
||||
name: "disallowed list svcacct via pod - feature disabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "serviceaccounts", Name: "svcacct0-node0", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
features: serviceAccountTokenForCredentialProvidersDisabled,
|
||||
expectReason: "can only create tokens for individual service accounts",
|
||||
},
|
||||
{
|
||||
name: "disallowed watch svcacct via pod - feature disabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "serviceaccounts", Name: "svcacct0-node0", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
features: serviceAccountTokenForCredentialProvidersDisabled,
|
||||
expectReason: "can only create tokens for individual service accounts",
|
||||
},
|
||||
{
|
||||
name: "disallowed get lease in namespace other than kube-node-lease - feature enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node0", Namespace: "foo"},
|
||||
@ -398,10 +435,11 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "disallowed unfiltered list ResourceSlices - selector authz enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "resourceslices", APIGroup: "resource.k8s.io"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
features: selectorAuthzEnabled,
|
||||
name: "disallowed unfiltered list ResourceSlices - selector authz enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "resourceslices", APIGroup: "resource.k8s.io"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
features: selectorAuthzEnabled,
|
||||
expectReason: "can only list/watch/deletecollection resourceslices with nodeName field selector",
|
||||
},
|
||||
{
|
||||
name: "allowed filtered watch ResourceSlices",
|
||||
@ -415,10 +453,11 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "disallowed unfiltered watch ResourceSlices - selector authz enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "resourceslices", APIGroup: "resource.k8s.io"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
features: selectorAuthzEnabled,
|
||||
name: "disallowed unfiltered watch ResourceSlices - selector authz enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "resourceslices", APIGroup: "resource.k8s.io"},
|
||||
expect: authorizer.DecisionNoOpinion,
|
||||
features: selectorAuthzEnabled,
|
||||
expectReason: "can only list/watch/deletecollection resourceslices with nodeName field selector",
|
||||
},
|
||||
{
|
||||
name: "allowed get ResourceSlice",
|
||||
@ -460,10 +499,11 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "get unrelated pod - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "pods", APIGroup: "", Name: "pod0-node1", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
name: "get unrelated pod - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "pods", APIGroup: "", Name: "pod0-node1", Namespace: "ns0"},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
expectReason: "no relationship found between node 'node0' and this object",
|
||||
},
|
||||
// list pods
|
||||
{
|
||||
@ -488,10 +528,11 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "list unrelated pods - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "pods", APIGroup: ""},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
name: "list unrelated pods - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "pods", APIGroup: ""},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
expectReason: "can only list/watch pods with spec.nodeName field selector",
|
||||
},
|
||||
// watch pods
|
||||
{
|
||||
@ -516,10 +557,11 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "watch unrelated pods - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "pods", APIGroup: ""},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
name: "watch unrelated pods - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "pods", APIGroup: ""},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
expectReason: "can only list/watch pods with spec.nodeName field selector",
|
||||
},
|
||||
// create, delete pods
|
||||
{
|
||||
@ -604,10 +646,11 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "get unrelated pod - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "nodes", APIGroup: "", Name: "node1"},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
name: "get unrelated pod - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "nodes", APIGroup: "", Name: "node1"},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
expectReason: "node 'node0' cannot read 'node1', only its own Node object",
|
||||
},
|
||||
// list nodes
|
||||
{
|
||||
@ -622,10 +665,11 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "list single unrelated node - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "nodes", APIGroup: "", Name: "node1"},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
name: "list single unrelated node - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "nodes", APIGroup: "", Name: "node1"},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
expectReason: "node 'node0' cannot read 'node1', only its own Node object",
|
||||
},
|
||||
{
|
||||
name: "list all nodes - selector disabled",
|
||||
@ -634,10 +678,11 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "list all nodes - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "nodes", APIGroup: ""},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
name: "list all nodes - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "nodes", APIGroup: ""},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
expectReason: "node 'node0' cannot read all nodes, only its own Node object",
|
||||
},
|
||||
// watch nodes
|
||||
{
|
||||
@ -652,10 +697,11 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "watch single unrelated node - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "nodes", APIGroup: "", Name: "node1"},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
name: "watch single unrelated node - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "nodes", APIGroup: "", Name: "node1"},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
expectReason: "node 'node0' cannot read 'node1', only its own Node object",
|
||||
},
|
||||
{
|
||||
name: "watch all nodes - selector disabled",
|
||||
@ -664,10 +710,11 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
features: selectorAuthzDisabled,
|
||||
},
|
||||
{
|
||||
name: "watch all nodes - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "nodes", APIGroup: ""},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
name: "watch all nodes - selector enabled",
|
||||
attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "nodes", APIGroup: ""},
|
||||
expect: authorizer.DecisionNoOpinion, // stricter with selector authz enabled
|
||||
features: selectorAuthzEnabled,
|
||||
expectReason: "node 'node0' cannot read all nodes, only its own Node object",
|
||||
},
|
||||
// create nodes
|
||||
{
|
||||
@ -737,6 +784,9 @@ func TestNodeAuthorizer(t *testing.T) {
|
||||
if decision != tc.expect {
|
||||
t.Errorf("expected %v, got %v (%s)", tc.expect, decision, reason)
|
||||
}
|
||||
if reason != tc.expectReason {
|
||||
t.Errorf("expected reason %q, got %q", tc.expectReason, reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -189,6 +189,11 @@ func NodeRules() []rbacv1.PolicyRule {
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundle) {
|
||||
nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("get", "list", "watch").Groups(certificatesGroup).Resources("clustertrustbundles").RuleOrDie())
|
||||
}
|
||||
// Kubelet needs access to ServiceAccounts to support sending service account tokens to the credential provider.
|
||||
// Use the Node authorizer to limit get to service accounts related to the node.
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.KubeletServiceAccountTokenForCredentialProviders) {
|
||||
nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("get").Groups(legacyGroup).Resources("serviceaccounts").RuleOrDie())
|
||||
}
|
||||
|
||||
return nodePolicyRules
|
||||
}
|
||||
|
@ -88,6 +88,66 @@ type CredentialProvider struct {
|
||||
// to pass argument to the plugin.
|
||||
// +optional
|
||||
Env []ExecEnvVar `json:"env,omitempty"`
|
||||
|
||||
// tokenAttributes is the configuration for the service account token that will be passed to the plugin.
|
||||
// The credential provider opts in to using service account tokens for image pull by setting this field.
|
||||
// When this field is set, kubelet will generate a service account token bound to the pod for which the
|
||||
// image is being pulled and pass to the plugin as part of CredentialProviderRequest along with other
|
||||
// attributes required by the plugin.
|
||||
//
|
||||
// The service account metadata and token attributes will be used as a dimension to cache
|
||||
// the credentials in kubelet. The cache key is generated by combining the service account metadata
|
||||
// (namespace, name, UID, and annotations key+value for the keys defined in
|
||||
// serviceAccountTokenAttribute.requiredServiceAccountAnnotationKeys and serviceAccountTokenAttribute.optionalServiceAccountAnnotationKeys).
|
||||
// The pod metadata (namespace, name, UID) that are in the service account token are not used as a dimension
|
||||
// to cache the credentials in kubelet. This means workloads that are using the same service account
|
||||
// could end up using the same credentials for image pull. For plugins that don't want this behavior, or
|
||||
// plugins that operate in pass-through mode; i.e., they return the service account token as-is, they
|
||||
// can set the credentialProviderResponse.cacheDuration to 0. This will disable the caching of
|
||||
// credentials in kubelet and the plugin will be invoked for every image pull. This does result in
|
||||
// token generation overhead for every image pull, but it is the only way to ensure that the
|
||||
// credentials are not shared across pods (even if they are using the same service account).
|
||||
// +optional
|
||||
TokenAttributes *ServiceAccountTokenAttributes `json:"tokenAttributes,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceAccountTokenAttributes is the configuration for the service account token that will be passed to the plugin.
|
||||
type ServiceAccountTokenAttributes struct {
|
||||
// serviceAccountTokenAudience is the intended audience for the projected service account token.
|
||||
// +required
|
||||
ServiceAccountTokenAudience string `json:"serviceAccountTokenAudience"`
|
||||
|
||||
// requireServiceAccount indicates whether the plugin requires the pod to have a service account.
|
||||
// If set to true, kubelet will only invoke the plugin if the pod has a service account.
|
||||
// If set to false, kubelet will invoke the plugin even if the pod does not have a service account
|
||||
// and will not include a token in the CredentialProviderRequest in that scenario. This is useful for plugins that
|
||||
// are used to pull images for pods without service accounts (e.g., static pods).
|
||||
// +required
|
||||
RequireServiceAccount *bool `json:"requireServiceAccount"`
|
||||
|
||||
// requiredServiceAccountAnnotationKeys is the list of annotation keys that the plugin is interested in
|
||||
// and that are required to be present in the service account.
|
||||
// The keys defined in this list will be extracted from the corresponding service account and passed
|
||||
// to the plugin as part of the CredentialProviderRequest. If any of the keys defined in this list
|
||||
// are not present in the service account, kubelet will not invoke the plugin and will return an error.
|
||||
// This field is optional and may be empty. Plugins may use this field to extract
|
||||
// additional information required to fetch credentials or allow workloads to opt in to
|
||||
// using service account tokens for image pull.
|
||||
// If non-empty, requireServiceAccount must be set to true.
|
||||
// +optional
|
||||
// +listType=set
|
||||
RequiredServiceAccountAnnotationKeys []string `json:"requiredServiceAccountAnnotationKeys,omitempty"`
|
||||
|
||||
// optionalServiceAccountAnnotationKeys is the list of annotation keys that the plugin is interested in
|
||||
// and that are optional to be present in the service account.
|
||||
// The keys defined in this list will be extracted from the corresponding service account and passed
|
||||
// to the plugin as part of the CredentialProviderRequest. The plugin is responsible for validating
|
||||
// the existence of annotations and their values.
|
||||
// This field is optional and may be empty. Plugins may use this field to extract
|
||||
// additional information required to fetch credentials.
|
||||
// +optional
|
||||
// +listType=set
|
||||
OptionalServiceAccountAnnotationKeys []string `json:"optionalServiceAccountAnnotationKeys,omitempty"`
|
||||
}
|
||||
|
||||
// ExecEnvVar is used for setting environment variables when executing an exec-based
|
||||
|
@ -49,6 +49,11 @@ func (in *CredentialProvider) DeepCopyInto(out *CredentialProvider) {
|
||||
*out = make([]ExecEnvVar, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.TokenAttributes != nil {
|
||||
in, out := &in.TokenAttributes, &out.TokenAttributes
|
||||
*out = new(ServiceAccountTokenAttributes)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -109,3 +114,34 @@ func (in *ExecEnvVar) DeepCopy() *ExecEnvVar {
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ServiceAccountTokenAttributes) DeepCopyInto(out *ServiceAccountTokenAttributes) {
|
||||
*out = *in
|
||||
if in.RequireServiceAccount != nil {
|
||||
in, out := &in.RequireServiceAccount, &out.RequireServiceAccount
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.RequiredServiceAccountAnnotationKeys != nil {
|
||||
in, out := &in.RequiredServiceAccountAnnotationKeys, &out.RequiredServiceAccountAnnotationKeys
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.OptionalServiceAccountAnnotationKeys != nil {
|
||||
in, out := &in.OptionalServiceAccountAnnotationKeys, &out.OptionalServiceAccountAnnotationKeys
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountTokenAttributes.
|
||||
func (in *ServiceAccountTokenAttributes) DeepCopy() *ServiceAccountTokenAttributes {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ServiceAccountTokenAttributes)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
@ -32,6 +32,17 @@ type CredentialProviderRequest struct {
|
||||
// credential provider plugin request. Plugins may optionally parse the image
|
||||
// to extract any information required to fetch credentials.
|
||||
Image string
|
||||
|
||||
// serviceAccountToken is the service account token bound to the pod for which
|
||||
// the image is being pulled. This token is only sent to the plugin if the
|
||||
// tokenAttributes.serviceAccountTokenAudience field is configured in the kubelet's credential provider configuration.
|
||||
ServiceAccountToken string
|
||||
|
||||
// serviceAccountAnnotations is a map of annotations on the service account bound to the
|
||||
// pod for which the image is being pulled. The list of annotations in the service account
|
||||
// that need to be passed to the plugin is configured in the kubelet's credential provider
|
||||
// configuration.
|
||||
ServiceAccountAnnotations map[string]string
|
||||
}
|
||||
|
||||
type PluginCacheKeyType string
|
||||
|
@ -32,6 +32,18 @@ type CredentialProviderRequest struct {
|
||||
// credential provider plugin request. Plugins may optionally parse the image
|
||||
// to extract any information required to fetch credentials.
|
||||
Image string `json:"image"`
|
||||
|
||||
// serviceAccountToken is the service account token bound to the pod for which
|
||||
// the image is being pulled. This token is only sent to the plugin if the
|
||||
// tokenAttributes.serviceAccountTokenAudience field is configured in the kubelet's credential
|
||||
// provider configuration.
|
||||
ServiceAccountToken string `json:"serviceAccountToken,omitempty" datapolicy:"token"`
|
||||
|
||||
// serviceAccountAnnotations is a map of annotations on the service account bound to the
|
||||
// pod for which the image is being pulled. The list of annotations in the service account
|
||||
// that need to be passed to the plugin is configured in the kubelet's credential provider
|
||||
// configuration.
|
||||
ServiceAccountAnnotations map[string]string `json:"serviceAccountAnnotations,omitempty"`
|
||||
}
|
||||
|
||||
type PluginCacheKeyType string
|
||||
|
@ -94,6 +94,8 @@ func Convert_credentialprovider_AuthConfig_To_v1_AuthConfig(in *credentialprovid
|
||||
|
||||
func autoConvert_v1_CredentialProviderRequest_To_credentialprovider_CredentialProviderRequest(in *CredentialProviderRequest, out *credentialprovider.CredentialProviderRequest, s conversion.Scope) error {
|
||||
out.Image = in.Image
|
||||
out.ServiceAccountToken = in.ServiceAccountToken
|
||||
out.ServiceAccountAnnotations = *(*map[string]string)(unsafe.Pointer(&in.ServiceAccountAnnotations))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -104,6 +106,8 @@ func Convert_v1_CredentialProviderRequest_To_credentialprovider_CredentialProvid
|
||||
|
||||
func autoConvert_credentialprovider_CredentialProviderRequest_To_v1_CredentialProviderRequest(in *credentialprovider.CredentialProviderRequest, out *CredentialProviderRequest, s conversion.Scope) error {
|
||||
out.Image = in.Image
|
||||
out.ServiceAccountToken = in.ServiceAccountToken
|
||||
out.ServiceAccountAnnotations = *(*map[string]string)(unsafe.Pointer(&in.ServiceAccountAnnotations))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,13 @@ func (in *AuthConfig) DeepCopy() *AuthConfig {
|
||||
func (in *CredentialProviderRequest) DeepCopyInto(out *CredentialProviderRequest) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
if in.ServiceAccountAnnotations != nil {
|
||||
in, out := &in.ServiceAccountAnnotations, &out.ServiceAccountAnnotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
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 v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
"k8s.io/kubelet/pkg/apis/credentialprovider"
|
||||
)
|
||||
|
||||
func Convert_credentialprovider_CredentialProviderRequest_To_v1alpha1_CredentialProviderRequest(in *credentialprovider.CredentialProviderRequest, out *CredentialProviderRequest, s conversion.Scope) error {
|
||||
// This conversion intentionally omits the serviceAccountToken and serviceAccountAnnotations fields which are only supported in v1 CredentialProviderRequest.
|
||||
return autoConvert_credentialprovider_CredentialProviderRequest_To_v1alpha1_CredentialProviderRequest(in, out, s)
|
||||
}
|
@ -52,11 +52,6 @@ func RegisterConversions(s *runtime.Scheme) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*credentialprovider.CredentialProviderRequest)(nil), (*CredentialProviderRequest)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_credentialprovider_CredentialProviderRequest_To_v1alpha1_CredentialProviderRequest(a.(*credentialprovider.CredentialProviderRequest), b.(*CredentialProviderRequest), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*CredentialProviderResponse)(nil), (*credentialprovider.CredentialProviderResponse)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_CredentialProviderResponse_To_credentialprovider_CredentialProviderResponse(a.(*CredentialProviderResponse), b.(*credentialprovider.CredentialProviderResponse), scope)
|
||||
}); err != nil {
|
||||
@ -67,6 +62,11 @@ func RegisterConversions(s *runtime.Scheme) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*credentialprovider.CredentialProviderRequest)(nil), (*CredentialProviderRequest)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_credentialprovider_CredentialProviderRequest_To_v1alpha1_CredentialProviderRequest(a.(*credentialprovider.CredentialProviderRequest), b.(*CredentialProviderRequest), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -104,14 +104,11 @@ func Convert_v1alpha1_CredentialProviderRequest_To_credentialprovider_Credential
|
||||
|
||||
func autoConvert_credentialprovider_CredentialProviderRequest_To_v1alpha1_CredentialProviderRequest(in *credentialprovider.CredentialProviderRequest, out *CredentialProviderRequest, s conversion.Scope) error {
|
||||
out.Image = in.Image
|
||||
// WARNING: in.ServiceAccountToken requires manual conversion: does not exist in peer-type
|
||||
// WARNING: in.ServiceAccountAnnotations requires manual conversion: does not exist in peer-type
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_credentialprovider_CredentialProviderRequest_To_v1alpha1_CredentialProviderRequest is an autogenerated conversion function.
|
||||
func Convert_credentialprovider_CredentialProviderRequest_To_v1alpha1_CredentialProviderRequest(in *credentialprovider.CredentialProviderRequest, out *CredentialProviderRequest, s conversion.Scope) error {
|
||||
return autoConvert_credentialprovider_CredentialProviderRequest_To_v1alpha1_CredentialProviderRequest(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_CredentialProviderResponse_To_credentialprovider_CredentialProviderResponse(in *CredentialProviderResponse, out *credentialprovider.CredentialProviderResponse, s conversion.Scope) error {
|
||||
out.CacheKeyType = credentialprovider.PluginCacheKeyType(in.CacheKeyType)
|
||||
out.CacheDuration = (*v1.Duration)(unsafe.Pointer(in.CacheDuration))
|
||||
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
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 v1beta1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
"k8s.io/kubelet/pkg/apis/credentialprovider"
|
||||
)
|
||||
|
||||
func Convert_credentialprovider_CredentialProviderRequest_To_v1beta1_CredentialProviderRequest(in *credentialprovider.CredentialProviderRequest, out *CredentialProviderRequest, s conversion.Scope) error {
|
||||
// This conversion intentionally omits the serviceAccountToken and serviceAccountAnnotations fields which are only supported in v1 CredentialProviderRequest.
|
||||
return autoConvert_credentialprovider_CredentialProviderRequest_To_v1beta1_CredentialProviderRequest(in, out, s)
|
||||
}
|
@ -52,11 +52,6 @@ func RegisterConversions(s *runtime.Scheme) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*credentialprovider.CredentialProviderRequest)(nil), (*CredentialProviderRequest)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_credentialprovider_CredentialProviderRequest_To_v1beta1_CredentialProviderRequest(a.(*credentialprovider.CredentialProviderRequest), b.(*CredentialProviderRequest), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*CredentialProviderResponse)(nil), (*credentialprovider.CredentialProviderResponse)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1beta1_CredentialProviderResponse_To_credentialprovider_CredentialProviderResponse(a.(*CredentialProviderResponse), b.(*credentialprovider.CredentialProviderResponse), scope)
|
||||
}); err != nil {
|
||||
@ -67,6 +62,11 @@ func RegisterConversions(s *runtime.Scheme) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*credentialprovider.CredentialProviderRequest)(nil), (*CredentialProviderRequest)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_credentialprovider_CredentialProviderRequest_To_v1beta1_CredentialProviderRequest(a.(*credentialprovider.CredentialProviderRequest), b.(*CredentialProviderRequest), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -104,14 +104,11 @@ func Convert_v1beta1_CredentialProviderRequest_To_credentialprovider_CredentialP
|
||||
|
||||
func autoConvert_credentialprovider_CredentialProviderRequest_To_v1beta1_CredentialProviderRequest(in *credentialprovider.CredentialProviderRequest, out *CredentialProviderRequest, s conversion.Scope) error {
|
||||
out.Image = in.Image
|
||||
// WARNING: in.ServiceAccountToken requires manual conversion: does not exist in peer-type
|
||||
// WARNING: in.ServiceAccountAnnotations requires manual conversion: does not exist in peer-type
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_credentialprovider_CredentialProviderRequest_To_v1beta1_CredentialProviderRequest is an autogenerated conversion function.
|
||||
func Convert_credentialprovider_CredentialProviderRequest_To_v1beta1_CredentialProviderRequest(in *credentialprovider.CredentialProviderRequest, out *CredentialProviderRequest, s conversion.Scope) error {
|
||||
return autoConvert_credentialprovider_CredentialProviderRequest_To_v1beta1_CredentialProviderRequest(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1beta1_CredentialProviderResponse_To_credentialprovider_CredentialProviderResponse(in *CredentialProviderResponse, out *credentialprovider.CredentialProviderResponse, s conversion.Scope) error {
|
||||
out.CacheKeyType = credentialprovider.PluginCacheKeyType(in.CacheKeyType)
|
||||
out.CacheDuration = (*v1.Duration)(unsafe.Pointer(in.CacheDuration))
|
||||
|
@ -46,6 +46,13 @@ func (in *AuthConfig) DeepCopy() *AuthConfig {
|
||||
func (in *CredentialProviderRequest) DeepCopyInto(out *CredentialProviderRequest) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
if in.ServiceAccountAnnotations != nil {
|
||||
in, out := &in.ServiceAccountAnnotations, &out.ServiceAccountAnnotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"k8s.io/kubernetes/test/e2e/feature"
|
||||
"k8s.io/kubernetes/test/e2e/framework"
|
||||
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
|
||||
@ -34,10 +35,12 @@ import (
|
||||
var _ = SIGDescribe("ImageCredentialProvider", feature.KubeletCredentialProviders, func() {
|
||||
f := framework.NewDefaultFramework("image-credential-provider")
|
||||
f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
|
||||
var serviceAccountClient typedcorev1.ServiceAccountInterface
|
||||
var podClient *e2epod.PodClient
|
||||
|
||||
ginkgo.BeforeEach(func() {
|
||||
podClient = e2epod.NewPodClient(f)
|
||||
serviceAccountClient = f.ClientSet.CoreV1().ServiceAccounts(f.Namespace.Name)
|
||||
})
|
||||
|
||||
/*
|
||||
@ -48,11 +51,28 @@ var _ = SIGDescribe("ImageCredentialProvider", feature.KubeletCredentialProvider
|
||||
ginkgo.It("should be able to create pod with image credentials fetched from external credential provider ", func(ctx context.Context) {
|
||||
privateimage := imageutils.GetConfig(imageutils.AgnhostPrivate)
|
||||
name := "pod-auth-image-" + string(uuid.NewUUID())
|
||||
|
||||
// The service account is required to exist for the credential provider plugin that's configured to use service account token.
|
||||
serviceAccount := &v1.ServiceAccount{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-service-account",
|
||||
// these annotations are validated by the test gcp-credential-provider-with-sa plugin
|
||||
// that runs in service account token mode.
|
||||
Annotations: map[string]string{
|
||||
"domain.io/identity-id": "123456",
|
||||
"domain.io/identity-type": "serviceaccount",
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err := serviceAccountClient.Create(ctx, serviceAccount, metav1.CreateOptions{})
|
||||
framework.ExpectNoError(err)
|
||||
|
||||
pod := &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
ServiceAccountName: "test-service-account",
|
||||
Containers: []v1.Container{
|
||||
{
|
||||
Name: "container-auth-image",
|
||||
|
@ -17,19 +17,29 @@ limitations under the License.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/go-jose/go-jose.v2/jwt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/klog/v2"
|
||||
credentialproviderv1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1"
|
||||
)
|
||||
|
||||
const metadataTokenEndpoint = "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/default/token"
|
||||
const (
|
||||
metadataTokenEndpoint = "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/default/token"
|
||||
|
||||
pluginModeEnvVar = "PLUGIN_MODE"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := getCredentials(metadataTokenEndpoint, os.Stdin, os.Stdout); err != nil {
|
||||
@ -56,6 +66,40 @@ func getCredentials(tokenEndpoint string, r io.Reader, w io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
pluginUsingServiceAccount := os.Getenv(pluginModeEnvVar) == "serviceaccount"
|
||||
if pluginUsingServiceAccount {
|
||||
if len(authRequest.ServiceAccountToken) == 0 {
|
||||
return errors.New("service account token is empty")
|
||||
}
|
||||
expectedAnnotations := map[string]string{
|
||||
"domain.io/identity-id": "123456",
|
||||
"domain.io/identity-type": "serviceaccount",
|
||||
}
|
||||
if !reflect.DeepEqual(authRequest.ServiceAccountAnnotations, expectedAnnotations) {
|
||||
return fmt.Errorf("unexpected service account annotations, want: %v, got: %v", expectedAnnotations, authRequest.ServiceAccountAnnotations)
|
||||
}
|
||||
// The service account token is not actually used for authentication by this test plugin.
|
||||
// We extract the claims from the token to validate the audience.
|
||||
// This is solely for testing assertions and is not an actual security layer.
|
||||
// Post validation in this block, we proceed with the default flow for fetching credentials.
|
||||
c, err := getClaims(authRequest.ServiceAccountToken)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The audience in the token should match the audience configured in tokenAttributes.serviceAccountTokenAudience
|
||||
// in CredentialProviderConfig.
|
||||
if len(c.Audience) != 1 || c.Audience[0] != "test-audience" {
|
||||
return fmt.Errorf("unexpected audience: %v", c.Audience)
|
||||
}
|
||||
} else {
|
||||
if len(authRequest.ServiceAccountToken) > 0 {
|
||||
return errors.New("service account token is not expected")
|
||||
}
|
||||
if len(authRequest.ServiceAccountAnnotations) > 0 {
|
||||
return errors.New("service account annotations are not expected")
|
||||
}
|
||||
}
|
||||
|
||||
auth, err := provider.Provide(authRequest.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -70,6 +114,10 @@ func getCredentials(tokenEndpoint string, r io.Reader, w io.Writer) error {
|
||||
Auth: auth,
|
||||
}
|
||||
|
||||
if pluginUsingServiceAccount {
|
||||
response.CacheKeyType = credentialproviderv1.GlobalPluginCacheKeyType
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
// The error from json.Marshal is intentionally not included so as to not leak credentials into the logs
|
||||
return errors.New("error marshaling response")
|
||||
@ -77,3 +125,55 @@ func getCredentials(tokenEndpoint string, r io.Reader, w io.Writer) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getClaims is used to extract claims from the service account token when the plugin is running in service account mode
|
||||
// This is solely for testing assertions and is not an actual security layer.
|
||||
// We get claims and validate the audience of the token (audience in the token matches the audience configured
|
||||
// in tokenAttributes.serviceAccountTokenAudience in CredentialProviderConfig).
|
||||
func getClaims(tokenData string) (claims, error) {
|
||||
if strings.HasPrefix(strings.TrimSpace(tokenData), "{") {
|
||||
return claims{}, errors.New("token is not a JWS")
|
||||
}
|
||||
parts := strings.Split(tokenData, ".")
|
||||
if len(parts) != 3 {
|
||||
return claims{}, errors.New("token is not a JWS")
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return claims{}, fmt.Errorf("error decoding token payload: %w", err)
|
||||
}
|
||||
|
||||
var c claims
|
||||
d := json.NewDecoder(strings.NewReader(string(payload)))
|
||||
d.DisallowUnknownFields()
|
||||
if err := d.Decode(&c); err != nil {
|
||||
return claims{}, fmt.Errorf("error decoding token payload: %w", err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type claims struct {
|
||||
jwt.Claims
|
||||
privateClaims
|
||||
}
|
||||
|
||||
// copied from https://github.com/kubernetes/kubernetes/blob/60c4c2b2521fb454ce69dee737e3eb91a25e0535/pkg/serviceaccount/claims.go#L51-L67
|
||||
|
||||
type privateClaims struct {
|
||||
Kubernetes kubernetes `json:"kubernetes.io,omitempty"`
|
||||
}
|
||||
|
||||
type kubernetes struct {
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
Svcacct ref `json:"serviceaccount,omitempty"`
|
||||
Pod *ref `json:"pod,omitempty"`
|
||||
Secret *ref `json:"secret,omitempty"`
|
||||
Node *ref `json:"node,omitempty"`
|
||||
WarnAfter *jwt.NumericDate `json:"warnafter,omitempty"`
|
||||
}
|
||||
|
||||
type ref struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
UID string `json:"uid,omitempty"`
|
||||
}
|
||||
|
@ -72,6 +72,21 @@ func (n *NodeE2ERemote) SetupTestPackage(tardir, systemSpecName string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// create a symlink of gcp-credential-provider binary to use for testing
|
||||
// service account token for credential providers.
|
||||
// feature-gate: KubeletServiceAccountTokenForCredentialProviders=true
|
||||
binary := "gcp-credential-provider" // Use relative path instead of full path
|
||||
symlink := filepath.Join(tardir, "gcp-credential-provider-with-sa")
|
||||
if _, err := os.Lstat(symlink); err == nil {
|
||||
if err := os.Remove(symlink); err != nil {
|
||||
return fmt.Errorf("failed to remove symlink %q: %w", symlink, err)
|
||||
}
|
||||
}
|
||||
klog.V(2).Infof("Creating symlink %s -> %s", symlink, binary)
|
||||
if err := os.Symlink(binary, symlink); err != nil {
|
||||
return fmt.Errorf("failed to create symlink %q: %w", symlink, err)
|
||||
}
|
||||
|
||||
if systemSpecName != "" {
|
||||
// Copy system spec file
|
||||
source := filepath.Join(rootDir, system.SystemSpecPath, systemSpecName+".yaml")
|
||||
@ -97,9 +112,10 @@ func prependMemcgNotificationFlag(args string) string {
|
||||
// a credential provider plugin.
|
||||
func prependCredentialProviderFlag(args, workspace string) string {
|
||||
credentialProviderConfig := filepath.Join(workspace, "credential-provider.yaml")
|
||||
featureGateFlag := "--kubelet-flags=--feature-gates=KubeletServiceAccountTokenForCredentialProviders=true"
|
||||
configFlag := fmt.Sprintf("--kubelet-flags=--image-credential-provider-config=%s", credentialProviderConfig)
|
||||
binFlag := fmt.Sprintf("--kubelet-flags=--image-credential-provider-bin-dir=%s", workspace)
|
||||
return fmt.Sprintf("%s %s %s", configFlag, binFlag, args)
|
||||
return fmt.Sprintf("%s %s %s %s", featureGateFlag, configFlag, binFlag, args)
|
||||
}
|
||||
|
||||
// osSpecificActions takes OS specific actions required for the node tests
|
||||
|
@ -53,14 +53,31 @@ const cniConfig = `{
|
||||
const credentialGCPProviderConfig = `kind: CredentialProviderConfig
|
||||
apiVersion: kubelet.config.k8s.io/v1
|
||||
providers:
|
||||
- name: gcp-credential-provider
|
||||
apiVersion: credentialprovider.kubelet.k8s.io/v1
|
||||
matchImages:
|
||||
- "gcr.io"
|
||||
- "*.gcr.io"
|
||||
- "container.cloud.google.com"
|
||||
- "*.pkg.dev"
|
||||
defaultCacheDuration: 1m`
|
||||
- name: gcp-credential-provider
|
||||
apiVersion: credentialprovider.kubelet.k8s.io/v1
|
||||
matchImages:
|
||||
- "gcr.io"
|
||||
- "*.gcr.io"
|
||||
- "container.cloud.google.com"
|
||||
- "*.pkg.dev"
|
||||
defaultCacheDuration: 1m
|
||||
- name: gcp-credential-provider-with-sa
|
||||
apiVersion: credentialprovider.kubelet.k8s.io/v1
|
||||
matchImages:
|
||||
- "gcr.io"
|
||||
- "*.gcr.io"
|
||||
- "container.cloud.google.com"
|
||||
- "*.pkg.dev"
|
||||
defaultCacheDuration: 1m
|
||||
tokenAttributes:
|
||||
serviceAccountTokenAudience: test-audience
|
||||
requireServiceAccount: true
|
||||
requiredServiceAccountAnnotationKeys:
|
||||
- "domain.io/identity-id"
|
||||
- "domain.io/identity-type"
|
||||
env:
|
||||
- name: PLUGIN_MODE
|
||||
value: "serviceaccount"`
|
||||
|
||||
const credentialAWSProviderConfig = `kind: CredentialProviderConfig
|
||||
apiVersion: kubelet.config.k8s.io/v1
|
||||
|
@ -670,6 +670,12 @@
|
||||
lockToDefault: false
|
||||
preRelease: Beta
|
||||
version: "1.31"
|
||||
- name: KubeletServiceAccountTokenForCredentialProviders
|
||||
versionedSpecs:
|
||||
- default: false
|
||||
lockToDefault: false
|
||||
preRelease: Alpha
|
||||
version: "1.33"
|
||||
- name: KubeletTracing
|
||||
versionedSpecs:
|
||||
- default: false
|
||||
|
Loading…
Reference in New Issue
Block a user