Skip to content

Commit

Permalink
service/s3: Add documentation for using unseekable body PutObject and…
Browse files Browse the repository at this point in the history
… UploadPart (#1176)

Adds additional documentation to the PutObject and UploadPart operations
on how to to use unseekable values to be uploaded to S3.

Updates the v4 package with a new helper to swap out the compute payload
hash middleware for the unsigned payload middleware. This allows API
operations to be updated to not compute the payload hash, and
use UNSIGNED-PAYLOAD instead.
  • Loading branch information
jasdel authored Mar 18, 2021
1 parent 75b83bd commit 6724d37
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .changes/next-release/sdk-feature-1615946574682127000.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"ID": "sdk-feature-1615946574682127000",
"SchemaVersion": 1,
"Module": "/",
"Type": "feature",
"Description": "Add helper to V4 signer package to swap compute payload hash middleware with unsigned payload middleware",
"MinVersion": "",
"AffectedModules": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"ID": "service.s3-bugfix-1615945295096775000",
"SchemaVersion": 1,
"Module": "service/s3",
"Type": "bugfix",
"Description": "Adds documentation to the PutObject and UploadPart operations Body member how to upload unseekable objects to an Amazon S3 Bucket.",
"MinVersion": "",
"AffectedModules": null
}
10 changes: 10 additions & 0 deletions aws/signer/v4/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,16 @@ func (m *computePayloadSHA256) HandleBuild(
return next.HandleBuild(ctx, in)
}

// SwapComputePayloadSHA256ForUnsignedPayloadMiddleware replaces the
// ComputePayloadSHA256 middleware with the UnsignedPayload middleware.
//
// Use this to disable computing the Payload SHA256 checksum and instead use
// UNSIGNED-PAYLOAD for the SHA256 value.
func SwapComputePayloadSHA256ForUnsignedPayloadMiddleware(stack *middleware.Stack) error {
_, err := stack.Build.Swap(computePayloadHashMiddlewareID, &unsignedPayload{})
return err
}

// contentSHA256Header sets the X-Amz-Content-Sha256 header value to
// the Payload hash stored in the context.
type contentSHA256Header struct{}
Expand Down
101 changes: 101 additions & 0 deletions aws/signer/v4/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/aws/smithy-go/logging"
"github.com/aws/smithy-go/middleware"
smithyhttp "github.com/aws/smithy-go/transport/http"
"github.com/google/go-cmp/cmp"
)

func TestComputePayloadHashMiddleware(t *testing.T) {
Expand Down Expand Up @@ -205,6 +206,106 @@ func TestSignHTTPRequestMiddleware(t *testing.T) {
}
}

func TestSwapComputePayloadSHA256ForUnsignedPayloadMiddleware(t *testing.T) {
cases := map[string]struct {
InitStep func(*middleware.Stack) error
Mutator func(*middleware.Stack) error
ExpectErr string
ExpectIDs []string
}{
"swap in place": {
InitStep: func(s *middleware.Stack) (err error) {
err = s.Build.Add(middleware.BuildMiddlewareFunc("before", nil), middleware.After)
if err != nil {
return err
}
err = AddComputePayloadSHA256Middleware(s)
if err != nil {
return err
}
err = s.Build.Add(middleware.BuildMiddlewareFunc("after", nil), middleware.After)
if err != nil {
return err
}
return nil
},
Mutator: SwapComputePayloadSHA256ForUnsignedPayloadMiddleware,
ExpectIDs: []string{
"before",
computePayloadHashMiddlewareID,
"after",
},
},

"already unsigned payload exists": {
InitStep: func(s *middleware.Stack) (err error) {
err = s.Build.Add(middleware.BuildMiddlewareFunc("before", nil), middleware.After)
if err != nil {
return err
}
err = AddUnsignedPayloadMiddleware(s)
if err != nil {
return err
}
err = s.Build.Add(middleware.BuildMiddlewareFunc("after", nil), middleware.After)
if err != nil {
return err
}
return nil
},
Mutator: SwapComputePayloadSHA256ForUnsignedPayloadMiddleware,
ExpectIDs: []string{
"before",
computePayloadHashMiddlewareID,
"after",
},
},

"no compute payload": {
InitStep: func(s *middleware.Stack) (err error) {
err = s.Build.Add(middleware.BuildMiddlewareFunc("before", nil), middleware.After)
if err != nil {
return err
}
err = s.Build.Add(middleware.BuildMiddlewareFunc("after", nil), middleware.After)
if err != nil {
return err
}
return nil
},
Mutator: SwapComputePayloadSHA256ForUnsignedPayloadMiddleware,
ExpectErr: "not found, " + computePayloadHashMiddlewareID,
},
}

for name, c := range cases {
t.Run(name, func(t *testing.T) {
stack := middleware.NewStack(t.Name(), smithyhttp.NewStackRequest)
if err := c.InitStep(stack); err != nil {
t.Fatalf("expect no error, got %v", err)
}

err := c.Mutator(stack)
if len(c.ExpectErr) != 0 {
if err == nil {
t.Fatalf("expect error, got none")
}
if e, a := c.ExpectErr, err.Error(); !strings.Contains(a, e) {
t.Fatalf("expect error to contain %v, got %v", e, a)
}
return
}
if err != nil {
t.Fatalf("expect no error, got %v", err)
}

if diff := cmp.Diff(c.ExpectIDs, stack.Build.List()); len(diff) != 0 {
t.Errorf("expect match\n%v", diff)
}
})
}
}

