Files
s3/weed/shell/command_ec_destructive_guards_test.go
zooqueen dd74d3e6d6 Replace AGPL MinIO fork with Apache-2.0 SeaweedFS fork; rebrand CLI weed->s3
hanzoai/s3 was an AGPL-3.0 MinIO fork whose network-copyleft blocks embedding
and reselling as a managed service. Replace it wholesale with a fork of
SeaweedFS (github.com/seaweedfs/seaweedfs, Apache-2.0, Go) which ships a
built-in S3 gateway + Filer.

- License: AGPL-3.0 -> Apache-2.0 (upstream LICENSE kept verbatim, incl.
  "Copyright 2025 Chris Lu"); NOTICE added crediting SeaweedFS. No AGPL/BUSL/
  SSPL/GPL or enterprise ("ee") files remain.
- Module: go.mod path set to github.com/hanzoai/s3 with a self-replace
  (replace github.com/seaweedfs/seaweedfs => ./) so the ~1800 internal Go files
  keep their upstream import paths with zero churn. Internal identifiers and
  import paths are intentionally NOT renamed.
- CLI rebrand: the binary is built as `s3` (was `weed`); root usage, per-command
  help/usage/flag strings, shell autocomplete program name, and the product name
  in CLI output now read "Hanzo S3" / `s3`. Users never see `weed`.
  (e.g. `s3 server`, `s3 filer`, `s3 master`, `s3 volume`, `s3 s3`, `s3 mount`.)
- Build: `make` (or `go build -o s3 ./weed`) builds the `s3` binary green;
  `cmd/weed-db` and `cmd/weed-sql` also build; `go vet ./weed/command/` clean.
- README/LLM.md rewritten for Hanzo S3; AGENTS.md/CLAUDE.md symlinks preserved.
- Dropped a 33MB prebuilt Mach-O `stress` binary upstream had committed; `.gitignore`
  updated to ignore the `s3` build output.
2026-06-18 23:41:25 -07:00

194 lines
7.0 KiB
Go

