Skip to content

Commit 2126481

Browse files
authored
Merge pull request #36146 from hashicorp/backport/b/s3-object-lock-file/firmly-curious-yak
Backport of s3: fix S3 Object Lock header issue for lock file writes into v1.10
2 parents bbf1784 + b9a55d2 commit 2126481

File tree

4 files changed

+344
-2
lines changed

4 files changed

+344
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ BUG FIXES:
55
- config: `templatefile` would panic if given and entirely unknown map of variables [GH-36118]
66
- config: `templatefile` would panic if the variables map contains marked values [GH-36127]
77
- config: Remove constraint that an expanded resource block must only be used in conjunction with imports using `for_each` [GH-36119]
8+
- backend/s3: Lock files could not be written to buckets with object locking enabled [GH-36120]
89

910
## 1.10.0 (November 27, 2024)
1011

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

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,6 +2038,76 @@ func TestBackendLockedWithFile(t *testing.T) {
20382038
backend.TestBackendStateForceUnlock(t, b1, b2)
20392039
}
20402040

2041+
func TestBackendLockedWithFile_ObjectLock_Compliance(t *testing.T) {
2042+
testACC(t)
2043+
objectLockPreCheck(t)
2044+
2045+
ctx := context.TODO()
2046+
2047+
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
2048+
keyName := "test/state"
2049+
2050+
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
2051+
"bucket": bucketName,
2052+
"key": keyName,
2053+
"encrypt": true,
2054+
"use_lockfile": true,
2055+
"region": "us-west-2",
2056+
})).(*Backend)
2057+
2058+
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
2059+
"bucket": bucketName,
2060+
"key": keyName,
2061+
"encrypt": true,
2062+
"use_lockfile": true,
2063+
"region": "us-west-2",
2064+
})).(*Backend)
2065+
2066+
createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region,
2067+
s3BucketWithVersioning,
2068+
s3BucketWithObjectLock(s3types.ObjectLockRetentionModeCompliance),
2069+
)
2070+
defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
2071+
2072+
backend.TestBackendStateLocks(t, b1, b2)
2073+
backend.TestBackendStateForceUnlock(t, b1, b2)
2074+
}
2075+
2076+
func TestBackendLockedWithFile_ObjectLock_Governance(t *testing.T) {
2077+
testACC(t)
2078+
objectLockPreCheck(t)
2079+
2080+
ctx := context.TODO()
2081+
2082+
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
2083+
keyName := "test/state"
2084+
2085+
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
2086+
"bucket": bucketName,
2087+
"key": keyName,
2088+
"encrypt": true,
2089+
"use_lockfile": true,
2090+
"region": "us-west-2",
2091+
})).(*Backend)
2092+
2093+
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
2094+
"bucket": bucketName,
2095+
"key": keyName,
2096+
"encrypt": true,
2097+
"use_lockfile": true,
2098+
"region": "us-west-2",
2099+
})).(*Backend)
2100+
2101+
createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region,
2102+
s3BucketWithVersioning,
2103+
s3BucketWithObjectLock(s3types.ObjectLockRetentionModeGovernance),
2104+
)
2105+
defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
2106+
2107+
backend.TestBackendStateLocks(t, b1, b2)
2108+
backend.TestBackendStateForceUnlock(t, b1, b2)
2109+
}
2110+
20412111
func TestBackendLockedWithFileAndDynamoDB(t *testing.T) {
20422112
testACC(t)
20432113

@@ -2158,6 +2228,116 @@ func TestBackend_LockFileCleanupOnDynamoDBLock(t *testing.T) {
21582228
}
21592229
}
21602230

