From 99091ad43691a9d7c4395b0f62be6a563f035157 Mon Sep 17 00:00:00 2001 From: Alex Vanin Date: Fri, 23 Jun 2023 13:17:34 +0300 Subject: [PATCH] Add test examples from different object storage clients Signed-off-by: Alex Vanin --- compare/aws_sdk_test.go | 99 ++++++++++++++++++++++++ compare/frostfs_client_test.go | 134 +++++++++++++++++++++++++++++++++ compare/frostfs_pool_test.go | 92 ++++++++++++++++++++++ compare/minio_go_test.go | 60 +++++++++++++++ go.mod | 32 +++++++- go.sum | Bin 72785 -> 78893 bytes 6 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 compare/aws_sdk_test.go create mode 100644 compare/frostfs_client_test.go create mode 100644 compare/frostfs_pool_test.go create mode 100644 compare/minio_go_test.go diff --git a/compare/aws_sdk_test.go b/compare/aws_sdk_test.go new file mode 100644 index 0000000..96ea93f --- /dev/null +++ b/compare/aws_sdk_test.go @@ -0,0 +1,99 @@ +package compare + +import ( + "bytes" + "context" + "io" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/require" +) + +var ( + accessKeyID = "9TtxuFepf2sibaAM" + secretKeyID = "SfxKtYws2We0uzsiXT011aWGMV4iTKlD" + bucketName = "test-bucket-5" + keyName = "foo" +) + +// 1. Initialize client +// 2. Create bucket / container +// 3. Put object +// 4. Get object + +// 1. Опции с публичными полями вместо структур с сеттерами +// 2. Credentials +// 3. Создавать контейнеры принимая аргументы с опциями контейнера, а не с самим контейнером, включая строковый Placement Policy +// 4. Сделать pool умнее путём раскладывания объектов в параллель / по плейсменту +// 5. Подумать о том можно ли как-то объеденить сахарные сценарии в pool и более прямые сценарии в SDK Client + +func TestAWSSDK(t *testing.T) { + // 1. Initialize client + + payloadReader := strings.NewReader("Hello World") // reader seeker + _ = payloadReader + + ctx := context.Background() + + cred := credentials.NewStaticCredentialsProvider(accessKeyID, secretKeyID, "") + cli := s3.New(s3.Options{ + Credentials: cred, + EndpointResolver: s3.EndpointResolverFromURL("http://127.0.0.1:9000"), + Retryer: aws.NopRetryer{}, + UsePathStyle: true, + }) + + cfg, err := config.LoadDefaultConfig(ctx) + require.NoError(t, err) + cli = s3.NewFromConfig(cfg, func(options *s3.Options) { + options.Retryer = aws.NopRetryer{} + options.UsePathStyle = true + options.EndpointResolver = s3.EndpointResolverFromURL("http://127.0.0.1:9000") + }) + + // 2. Create bucket / container + + respCreateBucket, err := cli.CreateBucket(ctx, &s3.CreateBucketInput{ + Bucket: aws.String(bucketName), + ACL: "public-read-write", + CreateBucketConfiguration: &types.CreateBucketConfiguration{ + LocationConstraint: "default", + }, + ObjectLockEnabledForBucket: false, + }) + require.NoError(t, err) + _ = respCreateBucket + + // 3. Put object + + bbuf := bytes.NewBufferString("lalala") + respPutObject, err := cli.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(keyName), + Body: bbuf, + }) + require.NoError(t, err) + spew.Dump(respPutObject) + + // 4. Get object + + objectReader, err := cli.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(keyName), + }) + require.NoError(t, err) + + spew.Dump(objectReader.ContentLength) + + buf, err := io.ReadAll(objectReader.Body) + require.NoError(t, err) + spew.Dump(buf) + require.NoError(t, objectReader.Body.Close()) +} diff --git a/compare/frostfs_client_test.go b/compare/frostfs_client_test.go new file mode 100644 index 0000000..e306272 --- /dev/null +++ b/compare/frostfs_client_test.go @@ -0,0 +1,134 @@ +package compare + +import ( + "context" + "io" + "testing" + "time" + + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client" + apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" + "github.com/davecgh/go-spew/spew" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/stretchr/testify/require" +) + +func TestFrostFSClient(t *testing.T) { + // 1. Initialize client + + payload := []byte("Hello World") + + ctx := context.Background() + k, err := keys.NewPrivateKeyFromWIF("L32sMMkUcEfsWFdjoFNKAPtj7y6cfkcpHwG2pmTap68AmUt86ZTo") + require.NoError(t, err) + + var prmInit client.PrmInit + prmInit.SetDefaultPrivateKey(k.PrivateKey) + + var prmDial client.PrmDial + prmDial.SetServerURI("localhost:8080") + + var cli client.Client + cli.Init(prmInit) + + err = cli.Dial(ctx, prmDial) + require.NoError(t, err) + + // 2. Create bucket / container + + var pp netmap.PlacementPolicy + err = pp.DecodeString("REP 1") + require.NoError(t, err) + + var owner user.ID + user.IDFromKey(&owner, k.PrivateKey.PublicKey) + + var cnr container.Container + cnr.Init() + cnr.SetOwner(owner) + cnr.SetBasicACL(acl.PublicRWExtended) + cnr.SetPlacementPolicy(pp) + + var prmContainerPut client.PrmContainerPut + prmContainerPut.SetContainer(cnr) + cnrPut, err := cli.ContainerPut(ctx, prmContainerPut) + require.NoError(t, err) + + // Wait for container to persist. + var ok bool + for i := 0; i < 100; i++ { + time.Sleep(200 * time.Millisecond) + var prmContainerGet client.PrmContainerGet + prmContainerGet.SetContainer(cnrPut.ID()) + cnrGet, err := cli.ContainerGet(ctx, prmContainerGet) + require.NoError(t, err) + if apistatus.IsSuccessful(cnrGet.Status()) { + ok = true + break + } + } + require.True(t, ok) + + // 3. Put object + + var sha checksum.Checksum + checksum.Calculate(&sha, checksum.SHA256, payload) + + var tz checksum.Checksum + checksum.Calculate(&sha, checksum.TZ, payload) + + var putHeader object.Object + putHeader.SetContainerID(cnrPut.ID()) + putHeader.SetOwnerID(&owner) + putHeader.SetPayloadChecksum(sha) + putHeader.SetPayloadHomomorphicHash(tz) + putHeader.SetPayloadSize(uint64(len(payload))) + + err = object.CalculateAndSetID(&putHeader) + require.NoError(t, err) + + err = object.CalculateAndSetSignature(k.PrivateKey, &putHeader) + require.NoError(t, err) + + objectWriter, err := cli.ObjectPutInit(ctx, client.PrmObjectPutInit{}) + require.NoError(t, err) + + ok = objectWriter.WriteHeader(putHeader) + require.True(t, ok) + + ok = objectWriter.WritePayloadChunk(payload) + require.NoError(t, err) + + objectPutResponse, err := objectWriter.Close() + require.NoError(t, err) + require.True(t, apistatus.IsSuccessful(objectPutResponse.Status())) + + // 4. Get object + + var prmObjectGet client.PrmObjectGet + prmObjectGet.ByID(objectPutResponse.StoredObjectID()) + prmObjectGet.FromContainer(cnrPut.ID()) + + var getHeader object.Object + + objectReader, err := cli.ObjectGetInit(ctx, prmObjectGet) + require.NoError(t, err) + + ok = objectReader.ReadHeader(&getHeader) + require.True(t, ok) + spew.Dump(getHeader) + + buf, err := io.ReadAll(objectReader) + require.NoError(t, err) + spew.Dump(buf) + + st, err := objectReader.Close() + require.NoError(t, err) + spew.Dump(st) +} diff --git a/compare/frostfs_pool_test.go b/compare/frostfs_pool_test.go new file mode 100644 index 0000000..1285fb1 --- /dev/null +++ b/compare/frostfs_pool_test.go @@ -0,0 +1,92 @@ +package compare + +import ( + "bytes" + "context" + "io" + "testing" + + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" + oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" + "github.com/davecgh/go-spew/spew" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/stretchr/testify/require" +) + +func TestFrostFSPool(t *testing.T) { + // 1. Initialize client + + payloadReader := bytes.NewBufferString("Hello World") + + ctx := context.Background() + k, err := keys.NewPrivateKeyFromWIF("L32sMMkUcEfsWFdjoFNKAPtj7y6cfkcpHwG2pmTap68AmUt86ZTo") + require.NoError(t, err) + + var prmInit pool.InitParameters + prmInit.SetKey(&k.PrivateKey) + prmInit.AddNode(pool.NewNodeParam(1, "localhost:8080", 1)) + + cli, err := pool.NewPool(prmInit) + require.NoError(t, err) + + err = cli.Dial(ctx) + require.NoError(t, err) + + // 2. Create bucket / container + + var pp netmap.PlacementPolicy + err = pp.DecodeString("REP 1") + require.NoError(t, err) + + var owner user.ID + user.IDFromKey(&owner, k.PrivateKey.PublicKey) + + var cnr container.Container + cnr.Init() + cnr.SetOwner(owner) + cnr.SetBasicACL(acl.PublicRWExtended) + cnr.SetPlacementPolicy(pp) + + var prmContainerPut pool.PrmContainerPut + prmContainerPut.SetContainer(cnr) + cnrID, err := cli.PutContainer(ctx, prmContainerPut) + require.NoError(t, err) + + // 3. Put object + + var putHeader object.Object + putHeader.SetOwnerID(&owner) + putHeader.SetContainerID(cnrID) + + var prmObjectPut pool.PrmObjectPut + prmObjectPut.SetHeader(putHeader) + prmObjectPut.SetPayload(payloadReader) + + objectID, err := cli.PutObject(ctx, prmObjectPut) + require.NoError(t, err) + + // 4. Get object + + var addr oid.Address + addr.SetContainer(cnrID) + addr.SetObject(objectID) + + var prmObjectGet pool.PrmObjectGet + prmObjectGet.SetAddress(addr) + + objectReader, err := cli.GetObject(ctx, prmObjectGet) + require.NoError(t, err) + + spew.Dump(objectReader.Header) + + buf, err := io.ReadAll(objectReader.Payload) + require.NoError(t, err) + require.NoError(t, objectReader.Payload.Close()) + + spew.Dump(buf) +} diff --git a/compare/minio_go_test.go b/compare/minio_go_test.go new file mode 100644 index 0000000..5dcac79 --- /dev/null +++ b/compare/minio_go_test.go @@ -0,0 +1,60 @@ +package compare + +import ( + "bytes" + "context" + "io" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/stretchr/testify/require" +) + +func TestMinioGo(t *testing.T) { + // 1. Initialize client + + payloadReader := bytes.NewBufferString("Hello World") + ctx := context.Background() + + cli, err := minio.New("127.0.0.1:9000", &minio.Options{ // do not pass scheme + Creds: credentials.NewStaticV4(accessKeyID, secretKeyID, ""), + Region: "", + BucketLookup: minio.BucketLookupPath, + }) + + core, err := minio.NewCore("http://127.0.0.1:9000", &minio.Options{ + Creds: credentials.NewStaticV4(accessKeyID, secretKeyID, ""), + BucketLookup: minio.BucketLookupPath, + }) + _ = core + + // 2. Create bucket / container + + err = cli.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{ + Region: "", + ObjectLocking: false, + }) + require.NoError(t, err) + + // 3. Put object + + respPutObject, err := cli.PutObject(ctx, bucketName, keyName, payloadReader, -1, minio.PutObjectOptions{}) + require.NoError(t, err) + spew.Dump(respPutObject) + + // 4. Get object + + objectReader, err := cli.GetObject(ctx, bucketName, keyName, minio.GetObjectOptions{}) + require.NoError(t, err) + + header, err := objectReader.Stat() + require.NoError(t, err) + spew.Dump(header) + + buf, err := io.ReadAll(objectReader) + require.NoError(t, err) + spew.Dump(buf) + require.NoError(t, objectReader.Close()) +} diff --git a/go.mod b/go.mod index 89f6fe3..5602a33 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,14 @@ require ( git.frostfs.info/TrueCloudLab/hrw v1.2.1 git.frostfs.info/TrueCloudLab/tzhash v1.8.0 github.com/antlr4-go/antlr/v4 v4.13.0 + github.com/aws/aws-sdk-go-v2 v1.18.1 + github.com/aws/aws-sdk-go-v2/config v1.18.27 + github.com/aws/aws-sdk-go-v2/credentials v1.13.26 + github.com/aws/aws-sdk-go-v2/service/s3 v1.35.0 + github.com/davecgh/go-spew v1.1.1 github.com/google/uuid v1.3.0 github.com/hashicorp/golang-lru/v2 v2.0.2 + github.com/minio/minio-go/v7 v7.0.57 github.com/mr-tron/base58 v1.2.0 github.com/nspcc-dev/neo-go v0.101.1 github.com/stretchr/testify v1.8.3 @@ -21,16 +27,39 @@ require ( require ( git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect + github.com/aws/smithy-go v1.13.5 // indirect github.com/benbjohnson/clock v1.1.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/golang-lru v0.6.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.5 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20221202075445-cb5c18dc73eb // indirect github.com/nspcc-dev/rfc6979 v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/xid v1.5.0 // indirect + github.com/sirupsen/logrus v1.9.2 // indirect github.com/twmb/murmur3 v1.1.8 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/goleak v1.2.1 // indirect @@ -42,5 +71,6 @@ require ( golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // 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 a409e2ffaf191c412fb2628db910ddbb3e934cf8..499e96ddc16c21e39526180ba95f8e9b1779a57d 100644 GIT binary patch delta 5800 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

l>n{4$^H&H1!9Ap}Mx2Q0Od zs62mDpB6vwCzl&GZ7YwI)iFVHh_rSkFwE)Zz}m1G9!-ks8#|jC@j=hJ=X>&tq9Fd^ z`xr&j^3sPL4?l<7M~ht~f@s3vJ1-$(%2CZ@bfBP>V{%!x)zjHco0XdjD z_}lut17AzPW9?cvAv`|_1PL58DPJuWogY?qUr7|^Z2R0;9=01RtXLm?zDA{=61=%r zX>^2nZxjYHQ%QYuVspC=4<5J@v__d^F@q^NQ>*|4Ql2lnPr?`ZbFl)u$(wF+t^xS< zdz8Lr!*#fi3L4g_g~c+v^%$MQDd~%Gl0;q~dPmy=aYKJT8>LVDAFJLRd9|Al7=`7! zT*3)52!yC^JDM3O$>r>PBojNr9c$cH%JVdPJn?z5LU7{!^*&WCA~wwo=h#zvR98cm z2|_Vnx&*sl<^jK62DEGkqkxglD1GW0J{G+>uRIczAcZ?8^J%egRS{Hp1D42Y*UFO7 zMjGDpQDZ6F)%2_TZ@&4{`yYM%kJtCFDx!HH?Kyay%3468SuN`(tW_H#N{%9)ucir% zZ4!Is`S)Lc^ON)Ee)915U$dWGKitnuKNlOeCNX-JHGR?wuDSPBsp!O>#@Y(aB$Fl= zBP5+a{Kdn6e#8IysRsM-nsraZSt{t5m0+Q4@D0QD1wk9(t`#@nbZ$ub2+2FAS@;sG zXwO%}>Q?CHDw?V)$*qvOzCP5b=~BY^%RANKYeUzL3*!$^MYKJ?bbZhAAAWZFug~s< zZ~qv1YTtR^!!8~03y#>uHFBV_Jv&#q= zt(eb48H<*yoK&Eio=MGq;d1A1I;{zoYBi@~rNL|-{^#4DsrTRh`|th~fhG_JO)&() z-$J+xW2l3i_YL>{7rzwW{R51je=rDoYeSZ0l9=h?-t^%Yzx;>4xg_&l2VY(>aef<@ zf;0EC9w30sV*t(@qaDL-SMK1*d_YqAYE|#8g-i+~U9T@gd|t%<-MP~~*SUczR-!lE r@4{ilG@Gbx@de(gDMYADM>h2za=uG-!ZcV{G8X@f{3oA%_Iv*iI1HJi delta 85 zcmV-b0IL73=mgQY1h54fv&|?B50lD