diff --git a/changelog/unreleased/add-go-micro-appprovider-registry.md b/changelog/unreleased/add-go-micro-appprovider-registry.md new file mode 100644 index 0000000000..8ecc568de3 --- /dev/null +++ b/changelog/unreleased/add-go-micro-appprovider-registry.md @@ -0,0 +1,6 @@ +Enhancement: We added a go-micro based app-provider registry + +We added a dynamic go-micro based app-provider registry with a dynamic TTL + +https://github.com/cs3org/reva/pull/4060 +https://github.com/owncloud/ocis/issues/3832 \ No newline at end of file diff --git a/internal/grpc/services/appprovider/appprovider.go b/internal/grpc/services/appprovider/appprovider.go index e745f368b7..d3b5ff557e 100644 --- a/internal/grpc/services/appprovider/appprovider.go +++ b/internal/grpc/services/appprovider/appprovider.go @@ -94,7 +94,10 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { provider: provider, } - go service.registerProvider() + go func() { + // TODO: implement me to call service.registerProvider with a ticker + service.registerProvider() + }() return service, nil } diff --git a/pkg/app/registry/loader/loader.go b/pkg/app/registry/loader/loader.go index 862c15ce25..175eab887c 100644 --- a/pkg/app/registry/loader/loader.go +++ b/pkg/app/registry/loader/loader.go @@ -20,6 +20,7 @@ package loader import ( // Load core app registry drivers. + _ "github.com/cs3org/reva/v2/pkg/app/registry/micro" _ "github.com/cs3org/reva/v2/pkg/app/registry/static" // Add your own here ) diff --git a/pkg/app/registry/micro/config.go b/pkg/app/registry/micro/config.go new file mode 100644 index 0000000000..938ae048b3 --- /dev/null +++ b/pkg/app/registry/micro/config.go @@ -0,0 +1,40 @@ +// Copyright 2018-2021 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 micro + +import "github.com/mitchellh/mapstructure" + +type config struct { + Namespace string `mapstructure:"namespace"` + MimeTypes []*mimeTypeConfig `mapstructure:"mime_types"` +} + +func (c *config) init() { + if c.Namespace == "" { + c.Namespace = "cs3" + } +} + +func parseConfig(m map[string]interface{}) (*config, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + return nil, err + } + return c, nil +} diff --git a/pkg/app/registry/micro/manager.go b/pkg/app/registry/micro/manager.go new file mode 100644 index 0000000000..26fa6b5f56 --- /dev/null +++ b/pkg/app/registry/micro/manager.go @@ -0,0 +1,265 @@ +// Copyright 2018-2021 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 micro + +import ( + "context" + "sort" + "strconv" + "sync" + "time" + + registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/v2/pkg/app" + "github.com/cs3org/reva/v2/pkg/appctx" + "github.com/cs3org/reva/v2/pkg/errtypes" + oreg "github.com/owncloud/ocis/v2/ocis-pkg/registry" + "github.com/rs/zerolog/log" + mreg "go-micro.dev/v4/registry" +) + +type manager struct { + namespace string + sync.RWMutex + cancelFunc context.CancelFunc + mimeTypes map[string][]*registrypb.ProviderInfo + providers []*registrypb.ProviderInfo + config *config +} + +// New returns an implementation of the app.Registry interface. +func New(m map[string]interface{}) (app.Registry, error) { + c, err := parseConfig(m) + if err != nil { + return nil, err + } + c.init() + + ctx, cancelFunc := context.WithCancel(context.Background()) + + newManager := manager{ + namespace: c.Namespace, + cancelFunc: cancelFunc, + config: c, + } + + err = newManager.updateProvidersFromMicroRegistry() + if err != nil { + if _, ok := err.(errtypes.NotFound); !ok { + return nil, err + } + } + + t := time.NewTicker(time.Second * 30) + + go func() { + for { + select { + case <-t.C: + log.Debug().Msg("app provider tick, updating local app list") + err = newManager.updateProvidersFromMicroRegistry() + if err != nil { + log.Error().Err(err).Msg("could not update the local provider cache") + continue + } + case <-ctx.Done(): + log.Debug().Msg("app provider stopped") + t.Stop() + } + } + }() + + return &newManager, nil +} + +// AddProvider does not do anything for this registry, it is a placeholder to satisfy the interface +func (m *manager) AddProvider(ctx context.Context, p *registrypb.ProviderInfo) error { + log := appctx.GetLogger(ctx) + + log.Info().Interface("provider", p).Msg("Tried to register through cs3 api, make sure the provider registers directly through go-micro") + + return nil +} + +// FindProvider returns all providers that can provide an app for the given mimeType +func (m *manager) FindProviders(ctx context.Context, mimeType string) ([]*registrypb.ProviderInfo, error) { + m.RLock() + defer m.RUnlock() + + if len(m.mimeTypes[mimeType]) < 1 { + return nil, mreg.ErrNotFound + } + + return m.mimeTypes[mimeType], nil +} + +// GetDefaultProviderForMimeType returns the default provider for the given mimeType +func (m *manager) GetDefaultProviderForMimeType(ctx context.Context, mimeType string) (*registrypb.ProviderInfo, error) { + m.RLock() + defer m.RUnlock() + + for _, mt := range m.config.MimeTypes { + if mt.MimeType != mimeType { + continue + } + for _, p := range m.mimeTypes[mimeType] { + if p.Name == mt.DefaultApp { + return p, nil + } + } + } + + return nil, mreg.ErrNotFound +} + +// ListProviders lists all registered Providers +func (m *manager) ListProviders(ctx context.Context) ([]*registrypb.ProviderInfo, error) { + return m.providers, nil +} + +// ListSupportedMimeTypes lists all supported mimeTypes +func (m *manager) ListSupportedMimeTypes(ctx context.Context) ([]*registrypb.MimeTypeInfo, error) { + m.RLock() + defer m.RUnlock() + + res := []*registrypb.MimeTypeInfo{} + for _, mime := range m.config.MimeTypes { + res = append(res, ®istrypb.MimeTypeInfo{ + MimeType: mime.MimeType, + Ext: mime.Extension, + Name: mime.Name, + Description: mime.Description, + Icon: mime.Icon, + AppProviders: m.mimeTypes[mime.MimeType], + AllowCreation: mime.AllowCreation, + DefaultApplication: mime.DefaultApp, + }) + } + return res, nil +} + +// SetDefaultProviderForMimeType sets the default provider for the given mimeType +func (m *manager) SetDefaultProviderForMimeType(ctx context.Context, mimeType string, p *registrypb.ProviderInfo) error { + m.Lock() + defer m.Unlock() + // NOTE: this is a dirty workaround: + + for _, mt := range m.config.MimeTypes { + if mt.MimeType == mimeType { + mt.DefaultApp = p.Name + return nil + } + } + + log.Info().Msgf("default provider for app is not set through the provider, but defined for the app") + return mreg.ErrNotFound +} + +func (m *manager) getProvidersFromMicroRegistry(ctx context.Context) ([]*registrypb.ProviderInfo, error) { + reg := oreg.GetRegistry() + services, err := reg.GetService(m.namespace+".api.app-provider", mreg.GetContext(ctx)) + if err != nil { + log.Warn().Err(err).Msg("getProvidersFromMicroRegistry") + } + + if len(services) == 0 { + return nil, errtypes.NotFound("no application provider service registered") + } + if len(services) > 1 { + return nil, errtypes.InternalError("more than one application provider services registered") + } + + providers := make([]*registrypb.ProviderInfo, 0, len(services[0].Nodes)) + for _, node := range services[0].Nodes { + p := m.providerFromMetadata(node.Metadata) + p.Address = node.Address + providers = append(providers, &p) + } + return providers, nil +} + +func (m *manager) providerFromMetadata(metadata map[string]string) registrypb.ProviderInfo { + p := registrypb.ProviderInfo{ + MimeTypes: splitMimeTypes(metadata[m.namespace+".app-provider.mime_type"]), + // Address: node.Address, + Name: metadata[m.namespace+".app-provider.name"], + Description: metadata[m.namespace+".app-provider.description"], + Icon: metadata[m.namespace+".app-provider.icon"], + DesktopOnly: metadata[m.namespace+".app-provider.desktop_only"] == "true", + Capability: registrypb.ProviderInfo_Capability(registrypb.ProviderInfo_Capability_value[metadata[m.namespace+".app-provider.capability"]]), + } + if metadata[m.namespace+".app-provider.priority"] != "" { + p.Opaque = &typesv1beta1.Opaque{Map: map[string]*typesv1beta1.OpaqueEntry{ + "priority": { + Decoder: "plain", + Value: []byte(metadata[m.namespace+".app-provider.priority"]), + }, + }} + } + return p +} + +func (m *manager) updateProvidersFromMicroRegistry() error { + lst, err := m.getProvidersFromMicroRegistry(context.Background()) + ma := map[string][]*registrypb.ProviderInfo{} + if err != nil { + return err + } + sortByPriority(lst) + for _, outer := range lst { + for _, inner := range outer.MimeTypes { + ma[inner] = append(ma[inner], outer) + } + } + m.Lock() + defer m.Unlock() + m.mimeTypes = ma + m.providers = lst + return nil +} + +func equalsProviderInfo(p1, p2 *registrypb.ProviderInfo) bool { + sameName := p1.Name == p2.Name + sameAddress := p1.Address == p2.Address + + if sameName && sameAddress { + return true + } + return false +} + +func getPriority(p *registrypb.ProviderInfo) string { + if p.Opaque != nil && len(p.Opaque.Map) != 0 { + if priority, ok := p.Opaque.Map["priority"]; ok { + return string(priority.GetValue()) + } + } + return defaultPriority +} + +func sortByPriority(providers []*registrypb.ProviderInfo) { + less := func(i, j int) bool { + prioI, _ := strconv.ParseInt(getPriority(providers[i]), 10, 64) + prioJ, _ := strconv.ParseInt(getPriority(providers[j]), 10, 64) + return prioI < prioJ + } + + sort.Slice(providers, less) +} diff --git a/pkg/app/registry/micro/micro.go b/pkg/app/registry/micro/micro.go new file mode 100644 index 0000000000..9d6df15b1c --- /dev/null +++ b/pkg/app/registry/micro/micro.go @@ -0,0 +1,50 @@ +// Copyright 2018-2021 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 micro + +import ( + "strings" + + "github.com/cs3org/reva/v2/pkg/app/registry/registry" +) + +const defaultPriority = "0" + +func init() { + registry.Register("micro", New) +} + +type mimeTypeConfig struct { + MimeType string `mapstructure:"mime_type"` + Extension string `mapstructure:"extension"` + Name string `mapstructure:"name"` + Description string `mapstructure:"description"` + Icon string `mapstructure:"icon"` + DefaultApp string `mapstructure:"default_app"` + AllowCreation bool `mapstructure:"allow_creation"` +} + +// use the UTF-8 record separator +func splitMimeTypes(s string) []string { + return strings.Split(s, "␞") +} + +func joinMimeTypes(mimetypes []string) string { + return strings.Join(mimetypes, "␞") +} diff --git a/pkg/app/registry/micro/micro_test.go b/pkg/app/registry/micro/micro_test.go new file mode 100644 index 0000000000..00832d2125 --- /dev/null +++ b/pkg/app/registry/micro/micro_test.go @@ -0,0 +1,1017 @@ +// Copyright 2018-2021 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 micro + +import ( + "context" + "testing" + "time" + + registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/google/uuid" + oreg "github.com/owncloud/ocis/v2/ocis-pkg/registry" + "go-micro.dev/v4/registry" + mreg "go-micro.dev/v4/registry" +) + +func TestFindProviders(t *testing.T) { + + testCases := []struct { + name string + mimeTypes []*mimeTypeConfig + regProviders []*registrypb.ProviderInfo + mimeType string + expectedRes []*registrypb.ProviderInfo + expectedErr error + registryNamespace string + }{ + { + name: "no mime types registered", + registryNamespace: "noMimeTypesRegistered", + mimeTypes: []*mimeTypeConfig{}, + mimeType: "SOMETHING", + expectedErr: registry.ErrNotFound, + }, + { + name: "one provider registered for one mime type", + registryNamespace: "oneProviderRegistererdForOneMimeType", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "some Address", + }, + }, + regProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "some Name", + }, + }, + mimeType: "text/json", + expectedErr: nil, + expectedRes: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "some Name", + }, + }, + }, + { + name: "more providers registered for one mime type", + registryNamespace: "moreProvidersRegisteredForOneMimeType", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "provider2", + }, + }, + regProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "provider1", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "provider2", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.3:65535", + Name: "provider3", + }, + }, + mimeType: "text/json", + expectedErr: nil, + expectedRes: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "provider1", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "provider2", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.3:65535", + Name: "provider3", + }, + }, + }, + { + name: "more providers registered for different mime types", + registryNamespace: "moreProvidersRegisteredForDifferentMimeTypes", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "provider2", + }, + { + MimeType: "text/xml", + Extension: "xml", + Name: "XML File", + Icon: "https://example.org/icons&file=xml.png", + DefaultApp: "provider1", + }, + }, + regProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "127.0.0.1:65535", + Name: "provider1", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "provider2", + }, + { + MimeTypes: []string{"text/xml"}, + Address: "127.0.0.3:65535", + Name: "provider3", + }, + }, + mimeType: "text/json", + expectedErr: nil, + expectedRes: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "127.0.0.1:65535", + Name: "provider1", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "provider2", + }, + }, + }, + } + + for _, tt := range testCases { + + t.Run(tt.name, func(t *testing.T) { + ctx := context.TODO() + + // register all the providers + for _, p := range tt.regProviders { + err := registerWithMicroReg(tt.registryNamespace, p) + if err != nil { + t.Error("unexpected error adding a new provider in the registry:", err) + } + } + + registry, err := New(map[string]interface{}{ + "mime_types": tt.mimeTypes, + "namespace": tt.registryNamespace, // TODO: move this to a const + }) + + if err != nil { + t.Error("unexpected error creating the registry:", err) + } + + providers, err := registry.FindProviders(ctx, tt.mimeType) + + // check that the error returned by FindProviders is the same as the expected + if tt.expectedErr != err { + t.Errorf("different error returned: got=%v expected=%v", err, tt.expectedErr) + } + + // expected: slice of pointers, got: slice of pointers with weird opague notation + if !providersEquals(providers, tt.expectedRes) { + t.Errorf("providers list different from expected: \n\tgot=%v\n\texp=%v", providers, tt.expectedRes) + } + + }) + + } + +} + +func TestFindProvidersWithPriority(t *testing.T) { + + testCases := []struct { + name string + mimeTypes []*mimeTypeConfig + regProviders []*registrypb.ProviderInfo + mimeType string + expectedRes []*registrypb.ProviderInfo + expectedErr error + registryNamespace string + }{ + { + name: "no mime types registered", + registryNamespace: "noMimeTypesRegistered", + mimeTypes: []*mimeTypeConfig{}, + mimeType: "SOMETHING", + expectedErr: registry.ErrNotFound, + }, + { + name: "one provider registered for one mime type", + registryNamespace: "oneProviderRegisteredForOneMimeType", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "some Address", + }, + }, + regProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "some Name", + Opaque: &typesv1beta1.Opaque{ + Map: map[string]*typesv1beta1.OpaqueEntry{ + "priority": { + Decoder: "plain", + Value: []byte("100"), + }, + }, + }, + }, + }, + mimeType: "text/json", + expectedErr: nil, + expectedRes: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "some Name", + }, + }, + }, + { + name: "more providers registered for one mime type", + registryNamespace: "moreProvidersRegisteredForOneMimeType", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "provider2", + }, + }, + regProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "provider1", + Opaque: &typesv1beta1.Opaque{ + Map: map[string]*typesv1beta1.OpaqueEntry{ + "priority": { + Decoder: "plain", + Value: []byte("10"), + }, + }, + }, + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "provider2", + Opaque: &typesv1beta1.Opaque{ + Map: map[string]*typesv1beta1.OpaqueEntry{ + "priority": { + Decoder: "plain", + Value: []byte("20"), + }, + }, + }, + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.3:65535", + Name: "provider3", + Opaque: &typesv1beta1.Opaque{ + Map: map[string]*typesv1beta1.OpaqueEntry{ + "priority": { + Decoder: "plain", + Value: []byte("5"), + }, + }, + }, + }, + }, + mimeType: "text/json", + expectedErr: nil, + expectedRes: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "provider2", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "provider1", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.3:65535", + Name: "provider3", + }, + }, + }, + { + name: "more providers registered for different mime types", + registryNamespace: "moreProvidersRegisteredForDifferentMimeTypes", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "provider2", + }, + { + MimeType: "text/xml", + Extension: "xml", + Name: "XML File", + Icon: "https://example.org/icons&file=xml.png", + DefaultApp: "provider1", + }, + }, + regProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "127.0.0.1:65535", + Name: "provider1", + Opaque: &typesv1beta1.Opaque{ + Map: map[string]*typesv1beta1.OpaqueEntry{ + "priority": { + Decoder: "plain", + Value: []byte("5"), + }, + }, + }, + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "provider2", + Opaque: &typesv1beta1.Opaque{ + Map: map[string]*typesv1beta1.OpaqueEntry{ + "priority": { + Decoder: "plain", + Value: []byte("100"), + }, + }, + }, + }, + { + MimeTypes: []string{"text/xml"}, + Address: "127.0.0.3:65535", + Name: "provider3", + Opaque: &typesv1beta1.Opaque{ + Map: map[string]*typesv1beta1.OpaqueEntry{ + "priority": { + Decoder: "plain", + Value: []byte("20"), + }, + }, + }, + }, + }, + mimeType: "text/json", + expectedErr: nil, + expectedRes: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "provider2", + }, + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "127.0.0.1:65535", + Name: "provider1", + }, + }, + }, + { + name: "more providers registered for different mime types2", + registryNamespace: "moreProvidersRegisteredForDifferentMimeTypes2", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "provider2", + }, + { + MimeType: "text/xml", + Extension: "xml", + Name: "XML File", + Icon: "https://example.org/icons&file=xml.png", + DefaultApp: "provider1", + }, + }, + regProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "127.0.0.1:65535", + Name: "provider1", + Opaque: &typesv1beta1.Opaque{ + Map: map[string]*typesv1beta1.OpaqueEntry{ + "priority": { + Decoder: "plain", + Value: []byte("5"), + }, + }, + }, + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "provider2", + Opaque: &typesv1beta1.Opaque{ + Map: map[string]*typesv1beta1.OpaqueEntry{ + "priority": { + Decoder: "plain", + Value: []byte("100"), + }, + }, + }, + }, + { + MimeTypes: []string{"text/xml"}, + Address: "127.0.0.3:65535", + Name: "provider3", + Opaque: &typesv1beta1.Opaque{ + Map: map[string]*typesv1beta1.OpaqueEntry{ + "priority": { + Decoder: "plain", + Value: []byte("20"), + }, + }, + }, + }, + }, + mimeType: "text/xml", + expectedErr: nil, + expectedRes: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/xml"}, + Address: "127.0.0.3:65535", + Name: "provider3", + }, + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "127.0.0.1:65535", + Name: "provider1", + }, + }, + }, + } + + for _, tt := range testCases { + + t.Run(tt.name, func(t *testing.T) { + + ctx := context.TODO() + + // register all the providers + for _, p := range tt.regProviders { + err := registerWithMicroReg(tt.registryNamespace, p) + if err != nil { + t.Fatal("unexpected error adding a new provider in the registry:", err) + } + } + + registry, err := New(map[string]interface{}{ + "mime_types": tt.mimeTypes, + "namespace": tt.registryNamespace, + }) + + if err != nil { + t.Fatal("unexpected error creating the registry:", err) + } + + providers, err := registry.FindProviders(ctx, tt.mimeType) + + // check that the error returned by FindProviders is the same as the expected + if tt.expectedErr != err { + t.Fatalf("different error returned: got=%v expected=%v", err, tt.expectedErr) + } + + if !providersEquals(providers, tt.expectedRes) { + t.Fatalf("providers list different from expected: \n\tgot=%v\n\texp=%v", providers, tt.expectedRes) + } + + }) + + } + +} + +func TestListSupportedMimeTypes(t *testing.T) { + testCases := []struct { + name string + registryNamespace string + mimeTypes []*mimeTypeConfig + newProviders []*registrypb.ProviderInfo + expected []*registrypb.MimeTypeInfo + }{ + { + name: "one mime type - no provider registered", + registryNamespace: "oneMimeTypeNoProviderRegistered", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "provider2", + }, + }, + newProviders: []*registrypb.ProviderInfo{}, + expected: []*registrypb.MimeTypeInfo{ + { + MimeType: "text/json", + Ext: "json", + AppProviders: []*registrypb.ProviderInfo{}, + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApplication: "provider2", + }, + }, + }, + { + name: "one mime type - only default provider registered", + registryNamespace: "oneMimeTypenOnlyDefaultProviderRegistered", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "provider1", + }, + }, + newProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "provider1", + }, + }, + expected: []*registrypb.MimeTypeInfo{ + { + MimeType: "text/json", + Ext: "json", + AppProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "provider1", + }, + }, + DefaultApplication: "provider1", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + }, + }, + }, + { + name: "one mime type - more providers", + registryNamespace: "oneMimeTypeMoreProviders", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "JSON_DEFAULT_PROVIDER", + }, + }, + newProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "NOT_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "JSON_DEFAULT_PROVIDER", + }, + }, + expected: []*registrypb.MimeTypeInfo{ + { + MimeType: "text/json", + Ext: "json", + AppProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "NOT_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "JSON_DEFAULT_PROVIDER", + }, + }, + DefaultApplication: "JSON_DEFAULT_PROVIDER", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + }, + }, + }, + { + name: "multiple mime types", + registryNamespace: "multipleMimeTypes", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "JSON_DEFAULT_PROVIDER", + }, + { + MimeType: "text/xml", + Extension: "xml", + Name: "XML File", + Icon: "https://example.org/icons&file=xml.png", + DefaultApp: "XML_DEFAULT_PROVIDER", + }, + }, + newProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "127.0.0.1:65535", + Name: "NOT_DEFAULT_PROVIDER2", + }, + { + MimeTypes: []string{"text/xml"}, + Address: "127.0.0.2:65535", + Name: "NOT_DEFAULT_PROVIDER1", + }, + { + MimeTypes: []string{"text/xml", "text/json"}, + Address: "127.0.0.3:65535", + Name: "JSON_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/xml", "text/json"}, + Address: "127.0.0.4:65535", + Name: "XML_DEFAULT_PROVIDER", + }, + }, + expected: []*registrypb.MimeTypeInfo{ + { + MimeType: "text/json", + Ext: "json", + AppProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "127.0.0.1:65535", + Name: "NOT_DEFAULT_PROVIDER2", + }, + { + MimeTypes: []string{"text/xml", "text/json"}, + Address: "127.0.0.3:65535", + Name: "JSON_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/xml", "text/json"}, + Address: "127.0.0.2:65535", + Name: "XML_DEFAULT_PROVIDER", + }, + }, + DefaultApplication: "JSON_DEFAULT_PROVIDER", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + }, + { + MimeType: "text/xml", + Ext: "xml", + AppProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json", "text/xml"}, + Address: "127.0.0.1:65535", + Name: "NOT_DEFAULT_PROVIDER2", + }, + { + MimeTypes: []string{"text/xml"}, + Address: "127.0.0.2:65535", + Name: "NOT_DEFAULT_PROVIDER1", + }, + { + MimeTypes: []string{"text/xml", "text/json"}, + Address: "127.0.0.3:65535", + Name: "JSON_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/xml", "text/json"}, + Address: "127.0.0.4:65535", + Name: "XML_DEFAULT_PROVIDER", + }, + }, + DefaultApplication: "XML_DEFAULT_PROVIDER", + Name: "XML File", + Icon: "https://example.org/icons&file=xml.png", + }, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + + ctx := context.TODO() + + // add all the providers + for _, p := range tt.newProviders { + err := registerWithMicroReg(tt.registryNamespace, p) + if err != nil { + t.Fatal("unexpected error creating adding new providers:", err) + } + } + + registry, err := New(map[string]interface{}{ + "mime_types": tt.mimeTypes, + "namespace": tt.registryNamespace, + }) + if err != nil { + t.Fatal("unexpected error creating the registry:", err) + } + + got, err := registry.ListSupportedMimeTypes(ctx) + if err != nil { + t.Error("unexpected error listing supported mime types:", err) + } + + if !mimeTypesEquals(got, tt.expected) { + t.Errorf("mime types list different from expected: \n\tgot=%v\n\texp=%v", got, tt.expected) + } + + }) + } +} + +func TestSetDefaultProviderForMimeType(t *testing.T) { + testCases := []struct { + name string + registryNamespace string + mimeTypes []*mimeTypeConfig + newDefault struct { + mimeType string + provider *registrypb.ProviderInfo + } + newProviders []*registrypb.ProviderInfo + }{ + { + name: "set new default - no new providers", + registryNamespace: "setNewDefaultNoNewProviders", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "JSON_DEFAULT_PROVIDER", + }, + }, + newProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "JSON_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "NEW_DEFAULT", + }, + }, + newDefault: struct { + mimeType string + provider *registrypb.ProviderInfo + }{ + mimeType: "text/json", + provider: ®istrypb.ProviderInfo{ + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "NEW_DEFAULT", + }, + }, + }, + { + name: "set default - other providers (one is the previous default)", + registryNamespace: "setDefaultOtherProvidersOneIsThePreviousDefault", + mimeTypes: []*mimeTypeConfig{ + { + MimeType: "text/json", + Extension: "json", + Name: "JSON File", + Icon: "https://example.org/icons&file=json.png", + DefaultApp: "JSON_DEFAULT_PROVIDER", + }, + }, + newProviders: []*registrypb.ProviderInfo{ + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.4:65535", + Name: "NO_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "NEW_DEFAULT", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.1:65535", + Name: "JSON_DEFAULT_PROVIDER", + }, + { + MimeTypes: []string{"text/json"}, + Address: "127.0.0.3:65535", + Name: "OTHER_PROVIDER", + }, + }, + newDefault: struct { + mimeType string + provider *registrypb.ProviderInfo + }{ + mimeType: "text/json", + provider: ®istrypb.ProviderInfo{ + MimeTypes: []string{"text/json"}, + Address: "127.0.0.2:65535", + Name: "NEW_DEFAULT", + }, + }, + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + + ctx := context.TODO() + + // add other provider to move things around internally :) + for _, p := range tt.newProviders { + err := registerWithMicroReg(tt.registryNamespace, p) + if err != nil { + t.Error("unexpected error adding a new provider:", err) + } + } + + registry, err := New(map[string]interface{}{ + "mime_types": tt.mimeTypes, + "namespace": tt.registryNamespace, + }) + if err != nil { + t.Error("unexpected error creating a new registry:", err) + } + + err = registry.SetDefaultProviderForMimeType(ctx, tt.newDefault.mimeType, tt.newDefault.provider) + if err != nil { + t.Error("unexpected error setting a default provider for mime type:", err) + } + + // check if the new default is the one set + got, err := registry.GetDefaultProviderForMimeType(ctx, tt.newDefault.mimeType) + if err != nil { + t.Error("unexpected error getting the default app provider:", err) + } + + if !equalsProviderInfo(got, tt.newDefault.provider) { + t.Errorf("provider differ from expected:\n\tgot=%v\n\texp=%v", got, tt.newDefault.provider) + } + + }) + } +} + +func equalsMimeTypeInfo(m1, m2 *registrypb.MimeTypeInfo) bool { + return m1.Description == m2.Description && + m1.AllowCreation == m2.AllowCreation && + providersEquals(m1.AppProviders, m2.AppProviders) && + m1.Ext == m2.Ext && + m1.MimeType == m2.MimeType && + m1.Name == m2.Name && + m1.DefaultApplication == m2.DefaultApplication +} + +func mimeTypesEquals(l1, l2 []*registrypb.MimeTypeInfo) bool { + if len(l1) != len(l2) { + return false + } + + if len(l1) < 1 && len(l2) < 1 { + return true + } + + for _, left := range l1 { + for _, right := range l2 { + if equalsMimeTypeInfo(left, right) { + return true + } + } + } + + return false +} + +// check that all providers in the two lists are equals +func providersEquals(pi1, pi2 []*registrypb.ProviderInfo) bool { + if len(pi1) != len(pi2) { + return false + } + + if len(pi1) < 1 && len(pi2) < 1 { + return true + } + + for _, left := range pi1 { + for _, right := range pi2 { + if equalsProviderInfo(left, right) { + return true + } + } + } + + return false +} + +// This is to mock registering with the go-micro registry and at the same time the reference implementation +func registerWithMicroReg(ns string, p *registrypb.ProviderInfo) error { + reg := oreg.GetRegistry() + + serviceID := ns + ".api.app-provider" + + node := &mreg.Node{ + Id: serviceID + "-" + uuid.New().String(), + Address: p.Address, + Metadata: make(map[string]string), + } + + node.Metadata["registry"] = reg.String() + node.Metadata["server"] = "grpc" + node.Metadata["transport"] = "grpc" + node.Metadata["protocol"] = "grpc" + + node.Metadata[ns+".app-provider.mime_type"] = joinMimeTypes(p.MimeTypes) + node.Metadata[ns+".app-provider.name"] = p.Name + node.Metadata[ns+".app-provider.description"] = p.Description + node.Metadata[ns+".app-provider.icon"] = p.Icon + + node.Metadata[ns+".app-provider.allow_creation"] = registrypb.ProviderInfo_Capability_name[int32(p.Capability)] + node.Metadata[ns+".app-provider.priority"] = getPriority(p) + if p.DesktopOnly { + node.Metadata[ns+".app-provider.desktop_only"] = "true" + } + + service := &mreg.Service{ + Name: serviceID, + //Version: version, + Nodes: []*mreg.Node{node}, + Endpoints: make([]*mreg.Endpoint, 0), + } + + rOpts := []mreg.RegisterOption{mreg.RegisterTTL(time.Minute)} + if err := reg.Register(service, rOpts...); err != nil { + return err + } + + return nil +}