remove useless go moudle

Signed-off-by: hongming <talonwan@yunify.com>
This commit is contained in:
hongming
2020-04-28 01:01:24 +08:00
parent b7a2705ac9
commit 8976ee242f
47 changed files with 187 additions and 5186 deletions

View File

@@ -37,6 +37,12 @@ const (
ResourceKindWorkspaceRole = "WorkspaceRole"
ResourcesSingularWorkspaceRole = "workspacerole"
ResourcesPluralWorkspaceRole = "workspaceroles"
ResourceKindClusterRole = "ClusterRole"
ResourcesSingularClusterRole = "clusterrole"
ResourcesPluralClusterRole = "clusterroles"
ResourceKindRole = "Role"
ResourcesSingularRole = "role"
ResourcesPluralRole = "roles"
RegoOverrideAnnotation = "iam.kubesphere.io/rego-override"
GlobalScope = "Global"
ClusterScope = "Cluster"

View File

@@ -11,13 +11,18 @@ import (
unionauth "k8s.io/apiserver/pkg/authentication/request/union"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/klog"
clusterv1alpha1 "kubesphere.io/kubesphere/pkg/apis/cluster/v1alpha1"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/basic"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/jwttoken"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/anonymous"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/basictoken"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/bearertoken"
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizerfactory"
authorizationoptions "kubesphere.io/kubesphere/pkg/apiserver/authorization/options"
"kubesphere.io/kubesphere/pkg/apiserver/authorization/path"
unionauthorizer "kubesphere.io/kubesphere/pkg/apiserver/authorization/union"
apiserverconfig "kubesphere.io/kubesphere/pkg/apiserver/config"
@@ -27,7 +32,7 @@ import (
"kubesphere.io/kubesphere/pkg/informers"
configv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/config/v1alpha2"
devopsv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/devops/v1alpha2"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/iam/v1alpha2"
iamapi "kubesphere.io/kubesphere/pkg/kapis/iam/v1alpha2"
loggingv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/logging/v1alpha2"
monitoringv1alpha3 "kubesphere.io/kubesphere/pkg/kapis/monitoring/v1alpha3"
networkv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/network/v1alpha2"
@@ -118,7 +123,6 @@ func (s *APIServer) PrepareRun() error {
s.container.Filter(logRequestAndResponse)
s.container.Router(restful.CurlyRouter{})
s.container.RecoverHandler(func(panicReason interface{}, httpWriter http.ResponseWriter) {
klog.Error(panicReason)
logStackOnRecover(panicReason, httpWriter)
})
@@ -144,7 +148,7 @@ func (s *APIServer) installKubeSphereAPIs() {
urlruntime.Must(networkv1alpha2.AddToContainer(s.container, s.Config.NetworkOptions.WeaveScopeHost))
urlruntime.Must(operationsv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes()))
urlruntime.Must(resourcesv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes(), s.InformerFactory))
urlruntime.Must(tenantv1alpha2.AddToContainer(s.container, s.KubernetesClient, s.InformerFactory))
urlruntime.Must(tenantv1alpha2.AddToContainer(s.container, s.InformerFactory))
urlruntime.Must(terminalv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes(), s.KubernetesClient.Config()))
urlruntime.Must(iamapi.AddToContainer(s.container, im.NewOperator(s.KubernetesClient.KubeSphere(), s.InformerFactory),
am.NewAMOperator(s.InformerFactory),
@@ -189,7 +193,6 @@ func (s *APIServer) buildHandlerChain() {
{Group: iamv1alpha2.SchemeGroupVersion.Group, Resource: iamv1alpha2.ResourcesPluralGlobalRoleBinding},
{Group: tenantv1alpha1.SchemeGroupVersion.Group, Resource: tenantv1alpha1.ResourcePluralWorkspace},
{Group: clusterv1alpha1.SchemeGroupVersion.Group, Resource: clusterv1alpha1.ResourcesPluralCluster},
{Group: clusterv1alpha1.SchemeGroupVersion.Group, Resource: clusterv1alpha1.ResourcesPluralAgent},
},
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/spf13/viper"
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
authorizationoptions "kubesphere.io/kubesphere/pkg/apiserver/authorization/options"
"kubesphere.io/kubesphere/pkg/simple/client/alerting"
"kubesphere.io/kubesphere/pkg/simple/client/cache"
"kubesphere.io/kubesphere/pkg/simple/client/devops/jenkins"
@@ -11,6 +12,7 @@ import (
"kubesphere.io/kubesphere/pkg/simple/client/ldap"
"kubesphere.io/kubesphere/pkg/simple/client/logging/elasticsearch"
"kubesphere.io/kubesphere/pkg/simple/client/monitoring/prometheus"
"kubesphere.io/kubesphere/pkg/simple/client/multicluster"
"kubesphere.io/kubesphere/pkg/simple/client/network"
"kubesphere.io/kubesphere/pkg/simple/client/notification"
"kubesphere.io/kubesphere/pkg/simple/client/openpitrix"
@@ -58,20 +60,20 @@ const (
// Config defines everything needed for apiserver to deal with external services
type Config struct {
DevopsOptions *jenkins.Options `json:"devops,omitempty" yaml:"devops,omitempty" mapstructure:"devops"`
SonarQubeOptions *sonarqube.Options `json:"sonarqube,omitempty" yaml:"sonarQube,omitempty" mapstructure:"sonarqube"`
KubernetesOptions *k8s.KubernetesOptions `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty" mapstructure:"kubernetes"`
ServiceMeshOptions *servicemesh.Options `json:"servicemesh,omitempty" yaml:"servicemesh,omitempty" mapstructure:"servicemesh"`
NetworkOptions *network.Options `json:"network,omitempty" yaml:"network,omitempty" mapstructure:"network"`
LdapOptions *ldap.Options `json:"ldap,omitempty" yaml:"ldap,omitempty" mapstructure:"ldap"`
RedisOptions *cache.Options `json:"redis,omitempty" yaml:"redis,omitempty" mapstructure:"redis"`
S3Options *s3.Options `json:"s3,omitempty" yaml:"s3,omitempty" mapstructure:"s3"`
OpenPitrixOptions *openpitrix.Options `json:"openpitrix,omitempty" yaml:"openpitrix,omitempty" mapstructure:"openpitrix"`
MonitoringOptions *prometheus.Options `json:"monitoring,omitempty" yaml:"monitoring,omitempty" mapstructure:"monitoring"`
LoggingOptions *elasticsearch.Options `json:"logging,omitempty" yaml:"logging,omitempty" mapstructure:"logging"`
AuthenticationOptions *authoptions.AuthenticationOptions `json:"authentication,omitempty" yaml:"authentication,omitempty" mapstructure:"authentication"`
DevopsOptions *jenkins.Options `json:"devops,omitempty" yaml:"devops,omitempty" mapstructure:"devops"`
SonarQubeOptions *sonarqube.Options `json:"sonarqube,omitempty" yaml:"sonarQube,omitempty" mapstructure:"sonarqube"`
KubernetesOptions *k8s.KubernetesOptions `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty" mapstructure:"kubernetes"`
ServiceMeshOptions *servicemesh.Options `json:"servicemesh,omitempty" yaml:"servicemesh,omitempty" mapstructure:"servicemesh"`
NetworkOptions *network.Options `json:"network,omitempty" yaml:"network,omitempty" mapstructure:"network"`
LdapOptions *ldap.Options `json:"-,omitempty" yaml:"ldap,omitempty" mapstructure:"ldap"`
RedisOptions *cache.Options `json:"redis,omitempty" yaml:"redis,omitempty" mapstructure:"redis"`
S3Options *s3.Options `json:"s3,omitempty" yaml:"s3,omitempty" mapstructure:"s3"`
OpenPitrixOptions *openpitrix.Options `json:"openpitrix,omitempty" yaml:"openpitrix,omitempty" mapstructure:"openpitrix"`
MonitoringOptions *prometheus.Options `json:"monitoring,omitempty" yaml:"monitoring,omitempty" mapstructure:"monitoring"`
LoggingOptions *elasticsearch.Options `json:"logging,omitempty" yaml:"logging,omitempty" mapstructure:"logging"`
AuthenticationOptions *authoptions.AuthenticationOptions `json:"authentication,omitempty" yaml:"authentication,omitempty" mapstructure:"authentication"`
AuthorizationOptions *authorizationoptions.AuthorizationOptions `json:"authorization,omitempty" yaml:"authorization,omitempty" mapstructure:"authorization"`
MultiClusterOptions *multicluster.Options `json:"multicluster,omitempty" yaml:"multicluster,omitempty" mapstructure:"multicluster"`
MultiClusterOptions *multicluster.Options `json:"multicluster,omitempty" yaml:"multicluster,omitempty" mapstructure:"multicluster"`
// Options used for enabling components, not actually used now. Once we switch Alerting/Notification API to kubesphere,
// we can add these options to kubesphere command lines
AlertingOptions *alerting.Options `json:"alerting,omitempty" yaml:"alerting,omitempty" mapstructure:"alerting"`
@@ -96,6 +98,7 @@ func New() *Config {
LoggingOptions: elasticsearch.NewElasticSearchOptions(),
AuthenticationOptions: authoptions.NewAuthenticateOptions(),
AuthorizationOptions: authorizationoptions.NewAuthorizationOptions(),
MultiClusterOptions: multicluster.NewOptions(),
}
}

View File

@@ -16,13 +16,13 @@ func TestParseQueryParameter(t *testing.T) {
}{
{
"test normal case",
"label=app.kubernetes.io/name:book&name=foo&status=Running&page=1&limit=10&ascending=true",
"label=app.kubernetes.io/name=book&name=foo&status=Running&page=1&limit=10&ascending=true",
&Query{
Pagination: newPagination(10, 0),
SortBy: FieldCreationTimeStamp,
Ascending: true,
Filters: map[Field]Value{
FieldLabel: Value("app.kubernetes.io/name:book"),
FieldLabel: Value("app.kubernetes.io/name=book"),
FieldName: Value("foo"),
FieldStatus: Value("Running"),
},

View File

@@ -136,12 +136,11 @@ func (h *iamHandler) ListNamespaceUsers(req *restful.Request, resp *restful.Resp
if subject.Kind == iamv1alpha2.ResourceKindUser {
user, err := h.im.DescribeUser(subject.Name)
if errors.IsNotFound(err) {
klog.Errorf("orphan subject: %+v", subject)
continue
}
if err != nil {
if errors.IsNotFound(err) {
klog.Errorf("orphan subject: %+v", subject)
continue
}
api.HandleInternalError(resp, req, err)
return
}
@@ -200,12 +199,11 @@ func (h *iamHandler) ListWorkspaceUsers(request *restful.Request, response *rest
if subject.Kind == iamv1alpha2.ResourceKindUser {
user, err := h.im.DescribeUser(subject.Name)
if errors.IsNotFound(err) {
klog.Errorf("orphan subject: %+v", subject)
continue
}
if err != nil {
if errors.IsNotFound(err) {
klog.Errorf("orphan subject: %+v", subject)
continue
}
api.HandleInternalError(response, request, err)
return
}

View File

@@ -47,7 +47,6 @@ func AddToContainer(container *restful.Container, im im.IdentityManagementInterf
ws.Route(ws.GET("/users").
To(handler.ListUsers).
Doc("List all users.").
Param(ws.PathParameter("user", "username")).
Returns(http.StatusOK, api.StatusOK, api.ListResult{}).
Metadata(restfulspec.KeyOpenAPITags, []string{constants.AccessManagementTag}))
// global resource

View File

@@ -9,14 +9,13 @@ import (
"kubesphere.io/kubesphere/pkg/apiserver/request"
"kubesphere.io/kubesphere/pkg/informers"
"kubesphere.io/kubesphere/pkg/models/tenant"
"kubesphere.io/kubesphere/pkg/simple/client/k8s"
)
type tenantHandler struct {
tenant tenant.Interface
}
func newTenantHandler(_ k8s.Client, factory informers.InformerFactory) *tenantHandler {
func newTenantHandler(factory informers.InformerFactory) *tenantHandler {
return &tenantHandler{
tenant: tenant.New(factory),

View File

@@ -27,7 +27,6 @@ import (
"kubesphere.io/kubesphere/pkg/constants"
"kubesphere.io/kubesphere/pkg/informers"
"kubesphere.io/kubesphere/pkg/models"
"kubesphere.io/kubesphere/pkg/simple/client/k8s"
"net/http"
)
@@ -37,9 +36,9 @@ const (
var GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha2"}
func AddToContainer(c *restful.Container, k8sClient k8s.Client, factory informers.InformerFactory) error {
func AddToContainer(c *restful.Container, factory informers.InformerFactory) error {
ws := runtime.NewWebService(GroupVersion)
handler := newTenantHandler(k8sClient, factory)
handler := newTenantHandler(factory)
ws.Route(ws.GET("/workspaces").
To(handler.ListWorkspaces).

View File

@@ -344,14 +344,13 @@ func (am *amOperator) ListGlobalRoles(query *query.Query) (*api.ListResult, erro
// GetRoleReferenceRules attempts to resolve the RoleBinding or ClusterRoleBinding.
func (am *amOperator) GetRoleReferenceRules(roleRef rbacv1.RoleRef, bindingNamespace string) ([]rbacv1.PolicyRule, error) {
switch roleRef.Kind {
case "Role":
case iamv1alpha2.ResourceKindRole:
role, err := am.k8sinformer.Rbac().V1().Roles().Lister().Roles(bindingNamespace).Get(roleRef.Name)
if err != nil {
return nil, err
}
return role.Rules, nil
case "ClusterRole":
case iamv1alpha2.ResourceKindClusterRole:
clusterRole, err := am.k8sinformer.Rbac().V1().ClusterRoles().Lister().Get(roleRef.Name)
if err != nil {
return nil, err

View File

@@ -19,6 +19,7 @@
package im
import (
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/api"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/query"
@@ -81,5 +82,12 @@ func (im *ldapOperator) CreateUser(user *iamv1alpha2.User) (*iamv1alpha2.User, e
}
func (im *ldapOperator) ListUsers(query *query.Query) (*api.ListResult, error) {
panic("not implement")
result, err := im.ldapClient.List(query)
if err != nil {
klog.Error(err)
return nil, err
}
return result, nil
}

View File

@@ -83,9 +83,9 @@ func (d *applicationsGetter) filter(object runtime.Object, filter query.Filter)
return v1alpha3.DefaultObjectMetaFilter(application.ObjectMeta, filter)
}
func lastUpdateTime(deployment *appv1beta1.Application) time.Time {
lut := deployment.CreationTimestamp.Time
for _, condition := range deployment.Status.Conditions {
func lastUpdateTime(application *appv1beta1.Application) time.Time {
lut := application.CreationTimestamp.Time
for _, condition := range application.Status.Conditions {
if condition.LastUpdateTime.After(lut) {
lut = condition.LastUpdateTime.Time
}

View File

@@ -143,7 +143,7 @@ func (t *tenantOperator) ListNamespaces(user user.Info, workspace string, queryP
if decision == authorizer.DecisionAllow {
queryParam.Filters[query.FieldLabel] = query.Value(fmt.Sprintf("%s:%s", tenantv1alpha1.WorkspaceLabel, workspace))
queryParam.Filters[query.FieldLabel] = query.Value(fmt.Sprintf("%s=%s", tenantv1alpha1.WorkspaceLabel, workspace))
result, err := t.resourceGetter.List("namespaces", "", queryParam)

View File

@@ -1,7 +1,9 @@
package ldap
import (
"kubesphere.io/kubesphere/pkg/api"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/query"
)
// Interface defines CRUD behaviors of manipulating users
@@ -20,4 +22,6 @@ type Interface interface {
// Authenticate checks if (name, password) is valid, return ErrInvalidCredentials if not
Authenticate(name string, password string) error
List(query *query.Query) (*api.ListResult, error)
}

View File

@@ -22,8 +22,13 @@ import (
"github.com/go-ldap/ldap"
"github.com/google/uuid"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog"
"kubesphere.io/kubesphere/pkg/api"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/query"
"kubesphere.io/kubesphere/pkg/server/errors"
"sort"
"strings"
"sync"
"time"
)
@@ -109,6 +114,7 @@ func (l *ldapInterfaceImpl) createSearchBase() error {
if err != nil {
return err
}
defer conn.Close()
createIfNotExistsFunc := func(request *ldap.AddRequest) error {
searchRequest := &ldap.SearchRequest{
@@ -165,10 +171,10 @@ func (l *ldapInterfaceImpl) newConn() (ldap.Client, error) {
if err != nil {
return nil, err
}
defer conn.Close()
err = conn.Bind(l.managerDN, l.managerPassword)
if err != nil {
conn.Close()
return nil, err
}
return conn, nil
@@ -357,6 +363,7 @@ func (l *ldapInterfaceImpl) Authenticate(username, password string) error {
if err != nil {
return err
}
defer conn.Close()
dn := l.dnForUsername(username)
err = conn.Bind(dn, password)
@@ -365,3 +372,106 @@ func (l *ldapInterfaceImpl) Authenticate(username, password string) error {
}
return err
}
func (l *ldapInterfaceImpl) List(query *query.Query) (*api.ListResult, error) {
conn, err := l.newConn()
if err != nil {
return nil, err
}
defer conn.Close()
pageControl := ldap.NewControlPaging(1000)
users := make([]iamv1alpha2.User, 0)
filter := "(&(objectClass=inetOrgPerson))"
if keyword := query.Filters["keyword"]; keyword != "" {
filter = fmt.Sprintf("(&(objectClass=inetOrgPerson)(|(uid=*%s*)(mail=*%s*)(description=*%s*)))", keyword, keyword, keyword)
}
if username := query.Filters["username"]; username != "" {
uidFilter := ""
for _, username := range strings.Split(string(username), "|") {
uidFilter += fmt.Sprintf("(uid=%s)", username)
}
filter = fmt.Sprintf("(&(objectClass=inetOrgPerson)(|%s))", uidFilter)
}
if email := query.Filters["email"]; email != "" {
emailFilter := ""
for _, username := range strings.Split(string(email), "|") {
emailFilter += fmt.Sprintf("(mail=%s)", username)
}
filter = fmt.Sprintf("(&(objectClass=inetOrgPerson)(|%s))", emailFilter)
}
for {
userSearchRequest := ldap.NewSearchRequest(
l.userSearchBase,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
filter,
[]string{"uid", "mail", "description", "preferredLanguage", "createTimestamp"},
[]ldap.Control{pageControl},
)
response, err := conn.Search(userSearchRequest)
if err != nil {
klog.Errorln("search user", err)
return nil, err
}
for _, entry := range response.Entries {
uid := entry.GetAttributeValue("uid")
email := entry.GetAttributeValue("mail")
description := entry.GetAttributeValue("description")
lang := entry.GetAttributeValue("preferredLanguage")
createTimestamp, _ := time.Parse("20060102150405Z", entry.GetAttributeValue("createTimestamp"))
user := iamv1alpha2.User{ObjectMeta: metav1.ObjectMeta{Name: uid, CreationTimestamp: metav1.Time{Time: createTimestamp}}, Spec: iamv1alpha2.UserSpec{
Email: email,
Lang: lang,
Description: description,
}}
users = append(users, user)
}
updatedControl := ldap.FindControl(response.Controls, ldap.ControlTypePaging)
if ctrl, ok := updatedControl.(*ldap.ControlPaging); ctrl != nil && ok && len(ctrl.Cookie) != 0 {
pageControl.SetCookie(ctrl.Cookie)
continue
}
break
}
sort.Slice(users, func(i, j int) bool {
if !query.Ascending {
i, j = j, i
}
switch query.SortBy {
case "username":
return strings.Compare(users[i].Name, users[j].Name) <= 0
case "createTime":
fallthrough
default:
return users[i].CreationTimestamp.Before(&users[j].CreationTimestamp)
}
})
items := make([]interface{}, 0)
for i, user := range users {
if i >= query.Pagination.Offset && len(items) < query.Pagination.Limit {
items = append(items, user)
}
}
return &api.ListResult{
Items: items,
TotalItems: len(users),
}, nil
}

View File

@@ -2,7 +2,9 @@ package ldap
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"kubesphere.io/kubesphere/pkg/api"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/query"
)
// simpleLdap is a implementation of ldap.Interface, you should never use this in production env!
@@ -74,3 +76,16 @@ func (s simpleLdap) Authenticate(name string, password string) error {
return nil
}
func (l *simpleLdap) List(query *query.Query) (*api.ListResult, error) {
items := make([]interface{}, 0)
for _, user := range l.store {
items = append(items, user)
}
return &api.ListResult{
Items: items,
TotalItems: len(items),
}, nil
}

View File

@@ -4,80 +4,8 @@ import (
"github.com/google/go-cmp/cmp"
"kubesphere.io/kubesphere/pkg/simple/client/logging"
"testing"
"time"
)
func TestMainBool(t *testing.T) {
var tests = []struct {
description string
searchFilter logging.SearchFilter
expected *bodyBuilder
}{
{
description: "filter 2 namespaces",
searchFilter: logging.SearchFilter{
NamespaceFilter: map[string]time.Time{
"kubesphere-system": time.Unix(1582000000, 0),
"kubesphere-logging-system": time.Unix(1582969999, 0),
},
},
expected: &bodyBuilder{Body{
Query: &Query{
Bool: Bool{
Filter: []Match{
{
Bool: &Bool{
Should: []Match{
{
Bool: &Bool{
Filter: []Match{
{
MatchPhrase: map[string]string{"kubernetes.namespace_name.keyword": "kubesphere-system"},
},
{
Range: &Range{&Time{Gte: func() *time.Time { t := time.Unix(1582000000, 0); return &t }()}},
},
},
},
},
{
Bool: &Bool{
Filter: []Match{
{
MatchPhrase: map[string]string{"kubernetes.namespace_name.keyword": "kubesphere-logging-system"},
},
{
Range: &Range{&Time{Gte: func() *time.Time { t := time.Unix(1582969999, 0); return &t }()}},
},
},
},
},
},
MinimumShouldMatch: 1,
},
},
},
},
},
}},
},
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
body, err := newBodyBuilder().mainBool(test.searchFilter).bytes()
expected, _ := test.expected.bytes()
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(body, expected); diff != "" {
t.Fatalf("%T differ (-got, +want): %s", expected, diff)
}
})
}
}
func TestCardinalityAggregation(t *testing.T) {
var test = struct {
description string