2231+
func TestBackend_LockFileCleanupOnDynamoDBLock_ObjectLock_Compliance(t *testing.T) {
2232+
testACC(t)
2233+
objectLockPreCheck(t)
2234+
2235+
ctx := context.TODO()
2236+
2237+
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
2238+
keyName := "test/state"
2239+
2240+
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
2241+
"bucket": bucketName,
2242+
"key": keyName,
2243+
"encrypt": true,
2244+
"use_lockfile": false, // Only use DynamoDB
2245+
"dynamodb_table": bucketName,
2246+
"region": "us-west-2",
2247+
})).(*Backend)
2248+
2249+
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
2250+
"bucket": bucketName,
2251+
"key": keyName,
2252+
"encrypt": true,
2253+
"use_lockfile": true, // Use both DynamoDB and lockfile
2254+
"dynamodb_table": bucketName,
2255+
"region": "us-west-2",
2256+
})).(*Backend)
2257+
2258+
createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region,
2259+
s3BucketWithVersioning,
2260+
s3BucketWithObjectLock(s3types.ObjectLockRetentionModeCompliance),
2261+
)
2262+
defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
2263+
createDynamoDBTable(ctx, t, b1.dynClient, bucketName)
2264+
defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName)
2265+
2266+
backend.TestBackendStateLocks(t, b1, b2)
2267+
2268+
// Attempt to retrieve the lock file from S3.
2269+
_, err := b1.s3Client.GetObject(ctx, &s3.GetObjectInput{
2270+
Bucket: aws.String(b1.bucketName),
2271+
Key: aws.String(b1.keyName + ".tflock"),
2272+
})
2273+
// We expect an error here, indicating that the lock file does not exist.
2274+
// The absence of the lock file is expected, as it should have been
2275+
// cleaned up following a failed lock acquisition due to `b1` already
2276+
// acquiring a DynamoDB lock.
2277+
if err != nil {
2278+
if !IsA[*s3types.NoSuchKey](err) {
2279+
t.Fatalf("unexpected error: %s", err)
2280+
}
2281+
} else {
2282+
t.Fatalf("expected error, got none")
2283+
}
2284+
}
2285+
2286+
func TestBackend_LockFileCleanupOnDynamoDBLock_ObjectLock_Governance(t *testing.T) {
2287+
testACC(t)
2288+
objectLockPreCheck(t)
2289+
2290+
ctx := context.TODO()
2291+
2292+
bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
2293+
keyName := "test/state"
2294+
2295+
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
2296+
"bucket": bucketName,
2297+
"key": keyName,
2298+
"encrypt": true,
2299+
"use_lockfile": false, // Only use DynamoDB
2300+
"dynamodb_table": bucketName,
2301+
"region": "us-west-2",
2302+
})).(*Backend)
2303+
2304+
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
2305+
"bucket": bucketName,
2306+
"key": keyName,
2307+
"encrypt": true,
2308+
"use_lockfile": true, // Use both DynamoDB and lockfile
2309+
"dynamodb_table": bucketName,
2310+
"region": "us-west-2",
2311+
})).(*Backend)
2312+
2313+
createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region,
2314+
s3BucketWithVersioning,
2315+
s3BucketWithObjectLock(s3types.ObjectLockRetentionModeGovernance),
2316+
)
2317+
defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
2318+
createDynamoDBTable(ctx, t, b1.dynClient, bucketName)
2319+
defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName)
2320+
2321+
backend.TestBackendStateLocks(t, b1, b2)
2322+
2323+
// Attempt to retrieve the lock file from S3.
2324+
_, err := b1.s3Client.GetObject(ctx, &s3.GetObjectInput{
2325+
Bucket: aws.String(b1.bucketName),
2326+
Key: aws.String(b1.keyName + ".tflock"),
2327+
})
2328+
// We expect an error here, indicating that the lock file does not exist.
2329+
// The absence of the lock file is expected, as it should have been
2330+
// cleaned up following a failed lock acquisition due to `b1` already
2331+
// acquiring a DynamoDB lock.
2332+
if err != nil {
2333+
if !IsA[*s3types.NoSuchKey](err) {
2334+
t.Fatalf("unexpected error: %s", err)
2335+
}
2336+
} else {
2337+
t.Fatalf("expected error, got none")
2338+
}
2339+
}
2340+
21612341
func TestBackend_LockDeletedOutOfBand(t *testing.T) {
21622342
testACC(t)
21632343

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,9 @@ func (c *RemoteClient) lockWithFile(ctx context.Context, info *statemgr.LockInfo
358358
Key: aws.String(c.lockFilePath),
359359
IfNoneMatch: aws.String("*"),
360360
}
361+
if !c.skipS3Checksum {
362+
input.ChecksumAlgorithm = s3types.ChecksumAlgorithmSha256
363+
}
361364

362365
if c.serverSideEncryption {
363366
if c.kmsKeyID != "" {
@@ -378,7 +381,8 @@ func (c *RemoteClient) lockWithFile(ctx context.Context, info *statemgr.LockInfo
378381

379382
log.Debug("Uploading lock file")
380383

381-
_, err = c.s3Client.PutObject(ctx, input)
384+
uploader := manager.NewUploader(c.s3Client)
385+
_, err = uploader.Upload(ctx, input)
382386
if err != nil {
383387
// Attempt to retrieve lock info from the file, and merge errors if it fails.
384388
lockInfo, infoErr := c.getLockInfoWithFile(ctx)

0 commit comments

Comments
 (0)