ResourceGetter v1beta1 (#5416)

* add resource getter & reader

Signed-off-by: Wenhao Zhou <wenhaozhou@yunify.com>

* add resource v1beta1 handler

* delete gvrToGvk map instead of using the dynamicRESTMapper for getting gvk, and rename the ResourceLister to ResourceGetter

* add unregisteredMiddleware filter

Signed-off-by: wenhaozhou <wenhaozhou@yunify.com>

* add secret contains benchmark & add fieldSelector to resourcev1beta1

Signed-off-by: Wenhao Zhou <wenhaozhou@yunify.com>

* delete crds models

Signed-off-by: wenhaozhou <wenhaozhou@yunify.com>

* delete parameterExtractor and instead of requestInfo

Signed-off-by: Wenhao Zhou <wenhaozhou@yunify.com>

* add benchmark test

* move fieldSelector to DefaultObjectMetaFilter

Signed-off-by: wenhaozhou <wenhaozhou@yunify.com>

* move fieldSelector to DefaultObjectMetaFilter

* change registeredGv type to set

Signed-off-by: wenhaozhou <wenhaozhou@yunify.com>

* update filter chains

Signed-off-by: wenhaozhou <wenhaozhou@yunify.com>

* fix fieldSelector cannot work

Signed-off-by: wenhaozhou <wenhaozhou@yunify.com>

* fix: list known type do not need served label

Signed-off-by: wenhaozhou <wenhaozhou@yunify.com>

---------

Signed-off-by: Wenhao Zhou <wenhaozhou@yunify.com>
Signed-off-by: wenhaozhou <wenhaozhou@yunify.com>
This commit is contained in:
Wenhao Zhou
2023-02-08 15:00:15 +08:00
committed by GitHub
parent 1c49fcd57e
commit 23df7b051b
14 changed files with 566 additions and 804 deletions

View File

@@ -24,6 +24,8 @@ import (
"strings" "strings"
"sync" "sync"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
openpitrixv1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v1" openpitrixv1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v1"
"kubesphere.io/kubesphere/pkg/utils/clusterclient" "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 { if err != nil {
klog.Fatalf("unable to create controller runtime cache: %v", err) klog.Fatalf("unable to create controller runtime cache: %v", err)
} }

View File

