diff --git a/cmd/controller-manager/app/server.go b/cmd/controller-manager/app/server.go index e0fe5df83..d6c85abb4 100644 --- a/cmd/controller-manager/app/server.go +++ b/cmd/controller-manager/app/server.go @@ -18,6 +18,8 @@ package app import ( "fmt" + "os" + "github.com/spf13/cobra" utilerrors "k8s.io/apimachinery/pkg/util/errors" cliflag "k8s.io/component-base/cli/flag" @@ -29,6 +31,7 @@ import ( appcontroller "kubesphere.io/kubesphere/pkg/controller/application" "kubesphere.io/kubesphere/pkg/controller/namespace" "kubesphere.io/kubesphere/pkg/controller/network/webhooks" + "kubesphere.io/kubesphere/pkg/controller/serviceaccount" "kubesphere.io/kubesphere/pkg/controller/user" "kubesphere.io/kubesphere/pkg/controller/workspace" "kubesphere.io/kubesphere/pkg/controller/workspacerole" @@ -43,7 +46,6 @@ import ( "kubesphere.io/kubesphere/pkg/simple/client/s3" "kubesphere.io/kubesphere/pkg/utils/metrics" "kubesphere.io/kubesphere/pkg/utils/term" - "os" application "sigs.k8s.io/application/controllers" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -232,6 +234,12 @@ func run(s *options.KubeSphereControllerManagerOptions, stopCh <-chan struct{}) klog.Fatal("Unable to create application controller") } + saReconciler := &serviceaccount.Reconciler{} + + if err = saReconciler.SetupWithManager(mgr); err != nil { + klog.Fatal("Unable to create ServiceAccount controller") + } + // TODO(jeff): refactor config with CRD servicemeshEnabled := s.ServiceMeshOptions != nil && len(s.ServiceMeshOptions.IstioPilotHost) != 0 if err = addControllers(mgr, diff --git a/pkg/api/types.go b/pkg/api/types.go index 5e10391e0..544901ccb 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -88,6 +88,7 @@ var SupportedGroupVersionResources = map[ClientType][]schema.GroupVersionResourc {Group: "", Version: "v1", Resource: "persistentvolumeclaims"}, {Group: "", Version: "v1", Resource: "secrets"}, {Group: "", Version: "v1", Resource: "configmaps"}, + {Group: "", Version: "v1", Resource: "serviceaccounts"}, {Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "roles"}, {Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "rolebindings"}, diff --git a/pkg/apis/iam/v1alpha2/types.go b/pkg/apis/iam/v1alpha2/types.go index 4d06f3ccc..03c1686b3 100644 --- a/pkg/apis/iam/v1alpha2/types.go +++ b/pkg/apis/iam/v1alpha2/types.go @@ -65,6 +65,7 @@ const ( UserReferenceLabel = "iam.kubesphere.io/user-ref" IdentifyProviderLabel = "iam.kubesphere.io/identify-provider" OriginUIDLabel = "iam.kubesphere.io/origin-uid" + ServiceAccountReferenceLabel = "iam.kubesphere.io/serviceaccount-ref" FieldEmail = "email" ExtraEmail = FieldEmail ExtraIdentityProvider = "idp" diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index f9a6735fa..00641b1d8 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -20,6 +20,12 @@ import ( "bytes" "context" "fmt" + "net/http" + rt "runtime" + "time" + + "strconv" + "github.com/emicklei/go-restful" "k8s.io/apimachinery/pkg/runtime/schema" urlruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -86,11 +92,7 @@ import ( "kubesphere.io/kubesphere/pkg/simple/client/sonarqube" "kubesphere.io/kubesphere/pkg/utils/metrics" utilnet "kubesphere.io/kubesphere/pkg/utils/net" - "net/http" - rt "runtime" runtimecache "sigs.k8s.io/controller-runtime/pkg/cache" - "strconv" - "time" ) const ( @@ -383,6 +385,8 @@ func (s *APIServer) waitForResourceSync(stopCh <-chan struct{}) error { {Group: "", Version: "v1", Resource: "persistentvolumeclaims"}, {Group: "", Version: "v1", Resource: "secrets"}, {Group: "", Version: "v1", Resource: "configmaps"}, + {Group: "", Version: "v1", Resource: "serviceaccounts"}, + {Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "roles"}, {Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "rolebindings"}, {Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "clusterroles"}, diff --git a/pkg/controller/serviceaccount/serviceaccount_controller.go b/pkg/controller/serviceaccount/serviceaccount_controller.go new file mode 100644 index 000000000..523cc20f2 --- /dev/null +++ b/pkg/controller/serviceaccount/serviceaccount_controller.go @@ -0,0 +1,134 @@ +/* +Copyright 2020 The KubeSphere 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 serviceaccount + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2" + controllerutils "kubesphere.io/kubesphere/pkg/controller/utils/controller" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + controllerName = "serviceaccount-controller" +) + +// Reconciler reconciles a ServiceAccount object +type Reconciler struct { + client.Client + logger logr.Logger + recorder record.EventRecorder + scheme *runtime.Scheme +} + +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + if r.Client == nil { + r.Client = mgr.GetClient() + } + if r.logger == nil { + r.logger = ctrl.Log.WithName("controllers").WithName(controllerName) + } + if r.scheme == nil { + r.scheme = mgr.GetScheme() + } + if r.recorder == nil { + r.recorder = mgr.GetEventRecorderFor(controllerName) + } + return ctrl.NewControllerManagedBy(mgr). + Named(controllerName). + For(&corev1.ServiceAccount{}). + Complete(r) +} + +// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=get;list;watch +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=get;list;watch;create;update;patch;delete +func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + logger := r.logger.WithValues("serivceaccount", req.NamespacedName) + ctx := context.Background() + sa := &corev1.ServiceAccount{} + if err := r.Get(ctx, req.NamespacedName, sa); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + if _, ok := sa.Annotations[iamv1alpha2.RoleAnnotation]; ok && sa.ObjectMeta.DeletionTimestamp.IsZero() { + if err := r.CreateOrUpdateRoleBinding(ctx, logger, sa); err != nil { + r.recorder.Event(sa, corev1.EventTypeWarning, controllerutils.FailedSynced, err.Error()) + return ctrl.Result{}, err + } + r.recorder.Event(sa, corev1.EventTypeNormal, controllerutils.SuccessSynced, controllerutils.MessageResourceSynced) + } + return ctrl.Result{}, nil +} + +func (r *Reconciler) CreateOrUpdateRoleBinding(ctx context.Context, logger logr.Logger, sa *corev1.ServiceAccount) error { + roleName := sa.Annotations[iamv1alpha2.RoleAnnotation] + if roleName == "" { + return nil + } + var role rbacv1.Role + if err := r.Get(ctx, types.NamespacedName{Name: roleName, Namespace: sa.Namespace}, &role); err != nil { + return err + } + + // Delete existing rolebindings. + saRoleBinding := &rbacv1.RoleBinding{} + _ = r.Client.DeleteAllOf(ctx, saRoleBinding, client.InNamespace(sa.Namespace), client.MatchingLabels{iamv1alpha2.ServiceAccountReferenceLabel: sa.Name}) + + saRoleBinding = &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: fmt.Sprintf("%s-%s-", sa.Name, roleName), + Labels: map[string]string{iamv1alpha2.ServiceAccountReferenceLabel: sa.Name}, + Namespace: sa.Namespace, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: iamv1alpha2.ResourceKindRole, + Name: roleName, + }, + Subjects: []rbacv1.Subject{ + { + Name: sa.Name, + Kind: rbacv1.ServiceAccountKind, + Namespace: sa.Namespace, + }, + }, + } + + if err := controllerutil.SetControllerReference(sa, saRoleBinding, r.scheme); err != nil { + logger.Error(err, "set controller reference failed") + return err + } + + logger.V(4).Info("create ServiceAccount rolebinding", "ServiceAccount", sa.Name) + if err := r.Client.Create(ctx, saRoleBinding); err != nil { + logger.Error(err, "create rolebinding failed") + return err + } + return nil +} diff --git a/pkg/controller/serviceaccount/serviceaccount_controller_suite_test.go b/pkg/controller/serviceaccount/serviceaccount_controller_suite_test.go new file mode 100644 index 000000000..f394cc4b3 --- /dev/null +++ b/pkg/controller/serviceaccount/serviceaccount_controller_suite_test.go @@ -0,0 +1,101 @@ +/* +Copyright 2019 The KubeSphere 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 serviceaccount + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/onsi/gomega/gexec" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/klog/klogr" + "kubesphere.io/kubesphere/pkg/apis" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/envtest/printer" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var k8sClient client.Client +var k8sManager ctrl.Manager +var testEnv *envtest.Environment + +func TestMain(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecsWithDefaultAndCustomReporters(t, + "ServiceAccount Controller Test Suite", + []Reporter{printer.NewlineReporter{}}) +} + +var _ = BeforeSuite(func(done Done) { + logf.SetLogger(klogr.New()) + + By("bootstrapping test environment") + t := true + if os.Getenv("TEST_USE_EXISTING_CLUSTER") == "true" { + testEnv = &envtest.Environment{ + UseExistingCluster: &t, + } + } else { + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crds")}, + AttachControlPlaneOutput: false, + } + } + + cfg, err := testEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + err = apis.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + MetricsBindAddress: "0", + }) + Expect(err).ToNot(HaveOccurred()) + + err = (&Reconciler{}).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + err = k8sManager.Start(ctrl.SetupSignalHandler()) + Expect(err).ToNot(HaveOccurred()) + }() + + k8sClient = k8sManager.GetClient() + Expect(k8sClient).ToNot(BeNil()) + + close(done) +}, 160) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + gexec.KillAndWait(5 * time.Second) + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) diff --git a/pkg/controller/serviceaccount/serviceaccount_controller_test.go b/pkg/controller/serviceaccount/serviceaccount_controller_test.go new file mode 100644 index 000000000..2d65ca3d7 --- /dev/null +++ b/pkg/controller/serviceaccount/serviceaccount_controller_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2019 The KubeSphere 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 serviceaccount + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("ServiceAccount", func() { + const ( + saName = "test-serviceaccount" + saNamespace = "default" + saRole = "test-role" + timeout = time.Second * 30 + interval = time.Second * 1 + ) + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: saRole, + Namespace: saNamespace, + }, + } + BeforeEach(func() { + // Create workspace + Expect(k8sClient.Create(context.Background(), role)).Should(Succeed()) + }) + + // Add Tests for OpenAPI validation (or additonal CRD features) specified in + // your API definition. + // Avoid adding tests for vanilla CRUD operations because they would + // test Kubernetes API server, which isn't the goal here. + Context("ServiceAccount Controller", func() { + It("Should create successfully", func() { + ctx := context.Background() + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: saName, + Namespace: saNamespace, + Annotations: map[string]string{iamv1alpha2.RoleAnnotation: saRole}, + }, + } + + By("Expecting to create serviceaccount successfully") + Expect(k8sClient.Create(ctx, sa)).Should(Succeed()) + expectedSa := &corev1.ServiceAccount{} + Eventually(func() bool { + k8sClient.Get(ctx, types.NamespacedName{Name: sa.Name, Namespace: sa.Namespace}, expectedSa) + return !expectedSa.CreationTimestamp.IsZero() + }, timeout, interval).Should(BeTrue()) + + By("Expecting to bind role successfully") + rolebindings := &rbacv1.RoleBindingList{} + Eventually(func() bool { + k8sClient.List(ctx, rolebindings, client.InNamespace(sa.Namespace), client.MatchingLabels{iamv1alpha2.ServiceAccountReferenceLabel: sa.Name}) + return len(rolebindings.Items) == 1 + }, timeout, interval).Should(BeTrue()) + }) + }) +}) diff --git a/pkg/controller/utils/controller/basecontroller.go b/pkg/controller/utils/controller/basecontroller.go index b0976aa0c..5d6bcd32e 100644 --- a/pkg/controller/utils/controller/basecontroller.go +++ b/pkg/controller/utils/controller/basecontroller.go @@ -30,6 +30,9 @@ import ( const ( // SuccessSynced is used as part of the Event 'reason' when a Foo is synced SuccessSynced = "Synced" + + // FailedSynced is used as part of the Event 'reason' when a Foo is not synced + FailedSynced = "FailedSync" // is synced successfully MessageResourceSynced = "Synced successfully" ) diff --git a/pkg/models/resources/v1alpha3/resource/resource.go b/pkg/models/resources/v1alpha3/resource/resource.go index df09fe0dd..14bb881ca 100644 --- a/pkg/models/resources/v1alpha3/resource/resource.go +++ b/pkg/models/resources/v1alpha3/resource/resource.go @@ -18,6 +18,7 @@ package resource import ( "errors" + snapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v3/apis/volumesnapshot/v1beta1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/runtime" @@ -66,6 +67,7 @@ import ( "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/role" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/rolebinding" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/service" + "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/serviceaccount" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/statefulset" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/user" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/volumesnapshot" @@ -93,6 +95,7 @@ func NewResourceGetter(factory informers.InformerFactory, cache cache.Cache) *Re getters[schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}] = configmap.New(factory.KubernetesSharedInformerFactory()) getters[schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}] = pod.New(factory.KubernetesSharedInformerFactory()) getters[schema.GroupVersionResource{Group: "", Version: "v1", Resource: "nodes"}] = node.New(factory.KubernetesSharedInformerFactory()) + getters[schema.GroupVersionResource{Group: "", Version: "v1", Resource: "serviceaccounts"}] = serviceaccount.New(factory.KubernetesSharedInformerFactory()) getters[schema.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "ingresses"}] = ingress.New(factory.KubernetesSharedInformerFactory()) getters[schema.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "networkpolicies"}] = networkpolicy.New(factory.KubernetesSharedInformerFactory()) getters[schema.GroupVersionResource{Group: "batch", Version: "v1", Resource: "jobs"}] = job.New(factory.KubernetesSharedInformerFactory()) diff --git a/pkg/models/resources/v1alpha3/serviceaccount/serviceaccount.go b/pkg/models/resources/v1alpha3/serviceaccount/serviceaccount.go new file mode 100644 index 000000000..f310c6655 --- /dev/null +++ b/pkg/models/resources/v1alpha3/serviceaccount/serviceaccount.go @@ -0,0 +1,76 @@ +/* +Copyright 2020 The KubeSphere 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 serviceaccount + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/informers" + "kubesphere.io/kubesphere/pkg/api" + "kubesphere.io/kubesphere/pkg/apiserver/query" + "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3" +) + +type serviceaccountsGetter struct { + informer informers.SharedInformerFactory +} + +func New(sharedInformers informers.SharedInformerFactory) v1alpha3.Interface { + return &serviceaccountsGetter{informer: sharedInformers} +} + +func (d *serviceaccountsGetter) Get(namespace, name string) (runtime.Object, error) { + return d.informer.Core().V1().ServiceAccounts().Lister().ServiceAccounts(namespace).Get(name) +} + +func (d *serviceaccountsGetter) List(namespace string, query *query.Query) (*api.ListResult, error) { + serviceaccounts, err := d.informer.Core().V1().ServiceAccounts().Lister().ServiceAccounts(namespace).List(query.Selector()) + if err != nil { + return nil, err + } + + var result []runtime.Object + for _, serviceaccount := range serviceaccounts { + result = append(result, serviceaccount) + } + + return v1alpha3.DefaultList(result, query, d.compare, d.filter), nil +} + +func (d *serviceaccountsGetter) compare(left runtime.Object, right runtime.Object, field query.Field) bool { + + leftCM, ok := left.(*corev1.ServiceAccount) + if !ok { + return false + } + + rightCM, ok := right.(*corev1.ServiceAccount) + if !ok { + return false + } + + return v1alpha3.DefaultObjectMetaCompare(leftCM.ObjectMeta, rightCM.ObjectMeta, field) +} + +func (d *serviceaccountsGetter) filter(object runtime.Object, filter query.Filter) bool { + serviceAccount, ok := object.(*corev1.ServiceAccount) + if !ok { + return false + } + + return v1alpha3.DefaultObjectMetaFilter(serviceAccount.ObjectMeta, filter) +} diff --git a/pkg/models/resources/v1alpha3/serviceaccount/serviceaccount_test.go b/pkg/models/resources/v1alpha3/serviceaccount/serviceaccount_test.go new file mode 100644 index 000000000..b1e10f549 --- /dev/null +++ b/pkg/models/resources/v1alpha3/serviceaccount/serviceaccount_test.go @@ -0,0 +1,108 @@ +/* +Copyright 2020 The KubeSphere 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 serviceaccount + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes/fake" + "kubesphere.io/kubesphere/pkg/api" + "kubesphere.io/kubesphere/pkg/apiserver/query" + "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3" +) + +func TestListServiceAccounts(t *testing.T) { + tests := []struct { + description string + namespace string + query *query.Query + expected *api.ListResult + expectedErr error + }{ + { + "test name filter", + "default", + &query.Query{ + Pagination: &query.Pagination{ + Limit: 10, + Offset: 0, + }, + SortBy: query.FieldName, + Ascending: false, + Filters: map[query.Field]query.Value{query.FieldNamespace: query.Value("default")}, + }, + &api.ListResult{ + Items: []interface{}{foo3, foo2, foo1}, + TotalItems: len(serviceAccounts), + }, + nil, + }, + } + + getter := prepare() + + for _, test := range tests { + got, err := getter.List(test.namespace, test.query) + if test.expectedErr != nil && err != test.expectedErr { + t.Errorf("expected error, got nothing") + } else if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(got, test.expected); diff != "" { + t.Errorf("%T differ (-got, +want): %s", test.expected, diff) + } + } +} + +var ( + foo1 = &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo1", + Namespace: "default", + }, + } + foo2 = &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo2", + Namespace: "default", + }, + } + foo3 = &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo3", + Namespace: "default", + }, + } + serviceAccounts = []interface{}{foo1, foo2, foo3} +) + +func prepare() v1alpha3.Interface { + + client := fake.NewSimpleClientset() + informer := informers.NewSharedInformerFactory(client, 0) + + for _, serviceAccount := range serviceAccounts { + informer.Core().V1().ServiceAccounts().Informer().GetIndexer().Add(serviceAccount) + } + + return New(informer) +}