Add an initial version of a search service.

It's still incomplete and isn't working yet.
This commit is contained in:
André Duffeck
2022-04-08 10:49:29 +02:00
parent c3f8d837bd
commit 3099d4a821
35 changed files with 2196 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
syntax = "proto3";
package ocis.messages.search.v0;
option go_package = "github.com/owncloud/ocis/protogen/gen/ocis/messages/search/v0";
message Match {
// key of the recorda
string key = 1;
// value in the record
bytes value = 2;
// time.Duration (signed int64 nanoseconds)
int64 expiry = 3;
// the associated metadata
map<string,Field> metadata = 4;
}

View File

@@ -0,0 +1,84 @@
syntax = "proto3";
package ocis.services.search.v0;
option go_package = "github.com/owncloud/ocis/protogen/gen/ocis/service/search/v0";
import "ocis/messages/search/v0/search.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "cs3/storage/provider/v1beta1/resources.proto";
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
title: "ownCloud Infinite Scale search";
version: "1.0.0";
contact: {
name: "ownCloud GmbH";
url: "https://github.com/owncloud/ocis";
email: "support@owncloud.com";
};
license: {
name: "Apache-2.0";
url: "https://github.com/owncloud/ocis/blob/master/LICENSE";
};
};
schemes: HTTP;
schemes: HTTPS;
consumes: "application/json";
produces: "application/json";
external_docs: {
description: "Developer Manual";
url: "https://owncloud.dev/extensions/search/";
};
};
service SearchProvider {
rpc Search(SearchRequest) returns (SearchResponse) {};
}
service IndexProvider {
rpc Search(SearchIndexRequest) returns (SearchIndexResponse) {};
rpc Index(IndexRequest) returns (IndexResponse) {};
rpc Remove(RemoveRequest) returns (RemoveResponse) {};
}
message SearchRequest {
// Optional. The maximum number of entries to return in the response
int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL];
// Optional. A pagination token returned from a previous call to `Get`
// that indicates from where search should continue
string page_token = 2 [(google.api.field_behavior) = OPTIONAL];
// Optional. Used to specify a subset of fields that should be
// returned by a get operation or modified by an update operation.
google.protobuf.FieldMask field_mask = 3;
string query = 4;
}
message SearchResponse {
repeated ocis.messages.search.v0.Match matches = 1;
// Token to retrieve the next page of results, or empty if there are no
// more results in the list
string next_page_token = 2;
}
message SearchIndexRequest {
// Optional. The maximum number of entries to return in the response
int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL];
// Optional. A pagination token returned from a previous call to `Get`
// that indicates from where search should continue
string page_token = 2 [(google.api.field_behavior) = OPTIONAL];
// Optional. Used to specify a subset of fields that should be
// returned by a get operation or modified by an update operation.
google.protobuf.FieldMask field_mask = 3;
string query = 4;
string
}
message SearchIndexResponse {
repeated ocis.messages.search.v0.Record records = 1;
}

View File

@@ -0,0 +1,53 @@
package command
import (
"fmt"
"net/http"
"github.com/owncloud/ocis/search/pkg/config"
"github.com/owncloud/ocis/search/pkg/config/parser"
"github.com/owncloud/ocis/search/pkg/logging"
"github.com/urfave/cli/v2"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "health",
Usage: "check health status",
Category: "info",
Before: func(c *cli.Context) error {
return parser.ParseConfig(cfg)
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
resp, err := http.Get(
fmt.Sprintf(
"http://%s/healthz",
cfg.Debug.Addr,
),
)
if err != nil {
logger.Fatal().
Err(err).
Msg("Failed to request health check")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
logger.Fatal().
Int("code", resp.StatusCode).
Msg("Health seems to be in bad state")
}
logger.Debug().
Int("code", resp.StatusCode).
Msg("Health got a good state")
return nil
},
}
}

View File

@@ -0,0 +1,64 @@
package command
import (
"context"
"os"
"github.com/owncloud/ocis/ocis-pkg/clihelper"
"github.com/thejerf/suture/v4"
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
"github.com/owncloud/ocis/search/pkg/config"
"github.com/urfave/cli/v2"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) cli.Commands {
return []*cli.Command{
// start this service
Server(cfg),
// interaction with this service
// infos about this service
Health(cfg),
Version(cfg),
}
}
// Execute is the entry point for the ocis-search command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cli.App{
Name: "ocis-search",
Usage: "Serve search API for oCIS",
Commands: GetCommands(cfg),
})
cli.HelpFlag = &cli.BoolFlag{
Name: "help,h",
Usage: "Show the help",
}
return app.Run(os.Args)
}
// SutureService allows for the search command to be embedded and supervised by a suture supervisor tree.
type SutureService struct {
cfg *config.Config
}
// NewSutureService creates a new search.SutureService
func NewSutureService(cfg *ociscfg.Config) suture.Service {
cfg.Search.Commons = cfg.Commons
return SutureService{
cfg: cfg.Search,
}
}
func (s SutureService) Serve(ctx context.Context) error {
s.cfg.Context = ctx
if err := Execute(s.cfg); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,93 @@
package command
import (
"context"
"fmt"
"github.com/oklog/run"
"github.com/owncloud/ocis/idp/pkg/server/http"
"github.com/owncloud/ocis/ocis-pkg/version"
"github.com/owncloud/ocis/search/pkg/config"
"github.com/owncloud/ocis/search/pkg/config/parser"
"github.com/owncloud/ocis/search/pkg/logging"
"github.com/owncloud/ocis/search/pkg/metrics"
"github.com/owncloud/ocis/search/pkg/server/debug"
"github.com/owncloud/ocis/search/pkg/tracing"
"github.com/urfave/cli/v2"
)
// Server is the entrypoint for the server command.
func Server(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "server",
Usage: fmt.Sprintf("start %s extension without runtime (unsupervised mode)", cfg.Service.Name),
Category: "server",
Before: func(c *cli.Context) error {
return parser.ParseConfig(cfg)
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
err := tracing.Configure(cfg)
if err != nil {
return err
}
gr := run.Group{}
ctx, cancel := func() (context.Context, context.CancelFunc) {
if cfg.Context == nil {
return context.WithCancel(context.Background())
}
return context.WithCancel(cfg.Context)
}()
mtrcs := metrics.New()
defer cancel()
mtrcs.BuildInfo.WithLabelValues(version.String).Set(1)
{
server, err := http.Server(
http.Logger(logger),
http.Context(ctx),
http.Config(cfg),
http.Metrics(mtrcs),
)
if err != nil {
logger.Info().Err(err).Str("transport", "http").Msg("Failed to initialize server")
return err
}
gr.Add(func() error {
return server.Run()
}, func(_ error) {
logger.Info().
Str("transport", "http").
Msg("Shutting down server")
cancel()
})
}
{
server, err := debug.Server(
debug.Logger(logger),
debug.Context(ctx),
debug.Config(cfg),
)
if err != nil {
logger.Info().Err(err).Str("transport", "debug").Msg("Failed to initialize server")
return err
}
gr.Add(server.ListenAndServe, func(_ error) {
_ = server.Shutdown(ctx)
cancel()
})
}
return gr.Run()
},
}
}

