From 8872b6f196846549b647af23036495e35d1b4469 Mon Sep 17 00:00:00 2001
From: Denis Kirillov <denis@nspcc.ru>
Date: Tue, 11 Jan 2022 13:09:34 +0300
Subject: [PATCH] [#285] Add resolving order

Signed-off-by: Denis Kirillov <denis@nspcc.ru>
---
 api/layer/layer.go        |  49 ++----------
 api/resolver/resolver.go  | 153 ++++++++++++++++++++++++++++++++++++++
 cmd/s3-gw/app.go          |  23 ++++++
 cmd/s3-gw/app_settings.go |  14 ++++
 4 files changed, 197 insertions(+), 42 deletions(-)
 create mode 100644 api/resolver/resolver.go

diff --git a/api/layer/layer.go b/api/layer/layer.go
index f5e382e8..5aa2ccc3 100644
--- a/api/layer/layer.go
+++ b/api/layer/layer.go
@@ -10,13 +10,12 @@ import (
 	"strings"
 	"time"
 
-	apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
-
 	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
 	"github.com/nspcc-dev/neofs-s3-gw/api"
 	"github.com/nspcc-dev/neofs-s3-gw/api/cache"
 	"github.com/nspcc-dev/neofs-s3-gw/api/data"
 	"github.com/nspcc-dev/neofs-s3-gw/api/errors"
+	"github.com/nspcc-dev/neofs-s3-gw/api/resolver"
 	"github.com/nspcc-dev/neofs-s3-gw/authmate"
 	"github.com/nspcc-dev/neofs-s3-gw/creds/accessbox"
 	"github.com/nspcc-dev/neofs-sdk-go/client"
@@ -26,7 +25,6 @@ import (
 	"github.com/nspcc-dev/neofs-sdk-go/object"
 	"github.com/nspcc-dev/neofs-sdk-go/owner"
 	"github.com/nspcc-dev/neofs-sdk-go/pool"
-	"github.com/nspcc-dev/neofs-sdk-go/resolver"
 	"github.com/nspcc-dev/neofs-sdk-go/session"
 	"go.uber.org/zap"
 )
@@ -36,6 +34,7 @@ type (
 		pool        pool.Pool
 		log         *zap.Logger
 		anonKey     AnonymousKey
+		resolver    *resolver.BucketResolver
 		listsCache  *cache.ObjectsListCache
 		objCache    *cache.ObjectsCache
 		namesCache  *cache.ObjectsNameCache
@@ -47,6 +46,7 @@ type (
 		ChainAddress string
 		Caches       *CachesConfig
 		AnonKey      AnonymousKey
+		Resolver     *resolver.BucketResolver
 	}
 
 	// AnonymousKey contains data for anonymous requests.
@@ -237,9 +237,8 @@ type (
 )
 
 const (
-	tagPrefix             = "S3-Tag-"
-	tagEmptyMark          = "\\"
-	networkSystemDNSParam = "SystemDNS"
+	tagPrefix    = "S3-Tag-"
+	tagEmptyMark = "\\"
 )
 
 func (t *VersionedObject) String() string {
@@ -264,6 +263,7 @@ func NewLayer(log *zap.Logger, conns pool.Pool, config *Config) Client {
 		pool:        conns,
 		log:         log,
 		anonKey:     config.AnonKey,
+		resolver:    config.Resolver,
 		listsCache:  cache.NewObjectsListCache(config.Caches.ObjectsList),
 		objCache:    cache.New(config.Caches.Objects),
 		namesCache:  cache.NewObjectsNameCache(config.Caches.Names),
@@ -651,42 +651,7 @@ func (n *layer) CreateBucket(ctx context.Context, p *CreateBucketParams) (*cid.I
 func (n *layer) ResolveBucket(ctx context.Context, name string) (*cid.ID, error) {
 	cnrID := cid.New()
 	if err := cnrID.Parse(name); err != nil {
-		conn, _, err := n.pool.Connection()
-		if err != nil {
-			return nil, err
-		}
-
-		networkInfoRes, err := conn.NetworkInfo(ctx)
-		if err == nil {
-			err = apistatus.ErrFromStatus(networkInfoRes.Status())
-		}
-		if err != nil {
-			return nil, err
-		}
-
-		networkInfo := networkInfoRes.Info()
-
-		var domain string
-		networkInfo.NetworkConfig().IterateParameters(func(parameter *netmap.NetworkParameter) bool {
-			if string(parameter.Key()) == networkSystemDNSParam {
-				domain = string(parameter.Value())
-				return true
-			}
-			return false
-		})
-
-		if domain != "" {
-			domain = name + "." + domain
-			if cnrID, err = resolver.ResolveContainerDomainName(domain); err == nil {
-				return cnrID, nil
-			}
-			n.log.Debug("trying fallback to direct nns since couldn't resolve system dns record",
-				zap.String("domain", domain), zap.Error(err))
-		}
-
-		// todo add fallback to use nns contract directly
-
-		return nil, fmt.Errorf("couldn't resolve container name '%s': not found", name)
+		return n.resolver.Resolve(ctx, name)
 	}
 
 	return cnrID, nil
diff --git a/api/resolver/resolver.go b/api/resolver/resolver.go
new file mode 100644
index 00000000..ae6d107e
--- /dev/null
+++ b/api/resolver/resolver.go
@@ -0,0 +1,153 @@
+package resolver
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/nspcc-dev/neo-go/pkg/rpc/client"
+	apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
+	cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
+	"github.com/nspcc-dev/neofs-sdk-go/netmap"
+	"github.com/nspcc-dev/neofs-sdk-go/pool"
+	"github.com/nspcc-dev/neofs-sdk-go/resolver"
+)
+
+const (
+	NNSResolver = "nns"
+	DNSResolver = "dns"
+
+	networkSystemDNSParam = "SystemDNS"
+)
+
+type Config struct {
+	Pool pool.Pool
+	RPC  *client.Client
+}
+
+type BucketResolver struct {
+	Name    string
+	resolve func(context.Context, string) (*cid.ID, error)
+
+	next *BucketResolver
+}
+
+func (r *BucketResolver) Resolve(ctx context.Context, name string) (*cid.ID, error) {
+	cnrID, err := r.resolve(ctx, name)
+	if err != nil {
+		if r.next != nil {
+			return r.next.Resolve(ctx, name)
+		}
+		return nil, err
+	}
+	return cnrID, err
+}
+
+func NewResolver(order []string, cfg *Config) (*BucketResolver, error) {
+	if len(order) == 0 {
+		return nil, fmt.Errorf("resolving order must not be empty")
+	}
+
+	bucketResolver, err := newResolver(order[len(order)-1], cfg, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	for i := len(order) - 2; i >= 0; i-- {
+		resolverName := order[i]
+		next := bucketResolver
+
+		bucketResolver, err = newResolver(resolverName, cfg, next)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return bucketResolver, nil
+}
+
+func newResolver(name string, cfg *Config, next *BucketResolver) (*BucketResolver, error) {
+	switch name {
+	case DNSResolver:
+		return NewDNSResolver(cfg.Pool, next)
+	case NNSResolver:
+		return NewNNSResolver(cfg.RPC, next)
+	default:
+		return nil, fmt.Errorf("unknown resolver: %s", name)
+	}
+}
+
+func NewDNSResolver(p pool.Pool, next *BucketResolver) (*BucketResolver, error) {
+	if p == nil {
+		return nil, fmt.Errorf("pool must not be nil for DNS resolver")
+	}
+
+	resolveFunc := func(ctx context.Context, name string) (*cid.ID, error) {
+		conn, _, err := p.Connection()
+		if err != nil {
+			return nil, err
+		}
+
+		networkInfoRes, err := conn.NetworkInfo(ctx)
+		if err == nil {
+			err = apistatus.ErrFromStatus(networkInfoRes.Status())
+		}
+		if err != nil {
+			return nil, err
+		}
+
+		networkInfo := networkInfoRes.Info()
+
+		var domain string
+		networkInfo.NetworkConfig().IterateParameters(func(parameter *netmap.NetworkParameter) bool {
+			if string(parameter.Key()) == networkSystemDNSParam {
+				domain = string(parameter.Value())
+				return true
+			}
+			return false
+		})
+
+		if domain == "" {
+			return nil, fmt.Errorf("couldn't resolve container '%s': not found", name)
+		}
+
+		domain = name + "." + domain
+		cnrID, err := resolver.ResolveContainerDomainName(domain)
+		if err != nil {
+			return nil, fmt.Errorf("couldn't resolve container '%s' as '%s': %w", name, domain, err)
+		}
+		return cnrID, nil
+	}
+
+	return &BucketResolver{
+		Name: DNSResolver,
+
+		resolve: resolveFunc,
+		next:    next,
+	}, nil
+}
+
+func NewNNSResolver(rpc *client.Client, next *BucketResolver) (*BucketResolver, error) {
+	if rpc == nil {
+		return nil, fmt.Errorf("rpc client must not be nil for NNS resolver")
+	}
+
+	nnsRPCResolver, err := resolver.NewNNSResolver(rpc)
+	if err != nil {
+		return nil, err
+	}
+
+	resolveFunc := func(_ context.Context, name string) (*cid.ID, error) {
+		cnrID, err := nnsRPCResolver.ResolveContainerName(name)
+		if err != nil {
+			return nil, fmt.Errorf("couldn't resolve container '%s': %w", name, err)
+		}
+		return cnrID, nil
+	}
+
+	return &BucketResolver{
+		Name: NNSResolver,
+
+		resolve: resolveFunc,
+		next:    next,
+	}, nil
+}
diff --git a/cmd/s3-gw/app.go b/cmd/s3-gw/app.go
index bea58987..6feb5f50 100644
--- a/cmd/s3-gw/app.go
+++ b/cmd/s3-gw/app.go
@@ -10,11 +10,13 @@ import (
 	"time"
 
 	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
+	"github.com/nspcc-dev/neo-go/pkg/rpc/client"
 	"github.com/nspcc-dev/neofs-s3-gw/api"
 	"github.com/nspcc-dev/neofs-s3-gw/api/auth"
 	"github.com/nspcc-dev/neofs-s3-gw/api/cache"
 	"github.com/nspcc-dev/neofs-s3-gw/api/handler"
 	"github.com/nspcc-dev/neofs-s3-gw/api/layer"
+	"github.com/nspcc-dev/neofs-s3-gw/api/resolver"
 	"github.com/nspcc-dev/neofs-s3-gw/internal/version"
 	"github.com/nspcc-dev/neofs-s3-gw/internal/wallet"
 	"github.com/nspcc-dev/neofs-sdk-go/policy"
@@ -119,11 +121,32 @@ func newApp(ctx context.Context, l *zap.Logger, v *viper.Viper) *App {
 		l.Fatal("couldn't generate random key", zap.Error(err))
 	}
 
+	resolveCfg := &resolver.Config{
+		Pool: conns,
+	}
+
+	if rpcEndpoint := v.GetString(cfgRPCEndpoint); rpcEndpoint != "" {
+		rpc, err := client.New(ctx, rpcEndpoint, client.Options{})
+		if err != nil {
+			l.Fatal("couldn't create rpc client", zap.String("endpoint", rpcEndpoint), zap.Error(err))
+		} else if err = rpc.Init(); err != nil {
+			l.Fatal("couldn't init rpc client", zap.String("endpoint", rpcEndpoint), zap.Error(err))
+		}
+		resolveCfg.RPC = rpc
+	}
+
+	order := v.GetStringSlice(cfgResolveOrder)
+	bucketResolver, err := resolver.NewResolver(order, resolveCfg)
+	if err != nil {
+		l.Fatal("failed to form resolver", zap.Error(err))
+	}
+
 	layerCfg := &layer.Config{
 		Caches: getCacheOptions(v, l),
 		AnonKey: layer.AnonymousKey{
 			Key: randomKey,
 		},
+		Resolver: bucketResolver,
 	}
 
 	// prepare object layer
diff --git a/cmd/s3-gw/app_settings.go b/cmd/s3-gw/app_settings.go
index 2340babc..816a86a6 100644
--- a/cmd/s3-gw/app_settings.go
+++ b/cmd/s3-gw/app_settings.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/nspcc-dev/neofs-s3-gw/api/resolver"
 	"github.com/nspcc-dev/neofs-s3-gw/internal/version"
 	"github.com/nspcc-dev/neofs-sdk-go/pool"
 	"github.com/spf13/pflag"
@@ -90,6 +91,12 @@ const ( // Settings.
 	// Peers.
 	cfgPeers = "peers"
 
+	// NeoGo.
+	cfgRPCEndpoint = "rpc-endpoint"
+
+	// Resolving.
+	cfgResolveOrder = "resolve-order"
+
 	// Application.
 	cfgApplicationName      = "app.name"
 	cfgApplicationVersion   = "app.version"
@@ -203,6 +210,9 @@ func newSettings() *viper.Viper {
 
 	peers := flags.StringArrayP(cfgPeers, "p", nil, "set NeoFS nodes")
 
+	flags.StringP(cfgRPCEndpoint, "r", "", "set RPC endpoint")
+	resolveMethods := flags.StringSlice(cfgResolveOrder, []string{resolver.DNSResolver}, "set bucket name resolve order")
+
 	domains := flags.StringArrayP(cfgListenDomains, "d", nil, "set domains to be listened")
 
 	// set prefers:
@@ -228,6 +238,10 @@ func newSettings() *viper.Viper {
 		panic(err)
 	}
 
+	if resolveMethods != nil {
+		v.SetDefault(cfgResolveOrder, *resolveMethods)
+	}
+
 	if peers != nil && len(*peers) > 0 {
 		for i := range *peers {
 			v.SetDefault(cfgPeers+"."+strconv.Itoa(i)+".address", (*peers)[i])