Skip to content

Commit

Permalink
s3: add mix of compliance and governance tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bschaatsbergen committed Nov 27, 2024
1 parent 7b73b16 commit 9db9647
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 7 deletions.
94 changes: 92 additions & 2 deletions internal/backend/remote-state/s3/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2038,7 +2038,7 @@ func TestBackendLockedWithFile(t *testing.T) {
backend.TestBackendStateForceUnlock(t, b1, b2)
}

func TestBackendLockedWithFile_ObjectLock(t *testing.T) {
func TestBackendLockedWithFile_ObjectLock_Compliance(t *testing.T) {
testACC(t)
objectLockPreCheck(t)

Expand Down Expand Up @@ -2073,6 +2073,41 @@ func TestBackendLockedWithFile_ObjectLock(t *testing.T) {
backend.TestBackendStateForceUnlock(t, b1, b2)
}

func TestBackendLockedWithFile_ObjectLock_Governance(t *testing.T) {
testACC(t)
objectLockPreCheck(t)

ctx := context.TODO()

bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
keyName := "test/state"

b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
"use_lockfile": true,
"region": "us-west-2",
})).(*Backend)

b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
"use_lockfile": true,
"region": "us-west-2",
})).(*Backend)

createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region,
s3BucketWithVersioning,
s3BucketWithObjectLock(s3types.ObjectLockRetentionModeGovernance),
)
defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)

backend.TestBackendStateLocks(t, b1, b2)
backend.TestBackendStateForceUnlock(t, b1, b2)
}

func TestBackendLockedWithFileAndDynamoDB(t *testing.T) {
testACC(t)

Expand Down Expand Up @@ -2193,7 +2228,7 @@ func TestBackend_LockFileCleanupOnDynamoDBLock(t *testing.T) {
}
}

func TestBackend_LockFileCleanupOnDynamoDBLock_ObjectLock(t *testing.T) {
func TestBackend_LockFileCleanupOnDynamoDBLock_ObjectLock_Compliance(t *testing.T) {
testACC(t)
objectLockPreCheck(t)

Expand Down Expand Up @@ -2248,6 +2283,61 @@ func TestBackend_LockFileCleanupOnDynamoDBLock_ObjectLock(t *testing.T) {
}
}

func TestBackend_LockFileCleanupOnDynamoDBLock_ObjectLock_Governance(t *testing.T) {
testACC(t)
objectLockPreCheck(t)

ctx := context.TODO()

bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
keyName := "test/state"

b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
"use_lockfile": false, // Only use DynamoDB
"dynamodb_table": bucketName,
"region": "us-west-2",
})).(*Backend)

b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
"use_lockfile": true, // Use both DynamoDB and lockfile
"dynamodb_table": bucketName,
"region": "us-west-2",
})).(*Backend)

createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region,
s3BucketWithVersioning,
s3BucketWithObjectLock(s3types.ObjectLockRetentionModeGovernance),
)
defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
createDynamoDBTable(ctx, t, b1.dynClient, bucketName)
defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName)

backend.TestBackendStateLocks(t, b1, b2)

// Attempt to retrieve the lock file from S3.
_, err := b1.s3Client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(b1.bucketName),
Key: aws.String(b1.keyName + ".tflock"),
})
// We expect an error here, indicating that the lock file does not exist.
// The absence of the lock file is expected, as it should have been
// cleaned up following a failed lock acquisition due to `b1` already
// acquiring a DynamoDB lock.
if err != nil {
if !IsA[*s3types.NoSuchKey](err) {
t.Fatalf("unexpected error: %s", err)
}
} else {
t.Fatalf("expected error, got none")
}
}