type nonSeeker struct{}

func (nonSeeker) Read(p []byte) (n int, err error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package software.amazon.smithy.aws.go.codegen.customization;

import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Logger;
import software.amazon.smithy.codegen.core.CodegenException;
import software.amazon.smithy.go.codegen.GoSettings;
import software.amazon.smithy.go.codegen.integration.GoIntegration;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.StructureShape;
import software.amazon.smithy.model.traits.DocumentationTrait;
import software.amazon.smithy.utils.MapUtils;
import software.amazon.smithy.utils.Pair;
import software.amazon.smithy.utils.SetUtils;

public class S3AddPutObjectUnseekableBodyDoc implements GoIntegration {
private static final Logger LOGGER = Logger.getLogger(S3AddPutObjectUnseekableBodyDoc.class.getName());

private static final Map<ShapeId, Set<Pair<ShapeId, String>>> SERVICE_TO_SHAPE_MAP = MapUtils.of(
ShapeId.from("com.amazonaws.s3#AmazonS3"), SetUtils.of(
new Pair(ShapeId.from("com.amazonaws.s3#PutObjectRequest"), "Body"),
new Pair(ShapeId.from("com.amazonaws.s3#UploadPartRequest"), "Body")
)
);

@Override
public byte getOrder() {
// This integration should happen before other integrations that rely on the presence of this trait
return -60;
}

@Override
public Model preprocessModel(
Model model, GoSettings settings
) {
ShapeId serviceId = settings.getService();
if (!SERVICE_TO_SHAPE_MAP.containsKey(serviceId)) {
return model;
}

Set<Pair<ShapeId, String>> shapeIds = SERVICE_TO_SHAPE_MAP.get(serviceId);

Model.Builder builder = model.toBuilder();
for (Pair<ShapeId, String> pair : shapeIds) {
ShapeId shapeId = pair.getLeft();
String memberName = pair.getRight();
StructureShape parent = model.expectShape(shapeId, StructureShape.class);

Optional<MemberShape> memberOpt = parent.getMember(memberName);
if (!memberOpt.isPresent()) {
// Throw in case member is not present, bad things must of happened.
throw new CodegenException("expect to find " + memberName + " member in shape " + parent.getId());
}

MemberShape member = memberOpt.get();
Shape target = model.expectShape(member.getTarget());

Optional<DocumentationTrait> docTrait = member.getTrait(DocumentationTrait.class);
String currentDocs = "";
if (docTrait.isPresent()) {
currentDocs = docTrait.get().getValue();
}
if (currentDocs.length() != 0) {
currentDocs += "<br/><br/>";
}

final String finalCurrentDocs = currentDocs;
StructureShape.Builder parentBuilder = parent.toBuilder();
parentBuilder.removeMember(memberName);
parentBuilder.addMember(memberName, target.getId(), (memberBuilder) -> {
memberBuilder
.addTraits(member.getAllTraits().values())
.addTrait(new DocumentationTrait(finalCurrentDocs +
"For using values that are not seekable (io.Seeker) see, " +
"https://aws.github.io/aws-sdk-go-v2/docs/sdk-utilisties/s3/#unseekable-streaming-input"));
});


builder.addShape(parentBuilder.build());
}

return builder.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ software.amazon.smithy.aws.go.codegen.AwsHttpPresignURLClientGenerator
software.amazon.smithy.aws.go.codegen.ResolveClientConfig
software.amazon.smithy.aws.go.codegen.customization.S3GetBucketLocation
software.amazon.smithy.aws.go.codegen.RequestResponseLogging
software.amazon.smithy.aws.go.codegen.customization.S3AddPutObjectUnseekableBodyDoc
3 changes: 3 additions & 0 deletions service/s3/api_op_PutObject.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions service/s3/api_op_UploadPart.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 6724d37

Please sign in to comment.