package shell
import (
"bytes"
"strings"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
)
// topoWith builds a one-node topology snapshot where regularVids appear as
// regular volumes (.dat) and ecVids appear as EC shards.
func topoWith(regularVids, ecVids []uint32) *master_pb.TopologyInfo {
di := &master_pb.DiskInfo{Type: "hdd"}
for _, v := range regularVids {
di.VolumeInfos = append(di.VolumeInfos, &master_pb.VolumeInformationMessage{Id: v})
}
for _, v := range ecVids {
di.EcShardInfos = append(di.EcShardInfos, &master_pb.VolumeEcShardInformationMessage{Id: v})
}
return &master_pb.TopologyInfo{
DataCenterInfos: []*master_pb.DataCenterInfo{{
RackInfos: []*master_pb.RackInfo{{
DataNodeInfos: []*master_pb.DataNodeInfo{{
Id: "n1",
DiskInfos: map[string]*master_pb.DiskInfo{"hdd": di},
}},
}},
}},
}
}
// TestAssertEncodableRegularVolumes: ec.encode must refuse an already-EC volume
// (no .dat — re-encoding would destroy its shards) and an unknown id, while
// allowing a regular volume and a failed-encode retry (regular .dat + orphans).
func TestAssertEncodableRegularVolumes(t *testing.T) {
cases := []struct {
name string
regular, ec []uint32
vids []needle.VolumeId
wantErrSubstr string
}{
{"regular volume", []uint32{1}, nil, []needle.VolumeId{1}, ""},
{"already EC", nil, []uint32{1}, []needle.VolumeId{1}, "already EC"},
{"regular plus stale orphan shards", []uint32{1}, []uint32{1}, []needle.VolumeId{1}, ""},
{"unknown id", nil, nil, []needle.VolumeId{1}, "not found"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := assertEncodableRegularVolumes(topoWith(tc.regular, tc.ec), tc.vids)
if tc.wantErrSubstr == "" {
if err != nil {
t.Fatalf("want nil error, got %v", err)
}
return
}
if err == nil || !strings.Contains(err.Error(), tc.wantErrSubstr) {
t.Fatalf("want error containing %q, got %v", tc.wantErrSubstr, err)
}
})
}
}
func newDryRunRebuilder(log *bytes.Buffer, nodes ...*EcNode) *ecRebuilder {
return &ecRebuilder{
commandEnv: &CommandEnv{env: make(map[string]string), noLock: true},
ecNodes: nodes,
writer: log,
applyChanges: false,
}
}
// TestPrepareDataToRecover_DryRunRecoverableNoSideEffects: a dry-run on a
// recoverable degraded volume the rebuilder holds none of must still succeed
// and report its plan, while recording zero shards to delete (so the deferred
// cleanup issues no VolumeEcShardsDelete RPC).
func TestPrepareDataToRecover_DryRunRecoverableNoSideEffects(t *testing.T) {
var log bytes.Buffer
rebuilder := newEcNode("dc1", "rack1", "rebuilder", 100)
remote := newEcNode("dc1", "rack1", "remote", 100).
addEcVolumeAndShardsForTest(1, "c1", []erasure_coding.ShardId{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11})
erb := newDryRunRebuilder(&log, rebuilder, remote)
locations := make(EcShardLocations, erasure_coding.MaxShardCount)
for i := 0; i < 12; i++ {
locations[i] = []*EcNode{remote}
}
copied, local, err := erb.prepareDataToRecover(rebuilder, "c1", needle.VolumeId(1), locations)
if err != nil {
t.Fatalf("dry-run on a recoverable volume must not error: %v", err)
}
if len(copied) != 0 {
t.Errorf("dry-run must record no copied shards (deletion set), got %v", copied)
}
if len(local) != 0 {
t.Errorf("rebuilder holds no shards locally, got %v", local)
}
if !strings.Contains(log.String(), "would copy") {
t.Errorf("dry-run should report the would-copy plan, got:\n%s", log.String())
}
}
// TestPrepareDataToRecover_UnionsLocalShardsAcrossDisks: shards the rebuilder
// holds on more than one disk must all count as local (not copied), so the
// per-disk overwrite that made one disk's shards look remote (then self-copied
// with O_TRUNC and node-wide deleted) cannot happen.
func TestPrepareDataToRecover_UnionsLocalShardsAcrossDisks(t *testing.T) {
var log bytes.Buffer
rebuilder := newEcNode("dc1", "rack1", "rebuilder", 100).
addEcVolumeAndShardsForTest(1, "c1", []erasure_coding.ShardId{0, 1}).
addEcVolumeShards(needle.VolumeId(1), "c1", []erasure_coding.ShardId{5}, types.SsdType)
remote := newEcNode("dc1", "rack1", "remote", 100)
erb := newDryRunRebuilder(&log, rebuilder)
locations := make(EcShardLocations, erasure_coding.MaxShardCount)
for i := 0; i < 12; i++ {
locations[i] = []*EcNode{remote}
}
locations[0] = []*EcNode{rebuilder}
locations[1] = []*EcNode{rebuilder}
locations[5] = []*EcNode{rebuilder}
copied, local, err := erb.prepareDataToRecover(rebuilder, "c1", needle.VolumeId(1), locations)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
missing := map[erasure_coding.ShardId]bool{0: true, 1: true, 5: true}
for _, s := range local {
delete(missing, s)
}
if len(missing) != 0 {
t.Errorf("shards %v should be local (union across disks); local=%v", missing, local)
}
for _, s := range copied {
if s == 0 || s == 1 || s == 5 {
t.Errorf("local shard %d must never be in the copy/delete set", s)
}
}
}
// TestCountLocalShards_UnionsAcrossDisks: slot accounting must union shards
// across the node's disks like prepareDataToRecover does, or slotsNeeded is
// overstated and selectAndReserveRebuilder can reject a valid node.
func TestCountLocalShards_UnionsAcrossDisks(t *testing.T) {
var log bytes.Buffer
rebuilder := newEcNode("dc1", "rack1", "rebuilder", 100).
addEcVolumeAndShardsForTest(1, "c1", []erasure_coding.ShardId{0, 1}).
addEcVolumeShards(needle.VolumeId(1), "c1", []erasure_coding.ShardId{5}, types.SsdType)
erb := newDryRunRebuilder(&log, rebuilder)
if got := erb.countLocalShards(rebuilder, "c1", needle.VolumeId(1)); got != 3 {
t.Errorf("countLocalShards must union across disks: want 3, got %d", got)
}
}
// TestPrepareDataToRecover_SelfSourceNotCopied: if the rebuilder is the only
// listed holder of a shard it does not report locally, it must be treated as
// local rather than copied onto itself and then deleted.
func TestPrepareDataToRecover_SelfSourceNotCopied(t *testing.T) {
var log bytes.Buffer
rebuilder := newEcNode("dc1", "rack1", "rebuilder", 100).
addEcVolumeAndShardsForTest(1, "c1", []erasure_coding.ShardId{0, 1, 2, 3, 4, 6, 7, 8, 9, 10})
erb := newDryRunRebuilder(&log, rebuilder)
locations := make(EcShardLocations, erasure_coding.MaxShardCount)
for _, s := range []int{0, 1, 2, 3, 4, 6, 7, 8, 9, 10} {
locations[s] = []*EcNode{rebuilder}
}
locations[5] = []*EcNode{rebuilder} // sole holder is the rebuilder, not in its reported shards
copied, local, err := erb.prepareDataToRecover(rebuilder, "c1", needle.VolumeId(1), locations)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for _, s := range copied {
if s == 5 {
t.Errorf("self-sourced shard 5 must not be copied/deleted")
}
}
found := false
for _, s := range local {
if s == 5 {
found = true
}
}
if !found {
t.Errorf("self-sourced shard 5 should be classified local, got local=%v", local)
}
}