func TestBackend_LockDeletedOutOfBand(t *testing.T) {
testACC(t)

Expand Down
6 changes: 3 additions & 3 deletions internal/backend/remote-state/s3/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,14 +346,14 @@ func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) {
// exist, the operation will succeed, acquiring the lock. If the lock file already exists, the operation
// will fail due to a conditional write, indicating that the lock is already held by another Terraform client.
func (c *RemoteClient) lockWithFile(ctx context.Context, info *statemgr.LockInfo, log hclog.Logger) error {
data, err := json.Marshal(info)
lockFileJson, err := json.Marshal(info)
if err != nil {
return err
}

input := &s3.PutObjectInput{
ContentType: aws.String("application/json"),
Body: bytes.NewReader(data),
Body: bytes.NewReader(lockFileJson),
Bucket: aws.String(c.bucketName),
Key: aws.String(c.lockFilePath),
IfNoneMatch: aws.String("*"),
Expand Down Expand Up @@ -381,7 +381,7 @@ func (c *RemoteClient) lockWithFile(ctx context.Context, info *statemgr.LockInfo

log.Debug("Uploading lock file")

uploader := manager.NewUploader(c.s3Client, func(u *manager.Uploader) {})
uploader := manager.NewUploader(c.s3Client)
_, err = uploader.Upload(ctx, input)
if err != nil {
// Attempt to retrieve lock info from the file, and merge errors if it fails.
Expand Down
121 changes: 119 additions & 2 deletions internal/backend/remote-state/s3/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,83 @@ func TestForceUnlock(t *testing.T) {
}
}

func TestForceUnlock_withLockfile(t *testing.T) {
testACC(t)

ctx := context.TODO()

bucketName := fmt.Sprintf("terraform-remote-s3-test-force-%x", time.Now().Unix())
keyName := "testState"

b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
"use_lockfile": true,
})).(*Backend)

b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"encrypt": true,
"use_lockfile": true,
})).(*Backend)

createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)

// first test with default
s1, err := b1.StateMgr(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}

info := statemgr.NewLockInfo()
info.Operation = "test"
info.Who = "clientA"

lockID, err := s1.Lock(info)
if err != nil {
t.Fatal("unable to get initial lock:", err)
}

// s1 is now locked, get the same state through s2 and unlock it
s2, err := b2.StateMgr(backend.DefaultStateName)
if err != nil {
t.Fatal("failed to get default state to force unlock:", err)
}

if err := s2.Unlock(lockID); err != nil {
t.Fatal("failed to force-unlock default state")
}

// now try the same thing with a named state
// first test with default
s1, err = b1.StateMgr("test")
if err != nil {
t.Fatal(err)
}

info = statemgr.NewLockInfo()
info.Operation = "test"
info.Who = "clientA"

lockID, err = s1.Lock(info)
if err != nil {
t.Fatal("unable to get initial lock:", err)
}

// s1 is now locked, get the same state through s2 and unlock it
s2, err = b2.StateMgr("test")
if err != nil {
t.Fatal("failed to get named state to force unlock:", err)
}

if err = s2.Unlock(lockID); err != nil {
t.Fatal("failed to force-unlock named state")
}
}

func TestRemoteClient_clientMD5(t *testing.T) {
testACC(t)

Expand Down Expand Up @@ -343,7 +420,7 @@ func TestRemoteClient_stateChecksum(t *testing.T) {
}
}

func TestRemoteClientPutLargeUploadWithObjectLock(t *testing.T) {
func TestRemoteClientPutLargeUploadWithObjectLock_Compliance(t *testing.T) {
testACC(t)
objectLockPreCheck(t)

Expand Down Expand Up @@ -382,7 +459,7 @@ func TestRemoteClientPutLargeUploadWithObjectLock(t *testing.T) {
}
}

func TestRemoteClientLockFileWithObjectLock(t *testing.T) {
func TestRemoteClientLockFileWithObjectLock_Compliance(t *testing.T) {
testACC(t)
objectLockPreCheck(t)

Expand Down Expand Up @@ -422,6 +499,46 @@ func TestRemoteClientLockFileWithObjectLock(t *testing.T) {
}
}

func TestRemoteClientLockFileWithObjectLock_Governance(t *testing.T) {
testACC(t)
objectLockPreCheck(t)

ctx := context.TODO()

bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
keyName := "testState"

b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
"bucket": bucketName,
"key": keyName,
"use_lockfile": true,
})).(*Backend)

createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region,
s3BucketWithVersioning,
s3BucketWithObjectLock(s3types.ObjectLockRetentionModeGovernance),
)
defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)

s1, err := b.StateMgr(backend.DefaultStateName)
if err != nil {
t.Fatal(err)
}
client := s1.(*remote.State).Client

var state bytes.Buffer
dataW := io.LimitReader(neverEnding('x'), manager.DefaultUploadPartSize)
_, err = state.ReadFrom(dataW)
if err != nil {
t.Fatalf("writing dummy data: %s", err)
}

err = client.Put(state.Bytes())
if err != nil {
t.Fatalf("putting data: %s", err)
}
}

type neverEnding byte

func (b neverEnding) Read(p []byte) (n int, err error) {
Expand Down

0 comments on commit 9db9647

Please sign in to comment.