package cache import ( "context" "fmt" "testing" "time" v1 "k8s.io/api/authorization/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes" fakekube "k8s.io/client-go/kubernetes/fake" clienttesting "k8s.io/client-go/testing" k8scache "k8s.io/client-go/tools/cache" fakeworkclient "open-cluster-management.io/api/client/work/clientset/versioned/fake" workinformers "open-cluster-management.io/api/client/work/informers/externalversions" workapiv1 "open-cluster-management.io/api/work/v1" testingcommon "open-cluster-management.io/ocm/pkg/common/testing" "open-cluster-management.io/ocm/pkg/work/spoke/auth/basic" "open-cluster-management.io/ocm/pkg/work/spoke/spoketesting" ) const ( denyNS = "test-deny" allowNS = "test-allow" clusterName = "cluster1" ) func newExecutorCacheValidator(ctx context.Context, clusterName string, kubeClient kubernetes.Interface, manifestWorkObjects ...runtime.Object) *sarCacheValidator { workClient := fakeworkclient.NewSimpleClientset(manifestWorkObjects...) workInformerFactory := workinformers.NewSharedInformerFactoryWithOptions( workClient, 5*time.Minute, workinformers.WithNamespace(clusterName)) basicValidater := basic.NewSARValidator(nil, kubeClient) validator := NewExecutorCacheValidator(kubeClient, workInformerFactory.Work().V1().ManifestWorks().Lister().ManifestWorks(clusterName), spoketesting.NewFakeRestMapper(), basicValidater, ) go func() { // Wait for cache synced before starting to make sure all manifestworks could be processed k8scache.WaitForNamedCacheSync("ExecutorCacheValidator", ctx.Done(), workInformerFactory.Work().V1().ManifestWorks().Informer().HasSynced) validator.Start(ctx) }() return validator } func TestValidate(t *testing.T) { tests := map[string]struct { executor *workapiv1.ManifestWorkExecutor namespace string name string expect error }{ "executor nil": { executor: nil, expect: nil, }, "unsupported type": { executor: &workapiv1.ManifestWorkExecutor{ Subject: workapiv1.ManifestWorkExecutorSubject{ Type: "test", }, }, expect: fmt.Errorf("only support %s type for the executor", workapiv1.ExecutorSubjectTypeServiceAccount), }, "sa nil": { executor: &workapiv1.ManifestWorkExecutor{ Subject: workapiv1.ManifestWorkExecutorSubject{ Type: workapiv1.ExecutorSubjectTypeServiceAccount, }, }, expect: fmt.Errorf("the executor service account is nil"), }, "forbidden": { executor: &workapiv1.ManifestWorkExecutor{ Subject: workapiv1.ManifestWorkExecutorSubject{ Type: workapiv1.ExecutorSubjectTypeServiceAccount, ServiceAccount: &workapiv1.ManifestWorkSubjectServiceAccount{ Namespace: "test-ns", Name: "test-name", }, }, }, namespace: denyNS, name: "test", expect: fmt.Errorf("not allowed to apply the resource secrets, test-deny test, will try again in 1m0s"), }, "allow": { executor: &workapiv1.ManifestWorkExecutor{ Subject: workapiv1.ManifestWorkExecutorSubject{ Type: workapiv1.ExecutorSubjectTypeServiceAccount, ServiceAccount: &workapiv1.ManifestWorkSubjectServiceAccount{ Namespace: "test-ns", Name: "test-name", }, }, }, namespace: allowNS, name: "test", expect: nil, }, } gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"} kubeClient := fakekube.NewSimpleClientset() kubeClient.PrependReactor("create", "subjectaccessreviews", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { obj := action.(clienttesting.CreateActionImpl).Object.(*v1.SubjectAccessReview) if obj.Spec.ResourceAttributes.Namespace == allowNS { return true, &v1.SubjectAccessReview{ Status: v1.SubjectAccessReviewStatus{ Allowed: true, }, }, nil } if obj.Spec.ResourceAttributes.Namespace == denyNS { return true, &v1.SubjectAccessReview{ Status: v1.SubjectAccessReviewStatus{ Denied: true, }, }, nil } return false, nil, nil }, ) ctx := context.TODO() cacheValidator := newExecutorCacheValidator(ctx, clusterName, kubeClient) for testName, test := range tests { t.Run(testName, func(t *testing.T) { err := cacheValidator.Validate(context.TODO(), test.executor, gvr, test.namespace, test.name, true, nil) if test.expect == nil { if err != nil { t.Errorf("expect nil but got %s", err) } } else if err == nil || err.Error() != test.expect.Error() { t.Errorf("expect %s but got %s", test.expect, err) } }) } } func TestCacheWorks(t *testing.T) { executor := &workapiv1.ManifestWorkExecutor{ Subject: workapiv1.ManifestWorkExecutorSubject{ Type: workapiv1.ExecutorSubjectTypeServiceAccount, ServiceAccount: &workapiv1.ManifestWorkSubjectServiceAccount{ Namespace: "test-ns", Name: "test-name", }, }, } tests := map[string]struct { executor *workapiv1.ManifestWorkExecutor namespace string name string expect error }{ "forbidden": { executor: executor, namespace: denyNS, name: "test", expect: fmt.Errorf("not allowed to apply the resource secrets, test-deny test, will try again in 1m0s"), }, "allow": { executor: executor, namespace: allowNS, name: "test", expect: nil, }, } gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"} kubeClient := fakekube.NewSimpleClientset() kubeClient.PrependReactor("create", "subjectaccessreviews", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) { obj := action.(clienttesting.CreateActionImpl).Object.(*v1.SubjectAccessReview) if obj.Spec.ResourceAttributes.Namespace == allowNS { return true, &v1.SubjectAccessReview{ Status: v1.SubjectAccessReviewStatus{ Allowed: true, }, }, nil } if obj.Spec.ResourceAttributes.Namespace == denyNS { return true, &v1.SubjectAccessReview{ Status: v1.SubjectAccessReviewStatus{ Denied: true, }, }, nil } return false, nil, nil }, ) ctx := context.TODO() work, _ := spoketesting.NewManifestWork(0, testingcommon.NewUnstructured("v1", "Secret", allowNS, "test"), testingcommon.NewUnstructured("v1", "Secret", denyNS, "test"), ) work.Spec.Executor = executor cacheValidator := newExecutorCacheValidator(ctx, clusterName, kubeClient, work) for testName, test := range tests { t.Run(testName, func(t *testing.T) { // call validate 10 times for i := 0; i < 10; i++ { err := cacheValidator.Validate(context.TODO(), test.executor, gvr, test.namespace, test.name, true, nil) if test.expect == nil { if err != nil { t.Errorf("expect nil but got %s", err) } } else if err == nil || err.Error() != test.expect.Error() { t.Errorf("expect %s but got %s", test.expect, err) } } }) } actualSARActions := []clienttesting.Action{} for _, action := range kubeClient.Actions() { if action.GetResource().Resource == "subjectaccessreviews" { actualSARActions = append(actualSARActions, action) } } // the expected 6 comes from: // * 5(allowed sar check for Get, List, Update, Patch, Delete) // * 1(denied sar check for Get; after the first Get check fails, subsequent checks do not need to be checked) if len(actualSARActions) != 6 { t.Errorf("Expected kube client has 6 subject access review action but got %#v", len(actualSARActions)) } }