diff --git a/.gitignore b/.gitignore index c4a98d8..c1ca465 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ bin temp /plugins/ /vendor/ +internal/frostfs/services/tree .test.env *~ diff --git a/Makefile b/Makefile index eb74c3e..aeea3a8 100755 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ METRICS_DUMP_OUT ?= ./metrics-dump.json BINDIR = bin DIRS = $(BINDIR) BINS = $(BINDIR)/frostfs-http-gw +SYNCDIR = internal/frostfs/services/tree .PHONY: all $(BINS) $(DIRS) dep docker/ test cover fmt image image-push dirty-image lint docker/lint pre-commit unpre-commit version clean @@ -27,8 +28,7 @@ PKG_VERSION ?= $(shell echo $(VERSION) | sed "s/^v//" | \ # Make all binaries all: $(BINS) - -$(BINS): $(DIRS) dep +$(BINS): sync-tree $(DIRS) dep @echo "⇒ Build $@" CGO_ENABLED=0 \ go build -v -trimpath \ @@ -39,6 +39,10 @@ $(DIRS): @echo "⇒ Ensure dir: $@" @mkdir -p $@ +# Synchronize tree service +sync-tree: + @./syncTree.sh + # Pull go dependencies dep: @printf "⇒ Download requirements: " @@ -132,6 +136,7 @@ version: clean: rm -rf vendor rm -rf $(BINDIR) + rm -rf $(SYNCDIR) # Package for Debian debpackage: diff --git a/api/layer/tree_service.go b/api/layer/tree_service.go new file mode 100644 index 0000000..9852257 --- /dev/null +++ b/api/layer/tree_service.go @@ -0,0 +1,22 @@ +package layer + +import ( + "context" + "errors" + + "git.frostfs.info/TrueCloudLab/frostfs-http-gw/api" + cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" +) + +// TreeService provide interface to interact with tree service using s3 data models. +type TreeService interface { + GetLatestVersion(ctx context.Context, cnrID *cid.ID, objectName string) (*api.NodeVersion, error) +} + +var ( + // ErrNodeNotFound is returned from Tree service in case of not found error. + ErrNodeNotFound = errors.New("not found") + + // ErrNodeAccessDenied is returned from Tree service in case of access denied error. + ErrNodeAccessDenied = errors.New("access denied") +) diff --git a/api/tree.go b/api/tree.go new file mode 100644 index 0000000..4d16cc7 --- /dev/null +++ b/api/tree.go @@ -0,0 +1,17 @@ +package api + +import ( + oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" +) + +// NodeVersion represent node from tree service. +type NodeVersion struct { + BaseNodeVersion + DeleteMarker bool +} + +// BaseNodeVersion is minimal node info from tree service. +// Basically used for "system" object. +type BaseNodeVersion struct { + OID oid.ID +} diff --git a/app.go b/app.go index 487fdae..a989c90 100644 --- a/app.go +++ b/app.go @@ -2,7 +2,6 @@ package main import ( "context" - "crypto/ecdsa" "fmt" "net/http" "os" @@ -14,9 +13,11 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/pkg/tracing" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/downloader" + "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/frostfs/services" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/metrics" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/resolver" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/response" + "git.frostfs.info/TrueCloudLab/frostfs-http-gw/tree" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/uploader" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/utils" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool" @@ -30,6 +31,8 @@ import ( "github.com/spf13/viper" "github.com/valyala/fasthttp" "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" ) type ( @@ -37,6 +40,7 @@ type ( log *zap.Logger logLevel zap.AtomicLevel pool *pool.Pool + key *keys.PrivateKey owner *user.ID cfg *viper.Viper webServer *fasthttp.Server @@ -93,7 +97,6 @@ func WithConfig(c *viper.Viper) Option { func newApp(ctx context.Context, opt ...Option) App { var ( - key *ecdsa.PrivateKey err error ) @@ -120,17 +123,17 @@ func newApp(ctx context.Context, opt ...Option) App { a.webServer.DisablePreParseMultipartForm = true a.webServer.StreamRequestBody = a.cfg.GetBool(cfgWebStreamRequestBody) // -- -- -- -- -- -- -- -- -- -- -- -- -- -- - key, err = getFrostFSKey(a) + a.key, err = getFrostFSKey(a) if err != nil { a.log.Fatal("failed to get frostfs credentials", zap.Error(err)) } var owner user.ID - user.IDFromKey(&owner, key.PublicKey) + user.IDFromKey(&owner, a.key.PrivateKey.PublicKey) a.owner = &owner var prm pool.InitParameters - prm.SetKey(key) + prm.SetKey(&a.key.PrivateKey) prm.SetNodeDialTimeout(a.cfg.GetDuration(cfgConTimeout)) prm.SetNodeStreamTimeout(a.cfg.GetDuration(cfgStreamTimeout)) prm.SetHealthcheckTimeout(a.cfg.GetDuration(cfgReqTimeout)) @@ -277,7 +280,7 @@ func remove(list []string, element string) []string { return list } -func getFrostFSKey(a *app) (*ecdsa.PrivateKey, error) { +func getFrostFSKey(a *app) (*keys.PrivateKey, error) { walletPath := a.cfg.GetString(cfgWalletPath) if len(walletPath) == 0 { @@ -286,7 +289,7 @@ func getFrostFSKey(a *app) (*ecdsa.PrivateKey, error) { if err != nil { return nil, err } - return &key.PrivateKey, nil + return key, nil } w, err := wallet.NewWalletFromFile(walletPath) if err != nil { @@ -304,7 +307,7 @@ func getFrostFSKey(a *app) (*ecdsa.PrivateKey, error) { return getKeyFromWallet(w, address, password) } -func getKeyFromWallet(w *wallet.Wallet, addrStr string, password *string) (*ecdsa.PrivateKey, error) { +func getKeyFromWallet(w *wallet.Wallet, addrStr string, password *string) (*keys.PrivateKey, error) { var addr util.Uint160 var err error @@ -334,7 +337,7 @@ func getKeyFromWallet(w *wallet.Wallet, addrStr string, password *string) (*ecds return nil, fmt.Errorf("couldn't decrypt account: %w", err) } - return &acc.PrivateKey().PrivateKey, nil + return acc.PrivateKey(), nil } func (a *app) Wait() { @@ -351,8 +354,9 @@ func (a *app) setHealthStatus() { } func (a *app) Serve(ctx context.Context) { + treeClient := a.initTree(ctx) uploadRoutes := uploader.New(ctx, a.AppParams(), a.settings.Uploader) - downloadRoutes := downloader.New(ctx, a.AppParams(), a.settings.Downloader) + downloadRoutes := downloader.New(ctx, a.AppParams(), a.settings.Downloader, treeClient) // Configure router. a.configureRouter(uploadRoutes, downloadRoutes) @@ -475,8 +479,8 @@ func (a *app) configureRouter(uploadRoutes *uploader.Uploader, downloadRoutes *d } r.POST("/upload/{cid}", a.logger(uploadRoutes.Upload)) a.log.Info("added path /upload/{cid}") - r.GET("/get/{cid}/{oid}", a.logger(downloadRoutes.DownloadByAddress)) - r.HEAD("/get/{cid}/{oid}", a.logger(downloadRoutes.HeadByAddress)) + r.GET("/get/{cid}/{oid:*}", a.logger(downloadRoutes.DownloadByAddressOrBucketName)) + r.HEAD("/get/{cid}/{oid:*}", a.logger(downloadRoutes.HeadByAddressOrBucketName)) a.log.Info("added path /get/{cid}/{oid}") r.GET("/get_by_attribute/{cid}/{attr_key}/{attr_val:*}", a.logger(downloadRoutes.DownloadByAttribute)) r.HEAD("/get_by_attribute/{cid}/{attr_key}/{attr_val:*}", a.logger(downloadRoutes.HeadByAttribute)) @@ -565,6 +569,19 @@ func (a *app) serverIndex(address string) int { return -1 } +func (a *app) initTree(ctx context.Context) *tree.Tree { + treeServiceEndpoint := a.cfg.GetString(cfgTreeServiceEndpoint) + grpcDialOpt := grpc.WithTransportCredentials(insecure.NewCredentials()) + treeGRPCClient, err := services.NewTreeServiceClientGRPC(ctx, treeServiceEndpoint, a.key, grpcDialOpt) + if err != nil { + a.log.Fatal("failed to create tree service", zap.Error(err)) + } + treeService := tree.NewTree(treeGRPCClient) + a.log.Info("init tree service", zap.String("endpoint", treeServiceEndpoint)) + + return treeService +} + func (a *app) initTracing(ctx context.Context) { instanceID := "" if len(a.servers) > 0 { diff --git a/config/config.env b/config/config.env index 2d5ea94..4dc9bb2 100644 --- a/config/config.env +++ b/config/config.env @@ -93,6 +93,9 @@ HTTP_GW_POOL_ERROR_THRESHOLD=100 # Enable zip compression to download files by common prefix. HTTP_GW_ZIP_COMPRESSION=false +# Endpoint of the tree service. Must be provided. Can be one of the node address (from the `peers` section). +HTTP_GW_TREE_SERVICE=grpc://s01.frostfs.devenv:8080 + HTTP_GW_TRACING_ENABLED=true HTTP_GW_TRACING_ENDPOINT="localhost:4317" HTTP_GW_TRACING_EXPORTER="otlp_grpc" \ No newline at end of file diff --git a/config/config.yaml b/config/config.yaml index 510cb43..a71c69d 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -93,6 +93,10 @@ resolve_order: upload_header: use_default_timestamp: false # Create timestamp for object if it isn't provided by header. +# Endpoint of the tree service. Must be provided. Can be one of the node address (from the `peers` section). +tree: + service: 127.0.0.1:8080 + connect_timeout: 5s # Timeout to dial node. stream_timeout: 10s # Timeout for individual operations in streaming RPC. request_timeout: 5s # Timeout to check node health during rebalance. diff --git a/downloader/download.go b/downloader/download.go index 7d29a26..b7c242b 100644 --- a/downloader/download.go +++ b/downloader/download.go @@ -18,6 +18,7 @@ import ( "git.frostfs.info/TrueCloudLab/frostfs-http-gw/resolver" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/response" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/tokens" + "git.frostfs.info/TrueCloudLab/frostfs-http-gw/tree" "git.frostfs.info/TrueCloudLab/frostfs-http-gw/utils" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client" @@ -212,6 +213,7 @@ type Downloader struct { pool *pool.Pool containerResolver *resolver.ContainerResolver settings *Settings + tree *tree.Tree } // Settings stores reloading parameters, so it has to provide atomic getters and setters. @@ -228,13 +230,14 @@ func (s *Settings) SetZipCompression(val bool) { } // New creates an instance of Downloader using specified options. -func New(ctx context.Context, params *utils.AppParams, settings *Settings) *Downloader { +func New(ctx context.Context, params *utils.AppParams, settings *Settings, tree *tree.Tree) *Downloader { return &Downloader{ appCtx: ctx, log: params.Logger, pool: params.Pool, settings: settings, containerResolver: params.Resolver, + tree: tree, } } @@ -245,9 +248,16 @@ func (d *Downloader) newRequest(ctx *fasthttp.RequestCtx, log *zap.Logger) *requ } } -// DownloadByAddress handles download requests using simple cid/oid format. -func (d *Downloader) DownloadByAddress(c *fasthttp.RequestCtx) { - d.byAddress(c, receiveFile) +// DownloadByAddressOrBucketName handles download requests using simple cid/oid or bucketname/key format. +func (d *Downloader) DownloadByAddressOrBucketName(c *fasthttp.RequestCtx) { + test, _ := c.UserValue("oid").(string) + var id oid.ID + err := id.DecodeString(test) + if err != nil { + d.byBucketname(c, receiveFile) + } else { + d.byAddress(c, receiveFile) + } } // byAddress is a wrapper for function (e.g. request.headObject, request.receiveFile) that @@ -290,6 +300,47 @@ func (d *Downloader) byAddress(c *fasthttp.RequestCtx, f func(context.Context, r f(ctx, *d.newRequest(c, log), d.pool, addr) } +// byBucketname is a wrapper for function (e.g. request.headObject, request.receiveFile) that +// prepares request and object address to it. +func (d *Downloader) byBucketname(c *fasthttp.RequestCtx, f func(context.Context, request, *pool.Pool, oid.Address)) { + var ( + bucketname = c.UserValue("cid").(string) + key = c.UserValue("oid").(string) + log = d.log.With(zap.String("bucketname", bucketname), zap.String("key", key)) + ) + + cnrID, err := utils.GetContainerID(d.appCtx, bucketname, d.containerResolver) + if err != nil { + log.Error("wrong container id", zap.Error(err)) + response.Error(c, "wrong container id", fasthttp.StatusBadRequest) + return + } + + ctx, err := tokens.StoreBearerTokenAppCtx(c, d.appCtx) + if err != nil { + log.Error("could not fetch and store bearer token", zap.Error(err)) + response.Error(c, "could not fetch and store bearer token: "+err.Error(), fasthttp.StatusBadRequest) + return + } + + foundOid, err := d.tree.GetLatestVersion(ctx, cnrID, key) + if err != nil { + log.Error("object wasn't found", zap.Error(err)) + response.Error(c, "object wasn't found", fasthttp.StatusNotFound) + return + } + if foundOid.DeleteMarker { + log.Error("object was deleted") + response.Error(c, "object deleted", fasthttp.StatusNotFound) + return + } + var addr oid.Address + addr.SetContainer(*cnrID) + addr.SetObject(foundOid.OID) + + f(ctx, *d.newRequest(c, log), d.pool, addr) +} + // DownloadByAttribute handles attribute-based download requests. func (d *Downloader) DownloadByAttribute(c *fasthttp.RequestCtx) { d.byAttribute(c, receiveFile) diff --git a/downloader/head.go b/downloader/head.go index 9745019..a81a275 100644 --- a/downloader/head.go +++ b/downloader/head.go @@ -121,9 +121,17 @@ func idsToResponse(resp *fasthttp.Response, obj *object.Object) { resp.Header.Set(hdrContainerID, cnrID.String()) } -// HeadByAddress handles head requests using simple cid/oid format. -func (d *Downloader) HeadByAddress(c *fasthttp.RequestCtx) { - d.byAddress(c, headObject) +// HeadByAddressOrBucketName handles head requests using simple cid/oid or bucketname/key format. +func (d *Downloader) HeadByAddressOrBucketName(c *fasthttp.RequestCtx) { + test, _ := c.UserValue("oid").(string) + var id oid.ID + + err := id.DecodeString(test) + if err != nil { + d.byBucketname(c, headObject) + } else { + d.byAddress(c, headObject) + } } // HeadByAttribute handles attribute-based head requests. diff --git a/go.mod b/go.mod index fe0be0d..21737d4 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.15.1-0.20230418080822-bd44a3f47b85 + git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20230505094539-15b4287092bd github.com/fasthttp/router v1.4.1 github.com/nspcc-dev/neo-go v0.101.0 @@ -18,11 +19,12 @@ require ( go.opentelemetry.io/otel/trace v1.14.0 go.uber.org/atomic v1.10.0 go.uber.org/zap v1.24.0 + google.golang.org/grpc v1.53.0 + google.golang.org/protobuf v1.28.1 ) require ( git.frostfs.info/TrueCloudLab/frostfs-contract v0.0.0-20230307110621-19a8ef2d02fb // indirect - git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 // indirect git.frostfs.info/TrueCloudLab/hrw v1.2.0 // indirect git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 // indirect git.frostfs.info/TrueCloudLab/tzhash v1.8.0 // indirect @@ -56,7 +58,7 @@ require ( github.com/hashicorp/golang-lru v0.6.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/klauspost/compress v1.15.0 // indirect + github.com/klauspost/compress v1.16.4 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -95,16 +97,15 @@ require ( go.opentelemetry.io/otel/sdk v1.14.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect go.uber.org/multierr v1.9.0 // indirect - golang.org/x/crypto v0.4.0 // indirect + golang.org/x/crypto v0.8.0 // indirect golang.org/x/exp v0.0.0-20221227203929-1b447090c38c // indirect - golang.org/x/net v0.7.0 // indirect + golang.org/x/net v0.9.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/term v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/term v0.7.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/time v0.3.0 // indirect google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect - google.golang.org/grpc v1.53.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 60ecb9f..90996f3 100644 --- a/go.sum +++ b/go.sum @@ -586,8 +586,9 @@ github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6 github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.16.4 h1:91KN02FnsOYhuunwU4ssRe8lc2JosWmizWa91B5v1PU= +github.com/klauspost/compress v1.16.4/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -1005,8 +1006,8 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= -golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1099,8 +1100,8 @@ golang.org/x/net v0.0.0-20211108170745-6635138e15ea/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1226,13 +1227,13 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210429154555-c04ba851c2a4/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1242,15 +1243,16 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180318012157-96caea41033d/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/frostfs/services/tree_client_grpc.go b/internal/frostfs/services/tree_client_grpc.go new file mode 100644 index 0000000..f25588a --- /dev/null +++ b/internal/frostfs/services/tree_client_grpc.go @@ -0,0 +1,114 @@ +package services + +import ( + "context" + "fmt" + "strings" + + grpcService "git.frostfs.info/TrueCloudLab/frostfs-http-gw/internal/frostfs/services/tree" + "git.frostfs.info/TrueCloudLab/frostfs-http-gw/tokens" + "git.frostfs.info/TrueCloudLab/frostfs-http-gw/tree" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "google.golang.org/grpc" +) + +type GetNodeByPathResponseInfoWrapper struct { + response *grpcService.GetNodeByPathResponse_Info +} + +func (n GetNodeByPathResponseInfoWrapper) GetMeta() []tree.Meta { + res := make([]tree.Meta, len(n.response.Meta)) + for i, value := range n.response.Meta { + res[i] = value + } + return res +} + +type GetSubTreeResponseBodyWrapper struct { + response *grpcService.GetSubTreeResponse_Body +} + +func (n GetSubTreeResponseBodyWrapper) GetMeta() []tree.Meta { + res := make([]tree.Meta, len(n.response.Meta)) + for i, value := range n.response.Meta { + res[i] = value + } + return res +} + +type ServiceClientGRPC struct { + key *keys.PrivateKey + conn *grpc.ClientConn + service grpcService.TreeServiceClient +} + +func NewTreeServiceClientGRPC(ctx context.Context, addr string, key *keys.PrivateKey, grpcOpts ...grpc.DialOption) (*ServiceClientGRPC, error) { + conn, err := grpc.Dial(addr, grpcOpts...) + if err != nil { + return nil, fmt.Errorf("did not connect: %v", err) + } + + c := grpcService.NewTreeServiceClient(conn) + if _, err = c.Healthcheck(ctx, &grpcService.HealthcheckRequest{}); err != nil { + return nil, fmt.Errorf("healthcheck: %w", err) + } + + return &ServiceClientGRPC{ + key: key, + conn: conn, + service: c, + }, nil +} + +func (c *ServiceClientGRPC) GetNodes(ctx context.Context, p *tree.GetNodesParams) ([]tree.NodeResponse, error) { + request := &grpcService.GetNodeByPathRequest{ + Body: &grpcService.GetNodeByPathRequest_Body{ + ContainerId: p.CnrID[:], + TreeId: p.TreeID, + Path: p.Path, + Attributes: p.Meta, + PathAttribute: tree.FileNameKey, + LatestOnly: p.LatestOnly, + AllAttributes: p.AllAttrs, + BearerToken: getBearer(ctx), + }, + } + + if err := c.signRequest(request.Body, func(key, sign []byte) { + request.Signature = &grpcService.Signature{ + Key: key, + Sign: sign, + } + }); err != nil { + return nil, err + } + + resp, err := c.service.GetNodeByPath(ctx, request) + if err != nil { + return nil, handleError("failed to get node by path", err) + } + + res := make([]tree.NodeResponse, len(resp.GetBody().GetNodes())) + for i, info := range resp.GetBody().GetNodes() { + res[i] = GetNodeByPathResponseInfoWrapper{info} + } + + return res, nil +} + +func getBearer(ctx context.Context) []byte { + token, err := tokens.LoadBearerToken(ctx) + if err != nil { + return nil + } + return token.Marshal() +} + +func handleError(msg string, err error) error { + if strings.Contains(err.Error(), "not found") { + return fmt.Errorf("%w: %s", tree.ErrNodeNotFound, err.Error()) + } else if strings.Contains(err.Error(), "is denied by") { + return fmt.Errorf("%w: %s", tree.ErrNodeAccessDenied, err.Error()) + } + return fmt.Errorf("%s: %w", msg, err) +} diff --git a/internal/frostfs/services/tree_client_grpc_signature.go b/internal/frostfs/services/tree_client_grpc_signature.go new file mode 100644 index 0000000..9dd38f9 --- /dev/null +++ b/internal/frostfs/services/tree_client_grpc_signature.go @@ -0,0 +1,29 @@ +/*REMOVE THIS AFTER SIGNATURE WILL BE AVAILABLE IN TREE CLIENT FROM FROSTFS NODE*/ +package services + +import ( + crypto "git.frostfs.info/TrueCloudLab/frostfs-crypto" + "google.golang.org/protobuf/proto" +) + +func (c *ServiceClientGRPC) signData(buf []byte, f func(key, sign []byte)) error { + // crypto package should not be used outside of API libraries (see neofs-node#491). + // For now tree service does not include into SDK Client nor SDK Pool, so there is no choice. + // When SDK library adopts Tree service client, this should be dropped. + sign, err := crypto.Sign(&c.key.PrivateKey, buf) + if err != nil { + return err + } + + f(c.key.PublicKey().Bytes(), sign) + return nil +} + +func (c *ServiceClientGRPC) signRequest(requestBody proto.Message, f func(key, sign []byte)) error { + buf, err := proto.Marshal(requestBody) + if err != nil { + return err + } + + return c.signData(buf, f) +} diff --git a/settings.go b/settings.go index 19188fd..87c56bc 100644 --- a/settings.go +++ b/settings.go @@ -59,6 +59,9 @@ const ( cfgRebalance = "rebalance_timer" cfgPoolErrorThreshold = "pool_error_threshold" + // Grpc path to tree service. + cfgTreeServiceEndpoint = "tree.service" + // Logger. cfgLoggerLevel = "logger.level" diff --git a/syncTree.sh b/syncTree.sh new file mode 100755 index 0000000..98d16a0 --- /dev/null +++ b/syncTree.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +mkdir -p internal/frostfs/services/tree 2>/dev/null + +REVISION="f07d4158f50ed5c7f44cc0bc224c3d03edf27f3b" + +echo "tree service revision ${REVISION}" + +# regexp below find all link to source code files which end with ".pb.go" and retrieve the file names +# we use `[^.]*` as non greedy workaround for `.*` +FILES=$(curl -s https://git.frostfs.info/TrueCloudLab/frostfs-node/src/commit/${REVISION}/pkg/services/tree | sed -n "s,.*\"/TrueCloudLab/frostfs-node/src/commit/${REVISION}/pkg/services/tree/\([^.]*\.pb\.go\)\".*,\1,p") + +for file in $FILES; do + if [[ $file == *"frostfs"* ]]; then + echo "skip '$file'" + continue + else + echo "sync '$file' in tree service" + fi + curl -s "https://git.frostfs.info/TrueCloudLab/frostfs-node/raw/commit/${REVISION}/pkg/services/tree/${file}" -o "./internal/frostfs/services/tree/${file}" +done \ No newline at end of file diff --git a/tokens/bearer-token.go b/tokens/bearer-token.go index 6cd98ca..f43cb85 100644 --- a/tokens/bearer-token.go +++ b/tokens/bearer-token.go @@ -60,6 +60,17 @@ func StoreBearerToken(ctx *fasthttp.RequestCtx) error { return nil } +// StoreBearerTokenAppCtx extracts a bearer token from the header or cookie and stores +// it in the application context. +func StoreBearerTokenAppCtx(ctx *fasthttp.RequestCtx, appCtx context.Context) (context.Context, error) { + tkn, err := fetchBearerToken(ctx) + if err != nil { + return nil, err + } + newCtx := context.WithValue(appCtx, bearerTokenKey, tkn) + return newCtx, nil +} + // LoadBearerToken returns a bearer token stored in the context given (if it's // present there). func LoadBearerToken(ctx context.Context) (*bearer.Token, error) { diff --git a/tree/tree.go b/tree/tree.go new file mode 100644 index 0000000..84b6707 --- /dev/null +++ b/tree/tree.go @@ -0,0 +1,156 @@ +package tree + +import ( + "context" + "fmt" + "strings" + + "git.frostfs.info/TrueCloudLab/frostfs-http-gw/api" + "git.frostfs.info/TrueCloudLab/frostfs-http-gw/api/layer" + cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" + oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" +) + +type ( + Tree struct { + service ServiceClient + } + + // ServiceClient is a client to interact with tree service. + // Each method must return ErrNodeNotFound or ErrNodeAccessDenied if relevant. + ServiceClient interface { + GetNodes(ctx context.Context, p *GetNodesParams) ([]NodeResponse, error) + } + + treeNode struct { + ObjID oid.ID + Meta map[string]string + } + + GetNodesParams struct { + CnrID cid.ID + TreeID string + Path []string + Meta []string + LatestOnly bool + AllAttrs bool + } +) + +var ( + // ErrNodeNotFound is returned from ServiceClient in case of not found error. + ErrNodeNotFound = layer.ErrNodeNotFound + + // ErrNodeAccessDenied is returned from ServiceClient service in case of access denied error. + ErrNodeAccessDenied = layer.ErrNodeAccessDenied +) + +const ( + FileNameKey = "FileName" +) + +const ( + oidKV = "OID" + + // keys for delete marker nodes. + isDeleteMarkerKV = "IsDeleteMarker" + + // versionTree -- ID of a tree with object versions. + versionTree = "version" + + separator = "/" +) + +// NewTree creates instance of Tree using provided address and create grpc connection. +func NewTree(service ServiceClient) *Tree { + return &Tree{service: service} +} + +type Meta interface { + GetKey() string + GetValue() []byte +} + +type NodeResponse interface { + GetMeta() []Meta +} + +func newTreeNode(nodeInfo NodeResponse) (*treeNode, error) { + treeNode := &treeNode{ + Meta: make(map[string]string, len(nodeInfo.GetMeta())), + } + + for _, kv := range nodeInfo.GetMeta() { + switch kv.GetKey() { + case oidKV: + if err := treeNode.ObjID.DecodeString(string(kv.GetValue())); err != nil { + return nil, err + } + default: + treeNode.Meta[kv.GetKey()] = string(kv.GetValue()) + } + } + + return treeNode, nil +} + +func (n *treeNode) Get(key string) (string, bool) { + value, ok := n.Meta[key] + return value, ok +} + +func (n *treeNode) FileName() (string, bool) { + value, ok := n.Meta[FileNameKey] + return value, ok +} + +func newNodeVersion(node NodeResponse) (*api.NodeVersion, error) { + treeNode, err := newTreeNode(node) + if err != nil { + return nil, fmt.Errorf("invalid tree node: %w", err) + } + + return newNodeVersionFromTreeNode(treeNode), nil +} + +func newNodeVersionFromTreeNode(treeNode *treeNode) *api.NodeVersion { + _, isDeleteMarker := treeNode.Get(isDeleteMarkerKV) + + version := &api.NodeVersion{ + BaseNodeVersion: api.BaseNodeVersion{ + OID: treeNode.ObjID, + }, + DeleteMarker: isDeleteMarker, + } + + return version +} + +func (c *Tree) GetLatestVersion(ctx context.Context, cnrID *cid.ID, objectName string) (*api.NodeVersion, error) { + meta := []string{oidKV, isDeleteMarkerKV} + path := pathFromName(objectName) + + p := &GetNodesParams{ + CnrID: *cnrID, + TreeID: versionTree, + Path: path, + Meta: meta, + LatestOnly: true, + AllAttrs: false, + } + nodes, err := c.service.GetNodes(ctx, p) + if err != nil { + return nil, err + } + + if len(nodes) == 0 { + return nil, layer.ErrNodeNotFound + } + + return newNodeVersion(nodes[0]) +} + +// pathFromName splits name by '/'. +func pathFromName(objectName string) []string { + return strings.Split(objectName, separator) +}