admin: respect filerGroup for cluster discovery (#10170)

* Respect filerGroup in admin discovery

Admin discovery previously queried master cluster nodes with an empty filer group, so filers registered under a non-default group could not appear in the admin UI. Add an admin filerGroup flag and carry it through cluster-node discovery requests while preserving the empty default behavior.

Constraint: SeaweedFS master ListClusterNodes filters by exact filer_group.

Rejected: Discover all groups implicitly | no existing admin or shell behavior exposes cross-group discovery.

Confidence: high

Scope-risk: narrow

Directive: Keep admin cluster discovery scoped to the configured filerGroup unless an explicit all-groups API is added.

Tested: docker run --rm -v "$PWD:/src" -w /src golang:1.25 go test ./weed/admin/dash -run TestListClusterNodesRequest -count=1

Tested: docker run --rm -v "$PWD:/src" -w /src golang:1.25 go test ./weed/command -run '^$' -count=1

Not-tested: full repository test suite

* mini: pass filer group to admin cluster discovery

miniAdminOptions.filerGroup was never initialized, so startAdminServer
dereferenced a nil *string. Share the filer.filerGroup flag pointer so the
co-located admin queries the same group the filer registers under.

---------

Co-authored-by: Chris Lu <chris.lu@gmail.com>
This commit is contained in:
Kwak Byoung MinandChris Lu authored and GitHub committed 2026-07-01 10:56:58 -07:00
1 parent ebeab4b6ec
commit 9d75048594
8 files changed
+59 -35

No files matched your search

+3 -9
View File
@@ -311,9 +311,7 @@ func (s *AdminServer) getFilerNodesStatus() []FilerNode {
// Get filer nodes from master using ListClusterNodes
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{
ClientType: cluster.FilerType,
})
resp, err := client.ListClusterNodes(context.Background(), s.listClusterNodesRequest(cluster.FilerType))
if err != nil {
return err
}
@@ -352,9 +350,7 @@ func (s *AdminServer) getMessageBrokerNodesStatus() []MessageBrokerNode {
// Get message broker nodes from master using ListClusterNodes
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{
ClientType: cluster.BrokerType,
})
resp, err := client.ListClusterNodes(context.Background(), s.listClusterNodesRequest(cluster.BrokerType))
if err != nil {
return err
}
@@ -393,9 +389,7 @@ func (s *AdminServer) getS3NodesStatus() []S3Node {
// Get S3 nodes from master using ListClusterNodes
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{
ClientType: cluster.S3Type,
})
resp, err := client.ListClusterNodes(context.Background(), s.listClusterNodesRequest(cluster.S3Type))
if err != nil {
return err
}
+15 -14
View File
@@ -85,6 +85,7 @@ type AdminServer struct {
masterClient *wdclient.MasterClient
templateFS http.FileSystem
dataDir string
filerGroup string
grpcDialOption grpc.DialOption
cacheExpiration time.Duration
lastCacheUpdate time.Time
@@ -134,13 +135,13 @@ type AdminServer struct {
// Type definitions moved to types.go
func NewAdminServer(masters string, templateFS http.FileSystem, dataDir string, icebergPort int) *AdminServer {
func NewAdminServer(masters string, filerGroup string, templateFS http.FileSystem, dataDir string, icebergPort int) *AdminServer {
grpcDialOption := security.LoadClientTLS(util.GetViper(), "grpc.admin")
// Create master client with multiple master support
masterClient := wdclient.NewMasterClient(
grpcDialOption,
"", // filerGroup - not needed for admin
filerGroup,
"admin", // clientType
"", // clientHost - not needed for admin
"", // dataCenter - not needed for admin
@@ -162,6 +163,7 @@ func NewAdminServer(masters string, templateFS http.FileSystem, dataDir string,
masterClient: masterClient,
templateFS: templateFS,
dataDir: dataDir,
filerGroup: filerGroup,
grpcDialOption: grpcDialOption,
cacheExpiration: defaultCacheTimeout,
filerCacheExpiration: defaultFilerCacheTimeout,
@@ -293,6 +295,13 @@ func NewAdminServer(masters string, templateFS http.FileSystem, dataDir string,
return server
}
func (s *AdminServer) listClusterNodesRequest(clientType string) *master_pb.ListClusterNodesRequest {
return &master_pb.ListClusterNodesRequest{
ClientType: clientType,
FilerGroup: s.filerGroup,
}
}
// vacuumToggler abstracts the master's vacuum enable/disable for testing.
type vacuumToggler interface {
disableVacuum() error
@@ -1112,9 +1121,7 @@ func (s *AdminServer) GetClusterFilers() (*ClusterFilersData, error) {
// Get filer information from master using ListClusterNodes
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{
ClientType: cluster.FilerType,
})
resp, err := client.ListClusterNodes(context.Background(), s.listClusterNodesRequest(cluster.FilerType))
if err != nil {
return err
}
@@ -1159,9 +1166,7 @@ func (s *AdminServer) GetClusterBrokers() (*ClusterBrokersData, error) {
// Get broker information from master using ListClusterNodes
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{
ClientType: cluster.BrokerType,
})
resp, err := client.ListClusterNodes(context.Background(), s.listClusterNodesRequest(cluster.BrokerType))
if err != nil {
return err
}
@@ -1206,9 +1211,7 @@ func (s *AdminServer) GetClusterS3Servers() (*ClusterS3ServersData, error) {
// Get S3 server information from master using ListClusterNodes
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{
ClientType: cluster.S3Type,
})
resp, err := client.ListClusterNodes(context.Background(), s.listClusterNodesRequest(cluster.S3Type))
if err != nil {
return err
}
@@ -1642,9 +1645,7 @@ func (s *AdminServer) UpdateTopicRetention(namespace, name string, enabled bool,
// Get broker information from master
var brokerAddress string
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{
ClientType: cluster.BrokerType,
})
resp, err := client.ListClusterNodes(context.Background(), s.listClusterNodesRequest(cluster.BrokerType))
if err != nil {
return err
}
@@ -0,0 +1,30 @@
package dash
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/cluster"
)
func TestListClusterNodesRequestIncludesFilerGroup(t *testing.T) {
server := &AdminServer{filerGroup: "tenant-a"}
req := server.listClusterNodesRequest(cluster.FilerType)
if req.ClientType != cluster.FilerType {
t.Fatalf("ClientType = %q, want %q", req.ClientType, cluster.FilerType)
}
if req.FilerGroup != "tenant-a" {
t.Fatalf("FilerGroup = %q, want %q", req.FilerGroup, "tenant-a")
}
}
func TestListClusterNodesRequestKeepsDefaultFilerGroupEmpty(t *testing.T) {
server := &AdminServer{}
req := server.listClusterNodesRequest(cluster.FilerType)
if req.FilerGroup != "" {
t.Fatalf("FilerGroup = %q, want empty", req.FilerGroup)
}
}
+1 -3
View File
@@ -76,9 +76,7 @@ func (s *AdminServer) getDiscoveredFilers() []string {
// Discover filers from masters
var filers []string
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{
ClientType: cluster.FilerType,
})
resp, err := client.ListClusterNodes(context.Background(), s.listClusterNodesRequest(cluster.FilerType))
if err != nil {
return err
}
+2 -4
View File
@@ -494,9 +494,7 @@ func (s *AdminServer) findBrokerLeader() (string, error) {
// First, try to find any broker from the cluster
var brokers []string
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{
ClientType: cluster.BrokerType,
})
resp, err := client.ListClusterNodes(context.Background(), s.listClusterNodesRequest(cluster.BrokerType))
if err != nil {
return err
}
@@ -524,7 +522,7 @@ func (s *AdminServer) findBrokerLeader() (string, error) {
// Try to find broker leader
_, err := client.FindBrokerLeader(ctx, &mq_pb.FindBrokerLeaderRequest{
FilerGroup: "",
FilerGroup: s.filerGroup,
})
if err == nil {
return nil // This broker is the leader
+1 -3
View File
@@ -805,9 +805,7 @@ func (s *AdminServer) buildDefaultPluginClusterContext() *plugin_pb.ClusterConte
s3Seen := map[string]struct{}{}
if err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{
ClientType: cluster.S3Type,
})
resp, err := client.ListClusterNodes(context.Background(), s.listClusterNodesRequest(cluster.S3Type))
if err != nil {
return err
}
+5 -2
View File
@@ -44,6 +44,7 @@ type AdminOptions struct {
grpcPort *int
master *string
masters *string // deprecated, for backward compatibility
filerGroup *string
adminUser *string
adminPassword *string
readOnlyUser *string
@@ -65,6 +66,7 @@ func init() {
a.grpcPort = cmdAdmin.Flag.Int("port.grpc", 0, "gRPC server port for worker connections (default: http port + 10000)")
a.master = cmdAdmin.Flag.String("master", "localhost:9333", "comma-separated master servers")
a.masters = cmdAdmin.Flag.String("masters", "", "comma-separated master servers (deprecated, use -master instead)")
a.filerGroup = cmdAdmin.Flag.String("filerGroup", "", "filerGroup for the filers, brokers, and S3 servers")
a.dataDir = cmdAdmin.Flag.String("dataDir", ".", "directory to store admin configuration and data files (default current dir; required for maintenance task state to persist)")
a.adminUser = cmdAdmin.Flag.String("adminUser", "admin", "admin interface username")
@@ -82,7 +84,7 @@ func init() {
}
var cmdAdmin = &Command{
UsageLine: "admin -port=23646 -master=localhost:9333 [-port.grpc=33646] [-dataDir=/path/to/data]",
UsageLine: "admin -port=23646 -master=localhost:9333 [-filerGroup=group] [-port.grpc=33646] [-dataDir=/path/to/data]",
Short: "start SeaweedFS web admin interface",
Long: `Start a web admin interface for SeaweedFS cluster management.
@@ -99,6 +101,7 @@ var cmdAdmin = &Command{
Example Usage:
weed admin -port=23646 -master="master1:9333,master2:9333"
weed admin -port=23646 -master="localhost:9333" -filerGroup="tenant-a"
weed admin -port=23646 -master="localhost:9333" -dataDir="/var/lib/seaweedfs-admin"
weed admin -port=23646 -port.grpc=33646 -master="localhost:9333" -dataDir="~/seaweedfs-admin"
weed admin -port=9900 -port.grpc=19900 -master="localhost:9333"
@@ -383,7 +386,7 @@ func startAdminServer(ctx context.Context, options AdminOptions, enableUI bool,
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", admin.StaticHandler()))
// Create admin server (plugin is always enabled)
adminServer := dash.NewAdminServer(*options.master, nil, dataDir, icebergPort)
adminServer := dash.NewAdminServer(*options.master, *options.filerGroup, nil, dataDir, icebergPort)
// Show discovered filers
filers := adminServer.GetAllFilers()
+2
View File
@@ -543,6 +543,8 @@ func initMiniAdminFlags() {
miniAdminOptions.port = cmdMini.Flag.Int("admin.port", 23646, "admin server http listen port")
miniAdminOptions.grpcPort = cmdMini.Flag.Int("admin.port.grpc", 0, "admin server grpc listen port (default: admin http port + GrpcPortOffset)")
miniAdminOptions.master = cmdMini.Flag.String("admin.master", "", "master server address (automatically set)")
// admin discovery must query the same group the co-located filer registers under
miniAdminOptions.filerGroup = miniFilerOptions.filerGroup
miniAdminOptions.dataDir = cmdMini.Flag.String("admin.dataDir", "", "directory to store admin configuration and data files")
miniAdminOptions.adminUser = cmdMini.Flag.String("admin.user", "admin", "admin interface username")
miniAdminOptions.adminPassword = cmdMini.Flag.String("admin.password", "", "admin interface password (if empty, auth is disabled)")