mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-27 02:23:23 +00:00
* admin: move volume-server read JWT helper into dash The Iceberg data preview page needs the same per-fileId read token the file browser uses when streaming chunks from volume servers. Claude-Session: https://claude.ai/code/session_015n3oKLTjnPjcnZtfigNKur * admin: add Iceberg table data preview page The admin UI browses the Iceberg catalog down to table details but not the data itself. Add a Browse Data page per table that walks the selected snapshot's manifests and shows sample rows from its Parquet data files, plus the data file list with per-file preview, a snapshot switcher, and a row limit selector. Rows are read through a ranged ReaderAt over stream-content so only the Parquet footer and needed pages are fetched, with the volume read JWT applied when configured. Iceberg locations resolve into /buckets with traversal guards, and the file parameter must match a manifest-listed data file. Snapshots with delete files get a warning that raw rows are shown. Claude-Session: https://claude.ai/code/session_015n3oKLTjnPjcnZtfigNKur * admin: integration test for Iceberg catalog and data preview pages Starts a weed mini cluster with the admin UI, creates a table bucket, namespace, and tables via the S3 Tables manager, uploads real Parquet files via S3, writes manifests and snapshots with iceberg-go, and asserts on the rendered pages: catalog browsing, table details, current and historical snapshot previews, per-file preview, row limits, unknown snapshot and file errors, and a metadata-less table. Claude-Session: https://claude.ai/code/session_015n3oKLTjnPjcnZtfigNKur * admin: write Iceberg preview chunk reads straight into the caller slice ReadAt wrapped the caller's buffer in a bytes.Buffer, which would silently allocate a fresh backing array and drop bytes if it ever grew. Copy directly into the destination slice and reject negative offsets so the ReaderAt contract holds. Claude-Session: https://claude.ai/code/session_015n3oKLTjnPjcnZtfigNKur * admin: link to snapshot history when the preview switcher truncates The snapshot switcher caps at 25 entries; add a trailing item pointing at the table details page so older snapshots stay reachable. Claude-Session: https://claude.ai/code/session_015n3oKLTjnPjcnZtfigNKur * test: hoist mini cluster context assignment out of the goroutine Set MiniClusterCtx before launching the cluster goroutine and clear it in stop(), so the assignment is not buried in the command loop. Claude-Session: https://claude.ai/code/session_015n3oKLTjnPjcnZtfigNKur
125 lines
4.2 KiB
Go
125 lines
4.2 KiB
Go
package dash
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/cluster"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/operation"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/security"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
// WithMasterClient executes a function with a master client connection
|
|
func (s *AdminServer) WithMasterClient(f func(client master_pb.SeaweedClient) error) error {
|
|
return s.masterClient.WithClient(false, f)
|
|
}
|
|
|
|
// WithFilerClient executes a function with a filer client connection
|
|
func (s *AdminServer) WithFilerClient(f func(client filer_pb.SeaweedFilerClient) error) error {
|
|
filerAddr := s.GetFilerAddress()
|
|
if filerAddr == "" {
|
|
return fmt.Errorf("no filer available")
|
|
}
|
|
|
|
return pb.WithGrpcFilerClient(false, 0, pb.ServerAddress(filerAddr), s.grpcDialOption, func(client filer_pb.SeaweedFilerClient) error {
|
|
return f(client)
|
|
})
|
|
}
|
|
|
|
// WithVolumeServerClient executes a function with a volume server client connection
|
|
func (s *AdminServer) WithVolumeServerClient(address pb.ServerAddress, f func(client volume_server_pb.VolumeServerClient) error) error {
|
|
return operation.WithVolumeServerClient(false, address, s.grpcDialOption, func(client volume_server_pb.VolumeServerClient) error {
|
|
return f(client)
|
|
})
|
|
}
|
|
|
|
// GetMasterClient returns the admin server's wdclient.MasterClient. It is used
|
|
// by file browser download paths that stream chunks straight from the volume
|
|
// servers via filer.PrepareStreamContent so they keep working when the filer
|
|
// has -disableHttp=true.
|
|
func (s *AdminServer) GetMasterClient() *wdclient.MasterClient {
|
|
return s.masterClient
|
|
}
|
|
|
|
// GetGrpcDialOption returns the dial option used for all admin-originated
|
|
// gRPC connections (TLS or insecure). File browser uploads need this when
|
|
// they perform the assign + volume HTTP POST + create-entry flow.
|
|
func (s *AdminServer) GetGrpcDialOption() grpc.DialOption {
|
|
return s.grpcDialOption
|
|
}
|
|
|
|
// VolumeServerReadJwt mints a per-fileId Bearer token for reads against a
|
|
// volume server when jwt.signing.read.key is configured. The volume servers
|
|
// are unaware of jwt.filer_signing.read.key — that one only gates the filer
|
|
// HTTP surface, which this code path doesn't touch.
|
|
func VolumeServerReadJwt(fileId string) string {
|
|
v := util.GetViper()
|
|
signingKey := security.SigningKey(v.GetString("jwt.signing.read.key"))
|
|
if len(signingKey) == 0 {
|
|
return ""
|
|
}
|
|
expiresAfterSec := v.GetInt("jwt.signing.read.expires_after_seconds")
|
|
return string(security.GenJwtForVolumeServer(signingKey, expiresAfterSec, fileId))
|
|
}
|
|
|
|
// GetFilerAddress returns a filer address, discovering from masters if needed
|
|
func (s *AdminServer) GetFilerAddress() string {
|
|
// Discover filers from masters
|
|
filers := s.getDiscoveredFilers()
|
|
if len(filers) > 0 {
|
|
return filers[0] // Return the first available filer
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// getDiscoveredFilers returns cached filers or discovers them from masters
|
|
func (s *AdminServer) getDiscoveredFilers() []string {
|
|
// Check if cache is still valid
|
|
if time.Since(s.lastFilerUpdate) < s.filerCacheExpiration && len(s.cachedFilers) > 0 {
|
|
return s.cachedFilers
|
|
}
|
|
|
|
// Discover filers from masters
|
|
var filers []string
|
|
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
|
|
resp, err := client.ListClusterNodes(context.Background(), s.listClusterNodesRequest(cluster.FilerType))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, node := range resp.ClusterNodes {
|
|
filers = append(filers, node.Address)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
currentMaster := s.masterClient.GetMaster(context.Background())
|
|
glog.Warningf("Failed to discover filers from master %s: %v", currentMaster, err)
|
|
// Return cached filers even if expired, better than nothing
|
|
return s.cachedFilers
|
|
}
|
|
|
|
// Update cache
|
|
s.cachedFilers = filers
|
|
s.lastFilerUpdate = time.Now()
|
|
|
|
return filers
|
|
}
|
|
|
|
// GetAllFilers returns all discovered filers
|
|
func (s *AdminServer) GetAllFilers() []string {
|
|
return s.getDiscoveredFilers()
|
|
}
|