[#455] Use contract to get containers

Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
This commit is contained in:
Marina Biryukova 2025-03-26 18:33:08 +03:00
parent 949fc0b484
commit 2ad2531d3a
16 changed files with 193 additions and 47 deletions

View file

@ -256,6 +256,7 @@ func prepareHandlerContextBase(config *handlerConfig, log *zap.Logger) (*handler
GateOwner: owner,
GateKey: key,
WorkerPool: pool,
CnrContract: tp,
}
if !config.withoutCORS {

View file

@ -7,14 +7,15 @@ import (
"strconv"
"strings"
containerContract "git.frostfs.info/TrueCloudLab/frostfs-contract/container"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"go.uber.org/zap"
)
@ -23,23 +24,23 @@ const (
AttributeLockEnabled = "LockEnabled"
)
func (n *Layer) containerInfo(ctx context.Context, prm frostfs.PrmContainer) (*data.BucketInfo, error) {
func (n *Layer) containerInfo(ctx context.Context, cnrID cid.ID) (*data.BucketInfo, error) {
var (
err error
res *container.Container
log = n.reqLogger(ctx).With(zap.Stringer("cid", prm.ContainerID))
log = n.reqLogger(ctx).With(zap.Stringer("cid", cnrID))
info = &data.BucketInfo{
CID: prm.ContainerID,
Name: prm.ContainerID.EncodeToString(),
CID: cnrID,
Name: cnrID.EncodeToString(),
}
reqInfo = middleware.GetReqInfo(ctx)
)
res, err = n.frostFS.Container(ctx, prm)
res, err = n.cnrContract.GetContainerByID(cnrID)
if err != nil {
if client.IsErrContainerNotFound(err) {
if strings.Contains(err.Error(), containerContract.NotFoundError) {
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrNoSuchBucket), err.Error())
}
return nil, fmt.Errorf("get frostfs container: %w", err)
@ -71,7 +72,7 @@ func (n *Layer) containerInfo(ctx context.Context, prm frostfs.PrmContainer) (*d
zone := n.features.FormContainerZone(reqInfo.Namespace)
if zone != info.Zone {
return nil, fmt.Errorf("ns '%s' and zone '%s' are mismatched for container '%s'", zone, info.Zone, prm.ContainerID)
return nil, fmt.Errorf("ns '%s' and zone '%s' are mismatched for container '%s'", zone, info.Zone, cnrID)
}
n.cache.PutBucket(info)
@ -95,11 +96,7 @@ func (n *Layer) containerList(ctx context.Context, listParams ListBucketsParams)
list := make([]*data.BucketInfo, 0, len(res))
for i := range res {
getPrm := frostfs.PrmContainer{
ContainerID: res[i],
SessionToken: stoken,
}
info, err := n.containerInfo(ctx, getPrm)
info, err := n.containerInfo(ctx, res[i])
if err != nil {
n.reqLogger(ctx).Error(logs.CouldNotFetchContainerInfo, zap.Error(err), logs.TagField(logs.TagExternalStorage))
continue

View file

@ -361,3 +361,8 @@ type FrostFS interface {
// NetmapSnapshot returns information about FrostFS network map.
NetmapSnapshot(context.Context) (netmap.NetMap, error)
}
type ContainerContract interface {
// GetContainerByID reads a container from contract by ID.
GetContainerByID(cid.ID) (*container.Container, error)
}

View file

@ -621,6 +621,16 @@ func (t *TestFrostFS) AddContainerPolicyChain(_ context.Context, prm frostfs.Prm
return nil
}
func (t *TestFrostFS) GetContainerByID(cid cid.ID) (*container.Container, error) {
for k, v := range t.containers {
if k == cid.EncodeToString() {
return v, nil
}
}
return nil, fmt.Errorf("container does not exist %s", cid)
}
func (t *TestFrostFS) checkAccess(cnrID cid.ID, owner user.ID) bool {
cnr, ok := t.containers[cnrID.EncodeToString()]
if !ok {

View file

@ -67,6 +67,7 @@ type (
lifecycleCnrInfo *data.BucketInfo
workerPool *ants.Pool
removalChan chan removalParams
cnrContract frostfs.ContainerContract
}
removalParams struct {
@ -89,6 +90,7 @@ type (
CORSCnrInfo *data.BucketInfo
LifecycleCnrInfo *data.BucketInfo
WorkerPool *ants.Pool
CnrContract frostfs.ContainerContract
}
// AnonymousKey contains data for anonymous requests.
@ -284,6 +286,7 @@ func NewLayer(ctx context.Context, log *zap.Logger, frostFS frostfs.FrostFS, con
workerPool: config.WorkerPool,
// TODO: consider closing channel
removalChan: make(chan removalParams, config.Features.RemoveOnReplaceQueue()),
cnrContract: config.CnrContract,
}
go layer.removalRoutine(ctx)
@ -379,12 +382,7 @@ func (n *Layer) GetBucketInfo(ctx context.Context, name string) (*data.BucketInf
return nil, err
}
prm := frostfs.PrmContainer{
ContainerID: containerID,
SessionToken: n.SessionTokenForRead(ctx),
}
return n.containerInfo(ctx, prm)
return n.containerInfo(ctx, containerID)
}
// ResolveCID returns container id by name.

View file

@ -176,6 +176,7 @@ func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext {
TreeService: NewTreeService(),
Features: &FeatureSettingsMock{},
GateOwner: owner,
CnrContract: tp,
}
return &testContext{
@ -237,6 +238,7 @@ func prepareCORSContext(t *testing.T, cachesConfig ...*CachesConfig) *testContex
CORSCnrInfo: corsCnrInfo,
LifecycleCnrInfo: lifecycleCnrInfo,
GateKey: key,
CnrContract: tp,
}
return &testContext{

View file

@ -15,6 +15,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/wallet"
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
@ -112,6 +113,11 @@ func parseKeys() (userKey *keys.PrivateKey, contractKey *keys.PrivateKey, err er
func initFrostFSIDContract(ctx context.Context, log *zap.Logger, key *keys.PrivateKey) (*ffsidContract.FrostFSID, error) {
log.Debug(logs.PrepareFrostfsIDClient)
rpcCli, err := rpcclient.New(ctx, viper.GetString(rpcEndpointFlag), rpcclient.Options{})
if err != nil {
return nil, fmt.Errorf("create rpc client: %w", err)
}
cfg := ffsidContract.Config{
RPCAddress: viper.GetString(rpcEndpointFlag),
Contract: viper.GetString(frostfsIDContractFlag),
@ -121,9 +127,10 @@ func initFrostFSIDContract(ctx context.Context, log *zap.Logger, key *keys.Priva
IgnoreAlreadyExistsError: false,
VerifyExecResults: true,
},
RPCClient: rpcCli,
}
cli, err := ffsidContract.New(ctx, cfg)
cli, err := ffsidContract.New(cfg)
if err != nil {
return nil, fmt.Errorf("create frostfsid client: %w", err)
}
@ -160,14 +167,20 @@ func registerPublicKey(log *zap.Logger, cli *ffsidContract.FrostFSID, key *keys.
func initPolicyContract(ctx context.Context, log *zap.Logger, key *keys.PrivateKey) (*policyContact.Client, error) {
log.Debug(logs.PreparePolicyClient)
rpcCli, err := rpcclient.New(ctx, viper.GetString(rpcEndpointFlag), rpcclient.Options{})
if err != nil {
return nil, fmt.Errorf("create rpc client: %w", err)
}
cfg := policyContact.Config{
RPCAddress: viper.GetString(rpcEndpointFlag),
Contract: viper.GetString(policyContractFlag),
ProxyContract: viper.GetString(proxyContractFlag),
Key: key,
RPCClient: rpcCli,
}
cli, err := policyContact.New(ctx, cfg)
cli, err := policyContact.New(cfg)
if err != nil {
return nil, fmt.Errorf("create policy client: %w", err)
}

View file

@ -31,6 +31,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs"
containerClient "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/container"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid"
ffidcontract "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid/contract"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/policy"
@ -51,6 +52,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"github.com/go-chi/chi/v5/middleware"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/panjf2000/ants/v2"
"github.com/spf13/viper"
"go.uber.org/zap"
@ -249,14 +251,23 @@ func (a *App) init(ctx context.Context) {
a.initResolver()
a.initAuthCenter(ctx)
a.setRuntimeParameters()
a.initFrostfsID(ctx)
a.initPolicyStorage(ctx)
a.initAPI(ctx)
rpcCli := a.initRPCClient(ctx)
a.initFrostfsID(rpcCli)
a.initPolicyStorage(rpcCli)
a.initAPI(ctx, rpcCli)
a.initMetrics()
a.initServers(ctx)
a.initTracing(ctx)
}
func (a *App) initRPCClient(ctx context.Context) *rpcclient.Client {
rpcCli, err := rpcclient.New(ctx, a.config().GetString(cfgRPCEndpoint), rpcclient.Options{})
if err != nil {
a.log.Fatal(logs.InitRPCClientFailed, zap.Error(err), logs.TagField(logs.TagApp))
}
return rpcCli
}
func (a *App) initAuthCenter(ctx context.Context) {
if a.config().IsSet(cfgContainersAccessBox) {
cnrID, err := a.resolveContainerID(ctx, cfgContainersAccessBox)
@ -276,7 +287,7 @@ func (a *App) initAuthCenter(ctx context.Context) {
a.ctr = auth.New(tokens.New(cfg), a.config().GetStringSlice(cfgAllowedAccessKeyIDPrefixes), a.settings)
}
func (a *App) initLayer(ctx context.Context) {
func (a *App) initLayer(ctx context.Context, rpcCli *rpcclient.Client) {
// prepare random key for anonymous requests
randomKey, err := keys.NewPrivateKey()
if err != nil {
@ -299,6 +310,16 @@ func (a *App) initLayer(ctx context.Context) {
}
}
cnrClient, err := containerClient.New(containerClient.Config{
RPCAddress: a.config().GetString(cfgRPCEndpoint),
Contract: a.config().GetString(cfgContractsContainerName),
Key: a.key,
RPCClient: rpcCli,
})
if err != nil {
a.log.Fatal(logs.InitContainerContractFailed, zap.Error(err), logs.TagField(logs.TagApp))
}
layerCfg := &layer.Config{
Cache: a.cache,
AnonKey: layer.AnonymousKey{
@ -312,6 +333,7 @@ func (a *App) initLayer(ctx context.Context) {
CORSCnrInfo: corsCnrInfo,
LifecycleCnrInfo: lifecycleCnrInfo,
WorkerPool: a.initWorkerPool(),
CnrContract: cnrClient,
}
// prepare object layer
@ -694,8 +716,8 @@ func (s *appSettings) LifecycleCopiesNumbers() []uint32 {
return s.lifecycleCopiesNumbers
}
func (a *App) initAPI(ctx context.Context) {
a.initLayer(ctx)
func (a *App) initAPI(ctx context.Context, rpcCli *rpcclient.Client) {
a.initLayer(ctx, rpcCli)
a.initHandler()
}
@ -712,8 +734,8 @@ func (a *App) initMetrics() {
a.loggerSettings.setMetrics(a.metrics)
}
func (a *App) initFrostfsID(ctx context.Context) {
cli, err := ffidcontract.New(ctx, ffidcontract.Config{
func (a *App) initFrostfsID(rpcCli *rpcclient.Client) {
cli, err := ffidcontract.New(ffidcontract.Config{
RPCAddress: a.config().GetString(cfgRPCEndpoint),
Contract: a.config().GetString(cfgFrostfsIDContract),
ProxyContract: a.config().GetString(cfgProxyContract),
@ -722,6 +744,7 @@ func (a *App) initFrostfsID(ctx context.Context) {
IgnoreAlreadyExistsError: false,
VerifyExecResults: true,
},
RPCClient: rpcCli,
})
if err != nil {
a.log.Fatal(logs.InitFrostfsIDFailed, zap.Error(err), logs.TagField(logs.TagApp))
@ -737,8 +760,8 @@ func (a *App) initFrostfsID(ctx context.Context) {
}
}
func (a *App) initPolicyStorage(ctx context.Context) {
policyContract, err := contract.New(ctx, contract.Config{
func (a *App) initPolicyStorage(rpcCli *rpcclient.Client) {
policyContract, err := contract.New(contract.Config{
RPCAddress: a.config().GetString(cfgRPCEndpoint),
Contract: a.config().GetString(cfgPolicyContract),
ProxyContract: a.config().GetString(cfgProxyContract),
@ -747,6 +770,7 @@ func (a *App) initPolicyStorage(ctx context.Context) {
IgnoreAlreadyExistsError: false,
VerifyExecResults: true,
},
RPCClient: rpcCli,
})
if err != nil {
a.log.Fatal(logs.InitPolicyContractFailed, zap.Error(err), logs.TagField(logs.TagApp))

View file

@ -306,6 +306,9 @@ const (
// Encryption.
cfgEncryptionTLSTerminationHeader = "encryption.tls_termination_header"
// Contracts.
cfgContractsContainerName = "contracts.container.name"
// envPrefix is an environment variables prefix used for configuration.
envPrefix = "S3_GW"
)
@ -1190,6 +1193,9 @@ func setDefaults(v *viper.Viper, flags *pflag.FlagSet) {
// proxy
v.SetDefault(cfgProxyContract, "proxy.frostfs")
// contracts
v.SetDefault(cfgContractsContainerName, "container.frostfs")
// resolve
v.SetDefault(cfgResolveNamespaceHeader, defaultNamespaceHeader)

View file

@ -307,3 +307,6 @@ S3_GW_MULTINET_SUBNETS_1_SOURCE_IPS=1.2.3.4 1.2.3.5
# Header for determining the termination of TLS.
S3_GW_ENCRYPTION_TLS_TERMINATION_TLS_HEADER=X-Frostfs-TLS-Termination
# Container contract hash (LE) or name in NNS.
S3_GW_CONTRACTS_CONTAINER_NAME=container.frostfs

View file

@ -362,3 +362,8 @@ multinet:
encryption:
tls_termination_header: X-Frostfs-TLS-Termination
contracts:
container:
# Container contract hash (LE) or name in NNS.
name: container.frostfs

View file

@ -202,6 +202,7 @@ There are some custom types used for brevity:
| `vhs` | [VHS configuration](#vhs-section) |
| `multinet` | [Multinet configuration](#multinet-section) |
| `encryption` | [Encryption configuration](#encryption-section) |
| `contracts` | [Contracts configuration](#contracts-section) |
### General section
@ -971,3 +972,16 @@ encryption:
| Parameter | Type | SIGHUP reload | Default value | Description |
|--------------------------|----------|---------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `tls_termination_header` | `string` | yes | | The header for determining whether TLS needs to be checked. If the system requests come through a proxy server and TLS can terminate at the proxy level, you should use this header to disable TLS verification at server-side encryption. If the header is not specified or an empty string is set as the value, TLS will always be checked. |
# `contracts` section
```yaml
contracts:
container:
name: container.frostfs
```
| Parameter | Type | SIGHUP reload | Default value | Description |
|------------------|----------|---------------|---------------------|----------------------------------------------|
| `container.name` | `string` | no | `container.frostfs` | Container contract hash (LE) or name in NNS. |

View file

@ -0,0 +1,72 @@
package container
import (
"fmt"
containerContract "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/container"
frostfsutil "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/wallet"
)
type Client struct {
contract *containerContract.Contract
}
type Config struct {
RPCAddress string
Contract string
Key *keys.PrivateKey
RPCClient *rpcclient.Client
}
func New(cfg Config) (*Client, error) {
contractHash, err := frostfsutil.ResolveContractHash(cfg.Contract, cfg.RPCAddress)
if err != nil {
return nil, fmt.Errorf("resolve frostfs contract hash: %w", err)
}
key := cfg.Key
if key == nil {
if key, err = keys.NewPrivateKey(); err != nil {
return nil, fmt.Errorf("generate anon private key for container contract: %w", err)
}
}
acc := wallet.NewAccountFromPrivateKey(key)
act, err := actor.NewSimple(cfg.RPCClient, acc)
if err != nil {
return nil, fmt.Errorf("create new actor: %w", err)
}
return &Client{
contract: containerContract.New(act, contractHash),
}, nil
}
func (c *Client) GetContainerByID(cnrID cid.ID) (*container.Container, error) {
items, err := c.contract.Get(cnrID[:])
if err != nil {
return nil, fmt.Errorf("get container by cid: %w", err)
}
if len(items) != 4 {
return nil, fmt.Errorf("unexpected container stack item count: %d", len(items))
}
cnrBytes, err := items[0].TryBytes()
if err != nil {
return nil, fmt.Errorf("could not get byte array of container: %w", err)
}
var cnr container.Container
if err = cnr.Unmarshal(cnrBytes); err != nil {
return nil, fmt.Errorf("can't unmarshal container: %w", err)
}
return &cnr, nil
}

View file

@ -1,7 +1,6 @@
package contract
import (
"context"
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-contract/commonclient"
@ -34,10 +33,13 @@ type Config struct {
// Waiter contains options for awaiting blockchain transactions
// submitted via the contract client.
Waiter commonclient.WaiterOptions
// RPCClient is a client to neo rpc.
RPCClient *rpcclient.Client
}
// New creates new FrostfsID contract wrapper that implements auth.FrostFSID interface.
func New(ctx context.Context, cfg Config) (*FrostFSID, error) {
func New(cfg Config) (*FrostFSID, error) {
contractHash, err := frostfsutil.ResolveContractHash(cfg.Contract, cfg.RPCAddress)
if err != nil {
return nil, fmt.Errorf("resolve frostfs contract hash: %w", err)
@ -50,11 +52,6 @@ func New(ctx context.Context, cfg Config) (*FrostFSID, error) {
}
}
rpcCli, err := rpcclient.New(ctx, cfg.RPCAddress, rpcclient.Options{})
if err != nil {
return nil, fmt.Errorf("init rpc client: %w", err)
}
var opt client.Options
opt.ProxyContract, err = frostfsutil.ResolveContractHash(cfg.ProxyContract, cfg.RPCAddress)
if err != nil {
@ -62,7 +59,7 @@ func New(ctx context.Context, cfg Config) (*FrostFSID, error) {
}
opt.Waiter = cfg.Waiter
cli, err := client.New(rpcCli, wallet.NewAccountFromPrivateKey(key), contractHash, opt)
cli, err := client.New(cfg.RPCClient, wallet.NewAccountFromPrivateKey(key), contractHash, opt)
if err != nil {
return nil, fmt.Errorf("init frostfsid client: %w", err)
}

View file

@ -1,7 +1,6 @@
package contract
import (
"context"
"errors"
"fmt"
"math/big"
@ -45,6 +44,9 @@ type Config struct {
// Waiter contains options for awaiting blockchain transactions
// submitted via the contract client.
Waiter commonclient.WaiterOptions
// RPCClient is a client to neo rpc.
RPCClient *rpcclient.Client
}
const (
@ -55,7 +57,7 @@ const (
var _ policy.Contract = (*Client)(nil)
// New creates new Policy contract wrapper.
func New(ctx context.Context, cfg Config) (*Client, error) {
func New(cfg Config) (*Client, error) {
contractHash, err := frostfsutil.ResolveContractHash(cfg.Contract, cfg.RPCAddress)
if err != nil {
return nil, fmt.Errorf("resolve frostfs contract hash: %w", err)
@ -68,17 +70,12 @@ func New(ctx context.Context, cfg Config) (*Client, error) {
}
}
rpcCli, err := rpcclient.New(ctx, cfg.RPCAddress, rpcclient.Options{})
if err != nil {
return nil, fmt.Errorf("create policy rpc client: %w", err)
}
proxyContractHash, err := frostfsutil.ResolveContractHash(cfg.ProxyContract, cfg.RPCAddress)
if err != nil {
return nil, fmt.Errorf("resolve frostfs contract hash: %w", err)
}
act, err := actor.New(rpcCli, getSigners(key, proxyContractHash, contractHash))
act, err := actor.New(cfg.RPCClient, getSigners(key, proxyContractHash, contractHash))
if err != nil {
return nil, fmt.Errorf("create new actor: %w", err)
}

View file

@ -94,6 +94,8 @@ const (
WarnDuplicateNamespaceVHS = "duplicate namespace with enabled VHS, config value skipped"
WarnValueVHSEnabledFlagWrongType = "the value of the VHS enable flag for the namespace is of the wrong type, config value skipped"
WarnDomainContainsInvalidPlaceholder = "the domain contains an invalid placeholder, domain skipped"
InitContainerContractFailed = "init container contract failed"
InitRPCClientFailed = "init rpc client failed"
)
// Datapath.