-
Notifications
You must be signed in to change notification settings - Fork 360
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
support putifabsent #8428
base: master
Are you sure you want to change the base?
support putifabsent #8428
Changes from all commits
d92b29d
f18a67c
81de1fc
7d2a511
768ef8c
b07a5ab
c8a508c
81c8a79
5fc97da
1dd06fd
a635bb4
717f224
d558f57
150776e
d7e22f4
4b1533b
1528c90
08cc251
2873d3a
7e42c88
b46deb3
126144f
4a56ace
c4387d9
51d8105
6f2431a
047bd48
55a2d72
c620816
69764b1
0b2e2b4
8b4d12f
67da789
005f174
278a345
94ed11f
e7508d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,9 +4,6 @@ import ( | |
"bytes" | ||
"context" | ||
"fmt" | ||
"github.com/aws/aws-sdk-go-v2/aws" | ||
"github.com/aws/aws-sdk-go-v2/service/s3" | ||
"github.com/go-openapi/swag" | ||
"io" | ||
"math/rand" | ||
"net/http" | ||
|
@@ -16,6 +13,14 @@ import ( | |
"testing" | ||
"time" | ||
|
||
"github.com/aws/aws-sdk-go-v2/aws" | ||
"github.com/aws/aws-sdk-go-v2/service/s3" | ||
"github.com/aws/aws-sdk-go-v2/service/s3/types" | ||
"github.com/aws/smithy-go/middleware" | ||
smithyhttp "github.com/aws/smithy-go/transport/http" | ||
"github.com/go-openapi/swag" | ||
"github.com/thanhpk/randstr" | ||
|
||
"github.com/minio/minio-go/v7" | ||
"github.com/minio/minio-go/v7/pkg/credentials" | ||
"github.com/minio/minio-go/v7/pkg/tags" | ||
|
@@ -181,6 +186,115 @@ func TestS3UploadAndDownload(t *testing.T) { | |
}) | ||
} | ||
} | ||
func TestMultipartUploadIfNoneMatch(t *testing.T) { | ||
ctx, logger, repo := setupTest(t) | ||
defer tearDownTest(repo) | ||
s3Endpoint := viper.GetString("s3_endpoint") | ||
s3Client := createS3Client(s3Endpoint, t) | ||
multipartNumberOfParts := 7 | ||
multipartPartSize := 5 * 1024 * 1024 | ||
type TestCase struct { | ||
Path string | ||
Content string | ||
IfNoneMatch string | ||
ExpectError bool | ||
} | ||
|
||
testCases := []TestCase{ | ||
{Path: "main/object1", Content: "data", IfNoneMatch: "", ExpectError: false}, | ||
{Path: "main/object1", Content: "data", IfNoneMatch: "*", ExpectError: true}, | ||
{Path: "main/object1", Content: "data", IfNoneMatch: "", ExpectError: false}, | ||
{Path: "main/object1", Content: "data", IfNoneMatch: "", ExpectError: false}, | ||
Comment on lines
+206
to
+207
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both lines seems redundant, any other intention here? or just delete one? |
||
{Path: "main/object2", Content: "data", IfNoneMatch: "*", ExpectError: false}, | ||
Comment on lines
+204
to
+208
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
for i, tc := range testCases { | ||
input := &s3.CreateMultipartUploadInput{ | ||
Bucket: aws.String(repo), | ||
Key: aws.String(tc.Path), | ||
} | ||
|
||
resp, err := s3Client.CreateMultipartUpload(ctx, input) | ||
require.NoError(t, err, "failed to create multipart upload") | ||
logger.Info("Created multipart upload request") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need in this log here, can spam the total output in the CI. |
||
|
||
parts := make([][]byte, multipartNumberOfParts) | ||
for i := 0; i < multipartNumberOfParts; i++ { | ||
parts[i] = randstr.Bytes(multipartPartSize + i) | ||
} | ||
|
||
completedParts := uploadMultipartParts(t, ctx, s3Client, logger, resp, parts, 0) | ||
|
||
completeInput := &s3.CompleteMultipartUploadInput{ | ||
Bucket: resp.Bucket, | ||
Key: resp.Key, | ||
UploadId: resp.UploadId, | ||
MultipartUpload: &types.CompletedMultipartUpload{ | ||
Parts: completedParts, | ||
}, | ||
} | ||
_, err = s3Client.CompleteMultipartUpload(ctx, completeInput, s3.WithAPIOptions(setHTTPHeaders(tc.IfNoneMatch))) | ||
if tc.ExpectError { | ||
require.Error(t, err, "was expecting an error with path %s and header %s in test case # %d", tc.Path, tc.IfNoneMatch, i+1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Part of the requirements is to return AWS errors, let's assert specific errors. So the require.Contains should just check that the code part of the string is contained. type TestCase struct {
ExpectError string
// other fields ...
} Then assert (example):
|
||
} else { | ||
require.NoError(t, err, "wasn't expecting error with path %s and header %s in test case # %d", tc.Path, tc.IfNoneMatch, i+1) | ||
} | ||
} | ||
} | ||
|
||
func setHTTPHeaders(ifNoneMatch string) func(*middleware.Stack) error { | ||
return func(stack *middleware.Stack) error { | ||
return stack.Build.Add(middleware.BuildMiddlewareFunc("AddIfNoneMatchHeader", func( | ||
ctx context.Context, in middleware.BuildInput, next middleware.BuildHandler, | ||
) ( | ||
middleware.BuildOutput, middleware.Metadata, error, | ||
) { | ||
if req, ok := in.Request.(*smithyhttp.Request); ok { | ||
// Add the If-None-Match header | ||
req.Header.Set("If-None-Match", ifNoneMatch) | ||
} | ||
return next.HandleBuild(ctx, in) | ||
}), middleware.Before) | ||
} | ||
} | ||
func TestS3IfNoneMatch(t *testing.T) { | ||
|
||
ctx, logger, repo := setupTest(t) | ||
defer tearDownTest(repo) | ||
|
||
s3Endpoint := viper.GetString("s3_endpoint") | ||
s3Client := createS3Client(s3Endpoint, t) | ||
|
||
type TestCase struct { | ||
Path string | ||
Content string | ||
IfNoneMatch string | ||
ExpectError bool | ||
} | ||
|
||
testCases := []TestCase{ | ||
{Path: "main/object1", Content: "data", IfNoneMatch: "", ExpectError: false}, | ||
{Path: "main/object1", Content: "data", IfNoneMatch: "*", ExpectError: true}, | ||
{Path: "main/object2", Content: "data", IfNoneMatch: "*", ExpectError: false}, | ||
{Path: "main/object2", Content: "data", IfNoneMatch: "", ExpectError: false}, | ||
{Path: "main/object2", Content: "data", IfNoneMatch: "*", ExpectError: true}, | ||
{Path: "main/object3", Content: "data", IfNoneMatch: "unsupported string", ExpectError: true}, | ||
} | ||
for i, tc := range testCases { | ||
input := &s3.PutObjectInput{ | ||
Bucket: aws.String(repo), | ||
Key: aws.String(tc.Path), | ||
Body: strings.NewReader(tc.Content), | ||
} | ||
logger.Info("Sending PutObject request for Path: %s with If-None-Match: %s\n", tc.Path, tc.IfNoneMatch) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need in log message here, especially not at We try to avoid it as much as possible because in the context of the CI, all tests run together and there are TONS of logs. |
||
_, err := s3Client.PutObject(ctx, input, s3.WithAPIOptions(setHTTPHeaders(tc.IfNoneMatch))) | ||
|
||
if tc.ExpectError { | ||
require.Error(t, err, "was expecting an error with path %s and header %s in test case # %d", tc.Path, tc.IfNoneMatch, i+1) | ||
} else { | ||
require.NoError(t, err, "wasn't expecting error with path %s and header %s in test case # %d", tc.Path, tc.IfNoneMatch, i+1) | ||
} | ||
} | ||
} | ||
|
||
func verifyObjectInfo(t *testing.T, got minio.ObjectInfo, expectedSize int) { | ||
if got.Err != nil { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ import ( | |
) | ||
|
||
const ( | ||
IfNoneMatchHeader = "If-None-Match" | ||
CopySourceHeader = "x-amz-copy-source" | ||
CopySourceRangeHeader = "x-amz-copy-source-range" | ||
QueryParamUploadID = "uploadId" | ||
|
@@ -30,7 +31,6 @@ type PutObject struct{} | |
|
||
func (controller *PutObject) RequiredPermissions(req *http.Request, repoID, _, destPath string) (permissions.Node, error) { | ||
copySource := req.Header.Get(CopySourceHeader) | ||
|
||
if len(copySource) == 0 { | ||
return permissions.Node{ | ||
Permission: permissions.Permission{ | ||
|
@@ -298,6 +298,21 @@ func handlePut(w http.ResponseWriter, req *http.Request, o *PathOperation) { | |
o.Incr("put_object", o.Principal, o.Repository.Name, o.Reference) | ||
storageClass := StorageClassFromHeader(req.Header) | ||
opts := block.PutOpts{StorageClass: storageClass} | ||
// before uploading object, check whether if-none-match header is added, | ||
// in order to not overwrite object | ||
allowOverwrite, err := o.checkIfAbsent(req) | ||
if err != nil { | ||
_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrNotImplemented)) | ||
return | ||
} | ||
if !allowOverwrite { | ||
_, err := o.Catalog.GetEntry(req.Context(), o.Repository.Name, o.Reference, o.Path, catalog.GetEntryParams{}) | ||
if err == nil { | ||
// In case object exists in catalog, no error returns | ||
_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrPreconditionFailed)) | ||
return | ||
} | ||
} | ||
address := o.PathProvider.NewPath() | ||
blob, err := upload.WriteBlob(req.Context(), o.BlockStore, o.Repository.StorageNamespace, address, req.Body, req.ContentLength, opts) | ||
if err != nil { | ||
|
@@ -309,7 +324,11 @@ func handlePut(w http.ResponseWriter, req *http.Request, o *PathOperation) { | |
// write metadata | ||
metadata := amzMetaAsMetadata(req) | ||
contentType := req.Header.Get("Content-Type") | ||
err = o.finishUpload(req, &blob.CreationDate, blob.Checksum, blob.PhysicalAddress, blob.Size, true, metadata, contentType) | ||
err = o.finishUpload(req, &blob.CreationDate, blob.Checksum, blob.PhysicalAddress, blob.Size, true, metadata, contentType, allowOverwrite) | ||
if errors.Is(err, graveler.ErrPreconditionFailed) { | ||
_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrPreconditionFailed)) | ||
return | ||
} | ||
if errors.Is(err, graveler.ErrWriteToProtectedBranch) { | ||
_ = o.EncodeError(w, req, err, gatewayErrors.Codes.ToAPIErr(gatewayErrors.ErrWriteToProtectedBranch)) | ||
return | ||
|
@@ -325,3 +344,14 @@ func handlePut(w http.ResponseWriter, req *http.Request, o *PathOperation) { | |
o.SetHeader(w, "ETag", httputil.ETag(blob.Checksum)) | ||
w.WriteHeader(http.StatusOK) | ||
} | ||
|
||
func (o *PathOperation) checkIfAbsent(req *http.Request) (bool, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find this function confusing because:
Now that is confusing because it's a "lie" - it only's only partially checking ifAbsent as optimization, since the real check performed later and also does input validation. I would prefer something much more explicit like a function that extracts the header and validates it, then inline check if object exist: // checkIfAbsent sets allowOverwrite and validates the header value if set
allowOverwrite, err := o.checkIfAbsent(req)
if err != nil {
// ...
}
if !allowOverwrite {
// first check if object exist as optimization to save resources
_, err := o.Catalog.GetEntry(req.Context(), o.Repository.Name, o.Reference, o.Path, catalog.GetEntryParams{})
// hadle if err != nil ...
} |
||
headerValue := req.Header.Get(IfNoneMatchHeader) | ||
if headerValue == "" { | ||
return true, nil | ||
} | ||
if headerValue == "*" { | ||
return false, nil | ||
} | ||
return false, gatewayErrors.ErrNotImplemented | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the reason here to override the global
svc
and pass here a custom clientsvc *s3.Client
?(Even if required please don't use the same name as the global
svc
since it's confusing)