Skip to content

Commit 57e2fc9

Browse files
authored
Merge pull request #34127 from hashicorp/s3/b-s3-checksum
backend/s3: Adds parameter `skip_s3_checksum` to skip checksum on upload
2 parents 752e5a1 + aab15a9 commit 57e2fc9

File tree

6 files changed

+148
-10
lines changed

6 files changed

+148
-10
lines changed

internal/backend/remote-state/s3/backend.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ type Backend struct {
4545
kmsKeyID string
4646
ddbTable string
4747
workspaceKeyPrefix string
48+
skipS3Checksum bool
4849
}
4950

5051
// ConfigSchema returns a description of the expected configuration
@@ -183,7 +184,7 @@ func (b *Backend) ConfigSchema() *configschema.Block {
183184
"skip_credentials_validation": {
184185
Type: cty.Bool,
185186
Optional: true,
186-
Description: "Skip the credentials validation via STS API.",
187+
Description: "Skip the credentials validation via STS API. Useful for testing and for AWS API implementations that do not have STS available.",
187188
},
188189
"skip_requesting_account_id": {
189190
Type: cty.Bool,
@@ -200,6 +201,11 @@ func (b *Backend) ConfigSchema() *configschema.Block {
200201
Optional: true,
201202
Description: "Skip static validation of region name.",
202203
},
204+
"skip_s3_checksum": {
205+
Type: cty.Bool,
206+
Optional: true,
207+
Description: "Do not include checksum when uploading S3 Objects. Useful for some S3-Compatible APIs.",
208+
},
203209
"sse_customer_key": {
204210
Type: cty.String,
205211
Optional: true,
@@ -903,6 +909,7 @@ func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics {
903909
b.serverSideEncryption = boolAttr(obj, "encrypt")
904910
b.kmsKeyID = stringAttr(obj, "kms_key_id")
905911
b.ddbTable = stringAttr(obj, "dynamodb_table")
912+
b.skipS3Checksum = boolAttr(obj, "skip_s3_checksum")
906913

907914
if _, ok := stringAttrOk(obj, "kms_key_id"); ok {
908915
if customerKey := os.Getenv("AWS_SSE_CUSTOMER_KEY"); customerKey != "" {

internal/backend/remote-state/s3/backend_state.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ func (b *Backend) remoteClient(name string) (*RemoteClient, error) {
151151
acl: b.acl,
152152
kmsKeyID: b.kmsKeyID,
153153
ddbTable: b.ddbTable,
154+
skipS3Checksum: b.skipS3Checksum,
154155
}
155156

156157
return client, nil

internal/backend/remote-state/s3/backend_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ func TestBackend_impl(t *testing.T) {
6161
var _ backend.Backend = new(Backend)
6262
}
6363

64+
func TestBackend_InternalValidate(t *testing.T) {
65+
b := New()
66+
67+
schema := b.ConfigSchema()
68+
if err := schema.InternalValidate(); err != nil {
69+
t.Fatalf("failed InternalValidate: %s", err)
70+
}
71+
}
72+
6473
func TestBackendConfig_original(t *testing.T) {
6574
testACC(t)
6675

internal/backend/remote-state/s3/client.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type RemoteClient struct {
4747
acl string
4848
kmsKeyID string
4949
ddbTable string
50+
skipS3Checksum bool
5051
}
5152

5253
var (
@@ -182,6 +183,10 @@ func (c *RemoteClient) get(ctx context.Context) (*remote.Payload, error) {
182183
}
183184

184185
func (c *RemoteClient) Put(data []byte) error {
186+
return c.put(data)
187+
}
188+
189+
func (c *RemoteClient) put(data []byte, optFns ...func(*s3.Options)) error {
185190
ctx := context.TODO()
186191
log := c.logger(operationClientPut)
187192

@@ -193,11 +198,13 @@ func (c *RemoteClient) Put(data []byte) error {
193198
sum := md5.Sum(data)
194199

195200
input := &s3.PutObjectInput{
196-
ContentType: aws.String(contentType),
197-
Body: bytes.NewReader(data),
198-
Bucket: aws.String(c.bucketName),
199-
Key: aws.String(c.path),
200-
ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256,
201+
ContentType: aws.String(contentType),
202+
Body: bytes.NewReader(data),
203+
Bucket: aws.String(c.bucketName),
204+
Key: aws.String(c.path),
205+
}
206+
if !c.skipS3Checksum {
207+
input.ChecksumAlgorithm = s3types.ChecksumAlgorithmSha256
201208
}
202209

203210
if c.serverSideEncryption {
@@ -219,16 +226,18 @@ func (c *RemoteClient) Put(data []byte) error {
219226

220227
log.Info("Uploading remote state")
221228

222-
uploader := manager.NewUploader(c.s3Client)
229+
uploader := manager.NewUploader(c.s3Client, func(u *manager.Uploader) {
230+
u.ClientOptions = optFns
231+
})
223232
_, err := uploader.Upload(ctx, input)
224233
if err != nil {
225-
return fmt.Errorf("failed to upload state: %s", err)
234+
return fmt.Errorf("failed to upload state: %w", err)
226235
}
227236

228237
if err := c.putMD5(ctx, sum[:]); err != nil {
229238
// if this errors out, we unfortunately have to error out altogether,
230239
// since the next Get will inevitably fail.
231-
return fmt.Errorf("failed to store state MD5: %s", err)
240+
return fmt.Errorf("failed to store state MD5: %w", err)
232241
}
233242

234243
return nil

internal/backend/remote-state/s3/client_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@ import (
77
"bytes"
88
"context"
99
"crypto/md5"
10+
"errors"
1011
"fmt"
1112
"io"
1213
"testing"
1314
"time"
1415

1516
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
17+
"github.com/aws/aws-sdk-go-v2/service/s3"
1618
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
19+
"github.com/aws/smithy-go/middleware"
20+
smithyhttp "github.com/aws/smithy-go/transport/http"
1721
"github.com/hashicorp/terraform/internal/backend"
1822
"github.com/hashicorp/terraform/internal/states/remote"
1923
"github.com/hashicorp/terraform/internal/states/statefile"
2024
"github.com/hashicorp/terraform/internal/states/statemgr"
25+
"golang.org/x/exp/maps"
2126
)
2227

2328
func TestRemoteClient_impl(t *testing.T) {
@@ -383,3 +388,106 @@ func (b neverEnding) Read(p []byte) (n int, err error) {
383388
}
384389
return len(p), nil
385390
}
391+
392+
func TestRemoteClientSkipS3Checksum(t *testing.T) {
393+
testACC(t)
394+
395+
ctx := context.TODO()
396+
397+
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
398+
keyName := "testState"
399+
400+
testcases := map[string]struct {
401+
config map[string]any
402+
expected string
403+
}{
404+
"default": {
405+
config: map[string]any{},
406+
expected: string(s3types.ChecksumAlgorithmSha256),
407+
},
408+
"true": {
409+
config: map[string]any{
410+
"skip_s3_checksum": true,
411+
},
412+
expected: "",
413+
},
414+
"false": {
415+
config: map[string]any{
416+
"skip_s3_checksum": false,
417+
},
418+
expected: string(s3types.ChecksumAlgorithmSha256),
419+
},
420+
}
421+
422+
for name, testcase := range testcases {
423+
t.Run(name, func(t *testing.T) {
424+
config := map[string]interface{}{
425+
"bucket": bucketName,
426+
"key": keyName,
427+
}
428+
maps.Copy(config, testcase.config)
429+
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)
430+
431+
createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)
432+
defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)
433+
434+
state, err := b.StateMgr(backend.DefaultStateName)
435+
if err != nil {
436+
t.Fatal(err)
437+
}
438+
439+
c := state.(*remote.State).Client
440+
client := c.(*RemoteClient)
441+
442+
s := statemgr.TestFullInitialState()
443+
sf := &statefile.File{State: s}
444+
var stateBuf bytes.Buffer
445+
if err := statefile.Write(sf, &stateBuf); err != nil {
446+
t.Fatal(err)
447+
}
448+
449+
var checksum string
450+
err = client.put(stateBuf.Bytes(), func(opts *s3.Options) {
451+
opts.APIOptions = append(opts.APIOptions,
452+
addRetrieveChecksumHeaderMiddleware(t, &checksum),
453+
addCancelRequestMiddleware(),
454+
)
455+
})
456+
if err == nil {
457+
t.Fatal("Expected an error, got none")
458+
} else if !errors.Is(err, errCancelOperation) {
459+
t.Fatalf("Unexpected error: %s", err)
460+
}
461+
462+
if a, e := checksum, testcase.expected; a != e {
463+
t.Fatalf("expected %q, got %q", e, a)
464+
}
465+
})
466+
}
467+
}
468+
469+
func addRetrieveChecksumHeaderMiddleware(t *testing.T, checksum *string) func(*middleware.Stack) error {
470+
return func(stack *middleware.Stack) error {
471+
return stack.Finalize.Add(
472+
retrieveChecksumHeaderMiddleware(t, checksum),
473+
middleware.After,
474+
)
475+
}
476+
}
477+
478+
func retrieveChecksumHeaderMiddleware(t *testing.T, checksum *string) middleware.FinalizeMiddleware {
479+
return middleware.FinalizeMiddlewareFunc(
480+
"Test: Retrieve Stuff",
481+
func(ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) {
482+
t.Helper()
483+
484+
request, ok := in.Request.(*smithyhttp.Request)
485+
if !ok {
486+
t.Fatalf("Expected *github.com/aws/smithy-go/transport/http.Request, got %s", fullTypeName(in.Request))
487+
}
488+
489+
*checksum = request.Header.Get("x-amz-sdk-checksum-algorithm")
490+
491+
return next.HandleFinalize(ctx, in)
492+
})
493+
}

website/docs/language/settings/backends/s3.mdx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,13 @@ The following configuration is optional:
173173
* `shared_credentials_file` - (Optional, **Deprecated**, use `shared_credentials_files` instead) Path to the AWS shared credentials file. Defaults to `~/.aws/credentials`.
174174
* `shared_credentials_files` - (Optional) List of paths to AWS shared credentials files. Defaults to `~/.aws/credentials`.
175175
* `skip_credentials_validation` - (Optional) Skip credentials validation via the STS API.
176+
Useful for testing and for AWS API implementations that do not have STS available.
176177
* `skip_region_validation` - (Optional) Skip validation of provided region name.
177-
* `skip_requesting_account_id` - (Optional) Whether to skip requesting the account ID. Useful for AWS API implementations that do not have the IAM, STS API, or metadata API.
178+
* `skip_requesting_account_id` - (Optional) Whether to skip requesting the account ID.
179+
Useful for AWS API implementations that do not have the IAM, STS API, or metadata API.
178180
* `skip_metadata_api_check` - (Optional) Skip usage of EC2 Metadata API.
181+
* `skip_s3_checksum` - (Optional) Do not include checksum when uploading S3 Objects.
182+
Useful for some S3-Compatible APIs.
179183
* `sts_endpoint` - (Optional, **Deprecated**) Custom endpoint URL for the AWS Security Token Service (STS) API.
180184
Use `endpoints.sts` instead.
181185
* `sts_region` - (Optional) AWS region for STS. If unset, AWS will use the same region for STS as other non-STS operations.

0 commit comments

Comments
 (0)