2022-05-12 19:37:38 +00:00
|
|
|
package s3
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-09-02 19:23:33 +00:00
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/hex"
|
2023-07-05 14:43:49 +00:00
|
|
|
"fmt"
|
2022-06-17 10:37:43 +00:00
|
|
|
"strconv"
|
2022-05-12 19:37:38 +00:00
|
|
|
"time"
|
|
|
|
|
2024-01-11 18:46:57 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/xk6-frostfs/internal/datagen"
|
2023-03-07 14:53:17 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/xk6-frostfs/internal/stats"
|
2022-05-12 19:37:38 +00:00
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
2023-07-05 14:43:49 +00:00
|
|
|
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
2022-05-12 19:37:38 +00:00
|
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
2022-06-17 10:37:43 +00:00
|
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
2022-05-12 19:37:38 +00:00
|
|
|
"go.k6.io/k6/js/modules"
|
|
|
|
"go.k6.io/k6/metrics"
|
|
|
|
)
|
|
|
|
|
|
|
|
type (
|
|
|
|
Client struct {
|
|
|
|
vu modules.VU
|
|
|
|
cli *s3.Client
|
|
|
|
}
|
2022-05-20 09:34:34 +00:00
|
|
|
|
|
|
|
PutResponse struct {
|
|
|
|
Success bool
|
|
|
|
Error string
|
|
|
|
}
|
|
|
|
|
2022-09-27 16:01:43 +00:00
|
|
|
DeleteResponse struct {
|
|
|
|
Success bool
|
|
|
|
Error string
|
|
|
|
}
|
|
|
|
|
2022-05-20 09:34:34 +00:00
|
|
|
GetResponse struct {
|
|
|
|
Success bool
|
|
|
|
Error string
|
|
|
|
}
|
2022-06-17 10:37:43 +00:00
|
|
|
|
|
|
|
CreateBucketResponse struct {
|
|
|
|
Success bool
|
|
|
|
Error string
|
|
|
|
}
|
2022-09-02 19:23:33 +00:00
|
|
|
|
|
|
|
VerifyHashResponse struct {
|
|
|
|
Success bool
|
|
|
|
Error string
|
|
|
|
}
|
2022-05-12 19:37:38 +00:00
|
|
|
)
|
|
|
|
|
2024-01-11 18:46:57 +00:00
|
|
|
func (c *Client) Put(bucket, key string, payload datagen.Payload) PutResponse {
|
|
|
|
rdr := payload.Reader()
|
|
|
|
sz := payload.Size()
|
2022-05-12 19:37:38 +00:00
|
|
|
|
|
|
|
start := time.Now()
|
|
|
|
_, err := c.cli.PutObject(c.vu.Context(), &s3.PutObjectInput{
|
|
|
|
Bucket: aws.String(bucket),
|
|
|
|
Key: aws.String(key),
|
|
|
|
Body: rdr,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
stats.Report(c.vu, objPutFails, 1)
|
2022-05-20 09:34:34 +00:00
|
|
|
return PutResponse{Success: false, Error: err.Error()}
|
2022-05-12 19:37:38 +00:00
|
|
|
}
|
|
|
|
|
2024-01-24 09:44:16 +00:00
|
|
|
stats.Report(c.vu, objPutSuccess, 1)
|
2022-05-12 19:37:38 +00:00
|
|
|
stats.ReportDataSent(c.vu, float64(sz))
|
|
|
|
stats.Report(c.vu, objPutDuration, metrics.D(time.Since(start)))
|
2024-01-24 09:44:16 +00:00
|
|
|
stats.Report(c.vu, objPutData, float64(sz))
|
2022-05-20 09:34:34 +00:00
|
|
|
return PutResponse{Success: true}
|
2022-05-12 19:37:38 +00:00
|
|
|
}
|
|
|
|
|
2023-07-05 14:43:49 +00:00
|
|
|
const multipartUploadMinPartSize = 5 * 1024 * 1024 // 5MB
|
|
|
|
|
2024-01-11 18:46:57 +00:00
|
|
|
func (c *Client) Multipart(bucket, key string, objPartSize, concurrency int, payload datagen.Payload) PutResponse {
|
2023-07-05 14:43:49 +00:00
|
|
|
if objPartSize < multipartUploadMinPartSize {
|
|
|
|
stats.Report(c.vu, objPutFails, 1)
|
|
|
|
return PutResponse{Success: false, Error: fmt.Sprintf("part size '%d' must be greater than '%d'(5 MB)", objPartSize, multipartUploadMinPartSize)}
|
|
|
|
}
|
|
|
|
|
|
|
|
start := time.Now()
|
|
|
|
uploader := manager.NewUploader(c.cli, func(u *manager.Uploader) {
|
|
|
|
u.PartSize = int64(objPartSize)
|
|
|
|
u.Concurrency = concurrency
|
|
|
|
})
|
|
|
|
|
2024-01-11 18:46:57 +00:00
|
|
|
payloadReader := payload.Reader()
|
|
|
|
sz := payload.Size()
|
2023-07-05 14:43:49 +00:00
|
|
|
|
|
|
|
_, err := uploader.Upload(c.vu.Context(), &s3.PutObjectInput{
|
|
|
|
Bucket: aws.String(bucket),
|
|
|
|
Key: aws.String(key),
|
|
|
|
Body: payloadReader,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
stats.Report(c.vu, objPutFails, 1)
|
|
|
|
return PutResponse{Success: false, Error: err.Error()}
|
|
|
|
}
|
|
|
|
|
2024-01-24 09:44:16 +00:00
|
|
|
stats.Report(c.vu, objPutSuccess, 1)
|
2023-07-05 14:43:49 +00:00
|
|
|
stats.ReportDataSent(c.vu, float64(sz))
|
|
|
|
stats.Report(c.vu, objPutDuration, metrics.D(time.Since(start)))
|
2024-01-24 09:44:16 +00:00
|
|
|
stats.Report(c.vu, objPutData, float64(sz))
|
2023-07-05 14:43:49 +00:00
|
|
|
return PutResponse{Success: true}
|
|
|
|
}
|
|
|
|
|
2022-09-27 16:01:43 +00:00
|
|
|
func (c *Client) Delete(bucket, key string) DeleteResponse {
|
2022-11-29 13:08:58 +00:00
|
|
|
start := time.Now()
|
|
|
|
|
2022-09-27 16:01:43 +00:00
|
|
|
_, err := c.cli.DeleteObject(c.vu.Context(), &s3.DeleteObjectInput{
|
|
|
|
Bucket: aws.String(bucket),
|
|
|
|
Key: aws.String(key),
|
|
|
|
})
|
|
|
|
if err != nil {
|
2022-11-29 13:08:58 +00:00
|
|
|
stats.Report(c.vu, objDeleteFails, 1)
|
2022-09-27 16:01:43 +00:00
|
|
|
return DeleteResponse{Success: false, Error: err.Error()}
|
|
|
|
}
|
|
|
|
|
2024-01-24 09:44:16 +00:00
|
|
|
stats.Report(c.vu, objDeleteSuccess, 1)
|
2022-11-29 13:08:58 +00:00
|
|
|
stats.Report(c.vu, objDeleteDuration, metrics.D(time.Since(start)))
|
2022-09-27 16:01:43 +00:00
|
|
|
return DeleteResponse{Success: true}
|
|
|
|
}
|
|
|
|
|
2022-05-20 09:34:34 +00:00
|
|
|
func (c *Client) Get(bucket, key string) GetResponse {
|
2022-05-12 19:37:38 +00:00
|
|
|
start := time.Now()
|
2022-09-02 19:23:33 +00:00
|
|
|
|
2024-01-24 09:44:16 +00:00
|
|
|
objSize := 0
|
2022-09-02 19:23:33 +00:00
|
|
|
err := get(c.cli, bucket, key, func(chunk []byte) {
|
|
|
|
objSize += len(chunk)
|
2022-05-12 19:37:38 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
stats.Report(c.vu, objGetFails, 1)
|
2022-05-20 09:34:34 +00:00
|
|
|
return GetResponse{Success: false, Error: err.Error()}
|
2022-05-12 19:37:38 +00:00
|
|
|
}
|
2022-09-02 19:23:33 +00:00
|
|
|
|
2024-01-24 09:44:16 +00:00
|
|
|
stats.Report(c.vu, objGetSuccess, 1)
|
2022-05-12 19:37:38 +00:00
|
|
|
stats.Report(c.vu, objGetDuration, metrics.D(time.Since(start)))
|
2022-09-02 19:23:33 +00:00
|
|
|
stats.ReportDataReceived(c.vu, float64(objSize))
|
2024-01-24 09:44:16 +00:00
|
|
|
stats.Report(c.vu, objGetData, float64(objSize))
|
2022-09-02 19:23:33 +00:00
|
|
|
return GetResponse{Success: true}
|
|
|
|
}
|
|
|
|
|
2024-11-06 09:34:48 +00:00
|
|
|
func (c *Client) DeleteObjectVersion(bucket, key, version string) DeleteResponse {
|
|
|
|
var toDelete []types.ObjectIdentifier
|
|
|
|
|
|
|
|
if version != "" {
|
|
|
|
toDelete = append(toDelete, types.ObjectIdentifier{
|
|
|
|
Key: aws.String(key),
|
|
|
|
VersionId: aws.String(version),
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
v, err := c.cli.GetBucketVersioning(c.vu.Context(), &s3.GetBucketVersioningInput{
|
|
|
|
Bucket: aws.String(bucket),
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
stats.Report(c.vu, objDeleteFails, 1)
|
|
|
|
return DeleteResponse{Success: false, Error: err.Error()}
|
|
|
|
}
|
|
|
|
if v.Status == "" {
|
|
|
|
// delete non-versioned object
|
|
|
|
return c.Delete(bucket, key)
|
|
|
|
}
|
|
|
|
|
|
|
|
versions, err := c.cli.ListObjectVersions(c.vu.Context(), &s3.ListObjectVersionsInput{
|
|
|
|
Bucket: aws.String(bucket),
|
|
|
|
Prefix: aws.String(key),
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
stats.Report(c.vu, objDeleteFails, 1)
|
|
|
|
return DeleteResponse{Success: false, Error: err.Error()}
|
|
|
|
}
|
|
|
|
toDelete = append(toDelete, filterObjectVersions(versions, key)...)
|
|
|
|
}
|
|
|
|
if len(toDelete) != 0 {
|
|
|
|
_, err := c.cli.DeleteObjects(c.vu.Context(), &s3.DeleteObjectsInput{
|
|
|
|
Bucket: aws.String(bucket),
|
|
|
|
Delete: &types.Delete{
|
|
|
|
Objects: toDelete,
|
|
|
|
Quiet: true,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
stats.Report(c.vu, objDeleteFails, 1)
|
|
|
|
return DeleteResponse{Success: false, Error: err.Error()}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return DeleteResponse{Success: true}
|
|
|
|
}
|
|
|
|
|
|
|
|
func filterObjectVersions(versions *s3.ListObjectVersionsOutput, key string) []types.ObjectIdentifier {
|
|
|
|
var result []types.ObjectIdentifier
|
|
|
|
|
|
|
|
for _, v := range versions.Versions {
|
|
|
|
if *v.Key == key {
|
|
|
|
result = append(result, types.ObjectIdentifier{
|
|
|
|
Key: v.Key,
|
|
|
|
VersionId: v.VersionId,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, marker := range versions.DeleteMarkers {
|
|
|
|
if *marker.Key == key {
|
|
|
|
result = append(result, types.ObjectIdentifier{
|
|
|
|
Key: marker.Key,
|
|
|
|
VersionId: marker.VersionId,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2022-09-02 19:23:33 +00:00
|
|
|
func get(
|
|
|
|
c *s3.Client,
|
|
|
|
bucket string,
|
|
|
|
key string,
|
|
|
|
onDataChunk func(chunk []byte),
|
|
|
|
) error {
|
|
|
|
var buf = make([]byte, 4*1024)
|
|
|
|
|
|
|
|
obj, err := c.GetObject(context.Background(), &s3.GetObjectInput{
|
|
|
|
Bucket: aws.String(bucket),
|
|
|
|
Key: aws.String(key),
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-05-12 19:37:38 +00:00
|
|
|
for {
|
|
|
|
n, err := obj.Body.Read(buf)
|
|
|
|
if n > 0 {
|
2022-09-02 19:23:33 +00:00
|
|
|
onDataChunk(buf[:n])
|
2022-05-12 19:37:38 +00:00
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2022-09-02 19:23:33 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) VerifyHash(bucket, key, expectedHash string) VerifyHashResponse {
|
|
|
|
hasher := sha256.New()
|
|
|
|
err := get(c.cli, bucket, key, func(data []byte) {
|
|
|
|
hasher.Write(data)
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return VerifyHashResponse{Success: false, Error: err.Error()}
|
|
|
|
}
|
2022-09-22 16:57:21 +00:00
|
|
|
actualHash := hex.EncodeToString(hasher.Sum(nil))
|
2022-09-02 19:23:33 +00:00
|
|
|
if actualHash != expectedHash {
|
2024-05-30 10:46:25 +00:00
|
|
|
return VerifyHashResponse{Success: false, Error: "hash mismatch"}
|
2022-09-02 19:23:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return VerifyHashResponse{Success: true}
|
2022-05-12 19:37:38 +00:00
|
|
|
}
|
2022-06-17 10:37:43 +00:00
|
|
|
|
|
|
|
func (c *Client) CreateBucket(bucket string, params map[string]string) CreateBucketResponse {
|
|
|
|
var err error
|
|
|
|
var lockEnabled bool
|
|
|
|
if lockEnabledStr, ok := params["lock_enabled"]; ok {
|
|
|
|
if lockEnabled, err = strconv.ParseBool(lockEnabledStr); err != nil {
|
|
|
|
stats.Report(c.vu, createBucketFails, 1)
|
|
|
|
return CreateBucketResponse{Success: false, Error: "invalid lock_enabled params"}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var bucketConfiguration *types.CreateBucketConfiguration
|
|
|
|
if locationConstraint, ok := params["location_constraint"]; ok {
|
|
|
|
bucketConfiguration = &types.CreateBucketConfiguration{
|
|
|
|
LocationConstraint: types.BucketLocationConstraint(locationConstraint),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
start := time.Now()
|
|
|
|
_, err = c.cli.CreateBucket(c.vu.Context(), &s3.CreateBucketInput{
|
|
|
|
Bucket: aws.String(bucket),
|
|
|
|
ACL: types.BucketCannedACL(params["acl"]),
|
|
|
|
CreateBucketConfiguration: bucketConfiguration,
|
|
|
|
ObjectLockEnabledForBucket: lockEnabled,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
stats.Report(c.vu, createBucketFails, 1)
|
|
|
|
return CreateBucketResponse{Success: false, Error: err.Error()}
|
|
|
|
}
|
|
|
|
|
2024-11-06 09:33:44 +00:00
|
|
|
var versioning bool
|
|
|
|
if strVersioned, ok := params["versioning"]; ok {
|
|
|
|
if versioning, err = strconv.ParseBool(strVersioned); err != nil {
|
|
|
|
stats.Report(c.vu, createBucketFails, 1)
|
|
|
|
return CreateBucketResponse{Success: false, Error: err.Error()}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if versioning {
|
|
|
|
_, err = c.cli.PutBucketVersioning(context.TODO(), &s3.PutBucketVersioningInput{
|
|
|
|
Bucket: aws.String(bucket),
|
|
|
|
VersioningConfiguration: &types.VersioningConfiguration{
|
|
|
|
Status: types.BucketVersioningStatusEnabled,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
stats.Report(c.vu, createBucketFails, 1)
|
|
|
|
return CreateBucketResponse{Success: false, Error: err.Error()}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-24 09:44:16 +00:00
|
|
|
stats.Report(c.vu, createBucketSuccess, 1)
|
2022-06-17 10:37:43 +00:00
|
|
|
stats.Report(c.vu, createBucketDuration, metrics.D(time.Since(start)))
|
|
|
|
return CreateBucketResponse{Success: true}
|
|
|
|
}
|