mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-10 22:08:48 +01:00
* vector: expose request metrics and registration-aware health endpoints * vector: report immutable read cache and storage fetch metrics * docs: describe vector engine health, metrics, and maintenance * vector: count canceled requests when their handler is dropped * vector: account for uncached large immutable storage reads * vector: reliably release the stalled credential reader in tests * vector: observe large reads as their streamed body is consumed * vector: bound heartbeats on established master registration streams * test: exercise hardened vector maintenance on Go and Rust volumes * vector: register engines in their configured filer group * vector: make retried index creation preserve compatible datasets * vector: allow registration to advertise a separate address * test: release stalled credential readers without blocking FIFO cleanup * test: allow hardened vector coverage in a named filer group * vector: discover engines in the same filer group as their gateway * vector: fall back when the advertised address is empty * admin: expose vector readiness, maintenance, and management tools (#12520) * s3vectors: share vector discovery with trusted management clients * admin: show vector engine readiness and recent maintenance activity * admin: suggest S3 Vectors IAM actions from the supported operations * admin: manage vector indexes, policies, tags, encryption, and query data * admin: validate SSE-S3 support before creating encrypted vector buckets * admin: discover vector engines in their registration group * admin: probe vector engines concurrently with independent time budgets * admin: verify encryption capability on the remote S3 gateway * admin: defer discovery until after the HTTP server can start * admin: distinguish unknown vector health services from failed engines * admin: count workers that can execute vector index maintenance * admin: allow read-only users to query vectors and inspect tags * vector: scope gateway and admin discovery to the configured filer group * admin: refresh policy state after mutations and serialize policy controls * vector: drain active RPCs before closing replaced discovery connections * vector: respect request cancellation while waiting for discovery * vector: prefer another registered engine after a transport failure * admin: check all masters before reporting no registered vector engines * iam: honor attached vector policies without broadening legacy grants * vector: allow short-lived discovery clients to close their connections * vector: refresh cached master addresses for each discovery lookup * iam: enforce vector bucket tag conditions in attached policies * iam: authorize vector bucket creation with the requested tags * test: exercise vector IAM grants and tag denies on Go and Rust volumes * admin: reject authenticated vector mutations without an admin role * test: run vector IAM assertions before the shared engine is stopped * vector: add native mini startup, shell tools, and a runnable Compose example (#12522) * s3vectors: share vector discovery with trusted management clients * admin: show vector engine readiness and recent maintenance activity * admin: suggest S3 Vectors IAM actions from the supported operations * admin: manage vector indexes, policies, tags, encryption, and query data * admin: validate SSE-S3 support before creating encrypted vector buckets * admin: discover vector engines in their registration group * admin: probe vector engines concurrently with independent time budgets * admin: verify encryption capability on the remote S3 gateway * admin: defer discovery until after the HTTP server can start * admin: distinguish unknown vector health services from failed engines * admin: count workers that can execute vector index maintenance * admin: allow read-only users to query vectors and inspect tags * vector: scope gateway and admin discovery to the configured filer group * admin: refresh policy state after mutations and serialize policy controls * vector: drain active RPCs before closing replaced discovery connections * vector: respect request cancellation while waiting for discovery * vector: prefer another registered engine after a transport failure * admin: check all masters before reporting no registered vector engines * iam: honor attached vector policies without broadening legacy grants * vector: allow short-lived discovery clients to close their connections * vector: refresh cached master addresses for each discovery lookup * iam: enforce vector bucket tag conditions in attached policies * iam: authorize vector bucket creation with the requested tags * test: exercise vector IAM grants and tag denies on Go and Rust volumes * admin: reject authenticated vector mutations without an admin role * test: run vector IAM assertions before the shared engine is stopped * mini: optionally start and supervise the vector engine * mini: bootstrap vector buckets with existing Lance catalog helpers * shell: manage vector buckets, indexes, and vector data * docs: provide a runnable vector and maintenance worker Compose stack * shell: scope vector discovery to the session and close its connections * mini: honor the bind address for vector and metrics listeners * mini: reserve vector ports before assigning gRPC and compatibility listeners * mini: wait for vector storage setup and master registration * mini: create requested vector buckets without requiring the Lance HTTP listener * shell: refresh master addresses during vector discovery * mini: resolve sibling vector executables with platform extensions * vector: preserve uncertain creates and fix mini listener allocation (#12525) * vector: retain catalog records when index creation has an unknown outcome * mini: keep chosen listener ports reserved throughout allocation * mini: resolve hostname binds before starting vector listeners * vector: fix integration test imports and recover interrupted index creation (#12533) * shell: register vector operations from the command package * s3vectors: resume interrupted index initialization safely * mini: exclude disabled WebDAV from listener allocation * s3vectors: retain uncertain creates across refused retries * s3vectors: claim pending indexes before retrying initialization * s3vectors: compare resolved distance metrics during recovery * s3vectors: preserve metadata keys when retrying initialization * s3vectors: verify ownership before initializing new catalog records
308 lines
11 KiB
Go
308 lines
11 KiB
Go
package dash
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/lance"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3vectors"
|
|
)
|
|
|
|
type S3VectorsBucketsData struct {
|
|
Username string `json:"username"`
|
|
Buckets []S3VectorsBucketSummary `json:"buckets"`
|
|
Operations VectorOperationsData `json:"operations"`
|
|
TotalBuckets int `json:"total_buckets"`
|
|
Truncated bool `json:"truncated,omitempty"`
|
|
LastUpdated time.Time `json:"last_updated"`
|
|
}
|
|
|
|
type S3VectorsBucketSummary struct {
|
|
Name string `json:"name"`
|
|
ARN string `json:"arn"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
// Encryption is the bucket's sseType, or empty when it declares none.
|
|
Encryption string `json:"encryption,omitempty"`
|
|
}
|
|
|
|
type S3VectorsBucketData struct {
|
|
Username string `json:"username"`
|
|
Bucket S3VectorsBucketSummary `json:"bucket"`
|
|
Policy string `json:"policy,omitempty"`
|
|
Indexes []*s3vectors.Index `json:"indexes"`
|
|
Operations VectorOperationsData `json:"operations"`
|
|
TotalIndexes int `json:"total_indexes"`
|
|
Truncated bool `json:"truncated,omitempty"`
|
|
LastUpdated time.Time `json:"last_updated"`
|
|
}
|
|
|
|
// The S3 Vectors operations reach the filer through the admin server's own
|
|
// filer connection.
|
|
type s3VectorsFilerClient struct{ s *AdminServer }
|
|
|
|
func (c s3VectorsFilerClient) WithFilerClient(_ bool, fn func(client filer_pb.SeaweedFilerClient) error) error {
|
|
return c.s.WithFilerClient(fn)
|
|
}
|
|
|
|
func newS3VectorsServer(s *AdminServer) *s3vectors.Server {
|
|
client := s3VectorsFilerClient{s}
|
|
backend := s3vectors.NewVectorDiscovery(s.masterClient.ListCachedMasters, s.grpcDialOption, s.filerGroup)
|
|
server := s3vectors.NewServer(client, nil, backend)
|
|
catalog := lance.NewServer(client, nil)
|
|
server.SetDescribe(func(ctx context.Context, id []string) (string, map[string]string, error) {
|
|
request := (&http.Request{Method: http.MethodPost, Header: make(http.Header)}).WithContext(ctx)
|
|
return catalog.DescribeTableInternal(request, id)
|
|
})
|
|
server.SetRegion(s3tables.DefaultRegion)
|
|
server.SetAccountID(s3tables.DefaultAccountID)
|
|
return server
|
|
}
|
|
|
|
func (s *AdminServer) executeS3VectorsOperation(ctx context.Context, operation string, req, resp interface{}) error {
|
|
return s.s3VectorsServer.Execute(ctx, operation, req, resp, s3_constants.AccountAdminId)
|
|
}
|
|
|
|
const s3VectorsAdminPage = 500
|
|
|
|
func (s *AdminServer) s3VectorsBucketSummary(ctx context.Context, name string) (S3VectorsBucketSummary, error) {
|
|
var resp s3vectors.GetVectorBucketResponse
|
|
if err := s.executeS3VectorsOperation(ctx, "GetVectorBucket", &s3vectors.GetVectorBucketRequest{VectorBucketName: name}, &resp); err != nil {
|
|
return S3VectorsBucketSummary{}, err
|
|
}
|
|
b := resp.VectorBucket
|
|
summary := S3VectorsBucketSummary{Name: b.VectorBucketName, ARN: b.VectorBucketArn, CreatedAt: b.CreationTime}
|
|
if sse, ok := b.EncryptionConfiguration["sseType"].(string); ok {
|
|
summary.Encryption = sse
|
|
}
|
|
return summary, nil
|
|
}
|
|
|
|
func (s *AdminServer) GetS3VectorsBucketsData(ctx context.Context) (S3VectorsBucketsData, error) {
|
|
data := S3VectorsBucketsData{Buckets: []S3VectorsBucketSummary{}}
|
|
page := s3VectorsAdminPage
|
|
req := &s3vectors.ListVectorBucketsRequest{MaxResults: &page}
|
|
for {
|
|
var resp s3vectors.ListVectorBucketsResponse
|
|
if err := s.executeS3VectorsOperation(ctx, "ListVectorBuckets", req, &resp); err != nil {
|
|
return S3VectorsBucketsData{}, err
|
|
}
|
|
for _, b := range resp.VectorBuckets {
|
|
if len(data.Buckets) == s3TablesAdminListLimit {
|
|
data.Truncated = true
|
|
break
|
|
}
|
|
summary, err := s.s3VectorsBucketSummary(ctx, b.VectorBucketName)
|
|
if isS3VectorsNotFound(err) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return S3VectorsBucketsData{}, err
|
|
}
|
|
data.Buckets = append(data.Buckets, summary)
|
|
}
|
|
if data.Truncated || resp.NextToken == "" {
|
|
break
|
|
}
|
|
req.NextToken = resp.NextToken
|
|
}
|
|
data.Operations = s.GetVectorOperationsData(ctx)
|
|
data.TotalBuckets = len(data.Buckets)
|
|
data.LastUpdated = time.Now()
|
|
return data, nil
|
|
}
|
|
|
|
func (s *AdminServer) GetS3VectorsBucketData(ctx context.Context, bucket string) (S3VectorsBucketData, error) {
|
|
summary, err := s.s3VectorsBucketSummary(ctx, bucket)
|
|
if err != nil {
|
|
return S3VectorsBucketData{}, err
|
|
}
|
|
data := S3VectorsBucketData{Bucket: summary, Indexes: []*s3vectors.Index{}, LastUpdated: time.Now()}
|
|
var policy s3vectors.GetVectorBucketPolicyResponse
|
|
switch err := s.executeS3VectorsOperation(ctx, "GetVectorBucketPolicy", &s3vectors.GetVectorBucketPolicyRequest{VectorBucketName: bucket}, &policy); {
|
|
case err == nil:
|
|
data.Policy = policy.Policy
|
|
case !isS3VectorsNotFound(err):
|
|
return S3VectorsBucketData{}, err
|
|
}
|
|
page := s3VectorsAdminPage
|
|
req := &s3vectors.ListIndexesRequest{VectorBucketName: bucket, MaxResults: &page}
|
|
for {
|
|
var resp s3vectors.ListIndexesResponse
|
|
if err := s.executeS3VectorsOperation(ctx, "ListIndexes", req, &resp); err != nil {
|
|
return S3VectorsBucketData{}, err
|
|
}
|
|
for _, summary := range resp.Indexes {
|
|
if len(data.Indexes) == s3TablesAdminListLimit {
|
|
data.Truncated = true
|
|
break
|
|
}
|
|
var index s3vectors.GetIndexResponse
|
|
err := s.executeS3VectorsOperation(ctx, "GetIndex", &s3vectors.GetIndexRequest{VectorBucketName: bucket, IndexName: summary.IndexName}, &index)
|
|
if isS3VectorsNotFound(err) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return S3VectorsBucketData{}, err
|
|
}
|
|
data.Indexes = append(data.Indexes, index.Index)
|
|
}
|
|
if data.Truncated || resp.NextToken == "" {
|
|
break
|
|
}
|
|
req.NextToken = resp.NextToken
|
|
}
|
|
data.Operations = s.GetVectorOperationsData(ctx)
|
|
data.TotalIndexes = len(data.Indexes)
|
|
return data, nil
|
|
}
|
|
|
|
func (s *AdminServer) ListS3VectorsBucketsAPI(w http.ResponseWriter, r *http.Request) {
|
|
data, err := s.GetS3VectorsBucketsData(r.Context())
|
|
if err != nil {
|
|
writeS3VectorsError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, data)
|
|
}
|
|
|
|
func (s *AdminServer) CreateS3VectorsBucket(w http.ResponseWriter, r *http.Request) {
|
|
if !requireSessionCSRFToken(w, r) {
|
|
return
|
|
}
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
EncryptionConfiguration map[string]interface{} `json:"encryptionConfiguration,omitempty"`
|
|
Tags map[string]string `json:"tags,omitempty"`
|
|
}
|
|
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
if req.EncryptionConfiguration["sseType"] == "AES256" && !s.vectorEncryptionConfigured(r.Context()) {
|
|
writeJSONError(w, http.StatusBadRequest, "Cannot verify SSE-S3 support on the S3 gateway; configure s3.public_endpoint and an SSE-S3 key on the gateway and vector engines")
|
|
return
|
|
}
|
|
var resp s3vectors.CreateVectorBucketResponse
|
|
if err := s.executeS3VectorsOperation(r.Context(), "CreateVectorBucket", &s3vectors.CreateVectorBucketRequest{VectorBucketName: req.Name, EncryptionConfiguration: req.EncryptionConfiguration, Tags: req.Tags}, &resp); err != nil {
|
|
writeS3VectorsError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, map[string]interface{}{"arn": resp.VectorBucketArn})
|
|
}
|
|
|
|
func (s *AdminServer) DeleteS3VectorsBucket(w http.ResponseWriter, r *http.Request) {
|
|
if !requireSessionCSRFToken(w, r) {
|
|
return
|
|
}
|
|
bucket := r.URL.Query().Get("bucket")
|
|
if bucket == "" {
|
|
writeJSONError(w, http.StatusBadRequest, "bucket query parameter is required")
|
|
return
|
|
}
|
|
if err := s.executeS3VectorsOperation(r.Context(), "DeleteVectorBucket", &s3vectors.DeleteVectorBucketRequest{VectorBucketName: bucket}, nil); err != nil {
|
|
writeS3VectorsError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"message": "Vector bucket deleted"})
|
|
}
|
|
|
|
func (s *AdminServer) GetS3VectorsBucketAPI(w http.ResponseWriter, r *http.Request) {
|
|
bucket := r.URL.Query().Get("bucket")
|
|
if bucket == "" {
|
|
writeJSONError(w, http.StatusBadRequest, "bucket query parameter is required")
|
|
return
|
|
}
|
|
data, err := s.GetS3VectorsBucketData(r.Context(), bucket)
|
|
if err != nil {
|
|
writeS3VectorsError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, data)
|
|
}
|
|
|
|
func (s *AdminServer) DeleteS3VectorsIndex(w http.ResponseWriter, r *http.Request) {
|
|
if !requireSessionCSRFToken(w, r) {
|
|
return
|
|
}
|
|
bucket, index := r.URL.Query().Get("bucket"), r.URL.Query().Get("index")
|
|
if bucket == "" || index == "" {
|
|
writeJSONError(w, http.StatusBadRequest, "bucket and index query parameters are required")
|
|
return
|
|
}
|
|
if err := s.executeS3VectorsOperation(r.Context(), "DeleteIndex", &s3vectors.DeleteIndexRequest{VectorBucketName: bucket, IndexName: index}, nil); err != nil {
|
|
writeS3VectorsError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{"message": "Index deleted"})
|
|
}
|
|
|
|
func isS3VectorsNotFound(err error) bool {
|
|
var e *s3vectors.Error
|
|
return errors.As(err, &e) && e.Code == "NotFoundException"
|
|
}
|
|
|
|
func writeS3VectorsError(w http.ResponseWriter, err error) {
|
|
var e *s3vectors.Error
|
|
if !errors.As(err, &e) {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
status := http.StatusInternalServerError
|
|
switch e.Code {
|
|
case "ValidationException":
|
|
status = http.StatusBadRequest
|
|
case "AccessDeniedException":
|
|
status = http.StatusForbidden
|
|
case "NotFoundException":
|
|
status = http.StatusNotFound
|
|
case "ConflictException":
|
|
status = http.StatusConflict
|
|
case "ServiceUnavailableException":
|
|
status = http.StatusServiceUnavailable
|
|
}
|
|
writeJSONError(w, status, e.Message)
|
|
}
|
|
|
|
func (s *AdminServer) ExecuteS3VectorsOperationAPI(w http.ResponseWriter, r *http.Request) {
|
|
if !requireSessionCSRFToken(w, r) {
|
|
return
|
|
}
|
|
var req struct {
|
|
Operation string `json:"operation"`
|
|
Request json.RawMessage `json:"request"`
|
|
}
|
|
if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error())
|
|
return
|
|
}
|
|
switch req.Operation {
|
|
case "CreateIndex", "PutVectorBucketPolicy", "DeleteVectorBucketPolicy", "TagResource", "UntagResource", "ListTagsForResource", "PutVectors", "GetVectors", "ListVectors", "DeleteVectors", "QueryVectors":
|
|
default:
|
|
writeJSONError(w, http.StatusBadRequest, "Unsupported vector operation")
|
|
return
|
|
}
|
|
switch req.Operation {
|
|
case "QueryVectors", "GetVectors", "ListVectors", "ListTagsForResource":
|
|
default:
|
|
if role := RoleFromContext(r.Context()); role != "admin" && (role != "" || UsernameFromContext(r.Context()) != "") {
|
|
writeJSONError(w, http.StatusForbidden, "This operation requires admin access")
|
|
return
|
|
}
|
|
}
|
|
var resp json.RawMessage
|
|
if err := s.executeS3VectorsOperation(r.Context(), req.Operation, req.Request, &resp); err != nil {
|
|
writeS3VectorsError(w, err)
|
|
return
|
|
}
|
|
if len(resp) == 0 {
|
|
resp = json.RawMessage(`{}`)
|
|
}
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|