Skip to content

Commit 687a2f0

Browse files
authored
Merge pull request kubernetes#130763 from aramase/aramase/t/kep_4412_alpha_plugin_unit_tests
Add unit tests for credential provider in service account mode
2 parents c79e13c + 95d4113 commit 687a2f0

File tree

2 files changed

+196
-9
lines changed

2 files changed

+196
-9
lines changed

pkg/credentialprovider/plugin/plugin.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ var (
7373
}
7474
)
7575

76-
// GetServiceAccountFunc is a function type that returns a service account token for the given namespace and name.
76+
// GetServiceAccountFunc is a function type that returns a service account for the given namespace and name.
7777
type GetServiceAccountFunc func(namespace, name string) (*v1.ServiceAccount, error)
7878

7979
// getServiceAccountTokenFunc is a function type that returns a service account token for the given namespace and name.
@@ -240,6 +240,11 @@ func (e requiredAnnotationNotFoundError) Error() string {
240240
return fmt.Sprintf("required annotation %s not found", string(e))
241241
}
242242

243+
func isRequiredAnnotationNotFoundError(err error) bool {
244+
var requiredAnnotationNotFoundErr requiredAnnotationNotFoundError
245+
return errors.As(err, &requiredAnnotationNotFoundErr)
246+
}
247+
243248
// getServiceAccountData returns the service account UID and required annotations for the service account.
244249
// If the service account does not exist, an error is returned.
245250
// saAnnotations is a map of annotation keys and values that the plugin requires to generate credentials
@@ -370,8 +375,7 @@ func (p *pluginProvider) provide(image, podNamespace, podName string, podUID typ
370375
// to pull images for pods without service accounts (e.g., static pods).
371376
if len(serviceAccountName) > 0 {
372377
if serviceAccountUID, saAnnotations, err = p.serviceAccountProvider.getServiceAccountData(podNamespace, serviceAccountName); err != nil {
373-
var requiredAnnotationNotFoundErr requiredAnnotationNotFoundError
374-
if errors.As(err, &requiredAnnotationNotFoundErr) {
378+
if isRequiredAnnotationNotFoundError(err) {
375379
// The required annotation could be a mechanism for individual workloads to opt in to using service account tokens
376380
// for image pull. If any of the required annotation is missing, we will not invoke the plugin. We will log the error
377381
// at higher verbosity level as it could be noisy.

pkg/credentialprovider/plugin/plugin_test.go

Lines changed: 189 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ package plugin
1919
import (
2020
"bytes"
2121
"context"
22+
"errors"
23+
"flag"
2224
"fmt"
2325
"reflect"
26+
"strings"
2427
"sync"
2528
"testing"
2629
"time"
@@ -35,6 +38,7 @@ import (
3538
"k8s.io/apimachinery/pkg/types"
3639
"k8s.io/apimachinery/pkg/util/rand"
3740
"k8s.io/client-go/tools/cache"
41+
"k8s.io/klog/v2"
3842
credentialproviderapi "k8s.io/kubelet/pkg/apis/credentialprovider"
3943
credentialproviderv1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1"
4044
credentialproviderv1alpha1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1"
@@ -188,12 +192,19 @@ func TestSingleflightProvide(t *testing.T) {
188192
}
189193

190194
func Test_Provide(t *testing.T) {
195+
klog.InitFlags(nil)
196+
if err := flag.Set("v", "6"); err != nil {
197+
t.Fatalf("failed to set log level: %v", err)
198+
}
199+
flag.Parse()
200+
191201
tclock := clock.RealClock{}
192202
testcases := []struct {
193203
name string
194204
pluginProvider *perPodPluginProvider
195205
image string
196206
dockerconfig credentialprovider.DockerConfig
207+
wantLog string
197208
}{
198209
{
199210
name: "exact image match, with Registry cache key",
@@ -357,17 +368,162 @@ func Test_Provide(t *testing.T) {
357368
},
358369
},
359370
},
371+
{
372+
name: "[service account mode] sa name required but empty",
373+
pluginProvider: &perPodPluginProvider{
374+
provider: &pluginProvider{
375+
matchImages: []string{"test.registry.io"},
376+
serviceAccountProvider: &serviceAccountProvider{
377+
requireServiceAccount: true,
378+
},
379+
},
380+
podName: "pod-name",
381+
podNamespace: "ns",
382+
},
383+
image: "test.registry.io/foo/bar",
384+
dockerconfig: credentialprovider.DockerConfig{},
385+
wantLog: "Service account name is empty for pod ns/pod-name",
386+
},
387+
{
388+
name: "[service account mode] sa does not have required annotations",
389+
pluginProvider: &perPodPluginProvider{
390+
provider: &pluginProvider{
391+
matchImages: []string{"test.registry.io"},
392+
serviceAccountProvider: &serviceAccountProvider{
393+
requireServiceAccount: true,
394+
requiredServiceAccountAnnotationKeys: []string{
395+
"domain.io/identity-id",
396+
},
397+
getServiceAccountFunc: func(_, _ string) (*v1.ServiceAccount, error) {
398+
return &v1.ServiceAccount{
399+
ObjectMeta: metav1.ObjectMeta{
400+
Name: "sa-name",
401+
Namespace: "ns",
402+
Annotations: map[string]string{},
403+
},
404+
}, nil
405+
},
406+
},
407+
},
408+
podName: "pod-name",
409+
podNamespace: "ns",
410+
serviceAccountName: "sa-name",
411+
},
412+
image: "test.registry.io/foo/bar",
413+
dockerconfig: credentialprovider.DockerConfig{},
414+
wantLog: "Failed to get service account data ns/sa-name: required annotation domain.io/identity-id not found",
415+
},
416+
{
417+
name: "[service account mode] failed to get service account token",
418+
pluginProvider: &perPodPluginProvider{
419+
provider: &pluginProvider{
420+
matchImages: []string{"test.registry.io"},
421+
serviceAccountProvider: &serviceAccountProvider{
422+
requireServiceAccount: true,
423+
requiredServiceAccountAnnotationKeys: []string{
424+
"domain.io/identity-id",
425+
},
426+
optionalServiceAccountAnnotationKeys: []string{
427+
"domain.io/identity-type",
428+
},
429+
getServiceAccountFunc: func(_, _ string) (*v1.ServiceAccount, error) {
430+
return &v1.ServiceAccount{
431+
ObjectMeta: metav1.ObjectMeta{
432+
Name: "sa-name",
433+
Namespace: "ns",
434+
Annotations: map[string]string{
435+
"domain.io/identity-id": "id",
436+
},
437+
},
438+
}, nil
439+
},
440+
getServiceAccountTokenFunc: func(_, _ string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) {
441+
return nil, errors.New("failed to get token")
442+
},
443+
},
444+
},
445+
podName: "pod-name",
446+
podNamespace: "ns",
447+
serviceAccountName: "sa-name",
448+
},
449+
image: "test.registry.io/foo/bar",
450+
dockerconfig: credentialprovider.DockerConfig{},
451+
wantLog: "Error getting service account token ns/sa-name: failed to get token",
452+
},
453+
{
454+
name: "[service account mode] exact image match",
455+
pluginProvider: &perPodPluginProvider{
456+
provider: &pluginProvider{
457+
clock: tclock,
458+
lastCachePurge: tclock.Now(),
459+
matchImages: []string{"test.registry.io"},
460+
cache: cache.NewExpirationStore(cacheKeyFunc, &cacheExpirationPolicy{clock: tclock}),
461+
plugin: &fakeExecPlugin{
462+
cacheKeyType: credentialproviderapi.ImagePluginCacheKeyType,
463+
auth: map[string]credentialproviderapi.AuthConfig{
464+
"test.registry.io/foo/bar": {
465+
Username: "user",
466+
Password: "password",
467+
},
468+
},
469+
},
470+
serviceAccountProvider: &serviceAccountProvider{
471+
requireServiceAccount: true,
472+
requiredServiceAccountAnnotationKeys: []string{
473+
"domain.io/identity-id",
474+
},
475+
optionalServiceAccountAnnotationKeys: []string{
476+
"domain.io/identity-type",
477+
},
478+
getServiceAccountFunc: func(_, _ string) (*v1.ServiceAccount, error) {
479+
return &v1.ServiceAccount{
480+
ObjectMeta: metav1.ObjectMeta{
481+
Name: "sa-name",
482+
Namespace: "ns",
483+
Annotations: map[string]string{
484+
"domain.io/identity-id": "id",
485+
"domain.io/identity-type": "type",
486+
},
487+
},
488+
}, nil
489+
},
490+
getServiceAccountTokenFunc: func(_, _ string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) {
491+
return &authenticationv1.TokenRequest{Status: authenticationv1.TokenRequestStatus{Token: "token"}}, nil
492+
},
493+
},
494+
},
495+
podName: "pod-name",
496+
podNamespace: "ns",
497+
serviceAccountName: "sa-name",
498+
},
499+
image: "test.registry.io/foo/bar",
500+
dockerconfig: credentialprovider.DockerConfig{
501+
"test.registry.io/foo/bar": credentialprovider.DockerConfigEntry{
502+
Username: "user",
503+
Password: "password",
504+
},
505+
},
506+
},
360507
}
361508

362509
for _, testcase := range testcases {
363510
testcase := testcase
364511
t.Run(testcase.name, func(t *testing.T) {
365-
t.Parallel()
366-
dockerconfig := testcase.pluginProvider.Provide(testcase.image)
367-
if !reflect.DeepEqual(dockerconfig, testcase.dockerconfig) {
368-
t.Logf("actual docker config: %v", dockerconfig)
369-
t.Logf("expected docker config: %v", testcase.dockerconfig)
370-
t.Error("unexpected docker config")
512+
var buf bytes.Buffer
513+
klog.SetOutput(&buf)
514+
klog.LogToStderr(false)
515+
defer klog.LogToStderr(true)
516+
517+
if got := testcase.pluginProvider.Provide(testcase.image); !reflect.DeepEqual(got, testcase.dockerconfig) {
518+
t.Errorf("unexpected docker config: %v, expected: %v", got, testcase.dockerconfig)
519+
}
520+
521+
klog.Flush()
522+
klog.SetOutput(&bytes.Buffer{}) // prevent further writes into buf
523+
capturedOutput := buf.String()
524+
525+
if len(testcase.wantLog) > 0 && !strings.Contains(capturedOutput, testcase.wantLog) {
526+
t.Errorf("expected log message %q not found in captured output: %q", testcase.wantLog, capturedOutput)
371527
}
372528
})
373529
}
@@ -1568,3 +1724,30 @@ func TestGenerateCacheKey(t *testing.T) {
15681724
})
15691725
}
15701726
}
1727+
1728+
func TestRequiredAnnotationNotFoundErr(t *testing.T) {
1729+
tests := []struct {
1730+
name string
1731+
err error
1732+
expected bool
1733+
}{
1734+
{
1735+
name: "required annotation not found error",
1736+
err: requiredAnnotationNotFoundError("something"),
1737+
expected: true,
1738+
},
1739+
{
1740+
name: "other error",
1741+
err: errors.New("some other error"),
1742+
expected: false,
1743+
},
1744+
}
1745+
1746+
for _, test := range tests {
1747+
t.Run(test.name, func(t *testing.T) {
1748+
if got := isRequiredAnnotationNotFoundError(test.err); got != test.expected {
1749+
t.Errorf("expected %v, got %v", test.expected, got)
1750+
}
1751+
})
1752+
}
1753+
}

0 commit comments

Comments
 (0)