Files
seaweedfs/weed/admin/view/app/volume_details_test.go
T
Chris Lu 75ec5ec193 admin: allow setting volume read-only and read/write modes (#11217)
* admin: support setting volume read-only and read/write modes

* admin: address PR review on volume access-mode persistence

Reject trailing JSON values in the SetVolumeReadOnly handler so
requests like {"read_only":true}{} no longer pass validation, and add
a trailing-value case to the invalid-request test.

Propagate .vif persistence failures through the access-mode chain.
PersistReadOnly now returns the SaveVolumeInfo error and rolls back
the in-memory volumeInfo on failure; Store.MarkVolumeReadonly and
Store.MarkVolumeWritable propagate that error and roll back their
noWrite flags, so the API reports failure instead of success while
restart would revert the mode.

* admin: make .vif persistence atomic and preserve error chain

SaveVolumeInfo now writes to a .vif.tmp file, syncs it, renames it
over the target, and fsyncs the directory. A write/sync/close failure
leaves the existing .vif intact, so the PersistReadOnly in-memory
rollback matches the durable state instead of diverging from a
partially written file that restart would apply.

Switch the error wrappers in PersistReadOnly, MarkVolumeReadonly, and
MarkVolumeWritable from %v to %w so callers can use errors.Is and
errors.As to classify persistence failures.

* admin: treat post-rename dir fsync failure as a warning

After os.Rename commits the new .vif, the on-disk file already holds
the requested mode. A directory fsync failure only risks losing the
rename across a crash; returning an error here would make
PersistReadOnly roll back in-memory state while the durable file keeps
the new mode, splitting the replica. Log the failure as a warning
instead, matching the best-effort nature of FsyncDir (already skipped
on Windows).

* admin: distinguish post-rename durability failures and use unique temp files

SaveVolumeInfo now uses os.CreateTemp for the staging file, preventing
concurrent saves for the same volume from colliding on a shared .tmp
path.

A directory fsync failure after os.Rename returns a
NotCrashDurableError instead of being silently swallowed. The rename
already committed the new metadata to disk, so PersistReadOnly,
MarkVolumeReadonly, and MarkVolumeWritable skip the in-memory rollback
for this error type (keeping state aligned with the durable file) while
still propagating the failure to the API. Pre-commit failures continue
to roll back as before.

* admin: continue post-commit work after NotCrashDurableError

MarkVolumeWritable now clears the EIO quarantine and the gRPC handlers
(makeVolumeReadonly step 3, makeVolumeWritable master notification)
proceed with their post-commit work when SaveVolumeInfo returns a
NotCrashDurableError, instead of aborting and leaving the volume
unavailable or the master unaware of the mode change. The durability
warning is still propagated to the API caller. Pre-commit failures
continue to abort early as before.

* admin: handle NotCrashDurableError in tier and EC callers

VolumeTierMoveDatFromRemote and VolumeEcShardsGenerate now check for
NotCrashDurableError from SaveVolumeInfo. When the rename has already
committed the new .vif, they continue with their post-commit work
(backend switch, remote deletion, keeping generated EC shards) instead
of aborting and leaving the on-disk metadata inconsistent with the
file layout. The durability warning is logged for the operator.
2026-09-07 18:40:37 -07:00

48 lines
1.4 KiB
Go

package app
import (
"bytes"
"testing"
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/stretchr/testify/require"
"golang.org/x/net/html"
)
func TestVolumeDetailsAccessControls(t *testing.T) {
for _, role := range []string{"", "admin", dash.RoleReadOnly} {
for _, readOnly := range []bool{false, true} {
data := dash.VolumeDetailsData{Volume: dash.VolumeWithTopology{
VolumeInformationMessage: &master_pb.VolumeInformationMessage{Id: 7, ReadOnly: readOnly},
Server: "node-a",
}}
var rendered bytes.Buffer
ctx := dash.WithAuthContext(t.Context(), "", role, "")
require.NoError(t, VolumeDetails(data).Render(ctx, &rendered))
doc, err := html.Parse(&rendered)
require.NoError(t, err)
var modes []string
var walk func(*html.Node)
walk = func(node *html.Node) {
if node.Type == html.ElementNode && node.Data == "button" {
for _, attr := range node.Attr {
if attr.Key == "data-read-only" {
modes = append(modes, attr.Val)
}
}
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
walk(child)
}
}
walk(doc)
if role == dash.RoleReadOnly {
require.Empty(t, modes)
} else {
require.Equal(t, []string{"true", "false"}, modes, "both modes must be available even when the displayed state is stale")
}
}
}
}