@@ -27,7 +27,7 @@ import (
"time" "time"
"github.com/emicklei/go-restful" "github.com/emicklei/go-restful"
extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
@@ -38,18 +38,15 @@ import (
"k8s.io/client-go/discovery" "k8s.io/client-go/discovery"
"k8s.io/client-go/util/retry" "k8s.io/client-go/util/retry"
"k8s.io/klog/v2" "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" clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1"
iamv1alpha2 "kubesphere.io/api/iam/v1alpha2" iamv1alpha2 "kubesphere.io/api/iam/v1alpha2"
notificationv2beta1 "kubesphere.io/api/notification/v2beta1" notificationv2beta1 "kubesphere.io/api/notification/v2beta1"
notificationv2beta2 "kubesphere.io/api/notification/v2beta2" notificationv2beta2 "kubesphere.io/api/notification/v2beta2"
tenantv1alpha1 "kubesphere.io/api/tenant/v1alpha1" tenantv1alpha1 "kubesphere.io/api/tenant/v1alpha1"
typesv1beta1 "kubesphere.io/api/types/v1beta1" 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" audit "kubesphere.io/kubesphere/pkg/apiserver/auditing"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/basic" "kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/basic"
@@ -67,6 +64,7 @@ import (
apiserverconfig "kubesphere.io/kubesphere/pkg/apiserver/config" apiserverconfig "kubesphere.io/kubesphere/pkg/apiserver/config"
"kubesphere.io/kubesphere/pkg/apiserver/dispatch" "kubesphere.io/kubesphere/pkg/apiserver/dispatch"
"kubesphere.io/kubesphere/pkg/apiserver/filters" "kubesphere.io/kubesphere/pkg/apiserver/filters"
"kubesphere.io/kubesphere/pkg/apiserver/proxies"
"kubesphere.io/kubesphere/pkg/apiserver/request" "kubesphere.io/kubesphere/pkg/apiserver/request"
"kubesphere.io/kubesphere/pkg/informers" "kubesphere.io/kubesphere/pkg/informers"
alertingv1 "kubesphere.io/kubesphere/pkg/kapis/alerting/v1" alertingv1 "kubesphere.io/kubesphere/pkg/kapis/alerting/v1"
@@ -74,7 +72,6 @@ import (
alertingv2beta1 "kubesphere.io/kubesphere/pkg/kapis/alerting/v2beta1" alertingv2beta1 "kubesphere.io/kubesphere/pkg/kapis/alerting/v2beta1"
clusterkapisv1alpha1 "kubesphere.io/kubesphere/pkg/kapis/cluster/v1alpha1" clusterkapisv1alpha1 "kubesphere.io/kubesphere/pkg/kapis/cluster/v1alpha1"
configv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/config/v1alpha2" configv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/config/v1alpha2"
"kubesphere.io/kubesphere/pkg/kapis/crd"
kapisdevops "kubesphere.io/kubesphere/pkg/kapis/devops" kapisdevops "kubesphere.io/kubesphere/pkg/kapis/devops"
edgeruntimev1alpha1 "kubesphere.io/kubesphere/pkg/kapis/edgeruntime/v1alpha1" edgeruntimev1alpha1 "kubesphere.io/kubesphere/pkg/kapis/edgeruntime/v1alpha1"
gatewayv1alpha1 "kubesphere.io/kubesphere/pkg/kapis/gateway/v1alpha1" gatewayv1alpha1 "kubesphere.io/kubesphere/pkg/kapis/gateway/v1alpha1"
@@ -83,6 +80,9 @@ import (
meteringv1alpha1 "kubesphere.io/kubesphere/pkg/kapis/metering/v1alpha1" meteringv1alpha1 "kubesphere.io/kubesphere/pkg/kapis/metering/v1alpha1"
monitoringv1alpha3 "kubesphere.io/kubesphere/pkg/kapis/monitoring/v1alpha3" monitoringv1alpha3 "kubesphere.io/kubesphere/pkg/kapis/monitoring/v1alpha3"
networkv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/network/v1alpha2" 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" "kubesphere.io/kubesphere/pkg/kapis/oauth"
openpitrixv1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v1" openpitrixv1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v1"
openpitrixv2alpha1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v2alpha1" openpitrixv2alpha1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v2alpha1"
@@ -101,6 +101,7 @@ import (
"kubesphere.io/kubesphere/pkg/models/openpitrix" "kubesphere.io/kubesphere/pkg/models/openpitrix"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/loginrecord" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/loginrecord"
"kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/user" "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/server/healthz"
"kubesphere.io/kubesphere/pkg/simple/client/alerting" "kubesphere.io/kubesphere/pkg/simple/client/alerting"
"kubesphere.io/kubesphere/pkg/simple/client/auditing" "kubesphere.io/kubesphere/pkg/simple/client/auditing"
@@ -182,7 +183,6 @@ func (s *APIServer) PrepareRun(stopCh <-chan struct{}) error {
}) })
s.installKubeSphereAPIs(stopCh) s.installKubeSphereAPIs(stopCh)
s.installCRDAPIs()
s.installMetricsAPI() s.installMetricsAPI()
s.installHealthz() s.installHealthz()
s.container.Filter(monitorRequest) 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)) 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 // installHealthz creates the healthz endpoint for this server
func (s *APIServer) installHealthz() { func (s *APIServer) installHealthz() {
urlruntime.Must(healthz.InstallHandler(s.container, []healthz.HealthChecker{}...)) urlruntime.Must(healthz.InstallHandler(s.container, []healthz.HealthChecker{}...))
@@ -350,6 +342,9 @@ func (s *APIServer) buildHandlerChain(stopCh <-chan struct{}) {
handler := s.Server.Handler handler := s.Server.Handler
handler = filters.WithKubeAPIServer(handler, s.KubernetesClient.Config(), &errorResponder{}) 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 { if s.Config.AuditingOptions.Enable {
handler = filters.WithAuditing(handler, handler = filters.WithAuditing(handler,
audit.NewAuditing(s.InformerFactory, s.Config.AuditingOptions, stopCh)) audit.NewAuditing(s.InformerFactory, s.Config.AuditingOptions, stopCh))
@@ -392,7 +387,6 @@ func (s *APIServer) buildHandlerChain(stopCh <-chan struct{}) {
userLister))) userLister)))
handler = filters.WithAuthentication(handler, authn) handler = filters.WithAuthentication(handler, authn)
handler = filters.WithRequestInfo(handler, requestInfoResolver) handler = filters.WithRequestInfo(handler, requestInfoResolver)
s.Server.Handler = handler s.Server.Handler = handler
} }

