fix bugs of pvc api

This commit is contained in:
zhangmin
2020-05-13 15:56:13 +08:00
parent f34a4e1c2a
commit 250dd4b065
9 changed files with 506 additions and 38 deletions

View File

@@ -28,6 +28,36 @@ func New(factory informers.InformerFactory) *Handler {
}
}
func (h *Handler) handleGetResources(request *restful.Request, response *restful.Response) {
namespace := request.PathParameter("namespace")
resourceType := request.PathParameter("resources")
name := request.PathParameter("name")
result, err := h.resourceGetterV1alpha3.Get(resourceType, namespace, name)
if err == nil {
response.WriteEntity(result)
return
}
if err != resource.ErrResourceNotSupported {
klog.Error(err)
api.HandleInternalError(response, nil, err)
return
}
// fallback to v1alpha2
resultV1alpha2, err := h.resourcesGetterV1alpha2.GetResource(namespace, resourceType, name)
if err != nil {
klog.Error(err)
api.HandleInternalError(response, nil, err)
return
}
response.WriteEntity(resultV1alpha2)
}
// handleListResources retrieves resources
func (h *Handler) handleListResources(request *restful.Request, response *restful.Response) {
query := query.ParseQueryParameter(request)

View File

@@ -70,6 +70,15 @@ func AddToContainer(c *restful.Container, informerFactory informers.InformerFact
Param(webservice.QueryParameter(query.ParameterOrderBy, "sort parameters, e.g. orderBy=createTime")).
Returns(http.StatusOK, ok, api.ListResult{}))
webservice.Route(webservice.GET("/namespaces/{namespace}/{resources}/{name}").
To(handler.handleGetResources).
Metadata(restfulspec.KeyOpenAPITags, []string{tagNamespacedResource}).
Doc("Namespace level get resource query").
Param(webservice.PathParameter("namespace", "the name of the project")).
Param(webservice.PathParameter("resources", "namespace level resource type, e.g. pods,jobs,configmaps,services.")).
Param(webservice.PathParameter("name", "the name of resource")).
Returns(http.StatusOK, ok, api.ListResult{}))
webservice.Route(webservice.GET("/components").
To(handler.handleGetComponents).
Metadata(restfulspec.KeyOpenAPITags, []string{tagComponentStatus}).

View File

@@ -0,0 +1,140 @@
/*
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 persistentvolumeclaim
import (
snapshotinformers "github.com/kubernetes-csi/external-snapshotter/v2/pkg/client/informers/externalversions"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"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"
"strconv"
"strings"
)
const (
storageClassName = "storageClassName"
annotationInUse = "kubesphere.io/in-use"
annotationAllowSnapshot = "kubesphere.io/allow-snapshot"
annotationStorageProvisioner = "volume.beta.kubernetes.io/storage-provisioner"
)
type persistentVolumeClaimGetter struct {
informers informers.SharedInformerFactory
snapshotInformers snapshotinformers.SharedInformerFactory
}
func New(informer informers.SharedInformerFactory, snapshotInformer snapshotinformers.SharedInformerFactory) v1alpha3.Interface {
return &persistentVolumeClaimGetter{informers: informer, snapshotInformers: snapshotInformer}
}
func (p *persistentVolumeClaimGetter) Get(namespace, name string) (runtime.Object, error) {
pvc, err := p.informers.Core().V1().PersistentVolumeClaims().Lister().PersistentVolumeClaims(namespace).Get(name)
if err != nil {
return pvc, err
}
p.annotatePVC(pvc)
return pvc, nil
}
func (p *persistentVolumeClaimGetter) List(namespace string, query *query.Query) (*api.ListResult, error) {
all, err := p.informers.Core().V1().PersistentVolumeClaims().Lister().PersistentVolumeClaims(namespace).List(query.Selector())
if err != nil {
return nil, err
}
var result []runtime.Object
for _, pvc := range all {
p.annotatePVC(pvc)
result = append(result, pvc)
}
return v1alpha3.DefaultList(result, query, p.compare, p.filter), nil
}
func (p *persistentVolumeClaimGetter) compare(left, right runtime.Object, field query.Field) bool {
leftSnapshot, ok := left.(*v1.PersistentVolumeClaim)
if !ok {
return false
}
rightSnapshot, ok := right.(*v1.PersistentVolumeClaim)
if !ok {
return false
}
return v1alpha3.DefaultObjectMetaCompare(leftSnapshot.ObjectMeta, rightSnapshot.ObjectMeta, field)
}
func (p *persistentVolumeClaimGetter) filter(object runtime.Object, filter query.Filter) bool {
pvc, ok := object.(*v1.PersistentVolumeClaim)
if !ok {
return false
}
switch filter.Field {
case query.FieldStatus:
return strings.EqualFold(string(pvc.Status.Phase), string(filter.Value))
case storageClassName:
return pvc.Spec.StorageClassName != nil && *pvc.Spec.StorageClassName == string(filter.Value)
default:
return v1alpha3.DefaultObjectMetaFilter(pvc.ObjectMeta, filter)
}
}
func (p *persistentVolumeClaimGetter) annotatePVC(pvc *v1.PersistentVolumeClaim) {
inUse := p.countPods(pvc.Name, pvc.Namespace)
isSnapshotAllow := p.isSnapshotAllowed(pvc.GetAnnotations()[annotationStorageProvisioner])
if pvc.Annotations == nil {
pvc.Annotations = make(map[string]string)
}
pvc.Annotations[annotationInUse] = strconv.FormatBool(inUse)
pvc.Annotations[annotationAllowSnapshot] = strconv.FormatBool(isSnapshotAllow)
}
func (p *persistentVolumeClaimGetter) countPods(name, namespace string) bool {
pods, err := p.informers.Core().V1().Pods().Lister().Pods(namespace).List(labels.Everything())
if err != nil {
return false
}
for _, pod := range pods {
for _, pvc := range pod.Spec.Volumes {
if pvc.PersistentVolumeClaim != nil && pvc.PersistentVolumeClaim.ClaimName == name {
return true
}
}
}
return false
}
func (p *persistentVolumeClaimGetter) isSnapshotAllowed(provisioner string) bool {
if len(provisioner) == 0 {
return false
}
volumeSnapshotClasses, err := p.snapshotInformers.Snapshot().V1beta1().VolumeSnapshotClasses().Lister().List(labels.Everything())
if err != nil {
return false
}
for _, volumeSnapshotClass := range volumeSnapshotClasses {
if volumeSnapshotClass.Driver == provisioner {
return true
}
}
return false
}

View File

@@ -0,0 +1,251 @@
/*
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 persistentvolumeclaim
import (
"github.com/google/go-cmp/cmp"
snapshot "github.com/kubernetes-csi/external-snapshotter/v2/pkg/apis/volumesnapshot/v1beta1"
snapshotefakeclient "github.com/kubernetes-csi/external-snapshotter/v2/pkg/client/clientset/versioned/fake"
snapshotinformers "github.com/kubernetes-csi/external-snapshotter/v2/pkg/client/informers/externalversions"
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"
"testing"
)
var (
testStorageClassName = "test-csi"
)
func TestListPods(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{}{pvc3, pvc2, pvc1},
TotalItems: len(persistentVolumeClaims),
},
nil,
},
{
"test status 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"),
query.FieldStatus: query.Value(pvc1.Status.Phase),
},
},
&api.ListResult{
Items: []interface{}{pvc1},
TotalItems: 1,
},
nil,
},
{
"test StorageClass filter and allow snapshot",
"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"),
query.Field(storageClassName): query.Value(*pvc2.Spec.StorageClassName),
},
},
&api.ListResult{
Items: []interface{}{pvcGet2},
TotalItems: 1,
},
nil,
},
{
"test pvc in use",
"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"),
query.FieldName: query.Value(pvc3.Name),
},
},
&api.ListResult{
Items: []interface{}{pvcGet3},
TotalItems: 1,
},
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("[%s] %T differ (-got, +want): %s", test.description, test.expected, diff)
}
}
}
var (
pvc1 = &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "pvc-1",
Namespace: "default",
},
Status: corev1.PersistentVolumeClaimStatus{
Phase: corev1.ClaimPending,
},
}
pvc2 = &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "pvc-2",
Namespace: "default",
Annotations: map[string]string{
annotationStorageProvisioner: testStorageClassName,
},
},
Spec: corev1.PersistentVolumeClaimSpec{
StorageClassName: &testStorageClassName,
},
}
pvcGet2 = &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "pvc-2",
Namespace: "default",
Annotations: map[string]string{
annotationInUse: "false",
annotationAllowSnapshot: "true",
annotationStorageProvisioner: testStorageClassName,
},
},
Spec: corev1.PersistentVolumeClaimSpec{
StorageClassName: &testStorageClassName,
},
}
pvc3 = &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "pvc-3",
Namespace: "default",
},
}
pvcGet3 = &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "pvc-3",
Namespace: "default",
Annotations: map[string]string{
annotationInUse: "true",
annotationAllowSnapshot: "false",
},
},
}
pod1 = &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1",
Namespace: "default",
},
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: "data",
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: pvc3.Name,
},
},
},
},
},
}
volumeSnapshotClass1 = &snapshot.VolumeSnapshotClass{
ObjectMeta: metav1.ObjectMeta{
Name: "VolumeSnapshotClass-1",
Namespace: "default",
},
Driver: testStorageClassName,
}
persistentVolumeClaims = []interface{}{pvc1, pvc2, pvc3}
pods = []interface{}{pod1}
volumeSnapshotClasses = []interface{}{volumeSnapshotClass1}
)
func prepare() v1alpha3.Interface {
client := fake.NewSimpleClientset()
informer := informers.NewSharedInformerFactory(client, 0)
snapshotClient := snapshotefakeclient.NewSimpleClientset()
snapshotInformers := snapshotinformers.NewSharedInformerFactory(snapshotClient, 0)
for _, pvc := range persistentVolumeClaims {
_ = informer.Core().V1().PersistentVolumeClaims().Informer().GetIndexer().Add(pvc)
}
for _, pod := range pods {
_ = informer.Core().V1().Pods().Informer().GetIndexer().Add(pod)
}
for _, volumeSnapshotClass := range volumeSnapshotClasses {
_ = snapshotInformers.Snapshot().V1beta1().VolumeSnapshotClasses().Informer().GetIndexer().Add(volumeSnapshotClass)
}
return New(informer, snapshotInformers)
}

View File

@@ -27,6 +27,12 @@ import (
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3"
)
const (
filedNameName = "nodeName"
filedPVCName = "pvcName"
filedServiceName = "serviceName"
)
type podsGetter struct {
informer informers.SharedInformerFactory
}
@@ -35,13 +41,13 @@ func New(sharedInformers informers.SharedInformerFactory) v1alpha3.Interface {
return &podsGetter{informer: sharedInformers}
}
func (d *podsGetter) Get(namespace, name string) (runtime.Object, error) {
return d.informer.Core().V1().Pods().Lister().Pods(namespace).Get(name)
func (p *podsGetter) Get(namespace, name string) (runtime.Object, error) {
return p.informer.Core().V1().Pods().Lister().Pods(namespace).Get(name)
}
func (d *podsGetter) List(namespace string, query *query.Query) (*api.ListResult, error) {
func (p *podsGetter) List(namespace string, query *query.Query) (*api.ListResult, error) {
all, err := d.informer.Core().V1().Pods().Lister().Pods(namespace).List(query.Selector())
all, err := p.informer.Core().V1().Pods().Lister().Pods(namespace).List(query.Selector())
if err != nil {
return nil, err
}
@@ -51,10 +57,10 @@ func (d *podsGetter) List(namespace string, query *query.Query) (*api.ListResult
result = append(result, app)
}
return v1alpha3.DefaultList(result, query, d.compare, d.filter), nil
return v1alpha3.DefaultList(result, query, p.compare, p.filter), nil
}
func (d *podsGetter) compare(left runtime.Object, right runtime.Object, field query.Field) bool {
func (p *podsGetter) compare(left runtime.Object, right runtime.Object, field query.Field) bool {
leftPod, ok := left.(*corev1.Pod)
if !ok {
@@ -69,30 +75,25 @@ func (d *podsGetter) compare(left runtime.Object, right runtime.Object, field qu
return v1alpha3.DefaultObjectMetaCompare(leftPod.ObjectMeta, rightPod.ObjectMeta, field)
}
func (d *podsGetter) filter(object runtime.Object, filter query.Filter) bool {
func (p *podsGetter) filter(object runtime.Object, filter query.Filter) bool {
pod, ok := object.(*corev1.Pod)
if !ok {
return false
}
switch filter.Field {
case "nodeName":
if pod.Spec.NodeName != string(filter.Value) {
return false
}
case "pvcName":
if !d.podBindPVC(pod, string(filter.Value)) {
return false
}
case "serviceName":
if !d.podBelongToService(pod, string(filter.Value)) {
return false
}
case filedNameName:
return pod.Spec.NodeName == string(filter.Value)
case filedPVCName:
return p.podBindPVC(pod, string(filter.Value))
case filedServiceName:
return p.podBelongToService(pod, string(filter.Value))
default:
return v1alpha3.DefaultObjectMetaFilter(pod.ObjectMeta, filter)
}
return v1alpha3.DefaultObjectMetaFilter(pod.ObjectMeta, filter)
}
func (s *podsGetter) podBindPVC(item *corev1.Pod, pvcName string) bool {
func (p *podsGetter) podBindPVC(item *corev1.Pod, pvcName string) bool {
for _, v := range item.Spec.Volumes {
if v.VolumeSource.PersistentVolumeClaim != nil &&
v.VolumeSource.PersistentVolumeClaim.ClaimName == pvcName {
@@ -102,8 +103,8 @@ func (s *podsGetter) podBindPVC(item *corev1.Pod, pvcName string) bool {
return false
}
func (s *podsGetter) podBelongToService(item *corev1.Pod, serviceName string) bool {
service, err := s.informer.Core().V1().Services().Lister().Services(item.Namespace).Get(serviceName)
func (p *podsGetter) podBelongToService(item *corev1.Pod, serviceName string) bool {
service, err := p.informer.Core().V1().Services().Lister().Services(item.Namespace).Get(serviceName)
if err != nil {
return false
}

View File

@@ -33,11 +33,32 @@ func TestListPods(t *testing.T) {
Filters: map[query.Field]query.Value{query.FieldNamespace: query.Value("default")},
},
&api.ListResult{
Items: []interface{}{foo3, foo2, foo1},
Items: []interface{}{foo4, foo3, foo2, foo1},
TotalItems: len(pods),
},
nil,
},
{
"test pvcName 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"),
filedPVCName: query.Value(foo4.Spec.Volumes[0].PersistentVolumeClaim.ClaimName),
},
},
&api.ListResult{
Items: []interface{}{foo4},
TotalItems: 1,
},
nil,
},
}
getter := prepare()
@@ -75,7 +96,26 @@ var (
Namespace: "default",
},
}
pods = []interface{}{foo1, foo2, foo3}
foo4 = &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo4",
Namespace: "default",
},
Spec: corev1.PodSpec{
Volumes: []corev1.Volume{
{
Name: "data",
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: "pvc-1",
ReadOnly: false,
},
},
},
},
},
}
pods = []interface{}{foo1, foo2, foo3, foo4}
)
func prepare() v1alpha3.Interface {
@@ -84,7 +124,7 @@ func prepare() v1alpha3.Interface {
informer := informers.NewSharedInformerFactory(client, 0)
for _, pod := range pods {
informer.Core().V1().Pods().Informer().GetIndexer().Add(pod)
_ = informer.Core().V1().Pods().Informer().GetIndexer().Add(pod)
}
return New(informer)

View File

@@ -39,6 +39,7 @@ import (
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/globalrole"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/namespace"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/networkpolicy"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/persistentvolumeclaim"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/pod"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/role"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/user"
@@ -68,6 +69,7 @@ func NewResourceGetter(factory informers.InformerFactory) *ResourceGetter {
getters[iamv1alpha2.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralUser)] = user.New(factory.KubeSphereSharedInformerFactory())
getters[rbacv1.SchemeGroupVersion.WithResource("roles")] = role.New(factory.KubernetesSharedInformerFactory())
getters[rbacv1.SchemeGroupVersion.WithResource("clusterroles")] = clusterrole.New(factory.KubernetesSharedInformerFactory())
getters[schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumeclaims"}] = persistentvolumeclaim.New(factory.KubernetesSharedInformerFactory(), factory.SnapshotSharedInformerFactory())
getters[snapshotv1beta1.SchemeGroupVersion.WithResource("volumesnapshots")] = volumesnapshot.New(factory.SnapshotSharedInformerFactory())
getters[schema.GroupVersionResource{Group: "cluster.kubesphere.io", Version: "v1alpha1", Resource: "clusters"}] = cluster.New(factory.KubeSphereSharedInformerFactory())
getters[schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"}] = customresourcedefinition.New(factory.ApiExtensionSharedInformerFactory())

View File

@@ -20,7 +20,6 @@ package volumesnapshot
import (
"github.com/kubernetes-csi/external-snapshotter/v2/pkg/apis/volumesnapshot/v1beta1"
"github.com/kubernetes-csi/external-snapshotter/v2/pkg/client/informers/externalversions"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"kubesphere.io/kubesphere/pkg/api"
"kubesphere.io/kubesphere/pkg/apiserver/query"
@@ -36,26 +35,26 @@ const (
)
type volumeSnapshotGetter struct {
informer externalversions.SharedInformerFactory
informers externalversions.SharedInformerFactory
}
func New(informer externalversions.SharedInformerFactory) v1alpha3.Interface {
return &volumeSnapshotGetter{informer: informer}
return &volumeSnapshotGetter{informers: informer}
}
func (v *volumeSnapshotGetter) Get(namespace, name string) (runtime.Object, error) {
return v.informer.Snapshot().V1beta1().VolumeSnapshots().Lister().VolumeSnapshots(namespace).Get(name)
return v.informers.Snapshot().V1beta1().VolumeSnapshots().Lister().VolumeSnapshots(namespace).Get(name)
}
func (v *volumeSnapshotGetter) List(namespace string, query *query.Query) (*api.ListResult, error) {
all, err := v.listVolumeSnapshots(namespace, query.Selector())
all, err := v.informers.Snapshot().V1beta1().VolumeSnapshots().Lister().VolumeSnapshots(namespace).List(query.Selector())
if err != nil {
return nil, err
}
var result []runtime.Object
for _, app := range all {
result = append(result, app)
for _, snapshot := range all {
result = append(result, snapshot)
}
return v1alpha3.DefaultList(result, query, v.compare, v.filter), nil
@@ -93,10 +92,6 @@ func (v *volumeSnapshotGetter) filter(object runtime.Object, filter query.Filter
}
}
func (v *volumeSnapshotGetter) listVolumeSnapshots(namespace string, selector labels.Selector) (ret []*v1beta1.VolumeSnapshot, err error) {
return v.informer.Snapshot().V1beta1().VolumeSnapshots().Lister().VolumeSnapshots(namespace).List(selector)
}
func snapshotStatus(item *v1beta1.VolumeSnapshot) string {
status := statusCreating
if *item.Status.ReadyToUse {

View File

@@ -290,7 +290,7 @@ func prepare() Interface {
k8sClient := fakek8s.NewSimpleClientset()
istioClient := fakeistio.NewSimpleClientset()
appClient := fakeapp.NewSimpleClientset()
fakeInformerFactory := informers.NewInformerFactories(k8sClient, ksClient, istioClient, appClient)
fakeInformerFactory := informers.NewInformerFactories(k8sClient, ksClient, istioClient, appClient, nil, nil)
for _, workspace := range workspaces {
fakeInformerFactory.KubeSphereSharedInformerFactory().Tenant().V1alpha1().