View File

@@ -0,0 +1,50 @@
package command
import (
"fmt"
"os"
"github.com/owncloud/ocis/ocis-pkg/registry"
"github.com/owncloud/ocis/ocis-pkg/version"
tw "github.com/olekukonko/tablewriter"
"github.com/owncloud/ocis/search/pkg/config"
"github.com/urfave/cli/v2"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "version",
Usage: "print the version of this binary and the running extension instances",
Category: "info",
Action: func(c *cli.Context) error {
fmt.Println("Version: " + version.String)
fmt.Printf("Compiled: %s\n", version.Compiled())
fmt.Println("")
reg := registry.GetRegistry()
services, err := reg.GetService(cfg.HTTP.Namespace + "." + cfg.Service.Name)
if err != nil {
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
return err
}
if len(services) == 0 {
fmt.Println("No running " + cfg.Service.Name + " service found.")
return nil
}
table := tw.NewWriter(os.Stdout)
table.SetHeader([]string{"Version", "Address", "Id"})
table.SetAutoFormatHeaders(false)
for _, s := range services {
for _, n := range s.Nodes {
table.Append([]string{s.Version, n.Address, n.Id})
}
}
table.Render()
return nil
},
}
}

View File

@@ -0,0 +1,25 @@
package config
import (
"context"
"github.com/owncloud/ocis/ocis-pkg/shared"
)
// Config combines all available configuration parts.
type Config struct {
*shared.Commons `ocisConfig:"-" yaml:"-"`
Service Service `ocisConfig:"-" yaml:"-"`
Tracing *Tracing `ocisConfig:"tracing"`
Log *Log `ocisConfig:"log"`
Debug Debug `ocisConfig:"debug"`
HTTP HTTP `ocisConfig:"http"`
Reva Reva `ocisConfig:"reva"`
TokenManager TokenManager `ocisConfig:"token_manager"`
Context context.Context `ocisConfig:"-" yaml:"-"`
}

View File

@@ -0,0 +1,9 @@
package config
// Debug defines the available debug configuration.
type Debug struct {
Addr string `ocisConfig:"addr" env:"SEARCH_DEBUG_ADDR"`
Token string `ocisConfig:"token" env:"SEARCH_DEBUG_TOKEN"`
Pprof bool `ocisConfig:"pprof" env:"SEARCH_DEBUG_PPROF"`
Zpages bool `ocisConfig:"zpages" env:"SEARCH_DEBUG_ZPAGES"`
}

View File

@@ -0,0 +1,62 @@
package defaults
import (
"strings"
"github.com/owncloud/ocis/search/pkg/config"
)
func DefaultConfig() *config.Config {
return &config.Config{
Debug: config.Debug{
Addr: "127.0.0.1:9124",
Token: "",
},
HTTP: config.HTTP{
Addr: "127.0.0.1:9120",
Namespace: "com.owncloud.search",
Root: "/search",
},
Service: config.Service{
Name: "search",
},
Reva: config.Reva{
Address: "127.0.0.1:9142",
},
TokenManager: config.TokenManager{
JWTSecret: "Pive-Fumkiu4",
},
}
}
func EnsureDefaults(cfg *config.Config) {
// provide with defaults for shared logging, since we need a valid destination address for BindEnv.
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
cfg.Log = &config.Log{
Level: cfg.Commons.Log.Level,
Pretty: cfg.Commons.Log.Pretty,
Color: cfg.Commons.Log.Color,
File: cfg.Commons.Log.File,
}
} else if cfg.Log == nil {
cfg.Log = &config.Log{}
}
// provide with defaults for shared tracing, since we need a valid destination address for BindEnv.
if cfg.Tracing == nil && cfg.Commons != nil && cfg.Commons.Tracing != nil {
cfg.Tracing = &config.Tracing{
Enabled: cfg.Commons.Tracing.Enabled,
Type: cfg.Commons.Tracing.Type,
Endpoint: cfg.Commons.Tracing.Endpoint,
Collector: cfg.Commons.Tracing.Collector,
}
} else if cfg.Tracing == nil {
cfg.Tracing = &config.Tracing{}
}
}
func Sanitize(cfg *config.Config) {
// sanitize config
if cfg.HTTP.Root != "/" {
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
}
}

View File

@@ -0,0 +1,8 @@
package config
// HTTP defines the available http configuration.
type HTTP struct {
Addr string `ocisConfig:"addr" env:"SEARCH_HTTP_ADDR"`
Namespace string `ocisConfig:"-" yaml:"-"`
Root string `ocisConfig:"root" env:"SEARCH_HTTP_ROOT"`
}

9
search/pkg/config/log.go Normal file
View File

@@ -0,0 +1,9 @@
package config
// Log defines the available log configuration.
type Log struct {
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;SEARCH_LOG_LEVEL"`
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;SEARCH_LOG_PRETTY"`
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;SEARCH_LOG_COLOR"`
File string `mapstructure:"file" env:"OCIS_LOG_FILE;SEARCH_LOG_FILE"`
}

