mirror of
https://github.com/versity/versitygw.git
synced 2025-12-23 13:15:18 +00:00
This adds a bunch of test cases for non-0 len object, 0 len object, and directory objects to match verified AWS responses for the various range bytes cases. This fixes the posix head/get range responses for these test cases as well.
1632 lines
42 KiB
Go
1632 lines
42 KiB
Go
// Copyright 2023 Versity Software
|
|
// This file is licensed under the Apache License, Version 2.0
|
|
// (the "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"hash"
|
|
"hash/crc32"
|
|
"hash/crc64"
|
|
"io"
|
|
"math/big"
|
|
"math/bits"
|
|
rnd "math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"os/exec"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/aws/smithy-go"
|
|
"github.com/versity/versitygw/s3err"
|
|
)
|
|
|
|
var (
|
|
bcktCount = 0
|
|
adminErrorPrefix = "XAdmin"
|
|
)
|
|
|
|
type user struct {
|
|
access string
|
|
secret string
|
|
role string
|
|
}
|
|
|
|
var (
|
|
testuser1 user = user{
|
|
access: "grt1",
|
|
secret: "grt1secret",
|
|
role: "user",
|
|
}
|
|
testuser2 user = user{
|
|
access: "grt2",
|
|
secret: "grt2secret",
|
|
role: "user",
|
|
}
|
|
testuserplus user = user{
|
|
access: "grtplus",
|
|
secret: "grt1plussecret",
|
|
role: "userplus",
|
|
}
|
|
testadmin user = user{
|
|
access: "admin",
|
|
secret: "adminsecret",
|
|
role: "admin",
|
|
}
|
|
)
|
|
|
|
func getBucketName() string {
|
|
bcktCount++
|
|
return fmt.Sprintf("test-bucket-%v", bcktCount)
|
|
}
|
|
|
|
func setup(s *S3Conf, bucket string, opts ...setupOpt) error {
|
|
s3client := s.GetClient()
|
|
|
|
cfg := new(setupCfg)
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.CreateBucket(ctx, &s3.CreateBucketInput{
|
|
Bucket: &bucket,
|
|
ObjectLockEnabledForBucket: &cfg.LockEnabled,
|
|
ObjectOwnership: cfg.Ownership,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cfg.VersioningStatus != "" {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{
|
|
Bucket: &bucket,
|
|
VersioningConfiguration: &types.VersioningConfiguration{
|
|
Status: cfg.VersioningStatus,
|
|
},
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func teardown(s *S3Conf, bucket string) error {
|
|
s3client := s.GetClient()
|
|
|
|
deleteObject := func(bucket, key, versionId *string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
|
Bucket: bucket,
|
|
Key: key,
|
|
VersionId: versionId,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete object %v: %w", *key, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if s.versioningEnabled {
|
|
in := &s3.ListObjectVersionsInput{Bucket: &bucket}
|
|
for {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
out, err := s3client.ListObjectVersions(ctx, in)
|
|
cancel()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list objects: %w", err)
|
|
}
|
|
|
|
for _, item := range out.Versions {
|
|
err = deleteObject(&bucket, item.Key, item.VersionId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, item := range out.DeleteMarkers {
|
|
err = deleteObject(&bucket, item.Key, item.VersionId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if out.IsTruncated != nil && *out.IsTruncated {
|
|
in.KeyMarker = out.KeyMarker
|
|
in.VersionIdMarker = out.NextVersionIdMarker
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
for {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
out, err := s3client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
|
|
Bucket: &bucket,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list objects: %w", err)
|
|
}
|
|
|
|
for _, item := range out.Contents {
|
|
err = deleteObject(&bucket, item.Key, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if out.IsTruncated != nil && *out.IsTruncated {
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := s3client.DeleteBucket(ctx, &s3.DeleteBucketInput{
|
|
Bucket: &bucket,
|
|
})
|
|
cancel()
|
|
return err
|
|
}
|
|
|
|
type setupCfg struct {
|
|
LockEnabled bool
|
|
VersioningStatus types.BucketVersioningStatus
|
|
Ownership types.ObjectOwnership
|
|
Anonymous bool
|
|
SkipTearDown bool
|
|
}
|
|
|
|
type setupOpt func(*setupCfg)
|
|
|
|
func withLock() setupOpt {
|
|
return func(s *setupCfg) { s.LockEnabled = true }
|
|
}
|
|
func withOwnership(o types.ObjectOwnership) setupOpt {
|
|
return func(s *setupCfg) { s.Ownership = o }
|
|
}
|
|
func withVersioning(v types.BucketVersioningStatus) setupOpt {
|
|
return func(s *setupCfg) { s.VersioningStatus = v }
|
|
}
|
|
func withAnonymousClient() setupOpt {
|
|
return func(s *setupCfg) { s.Anonymous = true }
|
|
}
|
|
func withSkipTearDown() setupOpt {
|
|
return func(s *setupCfg) { s.SkipTearDown = true }
|
|
}
|
|
|
|
func actionHandler(s *S3Conf, testName string, handler func(s3client *s3.Client, bucket string) error, opts ...setupOpt) error {
|
|
runF(testName)
|
|
bucketName := getBucketName()
|
|
|
|
cfg := new(setupCfg)
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
err := setup(s, bucketName, opts...)
|
|
if err != nil {
|
|
failF("%v: failed to create a bucket: %v", testName, err)
|
|
return fmt.Errorf("%v: failed to create a bucket: %w", testName, err)
|
|
}
|
|
|
|
var client *s3.Client
|
|
if cfg.Anonymous {
|
|
client = s.GetAnonymousClient()
|
|
} else {
|
|
client = s.GetClient()
|
|
}
|
|
|
|
handlerErr := handler(client, bucketName)
|
|
if handlerErr != nil {
|
|
failF("%v: %v", testName, handlerErr)
|
|
}
|
|
|
|
if !cfg.SkipTearDown {
|
|
err = teardown(s, bucketName)
|
|
if err != nil {
|
|
fmt.Printf(colorRed+"%v: failed to delete the bucket: %v", testName, err)
|
|
if handlerErr == nil {
|
|
return fmt.Errorf("%v: failed to delete the bucket: %w", testName, err)
|
|
}
|
|
}
|
|
}
|
|
if handlerErr == nil {
|
|
passF(testName)
|
|
}
|
|
|
|
return handlerErr
|
|
}
|
|
|
|
func actionHandlerNoSetup(s *S3Conf, testName string, handler func(s3client *s3.Client, bucket string) error, _ ...setupOpt) error {
|
|
runF(testName)
|
|
client := s.GetClient()
|
|
handlerErr := handler(client, "")
|
|
if handlerErr != nil {
|
|
failF("%v: %v", testName, handlerErr)
|
|
}
|
|
|
|
if handlerErr == nil {
|
|
passF(testName)
|
|
}
|
|
|
|
return handlerErr
|
|
}
|
|
|
|
type authConfig struct {
|
|
testName string
|
|
path string
|
|
method string
|
|
body []byte
|
|
service string
|
|
date time.Time
|
|
}
|
|
|
|
func authHandler(s *S3Conf, cfg *authConfig, handler func(req *http.Request) error) error {
|
|
runF(cfg.testName)
|
|
req, err := createSignedReq(cfg.method, s.endpoint, cfg.path, s.awsID, s.awsSecret, cfg.service, s.awsRegion, cfg.body, cfg.date, nil)
|
|
if err != nil {
|
|
failF("%v: %v", cfg.testName, err)
|
|
return fmt.Errorf("%v: %w", cfg.testName, err)
|
|
}
|
|
|
|
err = handler(req)
|
|
if err != nil {
|
|
failF("%v: %v", cfg.testName, err)
|
|
return fmt.Errorf("%v: %w", cfg.testName, err)
|
|
}
|
|
passF(cfg.testName)
|
|
return nil
|
|
}
|
|
|
|
func presignedAuthHandler(s *S3Conf, testName string, handler func(client *s3.PresignClient, bucket string) error) error {
|
|
runF(testName)
|
|
bucket := getBucketName()
|
|
err := setup(s, bucket)
|
|
if err != nil {
|
|
failF("%v: %v", testName, err)
|
|
return fmt.Errorf("%v: %w", testName, err)
|
|
}
|
|
clt := s.GetPresignClient()
|
|
|
|
err = handler(clt, bucket)
|
|
if err != nil {
|
|
failF("%v: %v", testName, err)
|
|
return fmt.Errorf("%v: %w", testName, err)
|
|
}
|
|
|
|
err = teardown(s, bucket)
|
|
if err != nil {
|
|
failF("%v: %v", testName, err)
|
|
return fmt.Errorf("%v: %w", testName, err)
|
|
}
|
|
|
|
passF(testName)
|
|
return nil
|
|
}
|
|
|
|
func createSignedReq(method, endpoint, path, access, secret, service, region string, body []byte, date time.Time, headers map[string]string) (*http.Request, error) {
|
|
req, err := http.NewRequest(method, fmt.Sprintf("%v/%v", endpoint, path), bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send the request: %w", err)
|
|
}
|
|
|
|
signer := v4.NewSigner()
|
|
|
|
hashedPayload := sha256.Sum256(body)
|
|
hexPayload := hex.EncodeToString(hashedPayload[:])
|
|
|
|
req.Header.Set("X-Amz-Content-Sha256", hexPayload)
|
|
for key, val := range headers {
|
|
req.Header.Add(key, val)
|
|
}
|
|
|
|
signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: access, SecretAccessKey: secret}, req, hexPayload, service, region, date)
|
|
if signErr != nil {
|
|
return nil, fmt.Errorf("failed to sign the request: %w", signErr)
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func checkHTTPResponseApiErr(resp *http.Response, apiErr s3err.APIError) error {
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var errResp s3err.APIErrorResponse
|
|
err = xml.Unmarshal(body, &errResp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if resp.StatusCode != apiErr.HTTPStatusCode {
|
|
return fmt.Errorf("expected response status code to be %v, instead got %v", apiErr.HTTPStatusCode, resp.StatusCode)
|
|
}
|
|
if errResp.Code != apiErr.Code {
|
|
return fmt.Errorf("expected error code to be %v, instead got %v", apiErr.Code, errResp.Code)
|
|
}
|
|
if errResp.Message != apiErr.Description {
|
|
return fmt.Errorf("expected error message to be %v, instead got %v", apiErr.Description, errResp.Message)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkApiErr(err error, apiErr s3err.APIError) error {
|
|
if err == nil {
|
|
return fmt.Errorf("expected %v, instead got nil", apiErr.Code)
|
|
}
|
|
var ae smithy.APIError
|
|
if errors.As(err, &ae) {
|
|
if ae.ErrorCode() != apiErr.Code {
|
|
return fmt.Errorf("expected error code to be %v, instead got %v", apiErr.Code, ae.ErrorCode())
|
|
}
|
|
|
|
if ae.ErrorMessage() != apiErr.Description {
|
|
return fmt.Errorf("expected error message to be %v, instead got %v", apiErr.Description, ae.ErrorMessage())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("expected aws api error, instead got: %w", err)
|
|
}
|
|
|
|
func checkSdkApiErr(err error, code string) error {
|
|
var ae smithy.APIError
|
|
if errors.As(err, &ae) {
|
|
if ae.ErrorCode() != code {
|
|
return fmt.Errorf("expected %v, instead got %v", code, ae.ErrorCode())
|
|
}
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func putObjects(client *s3.Client, objs []string, bucket string) ([]types.Object, error) {
|
|
var contents []types.Object
|
|
var size int64
|
|
for _, key := range objs {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
res, err := client.PutObject(ctx, &s3.PutObjectInput{
|
|
Key: &key,
|
|
Bucket: &bucket,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
k := key
|
|
contents = append(contents, types.Object{
|
|
Key: &k,
|
|
ETag: res.ETag,
|
|
StorageClass: types.ObjectStorageClassStandard,
|
|
Size: &size,
|
|
})
|
|
}
|
|
|
|
sort.SliceStable(contents, func(i, j int) bool {
|
|
return *contents[i].Key < *contents[j].Key
|
|
})
|
|
|
|
return contents, nil
|
|
}
|
|
|
|
func listObjects(client *s3.Client, bucket, prefix, delimiter string, maxKeys int32) ([]types.Object, []types.CommonPrefix, error) {
|
|
var contents []types.Object
|
|
var commonPrefixes []types.CommonPrefix
|
|
|
|
var continuationToken *string
|
|
|
|
for {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
res, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
|
|
Bucket: &bucket,
|
|
ContinuationToken: continuationToken,
|
|
Prefix: &prefix,
|
|
Delimiter: &delimiter,
|
|
MaxKeys: &maxKeys,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
contents = append(contents, res.Contents...)
|
|
commonPrefixes = append(commonPrefixes, res.CommonPrefixes...)
|
|
continuationToken = res.NextContinuationToken
|
|
|
|
if !*res.IsTruncated {
|
|
break
|
|
}
|
|
}
|
|
|
|
return contents, commonPrefixes, nil
|
|
}
|
|
|
|
func hasObjNames(objs []types.Object, names []string) bool {
|
|
if len(objs) != len(names) {
|
|
return false
|
|
}
|
|
|
|
for _, obj := range objs {
|
|
if slices.Contains(names, *obj.Key) {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func hasPrefixName(prefixes []types.CommonPrefix, names []string) bool {
|
|
if len(prefixes) != len(names) {
|
|
return false
|
|
}
|
|
|
|
for _, prefix := range prefixes {
|
|
if slices.Contains(names, *prefix.Prefix) {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
type putObjectOutput struct {
|
|
csum [32]byte
|
|
data []byte
|
|
res *s3.PutObjectOutput
|
|
}
|
|
|
|
func putObjectWithData(lgth int64, input *s3.PutObjectInput, client *s3.Client) (*putObjectOutput, error) {
|
|
data := make([]byte, lgth)
|
|
rand.Read(data)
|
|
csum := sha256.Sum256(data)
|
|
r := bytes.NewReader(data)
|
|
input.Body = r
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
res, err := client.PutObject(ctx, input)
|
|
cancel()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &putObjectOutput{
|
|
csum: csum,
|
|
data: data,
|
|
res: res,
|
|
}, nil
|
|
}
|
|
|
|
type mpCfg struct {
|
|
checksumAlgorithm types.ChecksumAlgorithm
|
|
checksumType types.ChecksumType
|
|
}
|
|
|
|
type mpOpt func(*mpCfg)
|
|
|
|
func withChecksum(algo types.ChecksumAlgorithm) mpOpt {
|
|
return func(mc *mpCfg) { mc.checksumAlgorithm = algo }
|
|
}
|
|
func withChecksumType(t types.ChecksumType) mpOpt {
|
|
return func(mc *mpCfg) { mc.checksumType = t }
|
|
}
|
|
|
|
func createMp(s3client *s3.Client, bucket, key string, opts ...mpOpt) (*s3.CreateMultipartUploadOutput, error) {
|
|
cfg := new(mpCfg)
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
out, err := s3client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
|
|
Bucket: &bucket,
|
|
Key: &key,
|
|
ChecksumAlgorithm: cfg.checksumAlgorithm,
|
|
ChecksumType: cfg.checksumType,
|
|
})
|
|
cancel()
|
|
return out, err
|
|
}
|
|
|
|
func isSameData(a, b []byte) bool {
|
|
return bytes.Equal(a, b)
|
|
}
|
|
|
|
func compareMultipartUploads(list1, list2 []types.MultipartUpload) bool {
|
|
if len(list1) != len(list2) {
|
|
return false
|
|
}
|
|
for i, item := range list1 {
|
|
if *item.Key != *list2[i].Key {
|
|
return false
|
|
}
|
|
if *item.UploadId != *list2[i].UploadId {
|
|
return false
|
|
}
|
|
if item.StorageClass != list2[i].StorageClass {
|
|
return false
|
|
}
|
|
if item.ChecksumAlgorithm != list2[i].ChecksumAlgorithm {
|
|
return false
|
|
}
|
|
if item.ChecksumType != list2[i].ChecksumType {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func compareParts(parts1, parts2 []types.Part) bool {
|
|
if len(parts1) != len(parts2) {
|
|
fmt.Printf("list length are not equal: %v != %v\n", len(parts1), len(parts2))
|
|
return false
|
|
}
|
|
|
|
for i, prt := range parts1 {
|
|
if *prt.PartNumber != *parts2[i].PartNumber {
|
|
fmt.Printf("partNumbers are not equal, %v != %v\n", *prt.PartNumber, *parts2[i].PartNumber)
|
|
return false
|
|
}
|
|
if *prt.ETag != *parts2[i].ETag {
|
|
fmt.Printf("etags are not equal, %v != %v\n", *prt.ETag, *parts2[i].ETag)
|
|
return false
|
|
}
|
|
if *prt.Size != *parts2[i].Size {
|
|
fmt.Printf("sizes are not equal, %v != %v\n", *prt.Size, *parts2[i].Size)
|
|
return false
|
|
}
|
|
if prt.ChecksumCRC32 != nil {
|
|
if *prt.ChecksumCRC32 != getString(parts2[i].ChecksumCRC32) {
|
|
fmt.Printf("crc32 checksums are not equal, %v != %v\n", *prt.ChecksumCRC32, getString(parts2[i].ChecksumCRC32))
|
|
return false
|
|
}
|
|
}
|
|
if prt.ChecksumCRC32C != nil {
|
|
if *prt.ChecksumCRC32C != getString(parts2[i].ChecksumCRC32C) {
|
|
fmt.Printf("crc32c checksums are not equal, %v != %v\n", *prt.ChecksumCRC32C, getString(parts2[i].ChecksumCRC32C))
|
|
return false
|
|
}
|
|
}
|
|
if prt.ChecksumSHA1 != nil {
|
|
if *prt.ChecksumSHA1 != getString(parts2[i].ChecksumSHA1) {
|
|
fmt.Printf("sha1 checksums are not equal, %v != %v\n", *prt.ChecksumSHA1, getString(parts2[i].ChecksumSHA1))
|
|
return false
|
|
}
|
|
}
|
|
if prt.ChecksumSHA256 != nil {
|
|
if *prt.ChecksumSHA256 != getString(parts2[i].ChecksumSHA256) {
|
|
fmt.Printf("sha256 checksums are not equal, %v != %v\n", *prt.ChecksumSHA256, getString(parts2[i].ChecksumSHA256))
|
|
return false
|
|
}
|
|
}
|
|
if prt.ChecksumCRC64NVME != nil {
|
|
if *prt.ChecksumCRC64NVME != getString(parts2[i].ChecksumCRC64NVME) {
|
|
fmt.Printf("crc64nvme checksums are not equal, %v != %v\n", *prt.ChecksumCRC64NVME, getString(parts2[i].ChecksumCRC64NVME))
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func areTagsSame(tags1, tags2 []types.Tag) bool {
|
|
if len(tags1) != len(tags2) {
|
|
return false
|
|
}
|
|
|
|
for _, tag := range tags1 {
|
|
if !containsTag(tag, tags2) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func containsTag(tag types.Tag, list []types.Tag) bool {
|
|
for _, item := range list {
|
|
if *item.Key == *tag.Key && *item.Value == *tag.Value {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func compareGrants(grts1, grts2 []types.Grant) bool {
|
|
if len(grts1) != len(grts2) {
|
|
return false
|
|
}
|
|
|
|
for i, grt := range grts1 {
|
|
if grt.Permission != grts2[i].Permission {
|
|
return false
|
|
}
|
|
if *grt.Grantee.ID != *grts2[i].Grantee.ID {
|
|
return false
|
|
}
|
|
if grt.Grantee.Type != grts2[i].Grantee.Type {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func execCommand(args ...string) ([]byte, error) {
|
|
cmd := exec.Command("./versitygw", args...)
|
|
|
|
return cmd.CombinedOutput()
|
|
}
|
|
|
|
func getString(str *string) string {
|
|
if str == nil {
|
|
return ""
|
|
}
|
|
return *str
|
|
}
|
|
|
|
func getPtr(str string) *string {
|
|
return &str
|
|
}
|
|
|
|
// mp1 needs to be the response from the server
|
|
// mp2 needs to be the expected values
|
|
// The keys from the server are always converted to lowercase
|
|
func areMapsSame(mp1, mp2 map[string]string) bool {
|
|
if len(mp1) != len(mp2) {
|
|
return false
|
|
}
|
|
for key, val := range mp2 {
|
|
if mp1[strings.ToLower(key)] != val {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func compareBuckets(list1 []types.Bucket, list2 []types.Bucket) bool {
|
|
if len(list1) != len(list2) {
|
|
return false
|
|
}
|
|
|
|
for i, elem := range list1 {
|
|
if *elem.Name != *list2[i].Name {
|
|
fmt.Printf("bucket names are not equal: %s != %s\n", *elem.Name, *list2[i].Name)
|
|
return false
|
|
}
|
|
if *elem.BucketRegion != *list2[i].BucketRegion {
|
|
fmt.Printf("bucket regions are not equal: %s != %s\n", *elem.BucketRegion, *list2[i].BucketRegion)
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func compareObjects(list1, list2 []types.Object) bool {
|
|
if len(list1) != len(list2) {
|
|
fmt.Println("list lengths are not equal")
|
|
return false
|
|
}
|
|
|
|
for i, obj := range list1 {
|
|
if *obj.Key != *list2[i].Key {
|
|
fmt.Printf("keys are not equal: %q != %q\n", *obj.Key, *list2[i].Key)
|
|
return false
|
|
}
|
|
if *obj.ETag != *list2[i].ETag {
|
|
fmt.Printf("etags are not equal: (%q %q) %q != %q\n",
|
|
*obj.Key, *list2[i].Key, *obj.ETag, *list2[i].ETag)
|
|
return false
|
|
}
|
|
if *obj.Size != *list2[i].Size {
|
|
fmt.Printf("sizes are not equal: (%q %q) %v != %v\n",
|
|
*obj.Key, *list2[i].Key, *obj.Size, *list2[i].Size)
|
|
return false
|
|
}
|
|
if obj.StorageClass != list2[i].StorageClass {
|
|
fmt.Printf("storage classes are not equal: (%q %q) %v != %v\n",
|
|
*obj.Key, *list2[i].Key, obj.StorageClass, list2[i].StorageClass)
|
|
return false
|
|
}
|
|
if len(obj.ChecksumAlgorithm) != 0 {
|
|
if obj.ChecksumAlgorithm[0] != list2[i].ChecksumAlgorithm[0] {
|
|
fmt.Printf("checksum algorithms are not equal: (%q %q) %v != %v\n",
|
|
*obj.Key, *list2[i].Key, obj.ChecksumAlgorithm[0], list2[i].ChecksumAlgorithm[0])
|
|
return false
|
|
}
|
|
}
|
|
if obj.ChecksumType != "" {
|
|
if obj.ChecksumType[0] != list2[i].ChecksumType[0] {
|
|
fmt.Printf("checksum types are not equal: (%q %q) %v != %v\n",
|
|
*obj.Key, *list2[i].Key, obj.ChecksumType, list2[i].ChecksumType)
|
|
return false
|
|
}
|
|
}
|
|
if obj.Owner != nil {
|
|
if *obj.Owner.ID != *list2[i].Owner.ID {
|
|
fmt.Printf("object owner IDs not equal: (%q %q) %v != %v\n",
|
|
*obj.Key, *list2[i].Key, *obj.Owner.ID, *list2[i].Owner.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func comparePrefixes(list1 []string, list2 []types.CommonPrefix) bool {
|
|
if len(list1) != len(list2) {
|
|
return false
|
|
}
|
|
|
|
elementMap := make(map[string]bool)
|
|
|
|
for _, elem := range list1 {
|
|
elementMap[elem] = true
|
|
}
|
|
|
|
for _, elem := range list2 {
|
|
if _, found := elementMap[*elem.Prefix]; !found {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func compareDelObjects(list1, list2 []types.DeletedObject) bool {
|
|
if len(list1) != len(list2) {
|
|
return false
|
|
}
|
|
|
|
for i, obj := range list1 {
|
|
if *obj.Key != *list2[i].Key {
|
|
return false
|
|
}
|
|
|
|
if obj.VersionId != nil {
|
|
if list2[i].VersionId == nil {
|
|
return false
|
|
}
|
|
if *obj.VersionId != *list2[i].VersionId {
|
|
return false
|
|
}
|
|
}
|
|
if obj.DeleteMarkerVersionId != nil {
|
|
if list2[i].DeleteMarkerVersionId == nil {
|
|
return false
|
|
}
|
|
if *obj.DeleteMarkerVersionId != *list2[i].DeleteMarkerVersionId {
|
|
return false
|
|
}
|
|
}
|
|
if obj.DeleteMarker != nil {
|
|
if list2[i].DeleteMarker == nil {
|
|
return false
|
|
}
|
|
if *obj.DeleteMarker != *list2[i].DeleteMarker {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func uploadParts(client *s3.Client, size, partCount int64, bucket, key, uploadId string, opts ...mpOpt) (parts []types.Part, csum string, err error) {
|
|
partSize := size / partCount
|
|
|
|
var hash hash.Hash
|
|
|
|
cfg := new(mpCfg)
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
switch cfg.checksumAlgorithm {
|
|
case types.ChecksumAlgorithmCrc32:
|
|
hash = crc32.NewIEEE()
|
|
case types.ChecksumAlgorithmCrc32c:
|
|
hash = crc32.New(crc32.MakeTable(crc32.Castagnoli))
|
|
case types.ChecksumAlgorithmSha1:
|
|
hash = sha1.New()
|
|
case types.ChecksumAlgorithmSha256:
|
|
hash = sha256.New()
|
|
case types.ChecksumAlgorithmCrc64nvme:
|
|
hash = crc64.New(crc64.MakeTable(bits.Reverse64(0xad93d23594c93659)))
|
|
default:
|
|
hash = sha256.New()
|
|
}
|
|
|
|
for partNumber := int64(1); partNumber <= partCount; partNumber++ {
|
|
partStart := (partNumber - 1) * partSize
|
|
partEnd := partStart + partSize - 1
|
|
if partEnd > size-1 {
|
|
partEnd = size - 1
|
|
}
|
|
|
|
partBuffer := make([]byte, partEnd-partStart+1)
|
|
rand.Read(partBuffer)
|
|
hash.Write(partBuffer)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
pn := int32(partNumber)
|
|
out, err := client.UploadPart(ctx, &s3.UploadPartInput{
|
|
Bucket: &bucket,
|
|
Key: &key,
|
|
UploadId: &uploadId,
|
|
Body: bytes.NewReader(partBuffer),
|
|
PartNumber: &pn,
|
|
ChecksumAlgorithm: cfg.checksumAlgorithm,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return parts, "", err
|
|
}
|
|
|
|
part := types.Part{
|
|
ETag: out.ETag,
|
|
PartNumber: &pn,
|
|
Size: &partSize,
|
|
}
|
|
|
|
switch cfg.checksumAlgorithm {
|
|
case types.ChecksumAlgorithmCrc32:
|
|
part.ChecksumCRC32 = out.ChecksumCRC32
|
|
case types.ChecksumAlgorithmCrc32c:
|
|
part.ChecksumCRC32C = out.ChecksumCRC32C
|
|
case types.ChecksumAlgorithmSha1:
|
|
part.ChecksumSHA1 = out.ChecksumSHA1
|
|
case types.ChecksumAlgorithmSha256:
|
|
part.ChecksumSHA256 = out.ChecksumSHA256
|
|
case types.ChecksumAlgorithmCrc64nvme:
|
|
part.ChecksumCRC64NVME = out.ChecksumCRC64NVME
|
|
}
|
|
|
|
parts = append(parts, part)
|
|
}
|
|
sum := hash.Sum(nil)
|
|
|
|
if cfg.checksumAlgorithm == "" {
|
|
csum = hex.EncodeToString(sum[:])
|
|
} else {
|
|
csum = base64.StdEncoding.EncodeToString(sum[:])
|
|
}
|
|
|
|
return parts, csum, err
|
|
}
|
|
|
|
func createUsers(s *S3Conf, users []user) error {
|
|
for _, usr := range users {
|
|
err := deleteUser(s, usr.access)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err := execCommand(s.getAdminCommand("-a", s.awsID, "-s", s.awsSecret, "-er", s.endpoint, "create-user", "-a", usr.access, "-s", usr.secret, "-r", usr.role)...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.Contains(string(out), adminErrorPrefix) {
|
|
return fmt.Errorf("failed to create user account: %s", out)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func deleteUser(s *S3Conf, access string) error {
|
|
out, err := execCommand(s.getAdminCommand("-a", s.awsID, "-s", s.awsSecret, "-er", s.endpoint, "delete-user", "-a", access)...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.Contains(string(out), adminErrorPrefix) {
|
|
return fmt.Errorf("failed to delete the user account, %s", out)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func changeBucketsOwner(s *S3Conf, buckets []string, owner string) error {
|
|
for _, bucket := range buckets {
|
|
out, err := execCommand(s.getAdminCommand("-a", s.awsID, "-s", s.awsSecret, "-er", s.endpoint, "change-bucket-owner", "-b", bucket, "-o", owner)...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.Contains(string(out), adminErrorPrefix) {
|
|
return fmt.Errorf("failed to change the bucket owner: %s", out)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func listBuckets(s *S3Conf) error {
|
|
out, err := execCommand(s.getAdminCommand("-a", s.awsID, "-s", s.awsSecret, "-er", s.endpoint, "list-buckets")...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.Contains(string(out), adminErrorPrefix) {
|
|
return fmt.Errorf("failed to list buckets, %s", out)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
|
|
func genRandString(length int) string {
|
|
source := rnd.NewSource(time.Now().UnixNano())
|
|
random := rnd.New(source)
|
|
result := make([]byte, length)
|
|
for i := range result {
|
|
result[i] = charset[random.Intn(len(charset))]
|
|
}
|
|
return string(result)
|
|
}
|
|
|
|
const (
|
|
credAccess int = iota
|
|
credDate
|
|
credRegion
|
|
credService
|
|
credTerminator
|
|
)
|
|
|
|
func changeAuthCred(uri, newVal string, index int) (string, error) {
|
|
urlParsed, err := url.Parse(uri)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
queries := urlParsed.Query()
|
|
creds := strings.Split(queries.Get("X-Amz-Credential"), "/")
|
|
creds[index] = newVal
|
|
queries.Set("X-Amz-Credential", strings.Join(creds, "/"))
|
|
urlParsed.RawQuery = queries.Encode()
|
|
|
|
return urlParsed.String(), nil
|
|
}
|
|
|
|
func genPolicyDoc(effect, principal, action, resource string) string {
|
|
jsonTemplate := `{
|
|
"Statement": [
|
|
{
|
|
"Effect": "%s",
|
|
"Principal": %s,
|
|
"Action": %s,
|
|
"Resource": %s
|
|
}
|
|
]
|
|
}
|
|
`
|
|
|
|
return fmt.Sprintf(jsonTemplate, effect, principal, action, resource)
|
|
}
|
|
|
|
type policyType string
|
|
|
|
const (
|
|
policyTypeBucket policyType = "bucket"
|
|
policyTypeObject policyType = "object"
|
|
policyTypeFull policyType = "full"
|
|
)
|
|
|
|
func grantPublicBucketPolicy(client *s3.Client, bucket string, tp policyType) error {
|
|
var doc string
|
|
|
|
switch tp {
|
|
case policyTypeBucket:
|
|
doc = genPolicyDoc("Allow", `"*"`, `"s3:*"`, fmt.Sprintf(`"arn:aws:s3:::%s"`, bucket))
|
|
case policyTypeObject:
|
|
doc = genPolicyDoc("Allow", `"*"`, `"s3:*"`, fmt.Sprintf(`"arn:aws:s3:::%s/*"`, bucket))
|
|
case policyTypeFull:
|
|
template := `
|
|
{
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Principal": "*",
|
|
"Action": "s3:*",
|
|
"Resource": "arn:aws:s3:::%s"
|
|
},
|
|
{
|
|
"Effect": "Allow",
|
|
"Principal": "*",
|
|
"Action": "s3:*",
|
|
"Resource": "arn:aws:s3:::%s/*"
|
|
}
|
|
]
|
|
}
|
|
`
|
|
doc = fmt.Sprintf(template, bucket, bucket)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
|
|
Bucket: &bucket,
|
|
Policy: &doc,
|
|
})
|
|
cancel()
|
|
return err
|
|
}
|
|
|
|
func getMalformedPolicyError(msg string) s3err.APIError {
|
|
return s3err.APIError{
|
|
Code: "MalformedPolicy",
|
|
Description: msg,
|
|
HTTPStatusCode: http.StatusBadRequest,
|
|
}
|
|
}
|
|
|
|
// if true enables, otherwise disables
|
|
func changeBucketObjectLockStatus(client *s3.Client, bucket string, status bool) error {
|
|
cfg := types.ObjectLockConfiguration{}
|
|
if status {
|
|
cfg.ObjectLockEnabled = types.ObjectLockEnabledEnabled
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
|
|
Bucket: &bucket,
|
|
ObjectLockConfiguration: &cfg,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func putBucketVersioningStatus(client *s3.Client, bucket string, status types.BucketVersioningStatus) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{
|
|
Bucket: &bucket,
|
|
VersioningConfiguration: &types.VersioningConfiguration{
|
|
Status: status,
|
|
},
|
|
})
|
|
cancel()
|
|
|
|
return err
|
|
}
|
|
|
|
func checkWORMProtection(client *s3.Client, bucket, object string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err := client.PutObject(ctx, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &object,
|
|
})
|
|
cancel()
|
|
if err := checkSdkApiErr(err, "InvalidRequest"); err != nil {
|
|
return err
|
|
}
|
|
// client sdk regression issue prevents getting full error message,
|
|
// change back to below once this is fixed:
|
|
// https://github.com/aws/aws-sdk-go-v2/issues/2921
|
|
// if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil {
|
|
// return err
|
|
// }
|
|
|
|
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err = client.DeleteObject(ctx, &s3.DeleteObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &object,
|
|
})
|
|
cancel()
|
|
if err := checkSdkApiErr(err, "InvalidRequest"); err != nil {
|
|
return err
|
|
}
|
|
// client sdk regression issue prevents getting full error message,
|
|
// change back to below once this is fixed:
|
|
// https://github.com/aws/aws-sdk-go-v2/issues/2921
|
|
// if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil {
|
|
// return err
|
|
// }
|
|
|
|
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
|
|
_, err = client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
|
|
Bucket: &bucket,
|
|
Delete: &types.Delete{
|
|
Objects: []types.ObjectIdentifier{
|
|
{
|
|
Key: &object,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
cancel()
|
|
if err := checkSdkApiErr(err, "InvalidRequest"); err != nil {
|
|
return err
|
|
}
|
|
// client sdk regression issue prevents getting full error message,
|
|
// change back to below once this is fixed:
|
|
// https://github.com/aws/aws-sdk-go-v2/issues/2921
|
|
// if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrObjectLocked)); err != nil {
|
|
// return err
|
|
// }
|
|
|
|
return nil
|
|
}
|
|
|
|
func objStrings(objs []types.Object) []string {
|
|
objStrs := make([]string, len(objs))
|
|
for i, obj := range objs {
|
|
objStrs[i] = *obj.Key
|
|
}
|
|
return objStrs
|
|
}
|
|
|
|
func pfxStrings(pfxs []types.CommonPrefix) []string {
|
|
pfxStrs := make([]string, len(pfxs))
|
|
for i, pfx := range pfxs {
|
|
pfxStrs[i] = *pfx.Prefix
|
|
}
|
|
return pfxStrs
|
|
}
|
|
|
|
type versCfg struct {
|
|
checksumAlgorithm types.ChecksumAlgorithm
|
|
}
|
|
|
|
type versOpt func(*versCfg)
|
|
|
|
func withChecksumAlgo(algo types.ChecksumAlgorithm) versOpt {
|
|
return func(vc *versCfg) { vc.checksumAlgorithm = algo }
|
|
}
|
|
|
|
func createObjVersions(client *s3.Client, bucket, object string, count int, opts ...versOpt) ([]types.ObjectVersion, error) {
|
|
cfg := new(versCfg)
|
|
for _, o := range opts {
|
|
o(cfg)
|
|
}
|
|
|
|
versions := []types.ObjectVersion{}
|
|
for i := range count {
|
|
rNumber, err := rand.Int(rand.Reader, big.NewInt(100000))
|
|
dataLength := rNumber.Int64()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r, err := putObjectWithData(dataLength, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &object,
|
|
}, client)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
isLatest := i == count-1
|
|
version := types.ObjectVersion{
|
|
ETag: r.res.ETag,
|
|
IsLatest: &isLatest,
|
|
Key: &object,
|
|
Size: &dataLength,
|
|
VersionId: r.res.VersionId,
|
|
StorageClass: types.ObjectVersionStorageClassStandard,
|
|
ChecksumType: r.res.ChecksumType,
|
|
}
|
|
|
|
switch {
|
|
case r.res.ChecksumCRC32 != nil:
|
|
version.ChecksumAlgorithm = []types.ChecksumAlgorithm{
|
|
types.ChecksumAlgorithmCrc32,
|
|
}
|
|
case r.res.ChecksumCRC32C != nil:
|
|
version.ChecksumAlgorithm = []types.ChecksumAlgorithm{
|
|
types.ChecksumAlgorithmCrc32c,
|
|
}
|
|
case r.res.ChecksumCRC64NVME != nil:
|
|
version.ChecksumAlgorithm = []types.ChecksumAlgorithm{
|
|
types.ChecksumAlgorithmCrc64nvme,
|
|
}
|
|
case r.res.ChecksumSHA1 != nil:
|
|
version.ChecksumAlgorithm = []types.ChecksumAlgorithm{
|
|
types.ChecksumAlgorithmSha1,
|
|
}
|
|
case r.res.ChecksumSHA256 != nil:
|
|
version.ChecksumAlgorithm = []types.ChecksumAlgorithm{
|
|
types.ChecksumAlgorithmSha256,
|
|
}
|
|
}
|
|
|
|
versions = append(versions, version)
|
|
}
|
|
|
|
versions = reverseSlice(versions)
|
|
|
|
return versions, nil
|
|
}
|
|
|
|
// ReverseSlice reverses a slice of any type
|
|
func reverseSlice[T any](s []T) []T {
|
|
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
|
s[i], s[j] = s[j], s[i]
|
|
}
|
|
return s
|
|
}
|
|
|
|
func compareVersions(v1, v2 []types.ObjectVersion) bool {
|
|
if len(v1) != len(v2) {
|
|
return false
|
|
}
|
|
|
|
for i, version := range v1 {
|
|
if version.Key == nil || v2[i].Key == nil {
|
|
return false
|
|
}
|
|
if *version.Key != *v2[i].Key {
|
|
return false
|
|
}
|
|
|
|
if version.VersionId == nil || v2[i].VersionId == nil {
|
|
return false
|
|
}
|
|
if *version.VersionId != *v2[i].VersionId {
|
|
return false
|
|
}
|
|
|
|
if version.IsLatest == nil || v2[i].IsLatest == nil {
|
|
return false
|
|
}
|
|
if *version.IsLatest != *v2[i].IsLatest {
|
|
return false
|
|
}
|
|
|
|
if version.Size == nil || v2[i].Size == nil {
|
|
return false
|
|
}
|
|
if *version.Size != *v2[i].Size {
|
|
return false
|
|
}
|
|
|
|
if version.ETag == nil || v2[i].ETag == nil {
|
|
return false
|
|
}
|
|
if *version.ETag != *v2[i].ETag {
|
|
return false
|
|
}
|
|
|
|
if version.StorageClass != v2[i].StorageClass {
|
|
return false
|
|
}
|
|
if version.ChecksumType != "" {
|
|
if version.ChecksumType != v2[i].ChecksumType {
|
|
return false
|
|
}
|
|
}
|
|
if len(version.ChecksumAlgorithm) != 0 {
|
|
if len(v2[i].ChecksumAlgorithm) == 0 {
|
|
return false
|
|
}
|
|
if version.ChecksumAlgorithm[0] != v2[i].ChecksumAlgorithm[0] {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func compareDelMarkers(d1, d2 []types.DeleteMarkerEntry) bool {
|
|
if len(d1) != len(d2) {
|
|
return false
|
|
}
|
|
|
|
for i, dEntry := range d1 {
|
|
if dEntry.Key == nil || d2[i].Key == nil {
|
|
return false
|
|
}
|
|
if *dEntry.Key != *d2[i].Key {
|
|
return false
|
|
}
|
|
|
|
if dEntry.IsLatest == nil || d2[i].IsLatest == nil {
|
|
return false
|
|
}
|
|
if *dEntry.IsLatest != *d2[i].IsLatest {
|
|
return false
|
|
}
|
|
|
|
if dEntry.VersionId == nil || d2[i].VersionId == nil {
|
|
return false
|
|
}
|
|
if *dEntry.VersionId != *d2[i].VersionId {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
type ObjectMetaProps struct {
|
|
ContentLength int64
|
|
ContentType string
|
|
ContentEncoding string
|
|
ContentDisposition string
|
|
ContentLanguage string
|
|
CacheControl string
|
|
ExpiresString string
|
|
Metadata map[string]string
|
|
}
|
|
|
|
func checkObjectMetaProps(client *s3.Client, bucket, object string, o ObjectMetaProps) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
out, err := client.HeadObject(ctx, &s3.HeadObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &object,
|
|
})
|
|
cancel()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if o.Metadata != nil {
|
|
if !areMapsSame(out.Metadata, o.Metadata) {
|
|
return fmt.Errorf("expected the object metadata to be %v, instead got %v", o.Metadata, out.Metadata)
|
|
}
|
|
}
|
|
if out.ContentLength == nil {
|
|
return fmt.Errorf("expected Content-Length %v, instead got nil", o.ContentLength)
|
|
}
|
|
if *out.ContentLength != o.ContentLength {
|
|
return fmt.Errorf("expected Content-Length %v, instead got %v", o.ContentLength, *out.ContentLength)
|
|
}
|
|
if o.ContentType != "" && getString(out.ContentType) != o.ContentType {
|
|
return fmt.Errorf("expected Content-Type %v, instead got %v", o.ContentType, getString(out.ContentType))
|
|
}
|
|
if o.ContentDisposition != "" && getString(out.ContentDisposition) != o.ContentDisposition {
|
|
return fmt.Errorf("expected Content-Disposition %v, instead got %v", o.ContentDisposition, getString(out.ContentDisposition))
|
|
}
|
|
if o.ContentEncoding != "" && getString(out.ContentEncoding) != o.ContentEncoding {
|
|
return fmt.Errorf("expected Content-Encoding %v, instead got %v", o.ContentEncoding, getString(out.ContentEncoding))
|
|
}
|
|
if o.ContentLanguage != "" && getString(out.ContentLanguage) != o.ContentLanguage {
|
|
return fmt.Errorf("expected Content-Language %v, instead got %v", o.ContentLanguage, getString(out.ContentLanguage))
|
|
}
|
|
if o.CacheControl != "" && getString(out.CacheControl) != o.CacheControl {
|
|
return fmt.Errorf("expected Cache-Control %v, instead got %v", o.CacheControl, getString(out.CacheControl))
|
|
}
|
|
if o.ExpiresString != "" && getString(out.ExpiresString) != o.ExpiresString {
|
|
return fmt.Errorf("expected Expires %v, instead got %v", o.ExpiresString, getString(out.ExpiresString))
|
|
}
|
|
if out.StorageClass != types.StorageClassStandard {
|
|
return fmt.Errorf("expected the storage class to be %v, instead got %v", types.StorageClassStandard, out.StorageClass)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getBoolPtr(b bool) *bool {
|
|
return &b
|
|
}
|
|
|
|
type PublicBucketTestCase struct {
|
|
Action string
|
|
Call func(ctx context.Context) error
|
|
ExpectedErr error
|
|
}
|
|
|
|
// randomizeCase randomizes the provided string latters case
|
|
func randomizeCase(s string) string {
|
|
var b strings.Builder
|
|
|
|
for _, ch := range s {
|
|
if rnd.Intn(2) == 0 {
|
|
b.WriteRune(unicode.ToLower(ch))
|
|
} else {
|
|
b.WriteRune(unicode.ToUpper(ch))
|
|
}
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func headObject_zero_len_with_range_helper(testName, obj string, s *S3Conf) error {
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
objLength := int64(0)
|
|
_, err := putObjectWithData(objLength, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
testRange := func(rg, contentRange string, cLength int64, expectErr bool) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
Range: &rg,
|
|
})
|
|
cancel()
|
|
if err == nil && expectErr {
|
|
return fmt.Errorf("%v: expected err 'RequestedRangeNotSatisfiable' error, instead got nil", rg)
|
|
}
|
|
if err != nil {
|
|
if !expectErr {
|
|
return err
|
|
}
|
|
var ae smithy.APIError
|
|
if errors.As(err, &ae) {
|
|
if ae.ErrorCode() != "RequestedRangeNotSatisfiable" {
|
|
return fmt.Errorf("%v: expected RequestedRangeNotSatisfiable, instead got %v", rg, ae.ErrorCode())
|
|
}
|
|
if ae.ErrorMessage() != "Requested Range Not Satisfiable" {
|
|
return fmt.Errorf("%v: expected the error message to be 'Requested Range Not Satisfiable', instead got %v", rg, ae.ErrorMessage())
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%v: invalid error got %w", rg, err)
|
|
}
|
|
|
|
if getString(res.AcceptRanges) != "bytes" {
|
|
return fmt.Errorf("%v: expected accept ranges to be 'bytes', instead got %v", rg, getString(res.AcceptRanges))
|
|
}
|
|
if res.ContentLength == nil {
|
|
return fmt.Errorf("%v: expected non nil content-length", rg)
|
|
}
|
|
if *res.ContentLength != cLength {
|
|
return fmt.Errorf("%v: expected content-length to be %v, instead got %v", rg, cLength, *res.ContentLength)
|
|
}
|
|
if getString(res.ContentRange) != contentRange {
|
|
return fmt.Errorf("%v: expected content-range to be %v, instead got %v", rg, contentRange, getString(res.ContentRange))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Reference server expectations for a 0-byte object.
|
|
for _, el := range []struct {
|
|
objRange string
|
|
contentRange string
|
|
contentLength int64
|
|
expectedErr bool
|
|
}{
|
|
{"bytes=abc", "", objLength, false},
|
|
{"bytes=a-z", "", objLength, false},
|
|
{"bytes=,", "", objLength, false},
|
|
{"bytes=0-0,1-2", "", objLength, false},
|
|
{"foo=0-1", "", objLength, false},
|
|
{"bytes=--1", "", objLength, false},
|
|
{"bytes=0--1", "", objLength, false},
|
|
{"bytes= -1", "", objLength, false},
|
|
{"bytes=0 -1", "", objLength, false},
|
|
{"bytes=-1", "", objLength, false}, // reference server returns no error, empty Content-Range
|
|
{"bytes=00-01", "", objLength, true}, // RequestedRangeNotSatisfiable
|
|
{"bytes=-0", "", 0, true},
|
|
{"bytes=0-0", "", 0, true},
|
|
{"bytes=0-", "", 0, true},
|
|
} {
|
|
if err := testRange(el.objRange, el.contentRange, el.contentLength, el.expectedErr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func getObject_zero_len_with_range_helper(testName, obj string, s *S3Conf) error {
|
|
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
|
objLength := int64(0)
|
|
res, err := putObjectWithData(objLength, &s3.PutObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
}, s3client)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
testGetObjectRange := func(rng, contentRange string, cLength int64, expData []byte, expErr error) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
|
defer cancel()
|
|
out, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
|
Bucket: &bucket,
|
|
Key: &obj,
|
|
Range: &rng,
|
|
})
|
|
if err == nil && expErr != nil {
|
|
return fmt.Errorf("%v: expected err %v, instead got nil", rng, expErr)
|
|
}
|
|
if err != nil {
|
|
if expErr == nil {
|
|
return err
|
|
}
|
|
parsedErr, ok := expErr.(s3err.APIError)
|
|
if !ok {
|
|
return fmt.Errorf("invalid error type provided, expected s3err.APIError")
|
|
}
|
|
return checkApiErr(err, parsedErr)
|
|
}
|
|
|
|
if out.ContentLength == nil {
|
|
return fmt.Errorf("%v: expected non nil content-length", rng)
|
|
}
|
|
if *out.ContentLength != cLength {
|
|
return fmt.Errorf("%v: expected content-length to be %v, instead got %v", rng, cLength, *out.ContentLength)
|
|
}
|
|
if getString(out.AcceptRanges) != "bytes" {
|
|
return fmt.Errorf("%v: expected accept-ranges to be 'bytes', instead got %v", rng, getString(out.AcceptRanges))
|
|
}
|
|
if getString(out.ContentRange) != contentRange {
|
|
return fmt.Errorf("%v: expected content-range to be %v, instead got %v", rng, contentRange, getString(out.ContentRange))
|
|
}
|
|
|
|
data, err := io.ReadAll(out.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("%v: read object data: %w", rng, err)
|
|
}
|
|
out.Body.Close()
|
|
if !isSameData(data, expData) {
|
|
return fmt.Errorf("%v: incorrect data retrieved", rng)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for _, el := range []struct {
|
|
rng string
|
|
contentRange string
|
|
cLength int64
|
|
expData []byte
|
|
expErr error
|
|
}{
|
|
{"bytes=abc", "", objLength, res.data, nil},
|
|
{"bytes=a-z", "", objLength, res.data, nil},
|
|
{"bytes=,", "", objLength, res.data, nil},
|
|
{"bytes=0-0,1-2", "", objLength, res.data, nil},
|
|
{"foo=0-1", "", objLength, res.data, nil},
|
|
{"bytes=--1", "", objLength, res.data, nil},
|
|
{"bytes=0--1", "", objLength, res.data, nil},
|
|
{"bytes= -1", "", objLength, res.data, nil},
|
|
{"bytes=0 -1", "", objLength, res.data, nil},
|
|
{"bytes=-1", "", objLength, res.data, nil},
|
|
// error (RequestedRangeNotSatisfiable)
|
|
{"bytes=00-01", "", objLength, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
|
{"bytes=-0", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
|
{"bytes=0-0", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
|
{"bytes=0-", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
|
} {
|
|
if err := testGetObjectRange(el.rng, el.contentRange, el.cLength, el.expData, el.expErr); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|