View File

@@ -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)
})
}

View File

@@ -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)
}

View File

@@ -36,6 +36,7 @@ const (
) )
// Query represents api search terms // Query represents api search terms
// TODO add fieldSelector
type Query struct { type Query struct {
Pagination *Pagination Pagination *Pagination

View File

@@ -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 ""
}

View File

@@ -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)
}
}
}

View File

@@ -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
}

View File

@@ -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))
}
})
}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -1,110 +1,71 @@
/* package v1beta1
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
import ( import (
"encoding/json"
"fmt"
"sort" "sort"
"strings" "strings"
"github.com/emicklei/go-restful" "github.com/oliveagle/jsonpath"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "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" "sigs.k8s.io/controller-runtime/pkg/client"
"kubesphere.io/kubesphere/pkg/api"
"kubesphere.io/kubesphere/pkg/apiserver/query" "kubesphere.io/kubesphere/pkg/apiserver/query"
) )
var ( type Interface interface {
Filters = map[schema.GroupVersionKind]FilterFunc{}
Comparers = map[schema.GroupVersionKind]CompareFunc{}
Transformers = map[schema.GroupVersionKind][]TransformFunc{}
)
type Reader interface {
// Get retrieves a single object by its namespace and name // 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 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 { type ResourceGetter interface {
GetResources(request *restful.Request, response *restful.Response) GetResource(schema.GroupVersionResource, string, string) (client.Object, error)
ListResources(request *restful.Request, response *restful.Response) ListResources(schema.GroupVersionResource, string, *query.Query) (client.ObjectList, error)
}
type Client interface {
Handler
Reader
} }
// CompareFunc return true is left great than right // 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 // selected matched ones
var filtered []runtime.Object 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 { if selected {
selected := true for _, transform := range transformFuncs {
o, err := meta.Accessor(obj) object = transform(object)
if err != nil { }
return err filtered = append(filtered, object)
}
for field, value := range q.Filters {
if !filterFunc(o, query.Filter{Field: field, Value: value}) {
selected = false
break
} }
} }
}
if selected {
for _, transform := range transformFuncs {
obj = transform(o)
}
filtered = append(filtered, obj)
}
return nil
})
// sort by sortBy field // sort by sortBy field
sort.Slice(filtered, func(i, j int) bool { 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 { 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) total := len(filtered)
@@ -115,10 +76,7 @@ func DefaultList(objList runtime.Object, q *query.Query, compareFunc CompareFunc
start, end := q.Pagination.GetValidPagination(total) start, end := q.Pagination.GetValidPagination(total)
return &api.ListResult{ return filtered[start:end]
TotalItems: len(filtered),
Items: objectsToInterfaces(filtered[start:end]),
}
} }
// DefaultObjectMetaCompare return true is left great than right // 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 // /namespaces?page=1&limit=10&label=kubesphere.io/workspace:system-workspace
case query.FieldLabel: case query.FieldLabel:
return labelMatch(item.GetLabels(), string(filter.Value)) return labelMatch(item.GetLabels(), string(filter.Value))
case query.ParameterFieldSelector:
return contains(item.(runtime.Object), filter.Value)
default: default:
return false return false
} }
@@ -218,10 +178,49 @@ func labelMatch(labels map[string]string, filter string) bool {
return false return false
} }
func objectsToInterfaces(objs []runtime.Object) []interface{} { // implement a generic query filter to support multiple field selectors with "jsonpath.JsonPathLookup"
res := make([]interface{}, 0) // https://github.com/oliveagle/jsonpath/blob/master/readme.md
for _, obj := range objs { func contains(object runtime.Object, queryValue query.Value) bool {
res = append(res, obj) // 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
} }

View File

@@ -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
}