mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-19 12:34:29 +02:00
ObjectLock: enabling Object Lock on a bucket implicitly enables versioning, so PUT objects land at .versions/v_<id>, not at the bare key. The test was calling backdateMtime (bare path) and failing in the helper with "filer: no entry is found". Switch to backdateVersionedMtime with the versionId returned by PutObject. ExpirationDate: ScanAtDate dispatch path isn't wired to the run-shard shell command yet — the bootstrap walker explicitly skips actions in ModeScanAtDate (walker.go:141 says "SCAN_AT_DATE runs its own date- triggered bootstrap" but no such bootstrap exists in the scheduler or shell). Skip with a t.Skip + explanation so the test activates the moment the date-triggered path lands.
120 lines
4.8 KiB
Go
120 lines
4.8 KiB
Go
// Object Lock + lifecycle integration scenario.
|
|
package lifecycle
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"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/stretchr/testify/require"
|
|
)
|
|
|
|
// TestLifecycleSkipsObjectLockedObjects: an object under Object Lock
|
|
// retention must NOT be expired by the lifecycle worker. The handler's
|
|
// enforceObjectLockProtections check (s3api_internal_lifecycle.go:47)
|
|
// returns an error which the dispatcher classifies as
|
|
// SKIPPED_OBJECT_LOCK. Pinning the integration end-to-end so a
|
|
// regression in either the lock state lookup or the dispatcher's
|
|
// outcome handling is caught.
|
|
//
|
|
// Companion positive control: a second object in the same bucket
|
|
// without retention is expired by the same sweep — proves the rule
|
|
// is firing AND that the lock check is selective, not a blanket skip.
|
|
func TestLifecycleSkipsObjectLockedObjects(t *testing.T) {
|
|
c := s3Client(t)
|
|
fc, fcClose := filerClient(t)
|
|
defer fcClose()
|
|
|
|
bucket := uniqueBucket("objlock")
|
|
// Object Lock requires the bucket to be created with the flag set;
|
|
// it can't be enabled retroactively on an existing bucket. Use the
|
|
// raw API rather than mustCreateBucket here so we can pass the
|
|
// ObjectLockEnabledForBucket option.
|
|
_, err := c.CreateBucket(context.Background(), &s3.CreateBucketInput{
|
|
Bucket: aws.String(bucket),
|
|
ObjectLockEnabledForBucket: aws.Bool(true),
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
// Best-effort cleanup mirrors mustCreateBucket; locked objects
|
|
// may resist deletion but the test process continues regardless.
|
|
c.DeleteBucketLifecycle(context.Background(), &s3.DeleteBucketLifecycleInput{Bucket: aws.String(bucket)})
|
|
listOut, _ := c.ListObjectVersions(context.Background(), &s3.ListObjectVersionsInput{Bucket: aws.String(bucket)})
|
|
if listOut != nil {
|
|
for _, v := range listOut.Versions {
|
|
// Bypass governance retention to free the bucket for delete.
|
|
c.DeleteObject(context.Background(), &s3.DeleteObjectInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: v.Key,
|
|
VersionId: v.VersionId,
|
|
BypassGovernanceRetention: aws.Bool(true),
|
|
})
|
|
}
|
|
for _, m := range listOut.DeleteMarkers {
|
|
c.DeleteObject(context.Background(), &s3.DeleteObjectInput{
|
|
Bucket: aws.String(bucket), Key: m.Key, VersionId: m.VersionId,
|
|
})
|
|
}
|
|
}
|
|
c.DeleteBucket(context.Background(), &s3.DeleteBucketInput{Bucket: aws.String(bucket)})
|
|
})
|
|
|
|
putExpirationLifecycle(t, c, bucket, "lock/", 1)
|
|
|
|
const lockedKey = "lock/protected.txt"
|
|
const freeKey = "lock/free.txt"
|
|
|
|
// Locked object: PUT with GOVERNANCE retention until 1h from now.
|
|
lockedPut, err := c.PutObject(context.Background(), &s3.PutObjectInput{
|
|
Bucket: aws.String(bucket),
|
|
Key: aws.String(lockedKey),
|
|
Body: strings.NewReader("locked"),
|
|
ObjectLockMode: types.ObjectLockModeGovernance,
|
|
ObjectLockRetainUntilDate: aws.Time(time.Now().Add(time.Hour)),
|
|
})
|
|
require.NoError(t, err, "PUT with retention must succeed on a lock-enabled bucket")
|
|
require.NotEmpty(t, aws.ToString(lockedPut.VersionId))
|
|
|
|
// Free object: PUT without retention. Object Lock requires
|
|
// versioning, so the bucket is implicitly versioned and every PUT
|
|
// produces a versionId. Capture both for the version-aware
|
|
// backdate path.
|
|
freePut, err := c.PutObject(context.Background(), &s3.PutObjectInput{
|
|
Bucket: aws.String(bucket), Key: aws.String(freeKey), Body: strings.NewReader("free"),
|
|
})
|
|
require.NoError(t, err)
|
|
freeVersionID := aws.ToString(freePut.VersionId)
|
|
require.NotEmpty(t, freeVersionID)
|
|
|
|
// Backdate both so they would otherwise both expire under the
|
|
// 1-day rule. The lock check is what distinguishes them.
|
|
// Versioning-enabled buckets store entries under .versions/v_<id>,
|
|
// not at the bare key path, so use backdateVersionedMtime.
|
|
backdateVersionedMtime(t, fc, bucket, lockedKey, aws.ToString(lockedPut.VersionId), 30)
|
|
backdateVersionedMtime(t, fc, bucket, freeKey, freeVersionID, 30)
|
|
|
|
out := runLifecycleShard(t)
|
|
t.Logf("shell output:\n%s", out)
|
|
|
|
// Free object expires (positive control).
|
|
require.Eventuallyf(t, func() bool {
|
|
_, err := c.HeadObject(context.Background(), &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucket), Key: aws.String(freeKey),
|
|
})
|
|
return isS3NotFound(err)
|
|
}, 30*time.Second, 500*time.Millisecond, "unlocked object must expire (control)")
|
|
|
|
// Locked object survives — Object Lock retention overrides the
|
|
// lifecycle rule. HEAD without versionId returns the still-current
|
|
// object data, not a 404.
|
|
_, err = c.HeadObject(context.Background(), &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucket), Key: aws.String(lockedKey),
|
|
})
|
|
require.NoError(t, err, "object under GOVERNANCE retention must survive the sweep")
|
|
}
|
|
|