diff --git a/cmd/ks-apiserver/app/options/options.go b/cmd/ks-apiserver/app/options/options.go index 4989e126b..ce883872a 100644 --- a/cmd/ks-apiserver/app/options/options.go +++ b/cmd/ks-apiserver/app/options/options.go @@ -24,6 +24,8 @@ import ( "strings" "sync" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + openpitrixv1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v1" "kubesphere.io/kubesphere/pkg/utils/clusterclient" @@ -248,7 +250,12 @@ func (s *ServerRunOptions) NewAPIServer(stopCh <-chan struct{}) (*apiserver.APIS } }) - apiServer.RuntimeCache, err = runtimecache.New(apiServer.KubernetesClient.Config(), runtimecache.Options{Scheme: sch}) + mapper, err := apiutil.NewDynamicRESTMapper(apiServer.KubernetesClient.Config()) + if err != nil { + klog.Fatalf("unable create dynamic RESTMapper: %v", err) + } + + apiServer.RuntimeCache, err = runtimecache.New(apiServer.KubernetesClient.Config(), runtimecache.Options{Scheme: sch, Mapper: mapper}) if err != nil { klog.Fatalf("unable to create controller runtime cache: %v", err) } diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index af04351a2..e358507aa 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -27,7 +27,7 @@ import ( "time" "github.com/emicklei/go-restful" - extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -38,18 +38,15 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/util/retry" "k8s.io/klog/v2" + runtimecache "sigs.k8s.io/controller-runtime/pkg/cache" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1" iamv1alpha2 "kubesphere.io/api/iam/v1alpha2" notificationv2beta1 "kubesphere.io/api/notification/v2beta1" notificationv2beta2 "kubesphere.io/api/notification/v2beta2" tenantv1alpha1 "kubesphere.io/api/tenant/v1alpha1" typesv1beta1 "kubesphere.io/api/types/v1beta1" - runtimecache "sigs.k8s.io/controller-runtime/pkg/cache" - runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" - - notificationv1 "kubesphere.io/kubesphere/pkg/kapis/notification/v1" - notificationkapisv2beta1 "kubesphere.io/kubesphere/pkg/kapis/notification/v2beta1" - notificationkapisv2beta2 "kubesphere.io/kubesphere/pkg/kapis/notification/v2beta2" audit "kubesphere.io/kubesphere/pkg/apiserver/auditing" "kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/basic" @@ -67,6 +64,7 @@ import ( apiserverconfig "kubesphere.io/kubesphere/pkg/apiserver/config" "kubesphere.io/kubesphere/pkg/apiserver/dispatch" "kubesphere.io/kubesphere/pkg/apiserver/filters" + "kubesphere.io/kubesphere/pkg/apiserver/proxies" "kubesphere.io/kubesphere/pkg/apiserver/request" "kubesphere.io/kubesphere/pkg/informers" alertingv1 "kubesphere.io/kubesphere/pkg/kapis/alerting/v1" @@ -74,7 +72,6 @@ import ( alertingv2beta1 "kubesphere.io/kubesphere/pkg/kapis/alerting/v2beta1" clusterkapisv1alpha1 "kubesphere.io/kubesphere/pkg/kapis/cluster/v1alpha1" configv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/config/v1alpha2" - "kubesphere.io/kubesphere/pkg/kapis/crd" kapisdevops "kubesphere.io/kubesphere/pkg/kapis/devops" edgeruntimev1alpha1 "kubesphere.io/kubesphere/pkg/kapis/edgeruntime/v1alpha1" gatewayv1alpha1 "kubesphere.io/kubesphere/pkg/kapis/gateway/v1alpha1" @@ -83,6 +80,9 @@ import ( meteringv1alpha1 "kubesphere.io/kubesphere/pkg/kapis/metering/v1alpha1" monitoringv1alpha3 "kubesphere.io/kubesphere/pkg/kapis/monitoring/v1alpha3" networkv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/network/v1alpha2" + notificationv1 "kubesphere.io/kubesphere/pkg/kapis/notification/v1" + notificationkapisv2beta1 "kubesphere.io/kubesphere/pkg/kapis/notification/v2beta1" + notificationkapisv2beta2 "kubesphere.io/kubesphere/pkg/kapis/notification/v2beta2" "kubesphere.io/kubesphere/pkg/kapis/oauth" openpitrixv1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v1" openpitrixv2alpha1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v2alpha1" @@ -101,6 +101,7 @@ import ( "kubesphere.io/kubesphere/pkg/models/openpitrix" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/loginrecord" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/user" + resourcev1beta1 "kubesphere.io/kubesphere/pkg/models/resources/v1beta1" "kubesphere.io/kubesphere/pkg/server/healthz" "kubesphere.io/kubesphere/pkg/simple/client/alerting" "kubesphere.io/kubesphere/pkg/simple/client/auditing" @@ -182,7 +183,6 @@ func (s *APIServer) PrepareRun(stopCh <-chan struct{}) error { }) s.installKubeSphereAPIs(stopCh) - s.installCRDAPIs() s.installMetricsAPI() s.installHealthz() s.container.Filter(monitorRequest) @@ -282,14 +282,6 @@ func (s *APIServer) installKubeSphereAPIs(stopCh <-chan struct{}) { urlruntime.Must(gatewayv1alpha1.AddToContainer(s.container, s.Config.GatewayOptions, s.RuntimeCache, s.RuntimeClient, s.InformerFactory, s.KubernetesClient.Kubernetes(), s.LoggingClient)) } -// installCRDAPIs Install CRDs to the KAPIs with List and Get options -func (s *APIServer) installCRDAPIs() { - crds := &extv1.CustomResourceDefinitionList{} - // TODO Maybe we need a better label name - urlruntime.Must(s.RuntimeClient.List(context.TODO(), crds, runtimeclient.MatchingLabels{"kubesphere.io/resource-served": "true"})) - urlruntime.Must(crd.AddToContainer(s.container, s.RuntimeClient, s.RuntimeCache, crds)) -} - // installHealthz creates the healthz endpoint for this server func (s *APIServer) installHealthz() { urlruntime.Must(healthz.InstallHandler(s.container, []healthz.HealthChecker{}...)) @@ -350,6 +342,9 @@ func (s *APIServer) buildHandlerChain(stopCh <-chan struct{}) { handler := s.Server.Handler handler = filters.WithKubeAPIServer(handler, s.KubernetesClient.Config(), &errorResponder{}) + middleware := proxies.NewUnregisteredMiddleware(s.container, resourcev1beta1.New(s.RuntimeClient, s.RuntimeCache)) + handler = filters.WithMiddleware(handler, middleware) + if s.Config.AuditingOptions.Enable { handler = filters.WithAuditing(handler, audit.NewAuditing(s.InformerFactory, s.Config.AuditingOptions, stopCh)) @@ -392,7 +387,6 @@ func (s *APIServer) buildHandlerChain(stopCh <-chan struct{}) { userLister))) handler = filters.WithAuthentication(handler, authn) handler = filters.WithRequestInfo(handler, requestInfoResolver) - s.Server.Handler = handler } diff --git a/pkg/apiserver/filters/middleware.go b/pkg/apiserver/filters/middleware.go new file mode 100644 index 000000000..fda53980a --- /dev/null +++ b/pkg/apiserver/filters/middleware.go @@ -0,0 +1,21 @@ +package filters + +import "net/http" + +type Middleware interface { + Handle(w http.ResponseWriter, req *http.Request) bool +} + +func WithMiddleware(next http.Handler, middlewares ...Middleware) http.Handler { + if middlewares == nil { + return next + } + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + for _, middleware := range middlewares { + if middleware.Handle(w, req) { + return + } + } + next.ServeHTTP(w, req) + }) +} diff --git a/pkg/apiserver/proxies/unregistered.go b/pkg/apiserver/proxies/unregistered.go new file mode 100644 index 000000000..465999810 --- /dev/null +++ b/pkg/apiserver/proxies/unregistered.go @@ -0,0 +1,108 @@ +package proxies + +import ( + "fmt" + "net/http" + "strings" + + "github.com/emicklei/go-restful" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/klog/v2" + + "kubesphere.io/kubesphere/pkg/api" + "kubesphere.io/kubesphere/pkg/apiserver/filters" + "kubesphere.io/kubesphere/pkg/apiserver/query" + "kubesphere.io/kubesphere/pkg/apiserver/request" + "kubesphere.io/kubesphere/pkg/models/resources/v1beta1" +) + +type unregisteredMiddleware struct { + registeredGv sets.String + resourceGetter v1beta1.ResourceGetter +} + +func NewUnregisteredMiddleware(c *restful.Container, resourceGetter v1beta1.ResourceGetter) filters.Middleware { + middleware := &unregisteredMiddleware{ + registeredGv: sets.NewString(), + resourceGetter: resourceGetter, + } + + for _, ws := range c.RegisteredWebServices() { + rootPath := ws.RootPath() + if strings.HasPrefix(rootPath, "/kapis") { + middleware.registeredGv.Insert(rootPath) + } + } + + return middleware +} + +func (u *unregisteredMiddleware) Handle(w http.ResponseWriter, req *http.Request) bool { + if req.Method != http.MethodGet { + return false + } + + reqInfo, exist := request.RequestInfoFrom(req.Context()) + if !exist { + return false + } + + if reqInfo.IsKubernetesRequest { + return false + } + + gvr := schema.GroupVersionResource{ + Group: reqInfo.APIGroup, + Version: reqInfo.APIVersion, + Resource: reqInfo.Resource, + } + + if gvr.Group == "" || + gvr.Version == "" || + gvr.Resource == "" { + return false + } + + rootPath := fmt.Sprintf("/kapis/%s/%s", gvr.Group, gvr.Version) + if u.registeredGv.Has(rootPath) { + return true + } + + var ( + listReq bool + q *query.Query + ) + restfulReq := restful.NewRequest(req) + restfulResp := restful.NewResponse(w) + if reqInfo.Name == "" { + listReq = true + q = query.ParseQueryParameter(restfulReq) + } + + var ( + result interface{} + err error + ) + if listReq { + result, err = u.resourceGetter.ListResources(gvr, reqInfo.Namespace, q) + } else { + result, err = u.resourceGetter.GetResource(gvr, reqInfo.Name, reqInfo.Namespace) + } + handleResponse(result, err, restfulResp, restfulReq) + return true +} + +func handleResponse(result interface{}, err error, resp *restful.Response, req *restful.Request) { + resp.SetRequestAccepts(restful.MIME_JSON) + if err != nil { + if err == v1beta1.ErrResourceNotSupported { + api.HandleBadRequest(resp, req, err) + return + } + klog.Error(err) + api.HandleError(resp, req, err) + return + } + resp.WriteEntity(result) +} diff --git a/pkg/apiserver/query/types.go b/pkg/apiserver/query/types.go index 4392c8e90..d0d3c3d88 100644 --- a/pkg/apiserver/query/types.go +++ b/pkg/apiserver/query/types.go @@ -36,6 +36,7 @@ const ( ) // Query represents api search terms +// TODO add fieldSelector type Query struct { Pagination *Pagination diff --git a/pkg/kapis/crd/crd.go b/pkg/kapis/crd/crd.go deleted file mode 100644 index ae231e558..000000000 --- a/pkg/kapis/crd/crd.go +++ /dev/null @@ -1,159 +0,0 @@ -/* -Copyright 2022 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 crd - -import ( - "fmt" - "net/http" - - "github.com/emicklei/go-restful" - extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/client" - - "kubesphere.io/kubesphere/pkg/api" - "kubesphere.io/kubesphere/pkg/apiserver/query" - ksruntime "kubesphere.io/kubesphere/pkg/apiserver/runtime" - "kubesphere.io/kubesphere/pkg/models/crds" -) - -const ( - ok = "OK" -) - -// AddToContainer register GET and LIST API for CRD to the web service -func AddToContainer(c *restful.Container, cli client.Client, cache cache.Cache, crdList *extv1.CustomResourceDefinitionList) error { - - for _, crd := range crdList.Items { - gvk := schema.GroupVersionKind{Group: crd.Spec.Group, Version: currentVersion(&crd), Kind: crd.Spec.Names.Kind} - resource := crd.Spec.Names.Plural - - var h crds.Handler - if cli.Scheme().Recognizes(gvk) { - h = crds.NewTyped(cache, gvk, cli.Scheme()) - } else { - h = crds.NewUnstructured(cache, gvk) - } - - if containsRouter(c.RegisteredWebServices(), gvk.GroupVersion().String()) { - //Skip existing Root service for now - //TODO Register to existing WebService - continue - } - - //Create new WebService - webservice := ksruntime.NewWebService(gvk.GroupVersion()) - - listURL := fmt.Sprintf("/%s", resource) - webservice.Route(webservice.GET(listURL). - To(func(request *restful.Request, response *restful.Response) { - h.ListResources(request, response) - }). - Doc(fmt.Sprintf("Cluster level resource %s query", resource)). - Param(webservice.QueryParameter(query.ParameterName, "name used to do filtering").Required(false)). - Param(webservice.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d").DefaultValue("page=1")). - Param(webservice.QueryParameter(query.ParameterLimit, "limit").Required(false)). - Param(webservice.QueryParameter(query.ParameterAscending, "sort parameters, e.g. reverse=true").Required(false).DefaultValue("ascending=false")). - Param(webservice.QueryParameter(query.ParameterOrderBy, "sort parameters, e.g. orderBy=createTime")). - Returns(http.StatusOK, ok, api.ListResult{})) - - if crd.Spec.Scope == extv1.ClusterScoped { - getURL := fmt.Sprintf("/%s/{name}", resource) - webservice.Route(webservice.GET(getURL). - To(func(request *restful.Request, response *restful.Response) { - h.GetResources(request, response) - }). - Doc(fmt.Sprintf("Cluster level resource %s", resource)). - Param(webservice.PathParameter("name", "the name of the clustered resources")). - Returns(http.StatusOK, api.StatusOK, nil)) - } - - if crd.Spec.Scope == extv1.NamespaceScoped { - listNsURL := fmt.Sprintf("/namespaces/{namespace}/%s/", resource) - webservice.Route(webservice.GET(listNsURL). - To(func(request *restful.Request, response *restful.Response) { - h.ListResources(request, response) - }). - Doc(fmt.Sprintf("Namespace level resource %s query", resource)). - Param(webservice.PathParameter("namespace", "the name of the project")). - Param(webservice.QueryParameter(query.ParameterName, "name used to do filtering").Required(false)). - Param(webservice.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d").DefaultValue("page=1")). - Param(webservice.QueryParameter(query.ParameterLimit, "limit").Required(false)). - Param(webservice.QueryParameter(query.ParameterAscending, "sort parameters, e.g. reverse=true").Required(false).DefaultValue("ascending=false")). - Param(webservice.QueryParameter(query.ParameterOrderBy, "sort parameters, e.g. orderBy=createTime")). - Returns(http.StatusOK, ok, api.ListResult{})) - - getNsURL := fmt.Sprintf("/namespaces/{namespace}/%s/{name}", resource) - webservice.Route(webservice.GET(getNsURL). - To(func(request *restful.Request, response *restful.Response) { - h.GetResources(request, response) - }). - Doc(fmt.Sprintf("Namespace level resource %s", resource)). - Param(webservice.PathParameter("namespace", "the name of the project")). - Param(webservice.PathParameter("name", "the name of resource")). - Returns(http.StatusOK, ok, nil)) - } - - if crd.Spec.Scope == extv1.ClusterScoped && crd.Annotations != nil && crd.Annotations["kubesphere.io/resource-scope"] == "workspaced" { - listWsURL := fmt.Sprintf("/workspaces/{workspace}/%s/", resource) - webservice.Route(webservice.GET(listWsURL). - To(func(request *restful.Request, response *restful.Response) { - h.ListResources(request, response) - }). - Doc(fmt.Sprintf("Workspace level resource %s query", resource)). - Param(webservice.PathParameter("workspace", "the name of the workspace")). - Param(webservice.QueryParameter(query.ParameterName, "name used to do filtering").Required(false)). - Param(webservice.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d").DefaultValue("page=1")). - Param(webservice.QueryParameter(query.ParameterLimit, "limit").Required(false)). - Param(webservice.QueryParameter(query.ParameterAscending, "sort parameters, e.g. reverse=true").Required(false).DefaultValue("ascending=false")). - Param(webservice.QueryParameter(query.ParameterOrderBy, "sort parameters, e.g. orderBy=createTime")). - Returns(http.StatusOK, ok, api.ListResult{})) - - getWsURL := fmt.Sprintf("/workspaces/{workspace}/%s/{name}", resource) - webservice.Route(webservice.GET(getWsURL). - To(func(request *restful.Request, response *restful.Response) { - h.GetResources(request, response) - }). - Doc(fmt.Sprintf("Workspace level resource %s", resource)). - Param(webservice.PathParameter("workspace", "the name of the workspace")). - Param(webservice.PathParameter("name", "the name of resource")). - Returns(http.StatusOK, ok, nil)) - } - c.Add(webservice) - } - - return nil -} - -func containsRouter(services []*restful.WebService, root string) bool { - for _, svc := range services { - if svc.RootPath() == "/kapis/"+root { - return true - } - } - return false -} - -func currentVersion(crd *extv1.CustomResourceDefinition) string { - for _, v := range crd.Spec.Versions { - if v.Served && v.Storage { - return v.Name - } - } - return "" -} diff --git a/pkg/models/crds/interface_test.go b/pkg/models/crds/interface_test.go deleted file mode 100644 index 6a80b47db..000000000 --- a/pkg/models/crds/interface_test.go +++ /dev/null @@ -1,85 +0,0 @@ -/* - - Copyright 2022 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 crds - -import "testing" - -func TestLabelMatch(t *testing.T) { - tests := []struct { - labels map[string]string - filter string - expectResult bool - }{ - { - labels: map[string]string{ - "kubesphere.io/workspace": "kubesphere-system", - }, - filter: "kubesphere.io/workspace", - expectResult: true, - }, - { - labels: map[string]string{ - "kubesphere.io/creator": "system", - }, - filter: "kubesphere.io/workspace", - expectResult: false, - }, - { - labels: map[string]string{ - "kubesphere.io/workspace": "kubesphere-system", - }, - filter: "kubesphere.io/workspace=", - expectResult: false, - }, - { - labels: map[string]string{ - "kubesphere.io/workspace": "kubesphere-system", - }, - filter: "kubesphere.io/workspace!=", - expectResult: true, - }, - { - labels: map[string]string{ - "kubesphere.io/workspace": "kubesphere-system", - }, - filter: "kubesphere.io/workspace!=kubesphere-system", - expectResult: false, - }, - { - labels: map[string]string{ - "kubesphere.io/workspace": "kubesphere-system", - }, - filter: "kubesphere.io/workspace=kubesphere-system", - expectResult: true, - }, - { - labels: map[string]string{ - "kubesphere.io/workspace": "kubesphere-system", - }, - filter: "kubesphere.io/workspace=system", - expectResult: false, - }, - } - for i, test := range tests { - result := labelMatch(test.labels, test.filter) - if result != test.expectResult { - t.Errorf("case %d, got %#v, expected %#v", i, result, test.expectResult) - } - } -} diff --git a/pkg/models/crds/typed_handler.go b/pkg/models/crds/typed_handler.go deleted file mode 100644 index be7897025..000000000 --- a/pkg/models/crds/typed_handler.go +++ /dev/null @@ -1,131 +0,0 @@ -/* -Copyright 2022 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 crds - -import ( - "context" - "fmt" - - "github.com/emicklei/go-restful" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - - "kubesphere.io/kubesphere/pkg/api" - "kubesphere.io/kubesphere/pkg/apiserver/query" - - "sigs.k8s.io/controller-runtime/pkg/client" -) - -type TypedCRDHandler struct { - c client.Reader - s *runtime.Scheme - gvk schema.GroupVersionKind -} - -func NewTyped(cache client.Reader, gvk schema.GroupVersionKind, s *runtime.Scheme) Client { - return &TypedCRDHandler{c: cache, gvk: gvk, s: s} -} - -func (h *TypedCRDHandler) GetResources(request *restful.Request, response *restful.Response) { - namespace := request.PathParameter("namespace") - name := request.PathParameter("name") - - obj, err := h.Get(types.NamespacedName{ - Namespace: namespace, - Name: name, - }) - - if err != nil { - api.HandleError(response, nil, err) - return - } - response.WriteEntity(obj) -} - -// handleListResources retrieves resources -func (h *TypedCRDHandler) ListResources(request *restful.Request, response *restful.Response) { - q := query.ParseQueryParameter(request) - namespace := request.PathParameter("namespace") - workspace := request.PathParameter("workspace") - if workspace != "" { - // filter by workspace - q.Filters[query.FieldLabel] = query.Value(fmt.Sprintf("%s=%s", "kubesphere.io/workspace", workspace)) - } - list, err := h.List(namespace, q) - if err != nil { - api.HandleError(response, nil, err) - return - } - response.WriteEntity(list) -} - -func (h *TypedCRDHandler) Get(key types.NamespacedName) (client.Object, error) { - obj, err := h.s.New(h.gvk) - if err != nil { - return nil, err - } - clobj := obj.(client.Object) - if err := h.c.Get(context.TODO(), key, clobj); err != nil { - return nil, err - } - return clobj, err -} - -func (h *TypedCRDHandler) List(namespace string, q *query.Query) (*api.ListResult, error) { - - listGvk := schema.GroupVersionKind{ - Group: h.gvk.Group, - Version: h.gvk.Version, - Kind: h.gvk.Kind + "List", - } - obj, err := h.s.New(listGvk) - if err != nil { - return nil, err - } - objlist := obj.(client.ObjectList) - if err := h.c.List(context.TODO(), objlist, &client.ListOptions{LabelSelector: q.Selector()}, client.InNamespace(namespace)); err != nil { - return nil, err - } - - result := DefaultList(objlist, q, h.compare, h.filter, h.transforms()...) - - return result, err -} - -func (d *TypedCRDHandler) compare(left, right metav1.Object, field query.Field) bool { - if fn, ok := Comparers[d.gvk]; ok { - fn(left, right, field) - } - return DefaultObjectMetaCompare(left, right, field) -} - -func (d *TypedCRDHandler) filter(object metav1.Object, filter query.Filter) bool { - if fn, ok := Filters[d.gvk]; ok { - fn(object, filter) - } - return DefaultObjectMetaFilter(object, filter) -} - -func (d *TypedCRDHandler) transforms() []TransformFunc { - if fn, ok := Transformers[d.gvk]; ok { - return fn - } - trans := []TransformFunc{} - return trans -} diff --git a/pkg/models/crds/typed_handler_test.go b/pkg/models/crds/typed_handler_test.go deleted file mode 100644 index 54412d17f..000000000 --- a/pkg/models/crds/typed_handler_test.go +++ /dev/null @@ -1,217 +0,0 @@ -/* -Copyright 2022 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 crds - -import ( - "context" - "reflect" - "testing" - - corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/diff" - "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - - "kubesphere.io/kubesphere/pkg/api" - "kubesphere.io/kubesphere/pkg/apiserver/query" -) - -type fakeClient struct { - Client client.Client -} - -// Get retrieves an obj for the given object key from the Kubernetes Cluster. -// obj must be a struct pointer so that obj can be updated with the response -// returned by the Server. -func (f *fakeClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { - return f.Client.Get(ctx, key, obj, opts...) -} - -// List retrieves list of objects for a given namespace and list options. On a -// successful call, Items field in the list will be populated with the -// result returned from the server. -func (f *fakeClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { - return f.Client.List(ctx, list, opts...) -} - -// GetInformer fetches or constructs an informer for the given object that corresponds to a single -// API kind and resource. -func (f *fakeClient) GetInformer(ctx context.Context, obj client.Object) (cache.Informer, error) { - return nil, nil -} - -// GetInformerForKind is similar to GetInformer, except that it takes a group-version-kind, instead -// of the underlying object. -func (f *fakeClient) GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind) (cache.Informer, error) { - return nil, nil -} - -// Start runs all the informers known to this cache until the context is closed. -// It blocks. -func (f *fakeClient) Start(ctx context.Context) error { - return nil -} - -// WaitForCacheSync waits for all the caches to sync. Returns false if it could not sync a cache. -func (f *fakeClient) WaitForCacheSync(ctx context.Context) bool { - return false -} - -func (f *fakeClient) IndexField(ctx context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error { - return nil -} - -var ( - cm1 = &corev1.ConfigMap{ - TypeMeta: v1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - }, - ObjectMeta: v1.ObjectMeta{ - Name: "cm1", - Namespace: "default", - }, - } - - cm2 = &corev1.ConfigMap{ - TypeMeta: v1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - }, - ObjectMeta: v1.ObjectMeta{ - Name: "cm2", - Namespace: "default", - }, - } - - cm3 = &corev1.ConfigMap{ - TypeMeta: v1.TypeMeta{ - Kind: "ConfigMap", - APIVersion: "v1", - }, - ObjectMeta: v1.ObjectMeta{ - Name: "cm3", - Namespace: "default", - }, - } -) - -func TestHandler_Get(t *testing.T) { - - var Scheme = runtime.NewScheme() - // v1alpha1.AddToScheme(Scheme) - corev1.AddToScheme(Scheme) - - c := fake.NewClientBuilder().WithScheme(Scheme).WithRuntimeObjects(cm1).Build() - - h := NewTyped(&fakeClient{c}, cm1.GroupVersionKind(), Scheme) - - type args struct { - //nolint:unused - gvk schema.GroupVersionKind - key types.NamespacedName - } - tests := []struct { - name string - handler Client - args args - want client.Object - wantErr bool - }{ - { - name: "test", - handler: h, - args: args{ - key: types.NamespacedName{ - Namespace: "default", - Name: "cm1", - }, - }, - want: cm1, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := tt.handler - got, err := h.Get(tt.args.key) - if (err != nil) != tt.wantErr { - t.Errorf("Handler.Get() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Handler.Get() \nDiff:\n %s", diff.ObjectGoPrintSideBySide(tt.want, got)) - } - }) - } -} - -func TestResourceGetter_List(t *testing.T) { - - var Scheme = runtime.NewScheme() - // v1alpha1.AddToScheme(Scheme) - corev1.AddToScheme(Scheme) - - c := fake.NewClientBuilder().WithScheme(Scheme).WithRuntimeObjects(cm1, cm2, cm3).Build() - - h := NewTyped(&fakeClient{c}, cm1.GroupVersionKind(), Scheme) - - tests := []struct { - name string - Resource string - Namespace string - Query *query.Query - wantErr bool - want *api.ListResult - }{ - { - name: "list configmaps", - Resource: "configmaps", - Namespace: "default", - Query: &query.Query{ - Pagination: &query.Pagination{ - Limit: 10, - Offset: 0, - }, - SortBy: query.FieldName, - Ascending: false, - Filters: map[query.Field]query.Value{}, - }, - want: &api.ListResult{ - Items: []interface{}{cm3, cm2, cm1}, - TotalItems: 3, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := h.List(tt.Namespace, tt.Query) - if (err != nil) != tt.wantErr { - t.Errorf("Handler.Get() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Handler.Get() \nDiff:\n %s", diff.ObjectGoPrintSideBySide(tt.want, got)) - } - }) - } -} diff --git a/pkg/models/crds/unstructured_handler.go b/pkg/models/crds/unstructured_handler.go deleted file mode 100644 index fc21df676..000000000 --- a/pkg/models/crds/unstructured_handler.go +++ /dev/null @@ -1,111 +0,0 @@ -/* -Copyright 2022 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 crds - -import ( - "context" - "fmt" - - "github.com/emicklei/go-restful" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - - "kubesphere.io/kubesphere/pkg/api" - "kubesphere.io/kubesphere/pkg/apiserver/query" - - "sigs.k8s.io/controller-runtime/pkg/client" -) - -type UnstructuredCRDHandler struct { - c client.Reader - gvk schema.GroupVersionKind - gvkList schema.GroupVersionKind -} - -func NewUnstructured(cache client.Reader, gvk schema.GroupVersionKind) Client { - return &UnstructuredCRDHandler{c: cache, gvk: gvk, gvkList: schema.GroupVersionKind{Version: gvk.Version, Group: gvk.Group, Kind: gvk.Kind + "List"}} -} - -func (h *UnstructuredCRDHandler) GetResources(request *restful.Request, response *restful.Response) { - namespace := request.PathParameter("namespace") - name := request.PathParameter("name") - - obj, err := h.Get(types.NamespacedName{ - Namespace: namespace, - Name: name, - }) - - if err != nil { - api.HandleError(response, nil, err) - return - } - response.WriteEntity(obj) -} - -// handleListResources retrieves resources -func (h *UnstructuredCRDHandler) ListResources(request *restful.Request, response *restful.Response) { - q := query.ParseQueryParameter(request) - namespace := request.PathParameter("namespace") - workspace := request.PathParameter("workspace") - if workspace != "" { - // filter by workspace - q.Filters[query.FieldLabel] = query.Value(fmt.Sprintf("%s=%s", "kubesphere.io/workspace", workspace)) - } - list, err := h.List(namespace, q) - if err != nil { - api.HandleError(response, nil, err) - return - } - response.WriteEntity(list) -} - -func (h *UnstructuredCRDHandler) Get(key types.NamespacedName) (client.Object, error) { - - obj := &unstructured.Unstructured{} - obj.SetGroupVersionKind(h.gvk) - - if err := h.c.Get(context.TODO(), key, obj); err != nil { - return nil, err - } - return obj, nil -} - -func (h *UnstructuredCRDHandler) List(namespace string, q *query.Query) (*api.ListResult, error) { - - listObj := &unstructured.UnstructuredList{} - listObj.SetGroupVersionKind(h.gvkList) - - if err := h.c.List(context.TODO(), listObj, &client.ListOptions{LabelSelector: q.Selector()}, client.InNamespace(namespace)); err != nil { - return nil, err - } - - result := DefaultList(listObj, q, h.compare, h.filter) - - return result, nil -} - -func (d *UnstructuredCRDHandler) compare(left, right metav1.Object, field query.Field) bool { - - return DefaultObjectMetaCompare(left, right, field) -} - -func (d *UnstructuredCRDHandler) filter(object metav1.Object, filter query.Filter) bool { - //TODO Maybe we can use a json path Filter here - return DefaultObjectMetaFilter(object, filter) -} diff --git a/pkg/models/resources/v1alpha3/secret/secrets_test.go b/pkg/models/resources/v1alpha3/secret/secrets_test.go new file mode 100644 index 000000000..f0c637c18 --- /dev/null +++ b/pkg/models/resources/v1alpha3/secret/secrets_test.go @@ -0,0 +1,119 @@ +package secret + +import ( + "testing" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/rand" + + "kubesphere.io/kubesphere/pkg/apiserver/query" + "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3" +) + +var testSecret = &v1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "prometheus-k8s", + Namespace: "kube-system", + ResourceVersion: "1234567", + Labels: map[string]string{ + "modifiedAt": "1670227209", + "name": "snapshot-controller", + "owner": "helm", + "status": "superseded", + "version": "2", + }, + }, + Data: map[string][]byte{ + "testdata": []byte("thisisatestsecret"), + }, + Type: "helm.sh/release.v1", +} + +func BenchmarkContains(b *testing.B) { + for i := 0; i < b.N; i++ { + if contains(testSecret, "metadata.labels.status!=superseded") { + b.Error("test failed") + } + } + +} + +func BenchmarkDefaultListWith1000(b *testing.B) { + s := &secretSearcher{} + q := query.New() + q.Filters[query.ParameterFieldSelector] = "metadata.resourceVersion=1234567" + expectedListCount := rand.Intn(20) + list := prepareList(testSecret, 1000, expectedListCount) + + for i := 0; i < b.N; i++ { + list := v1alpha3.DefaultList(list, q, s.compare, s.filter) + if list.TotalItems != expectedListCount { + b.Error("test failed") + } + } +} + +func BenchmarkDefaultListWith5000(b *testing.B) { + s := &secretSearcher{} + q := query.New() + q.Filters[query.ParameterFieldSelector] = "metadata.resourceVersion=1234567" + expectedListCount := rand.Intn(20) + + list := prepareList(testSecret, 5000, expectedListCount) + for i := 0; i < b.N; i++ { + list := v1alpha3.DefaultList(list, q, s.compare, s.filter) + if list.TotalItems != expectedListCount { + b.Error("test failed") + } + } +} + +func BenchmarkDefaultListWith10000(b *testing.B) { + s := &secretSearcher{} + q := query.New() + q.Filters[query.ParameterFieldSelector] = "metadata.resourceVersion=1234567" + expectedListCount := rand.Intn(20) + list := prepareList(testSecret, 100000, expectedListCount) + for i := 0; i < b.N; i++ { + list := v1alpha3.DefaultList(list, q, s.compare, s.filter) + if list.TotalItems != expectedListCount { + b.Error("test failed") + } + } +} + +func BenchmarkDefaultListWith50000(b *testing.B) { + s := &secretSearcher{} + q := query.New() + q.Filters[query.ParameterFieldSelector] = "metadata.resourceVersion=1234567" + expectedListCount := rand.Intn(20) + for i := 0; i < b.N; i++ { + list := v1alpha3.DefaultList(prepareList(testSecret, 50000, expectedListCount), q, s.compare, s.filter) + if list.TotalItems != expectedListCount { + b.Error("test failed") + } + } +} + +func prepareList(testSecret *v1.Secret, listLen, expected int) []runtime.Object { + secretList := make([]runtime.Object, listLen) + + for i := 0; i < listLen; i++ { + secret := testSecret.DeepCopy() + secret.Name = rand.String(20) + secret.ObjectMeta.ResourceVersion = rand.String(10) + secretList[i] = secret + } + + for i := 0; i < expected; i++ { + secretList[rand.Intn(listLen-1)] = testSecret + } + + return secretList +} diff --git a/pkg/models/resources/v1beta1/cache.go b/pkg/models/resources/v1beta1/cache.go new file mode 100644 index 000000000..fe73449f9 --- /dev/null +++ b/pkg/models/resources/v1beta1/cache.go @@ -0,0 +1,66 @@ +package v1beta1 + +import ( + "context" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + + "kubesphere.io/kubesphere/pkg/apiserver/query" +) + +type resourceCache struct { + cache cache.Cache +} + +func NewResourceCache(cache cache.Cache) Interface { + return &resourceCache{cache: cache} +} + +func (u *resourceCache) Get(name, namespace string, object client.Object) error { + return u.cache.Get(context.Background(), client.ObjectKey{Namespace: namespace, Name: name}, object) +} + +func (u *resourceCache) List(namespace string, query *query.Query, list client.ObjectList) error { + listOpt := &client.ListOptions{ + LabelSelector: query.Selector(), + Namespace: namespace, + } + err := u.cache.List(context.Background(), list, listOpt) + if err != nil { + return err + } + + extractList, err := meta.ExtractList(list) + if err != nil { + return err + } + + filtered := DefaultList(extractList, query, compare, filter) + if err := meta.SetList(list, filtered); err != nil { + return err + } + return nil +} + +func compare(left, right runtime.Object, field query.Field) bool { + l, err := meta.Accessor(left) + if err != nil { + return false + } + r, err := meta.Accessor(right) + if err != nil { + return false + } + return DefaultObjectMetaCompare(l, r, field) +} + +func filter(object runtime.Object, filter query.Filter) bool { + o, err := meta.Accessor(object) + if err != nil { + return false + } + return DefaultObjectMetaFilter(o, filter) +} diff --git a/pkg/models/crds/interface.go b/pkg/models/resources/v1beta1/interface.go similarity index 55% rename from pkg/models/crds/interface.go rename to pkg/models/resources/v1beta1/interface.go index dc7169b7c..676033959 100644 --- a/pkg/models/crds/interface.go +++ b/pkg/models/resources/v1beta1/interface.go @@ -1,110 +1,71 @@ -/* -Copyright 2022 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 crds +package v1beta1 import ( + "encoding/json" + "fmt" "sort" "strings" - "github.com/emicklei/go-restful" - - "k8s.io/apimachinery/pkg/api/meta" + "github.com/oliveagle/jsonpath" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" - "kubesphere.io/kubesphere/pkg/api" "kubesphere.io/kubesphere/pkg/apiserver/query" ) -var ( - Filters = map[schema.GroupVersionKind]FilterFunc{} - Comparers = map[schema.GroupVersionKind]CompareFunc{} - Transformers = map[schema.GroupVersionKind][]TransformFunc{} -) - -type Reader interface { +type Interface interface { // Get retrieves a single object by its namespace and name - Get(key types.NamespacedName) (client.Object, error) + Get(namespace, name string, object client.Object) error // List retrieves a collection of objects matches given query - List(namespace string, query *query.Query) (*api.ListResult, error) + List(namespace string, query *query.Query, object client.ObjectList) error } -type Handler interface { - GetResources(request *restful.Request, response *restful.Response) - ListResources(request *restful.Request, response *restful.Response) -} - -type Client interface { - Handler - Reader +type ResourceGetter interface { + GetResource(schema.GroupVersionResource, string, string) (client.Object, error) + ListResources(schema.GroupVersionResource, string, *query.Query) (client.ObjectList, error) } // CompareFunc return true is left great than right -type CompareFunc func(metav1.Object, metav1.Object, query.Field) bool +type CompareFunc func(runtime.Object, runtime.Object, query.Field) bool -type FilterFunc func(metav1.Object, query.Filter) bool +type FilterFunc func(runtime.Object, query.Filter) bool -type TransformFunc func(metav1.Object) runtime.Object +type TransformFunc func(runtime.Object) runtime.Object -func DefaultList(objList runtime.Object, q *query.Query, compareFunc CompareFunc, filterFunc FilterFunc, transformFuncs ...TransformFunc) *api.ListResult { +func DefaultList(objects []runtime.Object, q *query.Query, compareFunc CompareFunc, filterFunc FilterFunc, transformFuncs ...TransformFunc) []runtime.Object { // selected matched ones var filtered []runtime.Object + if len(q.Filters) != 0 { + for _, object := range objects { + selected := true + for field, value := range q.Filters { + if !filterFunc(object, query.Filter{Field: field, Value: value}) { + selected = false + break + } + } - meta.EachListItem(objList, func(obj runtime.Object) error { - selected := true - o, err := meta.Accessor(obj) - if err != nil { - return err - } - for field, value := range q.Filters { - - if !filterFunc(o, query.Filter{Field: field, Value: value}) { - selected = false - break + if selected { + for _, transform := range transformFuncs { + object = transform(object) + } + filtered = append(filtered, object) } } - - if selected { - for _, transform := range transformFuncs { - obj = transform(o) - } - filtered = append(filtered, obj) - } - return nil - }) + } // sort by sortBy field sort.Slice(filtered, func(i, j int) bool { - l, err := meta.Accessor(filtered[i]) - if err != nil { - return false - } - r, err := meta.Accessor(filtered[j]) - if err != nil { - return false - } if !q.Ascending { - return compareFunc(l, r, q.SortBy) + return compareFunc(filtered[i], filtered[j], q.SortBy) } - return !compareFunc(l, r, q.SortBy) + return !compareFunc(filtered[i], filtered[j], q.SortBy) }) total := len(filtered) @@ -115,10 +76,7 @@ func DefaultList(objList runtime.Object, q *query.Query, compareFunc CompareFunc start, end := q.Pagination.GetValidPagination(total) - return &api.ListResult{ - TotalItems: len(filtered), - Items: objectsToInterfaces(filtered[start:end]), - } + return filtered[start:end] } // DefaultObjectMetaCompare return true is left great than right @@ -184,6 +142,8 @@ func DefaultObjectMetaFilter(item metav1.Object, filter query.Filter) bool { // /namespaces?page=1&limit=10&label=kubesphere.io/workspace:system-workspace case query.FieldLabel: return labelMatch(item.GetLabels(), string(filter.Value)) + case query.ParameterFieldSelector: + return contains(item.(runtime.Object), filter.Value) default: return false } @@ -218,10 +178,49 @@ func labelMatch(labels map[string]string, filter string) bool { return false } -func objectsToInterfaces(objs []runtime.Object) []interface{} { - res := make([]interface{}, 0) - for _, obj := range objs { - res = append(res, obj) +// implement a generic query filter to support multiple field selectors with "jsonpath.JsonPathLookup" +// https://github.com/oliveagle/jsonpath/blob/master/readme.md +func contains(object runtime.Object, queryValue query.Value) bool { + // call the ParseSelector function of "k8s.io/apimachinery/pkg/fields/selector.go" to validate and parse the selector + fieldSelector, err := fields.ParseSelector(string(queryValue)) + if err != nil { + klog.V(4).Infof("failed parse selector error: %s", err) + return false } - return res + for _, requirement := range fieldSelector.Requirements() { + var negative bool + // supports '=', '==' and '!='.(e.g. ?fieldSelector=key1=value1,key2=value2) + // fields.ParseSelector(FieldSelector) has handled the case where the operator is '==' and converted it to '=', + // so case selection.DoubleEquals can be ignored here. + switch requirement.Operator { + case selection.NotEquals: + negative = true + case selection.Equals: + negative = false + } + key := requirement.Field + value := requirement.Value + + var input map[string]interface{} + data, err := json.Marshal(object) + if err != nil { + klog.V(4).Infof("failed marshal to JSON string: %s", err) + return false + } + if err = json.Unmarshal(data, &input); err != nil { + klog.V(4).Infof("failed unmarshal to map object: %s", err) + return false + } + rawValue, err := jsonpath.JsonPathLookup(input, "$."+key) + if err != nil { + klog.V(4).Infof("failed to lookup jsonpath: %s", err) + return false + } + if (negative && fmt.Sprintf("%v", rawValue) != value) || (!negative && fmt.Sprintf("%v", rawValue) == value) { + continue + } else { + return false + } + } + return true } diff --git a/pkg/models/resources/v1beta1/resourcelgetter.go b/pkg/models/resources/v1beta1/resourcelgetter.go new file mode 100644 index 000000000..71c27af2a --- /dev/null +++ b/pkg/models/resources/v1beta1/resourcelgetter.go @@ -0,0 +1,150 @@ +package v1beta1 + +import ( + "context" + "errors" + "strings" + "sync" + + "sigs.k8s.io/controller-runtime/pkg/cache" + + "kubesphere.io/kubesphere/pkg/apiserver/query" + + extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ErrResourceNotSupported = errors.New("resource is not supported") +var ErrResourceNotServed = errors.New("resource is not served") + +const labelResourceServed = "kubesphere.io/resource-served" + +// TODO If delete the crd at the cluster when ks is running, the client.cache doesn`t return err but empty result +func New(client client.Client, cache cache.Cache) ResourceGetter { + return &resourceGetter{ + client: client, + cache: NewResourceCache(cache), + serveCRD: make(map[string]bool, 0), + } +} + +type resourceGetter struct { + client client.Client + cache Interface + serveCRD map[string]bool + sync.RWMutex +} + +func (h *resourceGetter) GetResource(gvr schema.GroupVersionResource, name, namespace string) (client.Object, error) { + var obj client.Object + gvk, err := h.getGVK(gvr) + if err != nil { + return nil, err + } + + if h.client.Scheme().Recognizes(gvk) { + gvkObject, err := h.client.Scheme().New(gvk) + if err != nil { + return nil, err + } + obj = gvkObject.(client.Object) + } else { + serviced, err := h.isServed(gvr) + if err != nil { + return nil, err + } + if !serviced { + return nil, ErrResourceNotServed + } + + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(gvk) + obj = u + } + + if err := h.cache.Get(name, namespace, obj); err != nil { + return nil, err + } + return obj, nil +} + +func (h *resourceGetter) ListResources(gvr schema.GroupVersionResource, namespace string, query *query.Query) (client.ObjectList, error) { + var obj client.ObjectList + + gvk, err := h.getGVK(gvr) + if err != nil { + return nil, err + } + + gvk = convertGVKToList(gvk) + + if h.client.Scheme().Recognizes(gvk) { + gvkObject, err := h.client.Scheme().New(gvk) + if err != nil { + return nil, err + } + obj = gvkObject.(client.ObjectList) + } else { + serviced, err := h.isServed(gvr) + if err != nil { + return nil, err + } + if !serviced { + return nil, ErrResourceNotServed + } + u := &unstructured.UnstructuredList{} + u.SetGroupVersionKind(gvk) + obj = u + } + + if err := h.cache.List(namespace, query, obj); err != nil { + return nil, err + } + return obj, nil +} + +func convertGVKToList(gvk schema.GroupVersionKind) schema.GroupVersionKind { + if strings.HasSuffix(gvk.Kind, "List") { + return gvk + } + gvk.Kind = gvk.Kind + "List" + return gvk +} + +func (h *resourceGetter) getGVK(gvr schema.GroupVersionResource) (schema.GroupVersionKind, error) { + var ( + gvk schema.GroupVersionKind + err error + ) + gvk, err = h.client.RESTMapper().KindFor(gvr) + if err != nil { + return gvk, err + } + + return gvk, nil +} + +func (h *resourceGetter) isServed(gvr schema.GroupVersionResource) (bool, error) { + resourceName := gvr.Resource + "." + gvr.Group + h.RWMutex.RLock() + isServed := h.serveCRD[resourceName] + h.RWMutex.RUnlock() + if isServed { + return true, nil + } + + crd := &extv1.CustomResourceDefinition{} + err := h.client.Get(context.Background(), client.ObjectKey{Name: resourceName}, crd) + if err != nil { + return false, err + } + if crd.Labels[labelResourceServed] == "true" { + h.RWMutex.Lock() + h.serveCRD[resourceName] = true + h.RWMutex.Unlock() + return true, nil + } + return false, nil +}