View File

@@ -0,0 +1,33 @@
package parser
import (
"errors"
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
"github.com/owncloud/ocis/search/pkg/config"
"github.com/owncloud/ocis/search/pkg/config/defaults"
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
)
// ParseConfig loads accounts configuration from known paths.
func ParseConfig(cfg *config.Config) error {
_, err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg)
if err != nil {
return err
}
defaults.EnsureDefaults(cfg)
// load all env variables relevant to the config in the current context.
if err := envdecode.Decode(cfg); err != nil {
// no environment variable set for this config is an expected "error"
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
return err
}
}
defaults.Sanitize(cfg)
return nil
}

11
search/pkg/config/reva.go Normal file
View File

@@ -0,0 +1,11 @@
package config
// Reva defines all available REVA configuration.
type Reva struct {
Address string `ocisConfig:"address" env:"REVA_GATEWAY"`
}
// TokenManager is the config for using the reva token manager
type TokenManager struct {
JWTSecret string `ocisConfig:"jwt_secret" env:"OCIS_JWT_SECRET;SEARCH_JWT_SECRET"`
}

View File

@@ -0,0 +1,6 @@
package config
// Service defines the available service configuration.
type Service struct {
Name string `ocisConfig:"-" yaml:"-"`
}

View File

@@ -0,0 +1,9 @@
package config
// Tracing defines the available tracing configuration.
type Tracing struct {
Enabled bool `ocisConfig:"enabled" env:"OCIS_TRACING_ENABLED;SEARCH_TRACING_ENABLED"`
Type string `ocisConfig:"type" env:"OCIS_TRACING_TYPE;SEARCH_TRACING_TYPE"`
Endpoint string `ocisConfig:"endpoint" env:"OCIS_TRACING_ENDPOINT;SEARCH_TRACING_ENDPOINT"`
Collector string `ocisConfig:"collector" env:"OCIS_TRACING_COLLECTOR;SEARCH_TRACING_COLLECTOR"`
}

View File

@@ -0,0 +1,17 @@
package logging
import (
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/search/pkg/config"
)
// LoggerFromConfig initializes a service-specific logger instance.
func Configure(name string, cfg *config.Log) log.Logger {
return log.NewLogger(
log.Name(name),
log.Level(cfg.Level),
log.Pretty(cfg.Pretty),
log.Color(cfg.Color),
log.File(cfg.File),
)
}

View File

@@ -0,0 +1,33 @@
package metrics
import "github.com/prometheus/client_golang/prometheus"
var (
// Namespace defines the namespace for the defines metrics.
Namespace = "ocis"
// Subsystem defines the subsystem for the defines metrics.
Subsystem = "search"
)
// Metrics defines the available metrics of this service.
type Metrics struct {
// Counter *prometheus.CounterVec
BuildInfo *prometheus.GaugeVec
}
// New initializes the available metrics.
func New() *Metrics {
m := &Metrics{
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
Namespace: Namespace,
Subsystem: Subsystem,
Name: "build_info",
Help: "Build information",
}, []string{"version"}),
}
_ = prometheus.Register(m.BuildInfo)
// TODO: implement metrics
return m
}

View File

