From 3099d4a821415ef476b640dd90473086c67e0099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Fri, 8 Apr 2022 10:49:29 +0200 Subject: [PATCH] Add an initial version of a search service. It's still incomplete and isn't working yet. --- .../proto/ocis/messages/search/search.proto | 16 + .../proto/ocis/services/search/search.proto | 84 ++++ search/pkg/command/health.go | 53 +++ search/pkg/command/root.go | 64 +++ search/pkg/command/server.go | 93 ++++ search/pkg/command/version.go | 50 +++ search/pkg/config/config.go | 25 ++ search/pkg/config/debug.go | 9 + search/pkg/config/defaults/defaultconfig.go | 62 +++ search/pkg/config/http.go | 8 + search/pkg/config/log.go | 9 + search/pkg/config/parser/parse.go | 33 ++ search/pkg/config/reva.go | 11 + search/pkg/config/service.go | 6 + search/pkg/config/tracing.go | 9 + search/pkg/logging/logging.go | 17 + search/pkg/metrics/metrics.go | 33 ++ search/pkg/search/index/index.go | 122 +++++ search/pkg/search/index/index_suite_test.go | 13 + search/pkg/search/index/index_test.go | 123 ++++++ search/pkg/search/index/mocks/BleveIndex.go | 415 ++++++++++++++++++ search/pkg/search/mocks/IndexClient.go | 38 ++ search/pkg/search/mocks/ProviderClient.go | 38 ++ .../search/provider/provider_suite_test.go | 31 ++ search/pkg/search/provider/searchprovider.go | 99 +++++ .../search/provider/searchprovider_test.go | 258 +++++++++++ search/pkg/search/search.go | 61 +++ search/pkg/search/search_suite_test.go | 31 ++ search/pkg/server/debug/option.go | 50 +++ search/pkg/server/debug/server.go | 63 +++ search/pkg/server/grpc/option.go | 85 ++++ search/pkg/server/grpc/server.go | 36 ++ search/pkg/service/v0/option.go | 57 +++ search/pkg/service/v0/service.go | 71 +++ search/pkg/tracing/tracing.go | 23 + 35 files changed, 2196 insertions(+) create mode 100644 protogen/proto/ocis/messages/search/search.proto create mode 100644 protogen/proto/ocis/services/search/search.proto create mode 100644 search/pkg/command/health.go create mode 100644 search/pkg/command/root.go create mode 100644 search/pkg/command/server.go create mode 100644 search/pkg/command/version.go create mode 100644 search/pkg/config/config.go create mode 100644 search/pkg/config/debug.go create mode 100644 search/pkg/config/defaults/defaultconfig.go create mode 100644 search/pkg/config/http.go create mode 100644 search/pkg/config/log.go create mode 100644 search/pkg/config/parser/parse.go create mode 100644 search/pkg/config/reva.go create mode 100644 search/pkg/config/service.go create mode 100644 search/pkg/config/tracing.go create mode 100644 search/pkg/logging/logging.go create mode 100644 search/pkg/metrics/metrics.go create mode 100644 search/pkg/search/index/index.go create mode 100644 search/pkg/search/index/index_suite_test.go create mode 100644 search/pkg/search/index/index_test.go create mode 100644 search/pkg/search/index/mocks/BleveIndex.go create mode 100644 search/pkg/search/mocks/IndexClient.go create mode 100644 search/pkg/search/mocks/ProviderClient.go create mode 100644 search/pkg/search/provider/provider_suite_test.go create mode 100644 search/pkg/search/provider/searchprovider.go create mode 100644 search/pkg/search/provider/searchprovider_test.go create mode 100644 search/pkg/search/search.go create mode 100644 search/pkg/search/search_suite_test.go create mode 100644 search/pkg/server/debug/option.go create mode 100644 search/pkg/server/debug/server.go create mode 100644 search/pkg/server/grpc/option.go create mode 100644 search/pkg/server/grpc/server.go create mode 100644 search/pkg/service/v0/option.go create mode 100644 search/pkg/service/v0/service.go create mode 100644 search/pkg/tracing/tracing.go diff --git a/protogen/proto/ocis/messages/search/search.proto b/protogen/proto/ocis/messages/search/search.proto new file mode 100644 index 0000000000..948e5fd2af --- /dev/null +++ b/protogen/proto/ocis/messages/search/search.proto @@ -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 metadata = 4; +} \ No newline at end of file diff --git a/protogen/proto/ocis/services/search/search.proto b/protogen/proto/ocis/services/search/search.proto new file mode 100644 index 0000000000..7f95a226de --- /dev/null +++ b/protogen/proto/ocis/services/search/search.proto @@ -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; +} diff --git a/search/pkg/command/health.go b/search/pkg/command/health.go new file mode 100644 index 0000000000..d0389cc9c1 --- /dev/null +++ b/search/pkg/command/health.go @@ -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 + }, + } +} diff --git a/search/pkg/command/root.go b/search/pkg/command/root.go new file mode 100644 index 0000000000..f4578fb2d0 --- /dev/null +++ b/search/pkg/command/root.go @@ -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 +} diff --git a/search/pkg/command/server.go b/search/pkg/command/server.go new file mode 100644 index 0000000000..cff21ee668 --- /dev/null +++ b/search/pkg/command/server.go @@ -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() + }, + } +} diff --git a/search/pkg/command/version.go b/search/pkg/command/version.go new file mode 100644 index 0000000000..07a4959c8d --- /dev/null +++ b/search/pkg/command/version.go @@ -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 + }, + } +} diff --git a/search/pkg/config/config.go b/search/pkg/config/config.go new file mode 100644 index 0000000000..2cc26d0552 --- /dev/null +++ b/search/pkg/config/config.go @@ -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:"-"` +} diff --git a/search/pkg/config/debug.go b/search/pkg/config/debug.go new file mode 100644 index 0000000000..a6ab80cbda --- /dev/null +++ b/search/pkg/config/debug.go @@ -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"` +} diff --git a/search/pkg/config/defaults/defaultconfig.go b/search/pkg/config/defaults/defaultconfig.go new file mode 100644 index 0000000000..8fc349789b --- /dev/null +++ b/search/pkg/config/defaults/defaultconfig.go @@ -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, "/") + } +} diff --git a/search/pkg/config/http.go b/search/pkg/config/http.go new file mode 100644 index 0000000000..018f8c551f --- /dev/null +++ b/search/pkg/config/http.go @@ -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"` +} diff --git a/search/pkg/config/log.go b/search/pkg/config/log.go new file mode 100644 index 0000000000..6e4d2fa938 --- /dev/null +++ b/search/pkg/config/log.go @@ -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"` +} diff --git a/search/pkg/config/parser/parse.go b/search/pkg/config/parser/parse.go new file mode 100644 index 0000000000..b467524d74 --- /dev/null +++ b/search/pkg/config/parser/parse.go @@ -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 +} diff --git a/search/pkg/config/reva.go b/search/pkg/config/reva.go new file mode 100644 index 0000000000..68bfd2f629 --- /dev/null +++ b/search/pkg/config/reva.go @@ -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"` +} diff --git a/search/pkg/config/service.go b/search/pkg/config/service.go new file mode 100644 index 0000000000..c019b73046 --- /dev/null +++ b/search/pkg/config/service.go @@ -0,0 +1,6 @@ +package config + +// Service defines the available service configuration. +type Service struct { + Name string `ocisConfig:"-" yaml:"-"` +} diff --git a/search/pkg/config/tracing.go b/search/pkg/config/tracing.go new file mode 100644 index 0000000000..50c49234d6 --- /dev/null +++ b/search/pkg/config/tracing.go @@ -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"` +} diff --git a/search/pkg/logging/logging.go b/search/pkg/logging/logging.go new file mode 100644 index 0000000000..eeb49456f2 --- /dev/null +++ b/search/pkg/logging/logging.go @@ -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), + ) +} diff --git a/search/pkg/metrics/metrics.go b/search/pkg/metrics/metrics.go new file mode 100644 index 0000000000..c3076bb019 --- /dev/null +++ b/search/pkg/metrics/metrics.go @@ -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 +} diff --git a/search/pkg/search/index/index.go b/search/pkg/search/index/index.go new file mode 100644 index 0000000000..78f01ec26f --- /dev/null +++ b/search/pkg/search/index/index.go @@ -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 +} diff --git a/search/pkg/search/index/index_suite_test.go b/search/pkg/search/index/index_suite_test.go new file mode 100644 index 0000000000..09099db1a4 --- /dev/null +++ b/search/pkg/search/index/index_suite_test.go @@ -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") +} diff --git a/search/pkg/search/index/index_test.go b/search/pkg/search/index/index_test.go new file mode 100644 index 0000000000..5887c58a68 --- /dev/null +++ b/search/pkg/search/index/index_test.go @@ -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") + }) +}) diff --git a/search/pkg/search/index/mocks/BleveIndex.go b/search/pkg/search/index/mocks/BleveIndex.go new file mode 100644 index 0000000000..1bb4f1b28d --- /dev/null +++ b/search/pkg/search/index/mocks/BleveIndex.go @@ -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 +} diff --git a/search/pkg/search/mocks/IndexClient.go b/search/pkg/search/mocks/IndexClient.go new file mode 100644 index 0000000000..45f1c25e3a --- /dev/null +++ b/search/pkg/search/mocks/IndexClient.go @@ -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 +} diff --git a/search/pkg/search/mocks/ProviderClient.go b/search/pkg/search/mocks/ProviderClient.go new file mode 100644 index 0000000000..0e097d54e5 --- /dev/null +++ b/search/pkg/search/mocks/ProviderClient.go @@ -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 +} diff --git a/search/pkg/search/provider/provider_suite_test.go b/search/pkg/search/provider/provider_suite_test.go new file mode 100644 index 0000000000..abe3eb2175 --- /dev/null +++ b/search/pkg/search/provider/provider_suite_test.go @@ -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") +} diff --git a/search/pkg/search/provider/searchprovider.go b/search/pkg/search/provider/searchprovider.go new file mode 100644 index 0000000000..0afdc45247 --- /dev/null +++ b/search/pkg/search/provider/searchprovider.go @@ -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 +} diff --git a/search/pkg/search/provider/searchprovider_test.go b/search/pkg/search/provider/searchprovider_test.go new file mode 100644 index 0000000000..6b3ee5a17f --- /dev/null +++ b/search/pkg/search/provider/searchprovider_test.go @@ -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")) + + }) + }) + }) +}) diff --git a/search/pkg/search/search.go b/search/pkg/search/search.go new file mode 100644 index 0000000000..28b3f592f2 --- /dev/null +++ b/search/pkg/search/search.go @@ -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) +} diff --git a/search/pkg/search/search_suite_test.go b/search/pkg/search/search_suite_test.go new file mode 100644 index 0000000000..46c3962a2e --- /dev/null +++ b/search/pkg/search/search_suite_test.go @@ -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") +} diff --git a/search/pkg/server/debug/option.go b/search/pkg/server/debug/option.go new file mode 100644 index 0000000000..1a88ca1137 --- /dev/null +++ b/search/pkg/server/debug/option.go @@ -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 + } +} diff --git a/search/pkg/server/debug/server.go b/search/pkg/server/debug/server.go new file mode 100644 index 0000000000..9b69aa0fef --- /dev/null +++ b/search/pkg/server/debug/server.go @@ -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) + } + } +} diff --git a/search/pkg/server/grpc/option.go b/search/pkg/server/grpc/option.go new file mode 100644 index 0000000000..b1b969f71f --- /dev/null +++ b/search/pkg/server/grpc/option.go @@ -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 + } +} diff --git a/search/pkg/server/grpc/server.go b/search/pkg/server/grpc/server.go new file mode 100644 index 0000000000..48def49bdb --- /dev/null +++ b/search/pkg/server/grpc/server.go @@ -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 +} diff --git a/search/pkg/service/v0/option.go b/search/pkg/service/v0/option.go new file mode 100644 index 0000000000..4658ba8b0d --- /dev/null +++ b/search/pkg/service/v0/option.go @@ -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 + } +} diff --git a/search/pkg/service/v0/service.go b/search/pkg/service/v0/service.go new file mode 100644 index 0000000000..b2d479f74b --- /dev/null +++ b/search/pkg/service/v0/service.go @@ -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 +} diff --git a/search/pkg/tracing/tracing.go b/search/pkg/tracing/tracing.go new file mode 100644 index 0000000000..6d96926844 --- /dev/null +++ b/search/pkg/tracing/tracing.go @@ -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 +}