Files
upstream-seaweedfs/weed/filer/filer_conf_test.go
upstream-mirror 89f5be005d
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 03:23:42 +05:00

592 lines
22 KiB
Go

package filer
import (
"bytes"
"context"
"errors"
"fmt"
"reflect"
"sync"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"
)
func TestFilerConf(t *testing.T) {
fc := NewFilerConf()
conf := &filer_pb.FilerConf{Locations: []*filer_pb.FilerConf_PathConf{
{
LocationPrefix: "/buckets/abc",
Collection: "abc",
},
{
LocationPrefix: "/buckets/abcd",
Collection: "abcd",
},
{
LocationPrefix: "/buckets/",
Replication: "001",
},
{
LocationPrefix: "/buckets",
ReadOnly: false,
},
{
LocationPrefix: "/buckets/xxx",
ReadOnly: true,
},
{
LocationPrefix: "/buckets/xxx/yyy",
ReadOnly: false,
},
}}
fc.doLoadConf(conf)
assert.Equal(t, "abc", fc.MatchStorageRule("/buckets/abc/jasdf").Collection)
assert.Equal(t, "abcd", fc.MatchStorageRule("/buckets/abcd/jasdf").Collection)
assert.Equal(t, "001", fc.MatchStorageRule("/buckets/abc/jasdf").Replication)
assert.Equal(t, true, fc.MatchStorageRule("/buckets/xxx/yyy/zzz").ReadOnly)
assert.Equal(t, false, fc.MatchStorageRule("/buckets/other").ReadOnly)
}
func TestWormInheritance(t *testing.T) {
fc := NewFilerConf()
fc.doLoadConf(&filer_pb.FilerConf{
Version: FilerConfVersion,
Locations: []*filer_pb.FilerConf_PathConf{
{LocationPrefix: "/buckets/b/", Worm: proto.Bool(true), Ttl: "7d"},
{LocationPrefix: "/buckets/b/quiet/", Collection: "quiet"},
{LocationPrefix: "/buckets/b/scratch/", Worm: proto.Bool(false)},
{LocationPrefix: "/buckets/b/scratch/keep/", Worm: proto.Bool(true)},
},
})
// a rule that says nothing about worm keeps inheriting it, along with the ttl
rule := fc.MatchStorageRule("/buckets/b/quiet/x")
assert.True(t, rule.GetWorm())
assert.Equal(t, "7d", rule.Ttl)
// an explicit false turns it off, and a deeper rule turns it back on
assert.False(t, fc.MatchStorageRule("/buckets/b/scratch/x").GetWorm())
assert.True(t, fc.MatchStorageRule("/buckets/b/scratch/keep/x").GetWorm())
// paths with no rule at all are unaffected
assert.False(t, fc.MatchStorageRule("/buckets/other/x").GetWorm())
}
// TestWormLegacyFalseIsNotAnOverride pins that the explicit "worm": false every
// version 0 configuration carries does not read as a per-path opt-out.
func TestWormLegacyFalseIsNotAnOverride(t *testing.T) {
const conf = `{
"locations": [
{"locationPrefix": "/buckets/b/", "worm": true},
{"locationPrefix": "/buckets/b/sub/", "collection": "sub", "worm": false}
]
}`
fc := NewFilerConf()
assert.NoError(t, fc.LoadFromBytes([]byte(conf)))
assert.True(t, fc.MatchStorageRule("/buckets/b/sub/x").GetWorm())
// the same file at the current version means what it says
fc = NewFilerConf()
assert.NoError(t, fc.LoadFromBytes([]byte(`{"version": 1,`+conf[1:])))
assert.False(t, fc.MatchStorageRule("/buckets/b/sub/x").GetWorm())
}
// TestWormSurvivesRoundTrip guards the write side: an explicit false has to be
// stamped along with a version that says to honor it.
func TestWormSurvivesRoundTrip(t *testing.T) {
fc := NewFilerConf()
fc.SetLocationConf(&filer_pb.FilerConf_PathConf{LocationPrefix: "/buckets/b/", Worm: proto.Bool(true)})
fc.SetLocationConf(&filer_pb.FilerConf_PathConf{LocationPrefix: "/buckets/b/sub/", Worm: proto.Bool(false)})
fc.SetLocationConf(&filer_pb.FilerConf_PathConf{LocationPrefix: "/buckets/b/other/", Ttl: "7d"})
var buf bytes.Buffer
assert.NoError(t, fc.ToText(&buf))
reloaded := NewFilerConf()
assert.NoError(t, reloaded.LoadFromBytes(buf.Bytes()))
assert.False(t, reloaded.MatchStorageRule("/buckets/b/sub/x").GetWorm())
assert.True(t, reloaded.MatchStorageRule("/buckets/b/other/x").GetWorm())
}
// TestClonePathConf verifies that ClonePathConf copies all exported fields.
// Uses reflection to automatically detect new fields added to the protobuf,
// ensuring the test fails if ClonePathConf is not updated for new fields.
func TestClonePathConf(t *testing.T) {
// Create a fully-populated PathConf with non-zero values for all fields
src := &filer_pb.FilerConf_PathConf{
LocationPrefix: "/test/path",
Collection: "test_collection",
Replication: "001",
Ttl: "7d",
DiskType: "ssd",
Fsync: true,
VolumeGrowthCount: 5,
ReadOnly: true,
MaxFileNameLength: 255,
DataCenter: "dc1",
Rack: "rack1",
DataNode: "node1",
DisableChunkDeletion: true,
Worm: proto.Bool(true),
WormGracePeriodSeconds: 3600,
WormRetentionTimeSeconds: 86400,
}
clone := ClonePathConf(src)
// Verify it's a different object
assert.NotSame(t, src, clone, "ClonePathConf should return a new object, not the same pointer")
// Use reflection to compare all exported fields
// This will automatically catch any new fields added to the protobuf
srcVal := reflect.ValueOf(src).Elem()
cloneVal := reflect.ValueOf(clone).Elem()
srcType := srcVal.Type()
for i := 0; i < srcType.NumField(); i++ {
field := srcType.Field(i)
// Skip unexported fields (protobuf internal fields like sizeCache, unknownFields)
if !field.IsExported() {
continue
}
srcField := srcVal.Field(i)
cloneField := cloneVal.Field(i)
// Compare field values
if !reflect.DeepEqual(srcField.Interface(), cloneField.Interface()) {
t.Errorf("Field %s not copied correctly: src=%v, clone=%v",
field.Name, srcField.Interface(), cloneField.Interface())
}
}
// Additionally verify that all exported fields in src are non-zero
// This ensures we're testing with fully populated data
for i := 0; i < srcType.NumField(); i++ {
field := srcType.Field(i)
if !field.IsExported() {
continue
}
srcField := srcVal.Field(i)
if srcField.IsZero() {
t.Errorf("Test setup error: field %s has zero value, update test to set a non-zero value", field.Name)
}
}
// Verify mutation of clone doesn't affect source
clone.Collection = "modified"
clone.ReadOnly = false
*clone.Worm = false
assert.Equal(t, "test_collection", src.Collection, "Modifying clone should not affect source Collection")
assert.Equal(t, true, src.ReadOnly, "Modifying clone should not affect source ReadOnly")
assert.Equal(t, true, src.GetWorm(), "Modifying clone should not affect source Worm")
}
func TestClonePathConfNil(t *testing.T) {
clone := ClonePathConf(nil)
assert.NotNil(t, clone, "ClonePathConf(nil) should return a non-nil empty PathConf")
assert.Equal(t, "", clone.LocationPrefix, "ClonePathConf(nil) should return empty PathConf")
}
func TestApplyBucketQuotaReadOnly(t *testing.T) {
const prefix = "/buckets/b/"
// over quota: flips to read-only
fc := NewFilerConf()
readOnly, changed := fc.ApplyBucketQuotaReadOnly(prefix, 150, 100)
assert.True(t, changed)
assert.True(t, readOnly)
assert.True(t, fc.MatchStorageRule(prefix).ReadOnly)
// still over quota: no change
_, changed = fc.ApplyBucketQuotaReadOnly(prefix, 150, 100)
assert.False(t, changed)
// back under quota: flips to writable
readOnly, changed = fc.ApplyBucketQuotaReadOnly(prefix, 50, 100)
assert.True(t, changed)
assert.False(t, readOnly)
assert.False(t, fc.MatchStorageRule(prefix).ReadOnly)
// quota disabled leaves the flag untouched, so manual locks survive
fc = NewFilerConf()
fc.ApplyBucketQuotaReadOnly(prefix, 150, 100)
readOnly, changed = fc.ApplyBucketQuotaReadOnly(prefix, 150, -1)
assert.False(t, changed)
assert.True(t, readOnly)
// under quota and not read-only: no rule churn
fc = NewFilerConf()
_, changed = fc.ApplyBucketQuotaReadOnly(prefix, 50, 100)
assert.False(t, changed)
}
func TestClearReadOnly(t *testing.T) {
const prefix = "/buckets/b/"
fc := NewFilerConf()
assert.False(t, fc.ClearReadOnly(prefix), "no rule to clear")
// locked by quota enforcement, then quota removed: still clearable
fc.ApplyBucketQuotaReadOnly(prefix, 150, 100)
assert.True(t, fc.ClearReadOnly(prefix))
assert.False(t, fc.MatchStorageRule(prefix).ReadOnly)
assert.False(t, fc.ClearReadOnly(prefix), "already writable")
// clearing the flag keeps the rule's other settings
fc = NewFilerConf()
fc.SetLocationConf(&filer_pb.FilerConf_PathConf{LocationPrefix: prefix, Ttl: "7d", ReadOnly: true})
assert.True(t, fc.ClearReadOnly(prefix))
rule := fc.MatchStorageRule(prefix)
assert.False(t, rule.ReadOnly)
assert.Equal(t, "7d", rule.Ttl)
}
// fakeFilerConfClient is a minimal in-memory filer_pb.SeaweedFilerClient that
// only supports the single-file round trip ReadInsideFiler/SaveInsideFiler
// need: lookup, create-if-absent, update. Embedding the interface satisfies
// the rest of it; calling any other method panics on the nil embedded value.
type fakeFilerConfClient struct {
filer_pb.SeaweedFilerClient
mu sync.Mutex
entries map[string]*filer_pb.Entry // key: dir+"/"+name
}
func newFakeFilerConfClient() *fakeFilerConfClient {
return &fakeFilerConfClient{entries: make(map[string]*filer_pb.Entry)}
}
func (c *fakeFilerConfClient) key(dir, name string) string { return dir + "/" + name }
func (c *fakeFilerConfClient) LookupDirectoryEntry(_ context.Context, in *filer_pb.LookupDirectoryEntryRequest, _ ...grpc.CallOption) (*filer_pb.LookupDirectoryEntryResponse, error) {
c.mu.Lock()
defer c.mu.Unlock()
e, ok := c.entries[c.key(in.Directory, in.Name)]
if !ok {
return nil, filer_pb.ErrNotFound
}
// A real gRPC round trip always hands back an independent copy (proto
// marshal/unmarshal), so mutating what the caller gets back (as
// saveFilerConfConditionally does before re-sending it) must not affect
// what conditionHoldsLocked below compares against.
return &filer_pb.LookupDirectoryEntryResponse{Entry: cloneFakeEntry(e)}, nil
}
func cloneFakeEntry(e *filer_pb.Entry) *filer_pb.Entry {
if e == nil {
return nil
}
// proto.Clone rather than a struct copy: filer_pb.Entry embeds
// protoimpl.MessageState (a sync.Mutex), which a plain `*e` copy would
// duplicate by value — exactly what go vet's copylocks check flags.
return proto.Clone(e).(*filer_pb.Entry)
}
func (c *fakeFilerConfClient) CreateEntry(_ context.Context, in *filer_pb.CreateEntryRequest, _ ...grpc.CallOption) (*filer_pb.CreateEntryResponse, error) {
c.mu.Lock()
defer c.mu.Unlock()
key := c.key(in.Directory, in.Entry.Name)
if !conditionHoldsLocked(in.Condition, c.entries[key]) {
return nil, errors.New("precondition failed")
}
c.entries[key] = in.Entry
return &filer_pb.CreateEntryResponse{}, nil
}
func (c *fakeFilerConfClient) UpdateEntry(_ context.Context, in *filer_pb.UpdateEntryRequest, _ ...grpc.CallOption) (*filer_pb.UpdateEntryResponse, error) {
c.mu.Lock()
defer c.mu.Unlock()
key := c.key(in.Directory, in.Entry.Name)
if !conditionHoldsLocked(in.Condition, c.entries[key]) {
return nil, errors.New("precondition failed")
}
c.entries[key] = in.Entry
return &filer_pb.UpdateEntryResponse{}, nil
}
// conditionHoldsLocked is a minimal stand-in for the filer server's
// writeConditionSatisfied (weed/server/filer_grpc_server_condition.go),
// covering just the clause kinds saveFilerConfConditionally uses, so tests
// can exercise the CAS path without a real filer server.
func conditionHoldsLocked(cond *filer_pb.WriteCondition, current *filer_pb.Entry) bool {
if cond == nil {
return true
}
for _, clause := range cond.Clauses {
switch clause.Kind {
case filer_pb.WriteCondition_IF_NOT_EXISTS:
if current != nil {
return false
}
case filer_pb.WriteCondition_IF_UNMODIFIED_SINCE:
if current != nil && current.Attributes != nil && current.Attributes.Mtime > clause.UnixTime {
return false
}
case filer_pb.WriteCondition_IF_ETAG_MATCH:
if current == nil {
return false
}
stored := ""
if current.Attributes != nil {
stored = fmt.Sprintf("%x", current.Attributes.Md5)
}
matched := false
for _, want := range clause.Etags {
if want == stored {
matched = true
break
}
}
if !matched {
return false
}
}
}
return true
}
// putFilerConf seeds the fake client's filer.conf with the given rules.
func putFilerConf(t *testing.T, client *fakeFilerConfClient, rules ...*filer_pb.FilerConf_PathConf) {
t.Helper()
fc := NewFilerConf()
for _, r := range rules {
require.NoError(t, fc.SetLocationConf(r))
}
var buf bytes.Buffer
require.NoError(t, fc.ToText(&buf))
client.entries[client.key(DirectoryEtcSeaweedFS, FilerConfName)] = &filer_pb.Entry{
Name: FilerConfName,
Content: buf.Bytes(),
Attributes: &filer_pb.FuseAttributes{},
}
}
// readFilerConfText returns the current filer.conf content stored in the fake
// client, or "" if none exists yet.
func readFilerConfText(client *fakeFilerConfClient) string {
e, ok := client.entries[client.key(DirectoryEtcSeaweedFS, FilerConfName)]
if !ok {
return ""
}
return string(e.Content)
}
func TestClearBucketLifecycleDayTTLs_NoFilerConf(t *testing.T) {
client := newFakeFilerConfClient()
require.NoError(t, ClearBucketLifecycleDayTTLs(context.Background(), client, "/buckets", "mybucket", "mybucket"))
assert.Empty(t, readFilerConfText(client))
}
func TestClearBucketLifecycleDayTTLs_NoMatchingRule(t *testing.T) {
client := newFakeFilerConfClient()
putFilerConf(t, client, &filer_pb.FilerConf_PathConf{
LocationPrefix: "/buckets/other/",
Collection: "other",
Ttl: "7d",
})
require.NoError(t, ClearBucketLifecycleDayTTLs(context.Background(), client, "/buckets", "mybucket", "mybucket"))
assert.Contains(t, readFilerConfText(client), "other")
}
func TestClearBucketLifecycleDayTTLs_RemovesDayTTLUnderBucket(t *testing.T) {
client := newFakeFilerConfClient()
putFilerConf(t, client, &filer_pb.FilerConf_PathConf{
LocationPrefix: "/buckets/mybucket/",
Collection: "mybucket",
Ttl: "7d",
})
require.NoError(t, ClearBucketLifecycleDayTTLs(context.Background(), client, "/buckets", "mybucket", "mybucket"))
reloaded := NewFilerConf()
require.NoError(t, reloaded.LoadFromBytes([]byte(readFilerConfText(client))))
_, found := reloaded.GetLocationConf("/buckets/mybucket/")
assert.False(t, found, "day-TTL rule should have been removed")
}
func TestClearBucketLifecycleDayTTLs_KeepsNonDayTTL(t *testing.T) {
client := newFakeFilerConfClient()
putFilerConf(t, client, &filer_pb.FilerConf_PathConf{
LocationPrefix: "/buckets/mybucket/",
Collection: "mybucket",
Ttl: "7m", // minutes, not days: not a legacy lifecycle TTL rule
})
require.NoError(t, ClearBucketLifecycleDayTTLs(context.Background(), client, "/buckets", "mybucket", "mybucket"))
reloaded := NewFilerConf()
require.NoError(t, reloaded.LoadFromBytes([]byte(readFilerConfText(client))))
_, found := reloaded.GetLocationConf("/buckets/mybucket/")
assert.True(t, found, "non-day TTL rule should be left alone")
}
func TestClearBucketLifecycleDayTTLs_KeepsOperatorSettingsAndClearsOnlyTheTTL(t *testing.T) {
// The removed add path merged its TTL onto whatever already sat at the
// prefix, so a rule carrying operator settings must survive with only its
// TTL retired - deleting it outright would drop the disk type and the
// read-only flag with it.
client := newFakeFilerConfClient()
putFilerConf(t, client, &filer_pb.FilerConf_PathConf{
LocationPrefix: "/buckets/mybucket/logs/",
Collection: "mybucket",
Ttl: "30d",
DiskType: "hdd",
ReadOnly: true,
})
require.NoError(t, ClearBucketLifecycleDayTTLs(context.Background(), client, "/buckets", "mybucket", "mybucket"))
reloaded := NewFilerConf()
require.NoError(t, reloaded.LoadFromBytes([]byte(readFilerConfText(client))))
rule, found := reloaded.GetLocationConf("/buckets/mybucket/logs/")
require.True(t, found, "a rule carrying operator settings must not be deleted")
assert.Equal(t, "", rule.Ttl, "the legacy day TTL should be gone")
assert.Equal(t, "hdd", rule.DiskType, "the operator's disk type must survive")
assert.True(t, rule.ReadOnly, "the operator's read-only flag must survive")
}
func TestClearBucketLifecycleDayTTLs_KeepsWormSettings(t *testing.T) {
worm := true
client := newFakeFilerConfClient()
putFilerConf(t, client, &filer_pb.FilerConf_PathConf{
LocationPrefix: "/buckets/mybucket/locked/",
Collection: "mybucket",
Ttl: "7d",
Worm: &worm,
WormRetentionTimeSeconds: 3600,
})
require.NoError(t, ClearBucketLifecycleDayTTLs(context.Background(), client, "/buckets", "mybucket", "mybucket"))
reloaded := NewFilerConf()
require.NoError(t, reloaded.LoadFromBytes([]byte(readFilerConfText(client))))
rule, found := reloaded.GetLocationConf("/buckets/mybucket/locked/")
require.True(t, found, "a WORM rule must not be deleted to retire a TTL")
assert.Equal(t, "", rule.Ttl)
assert.True(t, rule.GetWorm())
assert.Equal(t, uint64(3600), rule.WormRetentionTimeSeconds)
}
func TestClearBucketLifecycleDayTTLs_KeepsOtherBucketsAndCollections(t *testing.T) {
client := newFakeFilerConfClient()
putFilerConf(t, client,
&filer_pb.FilerConf_PathConf{LocationPrefix: "/buckets/mybucket/", Collection: "mybucket", Ttl: "7d"},
&filer_pb.FilerConf_PathConf{LocationPrefix: "/buckets/other/", Collection: "other", Ttl: "7d"},
// nested under the target bucket's path but tagged to a different
// collection (e.g. a filer-group-prefixed name): must not be swept up
// by this bucket's cleanup, since GetCollectionTtls filters by collection.
&filer_pb.FilerConf_PathConf{LocationPrefix: "/buckets/mybucket/nested/", Collection: "group_mybucket", Ttl: "7d"},
)
require.NoError(t, ClearBucketLifecycleDayTTLs(context.Background(), client, "/buckets", "mybucket", "mybucket"))
reloaded := NewFilerConf()
require.NoError(t, reloaded.LoadFromBytes([]byte(readFilerConfText(client))))
_, found := reloaded.GetLocationConf("/buckets/mybucket/")
assert.False(t, found, "the target bucket's day-TTL rule should be gone")
text := readFilerConfText(client)
assert.Contains(t, text, "other")
assert.Contains(t, text, "group_mybucket")
}
// bumpMtime models a writer that landed after unmodifiedSince, so an
// IF_UNMODIFIED_SINCE precondition built on that timestamp no longer holds.
func bumpMtime(client *fakeFilerConfClient, unmodifiedSince int64) {
client.mu.Lock()
defer client.mu.Unlock()
e := client.entries[client.key(DirectoryEtcSeaweedFS, FilerConfName)]
if e == nil {
return
}
if e.Attributes == nil {
e.Attributes = &filer_pb.FuseAttributes{}
}
e.Attributes.Mtime = unmodifiedSince + 1
}
func TestSaveFilerConfConditionally_RejectsUpdateModifiedConcurrently(t *testing.T) {
client := newFakeFilerConfClient()
putFilerConf(t, client, &filer_pb.FilerConf_PathConf{LocationPrefix: "/buckets/a/", Collection: "a", Ttl: "7d"})
snap, err := readFilerConfSnapshot(context.Background(), client)
require.NoError(t, err)
require.NotNil(t, snap.entry)
require.Empty(t, snap.entry.Attributes.Md5, "test setup should model a pre-existing filer.conf with no stamped Md5")
// Someone else writes filer.conf after our read but before our write.
bumpMtime(client, snap.entry.Attributes.Mtime)
err = saveFilerConfConditionally(context.Background(), client, snap)
assert.Error(t, err, "expected a concurrent modification to fail the conditional write")
}
// TestSaveFilerConfConditionally_ExactContentCheckRejectsConcurrentModification
// exercises the primary path: once an entry has been written through this
// function (so it carries a content Md5), a later write with a stale
// snapshot is rejected by the exact IF_ETAG_MATCH check even when the
// concurrent writer happened to leave mtime unchanged — something a
// mtime-only precondition would have missed.
func TestSaveFilerConfConditionally_ExactContentCheckRejectsConcurrentModification(t *testing.T) {
client := newFakeFilerConfClient()
putFilerConf(t, client, &filer_pb.FilerConf_PathConf{LocationPrefix: "/buckets/a/", Collection: "a", Ttl: "7d"})
// First write through saveFilerConfConditionally stamps Md5, moving
// subsequent reads onto the exact-content check.
snap, err := readFilerConfSnapshot(context.Background(), client)
require.NoError(t, err)
require.NoError(t, saveFilerConfConditionally(context.Background(), client, snap))
snap2, err := readFilerConfSnapshot(context.Background(), client)
require.NoError(t, err)
require.NotEmpty(t, snap2.entry.Attributes.Md5, "expected Md5 to have been stamped by the prior write")
// A concurrent writer goes through SaveInsideFiler, the path every
// other filer.conf writer uses. It lands in the same wall-clock second,
// so only the content hash can catch it.
concurrentFc := NewFilerConf()
require.NoError(t, concurrentFc.LoadFromBytes([]byte(readFilerConfText(client))))
require.NoError(t, concurrentFc.SetLocationConf(&filer_pb.FilerConf_PathConf{
LocationPrefix: "/buckets/other/", Collection: "other", Ttl: "3d",
}))
var buf bytes.Buffer
require.NoError(t, concurrentFc.ToText(&buf))
require.NoError(t, SaveInsideFiler(context.Background(), client, DirectoryEtcSeaweedFS, FilerConfName, buf.Bytes()))
client.entries[client.key(DirectoryEtcSeaweedFS, FilerConfName)].Attributes.Mtime = snap2.entry.Attributes.Mtime
err = saveFilerConfConditionally(context.Background(), client, snap2)
assert.Error(t, err, "expected the exact-content check to reject a write whose baseline content hash no longer matches")
assert.Contains(t, readFilerConfText(client), "/buckets/other/", "the concurrent writer's content must survive")
}
func TestSaveFilerConfConditionally_RejectsCreateWhenCreatedConcurrently(t *testing.T) {
client := newFakeFilerConfClient()
snap, err := readFilerConfSnapshot(context.Background(), client)
require.NoError(t, err)
require.Nil(t, snap.entry, "filer.conf should not exist yet")
// Someone else creates filer.conf after our read but before our write.
putFilerConf(t, client, &filer_pb.FilerConf_PathConf{LocationPrefix: "/buckets/b/", Collection: "b", Ttl: "3d"})
err = saveFilerConfConditionally(context.Background(), client, snap)
assert.Error(t, err, "expected a concurrent create to fail the conditional write")
assert.Contains(t, readFilerConfText(client), "/buckets/b/", "the concurrent writer's content must survive")
}