@@ -0,0 +1,122 @@
// Copyright 2018-2022 CERN
//
// 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.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package index
import (
"context"
"strings"
"github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
"github.com/blevesearch/bleve/v2/mapping"
"github.com/owncloud/ocis/search/pkg/search"
sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
type Index struct {
bleveIndex bleve.Index
}
type Entity struct {
RootID string
ID string
Name string
Size uint64
}
func NewPersisted(path string) (*Index, error) {
bi, err := bleve.New(path, BuildMapping())
if err != nil {
return nil, err
}
return &Index{
bleveIndex: bi,
}, nil
}
func New(bleveIndex bleve.Index) (*Index, error) {
return &Index{
bleveIndex: bleveIndex,
}, nil
}
func (i *Index) Add(ref *sprovider.Reference, ri *sprovider.ResourceInfo) error {
entity := toEntity(ref, ri)
return i.bleveIndex.Index(entity.ID, entity)
}
func (i *Index) Search(ctx context.Context, req *search.SearchIndexRequest) (*search.SearchIndexResult, error) {
bleveReq := bleve.NewSearchRequest(bleve.NewMatchQuery(req.Query))
bleveReq.Fields = []string{"*"}
res, err := i.bleveIndex.Search(bleveReq)
if err != nil {
return nil, err
}
matches := []search.Match{}
for _, h := range res.Hits {
match, err := fromFields(h.Fields)
if err != nil {
return nil, err
}
matches = append(matches, match)
}
return &search.SearchIndexResult{
Matches: matches,
}, nil
}
func BuildMapping() mapping.IndexMapping {
indexMapping := bleve.NewIndexMapping()
indexMapping.DefaultAnalyzer = keyword.Name
return indexMapping
}
func toEntity(ref *sprovider.Reference, ri *sprovider.ResourceInfo) *Entity {
return &Entity{
RootID: ref.ResourceId.GetStorageId() + ":" + ref.ResourceId.GetOpaqueId(),
ID: ri.Id.GetStorageId() + ":" + ri.Id.GetOpaqueId(),
Name: ri.Path,
Size: ri.Size,
}
}
func fromFields(fields map[string]interface{}) (search.Match, error) {
rootIDParts := strings.SplitN(fields["RootID"].(string), ":", 2)
IDParts := strings.SplitN(fields["ID"].(string), ":", 2)
return search.Match{
Reference: &sprovider.Reference{
ResourceId: &sprovider.ResourceId{
StorageId: rootIDParts[0],
OpaqueId: rootIDParts[1],
},
},
Info: &sprovider.ResourceInfo{
Id: &sprovider.ResourceId{
StorageId: IDParts[0],
OpaqueId: IDParts[1],
},
Path: fields["Name"].(string),
},
}, nil
}

View File

@@ -0,0 +1,13 @@
package index_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestIndex(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Index Suite")
}

View File

@@ -0,0 +1,123 @@
package index_test
import (
"context"
"github.com/blevesearch/bleve/v2"
sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/owncloud/ocis/search/pkg/search"
"github.com/owncloud/ocis/search/pkg/search/index"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Index", func() {
var (
i *index.Index
bleveIndex bleve.Index
ref *sprovider.Reference
ri *sprovider.ResourceInfo
ctx context.Context
)
BeforeEach(func() {
var err error
bleveIndex, err = bleve.NewMemOnly(index.BuildMapping())
Expect(err).ToNot(HaveOccurred())
i, err = index.New(bleveIndex)
Expect(err).ToNot(HaveOccurred())
ref = &sprovider.Reference{
ResourceId: &sprovider.ResourceId{
StorageId: "storageid",
OpaqueId: "rootopaqueid",
},
}
ri = &sprovider.ResourceInfo{
Id: &sprovider.ResourceId{
StorageId: "storageid",
OpaqueId: "opaqueid",
},
Path: "foo.pdf",
}
})
Describe("New", func() {
It("returns a new index instance", func() {
i, err := index.New(bleveIndex)
Expect(err).ToNot(HaveOccurred())
Expect(i).ToNot(BeNil())
})
})
Describe("NewPersisted", func() {
It("returns a new index instance", func() {
i, err := index.NewPersisted("")
Expect(err).ToNot(HaveOccurred())
Expect(i).ToNot(BeNil())
})
})
Describe("Search", func() {
It("finds files by prefix", func() {
err := i.Add(ref, ri)
Expect(err).ToNot(HaveOccurred())
res, err := i.Search(ctx, &search.SearchIndexRequest{
Reference: &sprovider.Reference{
ResourceId: ref.ResourceId,
},
Query: "foo.pdf",
})
Expect(err).ToNot(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(len(res.Matches)).To(Equal(1))
Expect(res.Matches[0].Reference.ResourceId).To(Equal(ref.ResourceId))
Expect(res.Matches[0].Info.Id).To(Equal(ri.Id))
Expect(res.Matches[0].Info.Path).To(Equal(ri.Path))
})
PIt("finds files living deeper in the tree by prefix")
PIt("finds directories by prefix")
PIt("finds directories living deeper in the tree by prefix")
})
Describe("Scan", func() {
PIt("adds the given resource recursively")
})
Describe("Index", func() {
It("adds a resourceInfo to the index", func() {
err := i.Add(ref, ri)
Expect(err).ToNot(HaveOccurred())
count, err := bleveIndex.DocCount()
Expect(err).ToNot(HaveOccurred())
Expect(count).To(Equal(uint64(1)))
query := bleve.NewMatchQuery("foo.pdf")
res, err := bleveIndex.Search(bleve.NewSearchRequest(query))
Expect(err).ToNot(HaveOccurred())
Expect(res.Hits.Len()).To(Equal(1))
})
It("updates an existing resource in the index", func() {
err := i.Add(ref, ri)
Expect(err).ToNot(HaveOccurred())
count, _ := bleveIndex.DocCount()
Expect(count).To(Equal(uint64(1)))
err = i.Add(ref, ri)
Expect(err).ToNot(HaveOccurred())
count, _ = bleveIndex.DocCount()
Expect(count).To(Equal(uint64(1)))
})
})
Describe("Remove", func() {
PIt("removes a resource from the index")
})
})

View File

@@ -0,0 +1,415 @@
// Code generated by mockery v2.10.0. DO NOT EDIT.
package mocks
import (
context "context"
bleve "github.com/blevesearch/bleve/v2"
index "github.com/blevesearch/bleve_index_api"
mapping "github.com/blevesearch/bleve/v2/mapping"
mock "github.com/stretchr/testify/mock"
)
// BleveIndex is an autogenerated mock type for the BleveIndex type
type BleveIndex struct {
mock.Mock
}
// Advanced provides a mock function with given fields:
func (_m *BleveIndex) Advanced() (index.Index, error) {
ret := _m.Called()
var r0 index.Index
if rf, ok := ret.Get(0).(func() index.Index); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(index.Index)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Batch provides a mock function with given fields: b
func (_m *BleveIndex) Batch(b *bleve.Batch) error {
ret := _m.Called(b)
var r0 error
if rf, ok := ret.Get(0).(func(*bleve.Batch) error); ok {
r0 = rf(b)
} else {
r0 = ret.Error(0)
}
return r0
}
// Close provides a mock function with given fields:
func (_m *BleveIndex) Close() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Delete provides a mock function with given fields: id
func (_m *BleveIndex) Delete(id string) error {
ret := _m.Called(id)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(id)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteInternal provides a mock function with given fields: key
func (_m *BleveIndex) DeleteInternal(key []byte) error {
ret := _m.Called(key)
var r0 error
if rf, ok := ret.Get(0).(func([]byte) error); ok {
r0 = rf(key)
} else {
r0 = ret.Error(0)
}
return r0
}
// DocCount provides a mock function with given fields:
func (_m *BleveIndex) DocCount() (uint64, error) {
ret := _m.Called()
var r0 uint64
if rf, ok := ret.Get(0).(func() uint64); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(uint64)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Document provides a mock function with given fields: id
func (_m *BleveIndex) Document(id string) (index.Document, error) {
ret := _m.Called(id)
var r0 index.Document
if rf, ok := ret.Get(0).(func(string) index.Document); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(index.Document)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FieldDict provides a mock function with given fields: field
func (_m *BleveIndex) FieldDict(field string) (index.FieldDict, error) {
ret := _m.Called(field)
var r0 index.FieldDict
if rf, ok := ret.Get(0).(func(string) index.FieldDict); ok {
r0 = rf(field)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(index.FieldDict)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(field)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FieldDictPrefix provides a mock function with given fields: field, termPrefix
func (_m *BleveIndex) FieldDictPrefix(field string, termPrefix []byte) (index.FieldDict, error) {
ret := _m.Called(field, termPrefix)
var r0 index.FieldDict
if rf, ok := ret.Get(0).(func(string, []byte) index.FieldDict); ok {
r0 = rf(field, termPrefix)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(index.FieldDict)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []byte) error); ok {
r1 = rf(field, termPrefix)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FieldDictRange provides a mock function with given fields: field, startTerm, endTerm
func (_m *BleveIndex) FieldDictRange(field string, startTerm []byte, endTerm []byte) (index.FieldDict, error) {
ret := _m.Called(field, startTerm, endTerm)
var r0 index.FieldDict
if rf, ok := ret.Get(0).(func(string, []byte, []byte) index.FieldDict); ok {
r0 = rf(field, startTerm, endTerm)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(index.FieldDict)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, []byte, []byte) error); ok {
r1 = rf(field, startTerm, endTerm)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Fields provides a mock function with given fields:
func (_m *BleveIndex) Fields() ([]string, error) {
ret := _m.Called()
var r0 []string
if rf, ok := ret.Get(0).(func() []string); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetInternal provides a mock function with given fields: key
func (_m *BleveIndex) GetInternal(key []byte) ([]byte, error) {
ret := _m.Called(key)
var r0 []byte
if rf, ok := ret.Get(0).(func([]byte) []byte); ok {
r0 = rf(key)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]byte) error); ok {
r1 = rf(key)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Index provides a mock function with given fields: id, data
func (_m *BleveIndex) Index(id string, data interface{}) error {
ret := _m.Called(id, data)
var r0 error
if rf, ok := ret.Get(0).(func(string, interface{}) error); ok {
r0 = rf(id, data)
} else {
r0 = ret.Error(0)
}
return r0
}
// Mapping provides a mock function with given fields:
func (_m *BleveIndex) Mapping() mapping.IndexMapping {
ret := _m.Called()
var r0 mapping.IndexMapping
if rf, ok := ret.Get(0).(func() mapping.IndexMapping); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(mapping.IndexMapping)
}
}
return r0
}
// Name provides a mock function with given fields:
func (_m *BleveIndex) Name() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// NewBatch provides a mock function with given fields:
func (_m *BleveIndex) NewBatch() *bleve.Batch {
ret := _m.Called()
var r0 *bleve.Batch
if rf, ok := ret.Get(0).(func() *bleve.Batch); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*bleve.Batch)
}
}
return r0
}
// Search provides a mock function with given fields: req
func (_m *BleveIndex) Search(req *bleve.SearchRequest) (*bleve.SearchResult, error) {
ret := _m.Called(req)
var r0 *bleve.SearchResult
if rf, ok := ret.Get(0).(func(*bleve.SearchRequest) *bleve.SearchResult); ok {
r0 = rf(req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*bleve.SearchResult)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*bleve.SearchRequest) error); ok {
r1 = rf(req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SearchInContext provides a mock function with given fields: ctx, req
func (_m *BleveIndex) SearchInContext(ctx context.Context, req *bleve.SearchRequest) (*bleve.SearchResult, error) {
ret := _m.Called(ctx, req)
var r0 *bleve.SearchResult
if rf, ok := ret.Get(0).(func(context.Context, *bleve.SearchRequest) *bleve.SearchResult); ok {
r0 = rf(ctx, req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*bleve.SearchResult)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *bleve.SearchRequest) error); ok {
r1 = rf(ctx, req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetInternal provides a mock function with given fields: key, val
func (_m *BleveIndex) SetInternal(key []byte, val []byte) error {
ret := _m.Called(key, val)
var r0 error
if rf, ok := ret.Get(0).(func([]byte, []byte) error); ok {
r0 = rf(key, val)
} else {
r0 = ret.Error(0)
}
return r0
}
// SetName provides a mock function with given fields: _a0
func (_m *BleveIndex) SetName(_a0 string) {
_m.Called(_a0)
}
// Stats provides a mock function with given fields:
func (_m *BleveIndex) Stats() *bleve.IndexStat {
ret := _m.Called()
var r0 *bleve.IndexStat
if rf, ok := ret.Get(0).(func() *bleve.IndexStat); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*bleve.IndexStat)
}
}
return r0
}
// StatsMap provides a mock function with given fields:
func (_m *BleveIndex) StatsMap() map[string]interface{} {
ret := _m.Called()
var r0 map[string]interface{}
if rf, ok := ret.Get(0).(func() map[string]interface{}); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]interface{})
}
}
return r0
}

View File

@@ -0,0 +1,38 @@
// Code generated by mockery v2.10.0. DO NOT EDIT.
package mocks
import (
context "context"
search "github.com/owncloud/ocis/search/pkg/search"
mock "github.com/stretchr/testify/mock"
)
// IndexClient is an autogenerated mock type for the IndexClient type
type IndexClient struct {
mock.Mock
}
// Search provides a mock function with given fields: ctx, req
func (_m *IndexClient) Search(ctx context.Context, req *search.SearchIndexRequest) (*search.SearchIndexResult, error) {
ret := _m.Called(ctx, req)
var r0 *search.SearchIndexResult
if rf, ok := ret.Get(0).(func(context.Context, *search.SearchIndexRequest) *search.SearchIndexResult); ok {
r0 = rf(ctx, req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*search.SearchIndexResult)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *search.SearchIndexRequest) error); ok {
r1 = rf(ctx, req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@@ -0,0 +1,38 @@
// Code generated by mockery v2.10.0. DO NOT EDIT.
package mocks
import (
context "context"
search "github.com/owncloud/ocis/search/pkg/search"
mock "github.com/stretchr/testify/mock"
)
// ProviderClient is an autogenerated mock type for the ProviderClient type
type ProviderClient struct {
mock.Mock
}
// Search provides a mock function with given fields: ctx, req
func (_m *ProviderClient) Search(ctx context.Context, req *search.SearchRequest) (*search.SearchResult, error) {
ret := _m.Called(ctx, req)
var r0 *search.SearchResult
if rf, ok := ret.Get(0).(func(context.Context, *search.SearchRequest) *search.SearchResult); ok {
r0 = rf(ctx, req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*search.SearchResult)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *search.SearchRequest) error); ok {
r1 = rf(ctx, req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@@ -0,0 +1,31 @@
// Copyright 2018-2022 CERN
//
// 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.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package provider_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestProvider(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Provider Suite")
}

View File

@@ -0,0 +1,99 @@
// Copyright 2018-2022 CERN
//
// 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.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package provider
import (
"context"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/owncloud/ocis/search/pkg/search"
)
type Provider struct {
gwClient gateway.GatewayAPIClient
indexClient search.IndexClient
}
func New(gwClient gateway.GatewayAPIClient, indexClient search.IndexClient) *Provider {
return &Provider{
gwClient: gwClient,
indexClient: indexClient,
}
}
func (p *Provider) Search(ctx context.Context, req *search.SearchRequest) (*search.SearchResult, error) {
if req.Query == "" {
return nil, errtypes.PreconditionFailed("empty query provided")
}
listSpacesRes, err := p.gwClient.ListStorageSpaces(ctx, &providerv1beta1.ListStorageSpacesRequest{
Opaque: &typesv1beta1.Opaque{Map: map[string]*typesv1beta1.OpaqueEntry{
"path": {
Decoder: "plain",
Value: []byte("/"),
},
}},
})
if err != nil {
return nil, err
}
matches := []search.Match{}
for _, space := range listSpacesRes.StorageSpaces {
pathPrefix := ""
if space.SpaceType == "grant" {
gpRes, err := p.gwClient.GetPath(ctx, &providerv1beta1.GetPathRequest{
ResourceId: space.Root,
})
if err != nil {
return nil, err
}
if gpRes.Status.Code != rpcv1beta1.Code_CODE_OK {
return nil, errtypes.NewErrtypeFromStatus(gpRes.Status)
}
pathPrefix = utils.MakeRelativePath(gpRes.Path)
}
res, err := p.indexClient.Search(ctx, &search.SearchIndexRequest{
Query: req.Query,
Reference: &providerv1beta1.Reference{
ResourceId: space.Root,
Path: pathPrefix,
},
})
if err != nil {
return nil, err
}
for _, match := range res.Matches {
if pathPrefix != "" {
match.Reference.Path = utils.MakeRelativePath(strings.TrimPrefix(match.Reference.Path, pathPrefix))
}
matches = append(matches, match)
}
}
return &search.SearchResult{Matches: matches}, nil
}

View File

@@ -0,0 +1,258 @@
// Copyright 2018-2022 CERN
//
// 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.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package provider_test
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/mock"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
"github.com/owncloud/ocis/search/pkg/search"
"github.com/owncloud/ocis/search/pkg/search/mocks"
provider "github.com/owncloud/ocis/search/pkg/search/provider"
)
var _ = Describe("Searchprovider", func() {
var (
p *provider.Provider
gwClient *cs3mocks.GatewayAPIClient
indexClient *mocks.IndexClient
ctx context.Context
otherUser = &userv1beta1.User{
Id: &userv1beta1.UserId{
OpaqueId: "otheruser",
},
}
personalSpace = &sprovider.StorageSpace{
Opaque: &typesv1beta1.Opaque{
Map: map[string]*typesv1beta1.OpaqueEntry{
"path": {
Decoder: "plain",
Value: []byte("/foo"),
},
},
},
Id: &sprovider.StorageSpaceId{OpaqueId: "personalspace"},
Root: &sprovider.ResourceId{OpaqueId: "personalspaceroot"},
Name: "personalspace",
}
)
BeforeEach(func() {
ctx = context.Background()
gwClient = &cs3mocks.GatewayAPIClient{}
indexClient = &mocks.IndexClient{}
p = provider.New(gwClient, indexClient)
})
Describe("New", func() {
It("returns a new instance", func() {
p := provider.New(gwClient, indexClient)
Expect(p).ToNot(BeNil())
})
})
Describe("Search", func() {
It("fails when an empty query is given", func() {
res, err := p.Search(ctx, &search.SearchRequest{
Query: "",
})
Expect(err).To(HaveOccurred())
Expect(res).To(BeNil())
})
Context("with a personal space", func() {
BeforeEach(func() {
gwClient.On("ListStorageSpaces", mock.Anything, mock.MatchedBy(func(req *sprovider.ListStorageSpacesRequest) bool {
p := string(req.Opaque.Map["path"].Value)
return p == "/"
})).Return(&sprovider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*sprovider.StorageSpace{personalSpace},
}, nil)
indexClient.On("Search", mock.Anything, mock.Anything).Return(&search.SearchIndexResult{
Matches: []search.Match{
{
Reference: &sprovider.Reference{
ResourceId: personalSpace.Root,
Path: "./path/to/Foo.pdf",
},
Info: &sprovider.ResourceInfo{
Id: &sprovider.ResourceId{
StorageId: personalSpace.Root.StorageId,
OpaqueId: "foo-id",
},
Path: "Foo.pdf",
},
},
},
}, nil)
})
It("searches the personal user space", func() {
res, err := p.Search(ctx, &search.SearchRequest{
Query: "foo",
})
Expect(err).ToNot(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(len(res.Matches)).To(Equal(1))
match := res.Matches[0]
Expect(match.Info.Id.OpaqueId).To(Equal("foo-id"))
Expect(match.Info.Path).To(Equal("Foo.pdf"))
Expect(match.Reference.ResourceId).To(Equal(personalSpace.Root))
Expect(match.Reference.Path).To(Equal("./path/to/Foo.pdf"))
indexClient.AssertCalled(GinkgoT(), "Search", mock.Anything, mock.MatchedBy(func(req *search.SearchIndexRequest) bool {
return req.Query == "foo" && req.Reference.ResourceId == personalSpace.Root && req.Reference.Path == ""
}))
})
})
Context("with received shares", func() {
var (
grantSpace *sprovider.StorageSpace
)
BeforeEach(func() {
grantSpace = &sprovider.StorageSpace{
SpaceType: "grant",
Owner: otherUser,
Id: &sprovider.StorageSpaceId{OpaqueId: "otherspaceroot!otherspacegrant"},
Root: &sprovider.ResourceId{StorageId: "otherspaceroot", OpaqueId: "otherspacegrant"},
Name: "grantspace",
}
gwClient.On("GetPath", mock.Anything, mock.Anything).Return(&sprovider.GetPathResponse{
Status: status.NewOK(ctx),
Path: "/grant/path",
}, nil)
})
It("searches the received spaces (grants)", func() {
gwClient.On("ListStorageSpaces", mock.Anything, mock.MatchedBy(func(req *sprovider.ListStorageSpacesRequest) bool {
p := string(req.Opaque.Map["path"].Value)
return p == "/"
})).Return(&sprovider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*sprovider.StorageSpace{grantSpace},
}, nil)
indexClient.On("Search", mock.Anything, mock.Anything).Return(&search.SearchIndexResult{
Matches: []search.Match{
search.Match{
Reference: &sprovider.Reference{
ResourceId: grantSpace.Root,
Path: "./grant/path/to/Foo.pdf",
},
Info: &sprovider.ResourceInfo{
Id: &sprovider.ResourceId{
StorageId: grantSpace.Root.StorageId,
OpaqueId: "grant-foo-id",
},
Path: "Foo.pdf",
},
},
},
}, nil)
res, err := p.Search(ctx, &search.SearchRequest{
Query: "foo",
})
Expect(err).ToNot(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(len(res.Matches)).To(Equal(1))
match := res.Matches[0]
Expect(match.Info.Id.OpaqueId).To(Equal("grant-foo-id"))
Expect(match.Info.Path).To(Equal("Foo.pdf"))
Expect(match.Reference.ResourceId).To(Equal(grantSpace.Root))
Expect(match.Reference.Path).To(Equal("./to/Foo.pdf"))
indexClient.AssertCalled(GinkgoT(), "Search", mock.Anything, mock.MatchedBy(func(req *search.SearchIndexRequest) bool {
return req.Query == "foo" && req.Reference.ResourceId == grantSpace.Root && req.Reference.Path == "./grant/path"
}))
})
It("finds matches in both the personal space AND the grant", func() {
gwClient.On("ListStorageSpaces", mock.Anything, mock.MatchedBy(func(req *sprovider.ListStorageSpacesRequest) bool {
p := string(req.Opaque.Map["path"].Value)
return p == "/"
})).Return(&sprovider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*sprovider.StorageSpace{personalSpace, grantSpace},
}, nil)
indexClient.On("Search", mock.Anything, mock.MatchedBy(func(req *search.SearchIndexRequest) bool {
return req.Reference.ResourceId == grantSpace.Root
})).Return(&search.SearchIndexResult{
Matches: []search.Match{
search.Match{
Reference: &sprovider.Reference{
ResourceId: grantSpace.Root,
Path: "./grant/path/to/Foo.pdf",
},
Info: &sprovider.ResourceInfo{
Id: &sprovider.ResourceId{
StorageId: grantSpace.Root.StorageId,
OpaqueId: "grant-foo-id",
},
Path: "Foo.pdf",
},
},
},
}, nil)
indexClient.On("Search", mock.Anything, mock.MatchedBy(func(req *search.SearchIndexRequest) bool {
return req.Reference.ResourceId == personalSpace.Root
})).Return(&search.SearchIndexResult{
Matches: []search.Match{
search.Match{
Reference: &sprovider.Reference{
ResourceId: personalSpace.Root,
Path: "./path/to/Foo.pdf",
},
Info: &sprovider.ResourceInfo{
Id: &sprovider.ResourceId{
StorageId: personalSpace.Root.StorageId,
OpaqueId: "foo-id",
},
Path: "Foo.pdf",
},
},
},
}, nil)
res, err := p.Search(ctx, &search.SearchRequest{
Query: "foo",
})
Expect(err).ToNot(HaveOccurred())
Expect(res).ToNot(BeNil())
Expect(len(res.Matches)).To(Equal(2))
ids := []string{res.Matches[0].Info.Id.OpaqueId, res.Matches[1].Info.Id.OpaqueId}
Expect(ids).To(ConsistOf("foo-id", "grant-foo-id"))
})
})
})
})

View File

@@ -0,0 +1,61 @@
// Copyright 2018-2022 CERN
//
// 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.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package search
import (
"context"
sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
//go:generate mockery --name=ProviderClient
//go:generate mockery --name=IndexClient
type SearchRequest struct {
Query string
}
type Match struct {
Reference *sprovider.Reference
Info *sprovider.ResourceInfo
}
type SearchResult struct {
Matches []Match
}
type SearchIndexRequest struct {
// Reference is not a list because the Path is used as a filter which is
// cut off in the matches by the provider. Multiple paths would not be
// distinguishable.
Reference *sprovider.Reference
Query string
}
type SearchIndexResult struct {
Matches []Match
}
type ProviderClient interface {
Search(ctx context.Context, req *SearchRequest) (*SearchResult, error)
}
type IndexClient interface {
Search(ctx context.Context, req *SearchIndexRequest) (*SearchIndexResult, error)
}

View File

@@ -0,0 +1,31 @@
// Copyright 2018-2022 CERN
//
// 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.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
package search_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestSearch(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Search Suite")
}

View File

@@ -0,0 +1,50 @@
package debug
import (
"context"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/search/pkg/config"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Context context.Context
Config *config.Config
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}

View File

@@ -0,0 +1,63 @@
package debug
import (
"io"
"net/http"
"github.com/owncloud/ocis/ocis-pkg/service/debug"
"github.com/owncloud/ocis/ocis-pkg/version"
"github.com/owncloud/ocis/search/pkg/config"
)
// Server initializes the debug service and server.
func Server(opts ...Option) (*http.Server, error) {
options := newOptions(opts...)
return debug.NewService(
debug.Logger(options.Logger),
debug.Name(options.Config.Service.Name),
debug.Version(version.String),
debug.Address(options.Config.Debug.Addr),
debug.Token(options.Config.Debug.Token),
debug.Pprof(options.Config.Debug.Pprof),
debug.Zpages(options.Config.Debug.Zpages),
debug.Health(health(options.Config)),
debug.Ready(ready(options.Config)),
debug.CorsAllowedOrigins(options.Config.HTTP.CORS.AllowedOrigins),
debug.CorsAllowedMethods(options.Config.HTTP.CORS.AllowedMethods),
debug.CorsAllowedHeaders(options.Config.HTTP.CORS.AllowedHeaders),
debug.CorsAllowCredentials(options.Config.HTTP.CORS.AllowCredentials),
), nil
}
// health implements the health check.
func health(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
// TODO: check if services are up and running
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
// io.WriteString should not fail but if it does we want to know.
if err != nil {
panic(err)
}
}
}
// ready implements the ready check.
func ready(cfg *config.Config) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
// TODO: check if services are up and running
_, err := io.WriteString(w, http.StatusText(http.StatusOK))
// io.WriteString should not fail but if it does we want to know.
if err != nil {
panic(err)
}
}
}

View File

@@ -0,0 +1,85 @@
package grpc
import (
"context"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/search/pkg/config"
"github.com/owncloud/ocis/search/pkg/metrics"
svc "github.com/owncloud/ocis/search/pkg/service/v0"
"github.com/urfave/cli/v2"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Name string
Logger log.Logger
Context context.Context
Config *config.Config
Metrics *metrics.Metrics
Flags []cli.Flag
Handler *svc.Service
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Name provides a name for the service.
func Name(val string) Option {
return func(o *Options) {
o.Name = val
}
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Context provides a function to set the context option.
func Context(val context.Context) Option {
return func(o *Options) {
o.Context = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Metrics provides a function to set the metrics option.
func Metrics(val *metrics.Metrics) Option {
return func(o *Options) {
o.Metrics = val
}
}
// Flags provides a function to set the flags option.
func Flags(val []cli.Flag) Option {
return func(o *Options) {
o.Flags = append(o.Flags, val...)
}
}
// Handler provides a function to set the handler option.
func Handler(val *svc.Service) Option {
return func(o *Options) {
o.Handler = val
}
}

View File

@@ -0,0 +1,36 @@
package grpc
import (
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
"github.com/owncloud/ocis/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/ocis-pkg/version"
)
// Server initializes a new go-micro service ready to run
func Server(opts ...Option) grpc.Service {
options := newOptions(opts...)
handler := options.Handler
service := grpc.NewService(
grpc.Name(options.Config.Service.Name),
grpc.Context(options.Context),
grpc.Address(options.Config.GRPC.Addr),
grpc.Namespace(options.Config.GRPC.Namespace),
grpc.Logger(options.Logger),
grpc.Flags(options.Flags...),
grpc.Version(version.String),
)
if err := accountssvc.RegisterAccountsServiceHandler(service.Server(), handler); err != nil {
options.Logger.Fatal().Err(err).Msg("could not register service handler")
}
if err := accountssvc.RegisterGroupsServiceHandler(service.Server(), handler); err != nil {
options.Logger.Fatal().Err(err).Msg("could not register groups handler")
}
if err := accountssvc.RegisterIndexServiceHandler(service.Server(), handler); err != nil {
options.Logger.Fatal().Err(err).Msg("could not register index handler")
}
return service
}

View File

@@ -0,0 +1,57 @@
package service
import (
"github.com/owncloud/ocis/accounts/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/owncloud/ocis/ocis-pkg/roles"
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Config *config.Config
RoleService settingssvc.RoleService
RoleManager *roles.Manager
}
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the Logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Config provides a function to set the Config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// RoleService provides a function to set the RoleService option.
func RoleService(val settingssvc.RoleService) Option {
return func(o *Options) {
o.RoleService = val
}
}
// RoleManager provides a function to set the RoleManager option.
func RoleManager(val *roles.Manager) Option {
return func(o *Options) {
o.RoleManager = val
}
}

View File

@@ -0,0 +1,71 @@
package service
import (
"time"
"github.com/pkg/errors"
"github.com/owncloud/ocis/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/ocis-pkg/indexer"
"github.com/owncloud/ocis/ocis-pkg/log"
oreg "github.com/owncloud/ocis/ocis-pkg/registry"
"github.com/owncloud/ocis/ocis-pkg/roles"
settingssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/search/pkg/config"
)
// userDefaultGID is the default integer representing the "users" group.
const userDefaultGID = 30000
// New returns a new instance of Service
func New(opts ...Option) (s *Service, err error) {
options := newOptions(opts...)
logger := options.Logger
cfg := options.Config
roleService := options.RoleService
if roleService == nil {
roleService = settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient)
}
roleManager := options.RoleManager
if roleManager == nil {
m := roles.NewManager(
roles.CacheSize(1024),
roles.CacheTTL(time.Hour*24*7),
roles.Logger(options.Logger),
roles.RoleService(roleService),
)
roleManager = &m
}
storage, err := createMetadataStorage(cfg, logger)
if err != nil {
return nil, errors.Wrap(err, "could not create metadata storage")
}
s = &Service{
id: cfg.GRPC.Namespace + "." + cfg.Service.Name,
log: logger,
Config: cfg,
}
r := oreg.GetRegistry()
if cfg.Repo.Backend == "cs3" {
if _, err := r.GetService("com.owncloud.storage.metadata"); err != nil {
logger.Error().Err(err).Msg("index: storage-metadata service not present")
return nil, err
}
}
return
}
// Service implements the searchServiceHandler interface
type Service struct {
id string
log log.Logger
Config *config.Config
index *indexer.Indexer
}

View File

@@ -0,0 +1,23 @@
package tracing
import (
pkgtrace "github.com/owncloud/ocis/ocis-pkg/tracing"
"github.com/owncloud/ocis/search/pkg/config"
"go.opentelemetry.io/otel/trace"
)
var (
// TraceProvider is the global trace provider for the proxy service.
TraceProvider = trace.NewNoopTracerProvider()
)
func Configure(cfg *config.Config) error {
var err error
if cfg.Tracing.Enabled {
if TraceProvider, err = pkgtrace.GetTraceProvider(cfg.Tracing.Endpoint, cfg.Tracing.Collector, cfg.Service.Name, cfg.Tracing.Type); err != nil {
return err
}
}
return nil
}