From a074538c8f8331bf1f711909480f026c59f7645e Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Mon, 2 Nov 2026 11:25:24 -0800 Subject: [PATCH] admin: manage S3 vector buckets (#12334) * s3vectors: run an operation for trusted local tooling Like s3tables.Manager.Execute, Server.Execute runs one S3 Vectors operation through its handler as a given identity, without a signed request. The admin console can then manage vector buckets through the same validation, authorization and catalog steps as the API. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * admin: list, inspect, create and delete vector buckets over the API /api/s3vectors runs the S3 Vectors operations through Server.Execute as the admin account: list vector buckets with their encryption, read one bucket with its policy and each index's data type, dimension, distance metric and non-filterable keys, create and delete a bucket, and delete an index. Going through the operations keeps the API's rules, such as ICEBERG buckets not being vector buckets and the implicit index namespace not blocking a delete. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * s3vectors: export the rule for which table buckets are vector buckets IsVectorBucketFormat names the check ListVectorBuckets and isVectorBucket already make, so the admin console can tell the same buckets apart. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * admin: vector bucket pages Object Store > Vector Buckets lists the vector buckets with their ARN, encryption and creation time, creates and deletes them, and opens one bucket with its indexes (data type, dimension, distance metric, non-filterable keys) and its policy, where an index can be deleted. A LANCE row on the Table Buckets page links to the same bucket's vector page, since both APIs serve it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * admin: stop the vector bucket and index lists at the list limit A page cut short by filtered ICEBERG buckets let the next page carry the list past s3TablesAdminListLimit. Co-Authored-By: Claude Opus 5.5 * admin: say when a vector bucket or index list is cut at the limit The lists stop at s3TablesAdminListLimit while more remain, so mark them truncated and point to an S3 Vectors client for the rest. Co-Authored-By: Claude Opus 5.5 * admin: indent the vector bucket policy A policy stored as one line ran off the right edge of the details page. Policies that are not valid JSON are shown as stored. Co-Authored-By: Claude Opus 5.5 * admin: show a deeply nested vector bucket policy as stored Indenting multiplies the size of a deeply nested document, so a policy past 32 levels of nesting is shown without indenting. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- weed/admin/dash/admin_server.go | 4 + weed/admin/dash/s3vectors_management.go | 254 +++++++++ weed/admin/dash/s3vectors_management_test.go | 174 +++++++ weed/admin/handlers/admin_handlers.go | 50 ++ weed/admin/view/app/s3tables_buckets.templ | 4 + weed/admin/view/app/s3tables_buckets_templ.go | 290 ++++++----- .../view/app/s3vectors_bucket_details.templ | 210 ++++++++ .../app/s3vectors_bucket_details_templ.go | 392 ++++++++++++++ weed/admin/view/app/s3vectors_buckets.templ | 189 +++++++ .../admin/view/app/s3vectors_buckets_templ.go | 202 ++++++++ weed/admin/view/app/s3vectors_buckets_test.go | 80 +++ weed/admin/view/layout/layout.templ | 5 + weed/admin/view/layout/layout_templ.go | 485 +++++++++--------- weed/s3api/s3vectors/execute.go | 54 ++ weed/s3api/s3vectors/execute_test.go | 66 +++ weed/s3api/s3vectors/handlers.go | 14 +- 16 files changed, 2099 insertions(+), 374 deletions(-) create mode 100644 weed/admin/dash/s3vectors_management.go create mode 100644 weed/admin/dash/s3vectors_management_test.go create mode 100644 weed/admin/view/app/s3vectors_bucket_details.templ create mode 100644 weed/admin/view/app/s3vectors_bucket_details_templ.go create mode 100644 weed/admin/view/app/s3vectors_buckets.templ create mode 100644 weed/admin/view/app/s3vectors_buckets_templ.go create mode 100644 weed/admin/view/app/s3vectors_buckets_test.go create mode 100644 weed/s3api/s3vectors/execute.go create mode 100644 weed/s3api/s3vectors/execute_test.go diff --git a/weed/admin/dash/admin_server.go b/weed/admin/dash/admin_server.go index 9c3eb6696f..eadd89b3c9 100644 --- a/weed/admin/dash/admin_server.go +++ b/weed/admin/dash/admin_server.go @@ -43,6 +43,7 @@ import ( "github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle" "github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle/scheduler" "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3vectors" "github.com/seaweedfs/seaweedfs/weed/worker/tasks" _ "github.com/seaweedfs/seaweedfs/weed/credential/grpc" // Register gRPC credential store @@ -167,6 +168,7 @@ type AdminServer struct { collectionStatsCacheThreshold time.Duration s3TablesManager *s3tables.Manager + s3VectorsServer *s3vectors.Server icebergPort int lancePort int @@ -220,6 +222,8 @@ func NewAdminServer(masters string, filerGroup string, templateFS http.FileSyste bgCancel: bgCancel, } + server.s3VectorsServer = newS3VectorsServer(server) + // Initialize topic retention purger server.topicRetentionPurger = NewTopicRetentionPurger(server) diff --git a/weed/admin/dash/s3vectors_management.go b/weed/admin/dash/s3vectors_management.go new file mode 100644 index 0000000000..58204fd2bb --- /dev/null +++ b/weed/admin/dash/s3vectors_management.go @@ -0,0 +1,254 @@ +package dash + +import ( + "context" + "errors" + "net/http" + "time" + + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" + "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"` + 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"` + 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) +} + +// newS3VectorsServer serves the catalog side of S3 Vectors for the admin +// console. It has no vector engine, so it never runs an operation that needs +// one, such as CreateIndex. +func newS3VectorsServer(s *AdminServer) *s3vectors.Server { + server := s3vectors.NewServer(s3VectorsFilerClient{s}, nil, nil) + 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.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.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"` + } + if err := decodeJSONBody(newJSONMaxReader(w, r), &req); err != nil { + writeJSONError(w, http.StatusBadRequest, "Invalid request: "+err.Error()) + return + } + var resp s3vectors.CreateVectorBucketResponse + if err := s.executeS3VectorsOperation(r.Context(), "CreateVectorBucket", &s3vectors.CreateVectorBucketRequest{VectorBucketName: req.Name}, &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) +} diff --git a/weed/admin/dash/s3vectors_management_test.go b/weed/admin/dash/s3vectors_management_test.go new file mode 100644 index 0000000000..bd0c36a65d --- /dev/null +++ b/weed/admin/dash/s3vectors_management_test.go @@ -0,0 +1,174 @@ +package dash + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables/s3tablestest" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3vectors" +) + +func newS3VectorsTestServer(t *testing.T) (*AdminServer, *s3tables.ManagerClient) { + t.Helper() + fs := s3tablestest.Start(t) + client := s3tables.NewManagerClient(fs.Client) + server := s3vectors.NewServer(client, nil, nil) + server.SetRegion(s3tables.DefaultRegion) + server.SetAccountID(s3tables.DefaultAccountID) + return &AdminServer{s3VectorsServer: server}, client +} + +// createS3VectorsIndexRecord writes the catalog record CreateIndex leaves +// behind; the admin server has no engine to run CreateIndex itself. +func createS3VectorsIndexRecord(t *testing.T, client *s3tables.ManagerClient, bucket, index string, metadata map[string]string) { + t.Helper() + m := s3tables.NewManager() + m.SetTrusted(true) + arn := "arn:aws:s3tables:us-east-1:000000000000:bucket/" + bucket + ctx := context.Background() + err := m.Execute(ctx, client, "CreateNamespace", &s3tables.CreateNamespaceRequest{TableBucketARN: arn, Namespace: []string{s3tables.VectorIndexNamespace}}, nil, s3_constants.AccountAdminId) + if err != nil && !strings.Contains(err.Error(), "already exists") { + t.Fatalf("create namespace: %v", err) + } + full, _ := json.Marshal(metadata) + if err := m.Execute(ctx, client, "CreateTable", &s3tables.CreateTableRequest{ + TableBucketARN: arn, Namespace: []string{s3tables.VectorIndexNamespace}, Name: index, + Format: s3tables.FormatLance, Metadata: &s3tables.TableMetadata{FullMetadata: full}, + }, nil, s3_constants.AccountAdminId); err != nil { + t.Fatalf("create index record: %v", err) + } +} + +func s3VectorsRequest(t *testing.T, handler http.HandlerFunc, method, target, body string) *httptest.ResponseRecorder { + t.Helper() + w := httptest.NewRecorder() + handler(w, httptest.NewRequest(method, target, strings.NewReader(body))) + return w +} + +func TestS3VectorsBucketsListsOnlyVectorBuckets(t *testing.T) { + s, client := newS3VectorsTestServer(t) + ctx := context.Background() + if w := s3VectorsRequest(t, s.CreateS3VectorsBucket, http.MethodPost, "/api/s3vectors/buckets", `{"name":"media"}`); w.Code != http.StatusCreated { + t.Fatalf("create: %d %s", w.Code, w.Body) + } + if err := s.executeS3VectorsOperation(ctx, "CreateVectorBucket", &s3vectors.CreateVectorBucketRequest{ + VectorBucketName: "sealed", EncryptionConfiguration: map[string]interface{}{"sseType": "AES256"}, + }, nil); err != nil { + t.Fatalf("create sealed: %v", err) + } + if err := s3tables.NewManager().Execute(ctx, client, "CreateTableBucket", &s3tables.CreateTableBucketRequest{ + Name: "warehouse", Format: s3tables.FormatIceberg, + }, nil, s3_constants.AccountAdminId); err != nil { + t.Fatalf("create iceberg bucket: %v", err) + } + + data, err := s.GetS3VectorsBucketsData(ctx) + if err != nil { + t.Fatal(err) + } + got := map[string]string{} + for _, b := range data.Buckets { + got[b.Name] = b.Encryption + if want := "arn:aws:s3vectors:us-east-1:000000000000:bucket/" + b.Name; b.ARN != want || b.CreatedAt.IsZero() { + t.Errorf("%s: arn %q created %v, want %q and a time", b.Name, b.ARN, b.CreatedAt, want) + } + } + if len(got) != 2 || got["media"] != "" || got["sealed"] != "AES256" || data.TotalBuckets != 2 || data.Truncated { + t.Fatalf("buckets %v (total %d), want media and sealed (AES256) without the iceberg bucket", got, data.TotalBuckets) + } +} + +func TestS3VectorsBucketShowsIndexesAndPolicy(t *testing.T) { + s, client := newS3VectorsTestServer(t) + ctx := context.Background() + if err := s.executeS3VectorsOperation(ctx, "CreateVectorBucket", &s3vectors.CreateVectorBucketRequest{VectorBucketName: "media"}, nil); err != nil { + t.Fatal(err) + } + createS3VectorsIndexRecord(t, client, "media", "movies", map[string]string{ + "dimension": "3", "distanceMetric": "cosine", "nonFilterableMetadataKeys": `["summary"]`, + }) + policy := `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":"*","Action":"s3vectors:QueryVectors","Resource":"*"}]}` + if err := s.executeS3VectorsOperation(ctx, "PutVectorBucketPolicy", &s3vectors.PutVectorBucketPolicyRequest{VectorBucketName: "media", Policy: policy}, nil); err != nil { + t.Fatal(err) + } + + w := s3VectorsRequest(t, s.GetS3VectorsBucketAPI, http.MethodGet, "/api/s3vectors/indexes?bucket=media", "") + if w.Code != http.StatusOK { + t.Fatalf("get: %d %s", w.Code, w.Body) + } + var data S3VectorsBucketData + if err := json.Unmarshal(w.Body.Bytes(), &data); err != nil { + t.Fatal(err) + } + if data.Policy != policy { + t.Errorf("policy %q, want %q", data.Policy, policy) + } + if data.TotalIndexes != 1 { + t.Fatalf("indexes %+v", data.Indexes) + } + idx := data.Indexes[0] + if idx.IndexName != "movies" || idx.Dimension != 3 || idx.DistanceMetric != "cosine" || idx.DataType != "float32" || + idx.MetadataConfiguration == nil || len(idx.MetadataConfiguration.NonFilterableMetadataKeys) != 1 { + t.Fatalf("index %+v", idx) + } + + if w := s3VectorsRequest(t, s.GetS3VectorsBucketAPI, http.MethodGet, "/api/s3vectors/indexes?bucket=missing", ""); w.Code != http.StatusNotFound { + t.Fatalf("missing bucket: %d, want 404", w.Code) + } +} + +func TestS3VectorsDeleteIndexThenBucket(t *testing.T) { + s, client := newS3VectorsTestServer(t) + ctx := context.Background() + if err := s.executeS3VectorsOperation(ctx, "CreateVectorBucket", &s3vectors.CreateVectorBucketRequest{VectorBucketName: "media"}, nil); err != nil { + t.Fatal(err) + } + createS3VectorsIndexRecord(t, client, "media", "movies", map[string]string{"dimension": "3", "distanceMetric": "cosine"}) + + if w := s3VectorsRequest(t, s.DeleteS3VectorsBucket, http.MethodDelete, "/api/s3vectors/buckets?bucket=media", ""); w.Code != http.StatusConflict { + t.Fatalf("delete a bucket with an index: %d %s, want 409", w.Code, w.Body) + } + if w := s3VectorsRequest(t, s.DeleteS3VectorsIndex, http.MethodDelete, "/api/s3vectors/indexes?bucket=media&index=movies", ""); w.Code != http.StatusOK { + t.Fatalf("delete index: %d %s", w.Code, w.Body) + } + // The emptied index namespace does not keep the bucket alive. + if w := s3VectorsRequest(t, s.DeleteS3VectorsBucket, http.MethodDelete, "/api/s3vectors/buckets?bucket=media", ""); w.Code != http.StatusOK { + t.Fatalf("delete bucket: %d %s", w.Code, w.Body) + } + data, err := s.GetS3VectorsBucketsData(ctx) + if err != nil || data.TotalBuckets != 0 { + t.Fatalf("after delete: %+v, %v", data.Buckets, err) + } +} + +func TestS3VectorsBucketsStopAtTheListLimit(t *testing.T) { + s, client := newS3VectorsTestServer(t) + ctx := context.Background() + // The ICEBERG bucket leaves the first page short, so the last page + // would carry the list past the limit. + if err := s3tables.NewManager().Execute(ctx, client, "CreateTableBucket", &s3tables.CreateTableBucketRequest{ + Name: "iceberg", Format: s3tables.FormatIceberg, + }, nil, s3_constants.AccountAdminId); err != nil { + t.Fatal(err) + } + for i := 0; i <= s3TablesAdminListLimit; i++ { + if err := s.executeS3VectorsOperation(ctx, "CreateVectorBucket", &s3vectors.CreateVectorBucketRequest{VectorBucketName: fmt.Sprintf("media-%04d", i)}, nil); err != nil { + t.Fatal(err) + } + } + data, err := s.GetS3VectorsBucketsData(ctx) + if err != nil { + t.Fatal(err) + } + if data.TotalBuckets != s3TablesAdminListLimit || !data.Truncated { + t.Fatalf("listed %d buckets (truncated %v), want the limit %d and truncated", data.TotalBuckets, data.Truncated, s3TablesAdminListLimit) + } +} diff --git a/weed/admin/handlers/admin_handlers.go b/weed/admin/handlers/admin_handlers.go index 007d1e14b1..80018e1d76 100644 --- a/weed/admin/handlers/admin_handlers.go +++ b/weed/admin/handlers/admin_handlers.go @@ -1,6 +1,7 @@ package handlers import ( + "errors" "net/http" "net/url" "strconv" @@ -14,6 +15,7 @@ import ( "github.com/seaweedfs/seaweedfs/weed/admin/view/layout" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3vectors" "github.com/seaweedfs/seaweedfs/weed/stats" ) @@ -111,6 +113,8 @@ func (h *AdminHandlers) registerUIRoutes(r *mux.Router) { r.HandleFunc("/object-store/groups", h.groupHandlers.ShowGroups).Methods(http.MethodGet) r.HandleFunc("/object-store/service-accounts", h.serviceAccountHandlers.ShowServiceAccounts).Methods(http.MethodGet) r.HandleFunc("/object-store/s3tables/buckets", h.ShowS3TablesBuckets).Methods(http.MethodGet) + r.HandleFunc("/object-store/vector-buckets", h.ShowS3VectorsBuckets).Methods(http.MethodGet) + r.HandleFunc("/object-store/vector-buckets/{bucket}", h.ShowS3VectorsBucketDetails).Methods(http.MethodGet) r.HandleFunc("/object-store/s3tables/buckets/{bucket}/namespaces", h.ShowS3TablesNamespaces).Methods(http.MethodGet) r.HandleFunc("/object-store/s3tables/buckets/{bucket}/namespaces/{namespace}/tables", h.ShowS3TablesTables).Methods(http.MethodGet) r.HandleFunc("/object-store/s3tables/buckets/{bucket}/namespaces/{namespace}/tables/{table}", h.ShowS3TablesTableDetails).Methods(http.MethodGet) @@ -251,6 +255,13 @@ func (h *AdminHandlers) registerAPIRoutes(api *mux.Router, enforceWrite bool) { s3TablesApi.HandleFunc("/tags", h.adminServer.ListS3TablesTags).Methods(http.MethodGet) s3TablesApi.Handle("/tags", wrapWrite(h.adminServer.UntagS3TablesResource)).Methods(http.MethodDelete) + s3VectorsApi := api.PathPrefix("/s3vectors").Subrouter() + s3VectorsApi.HandleFunc("/buckets", h.adminServer.ListS3VectorsBucketsAPI).Methods(http.MethodGet) + s3VectorsApi.Handle("/buckets", wrapWrite(h.adminServer.CreateS3VectorsBucket)).Methods(http.MethodPost) + s3VectorsApi.Handle("/buckets", wrapWrite(h.adminServer.DeleteS3VectorsBucket)).Methods(http.MethodDelete) + s3VectorsApi.HandleFunc("/indexes", h.adminServer.GetS3VectorsBucketAPI).Methods(http.MethodGet) + s3VectorsApi.Handle("/indexes", wrapWrite(h.adminServer.DeleteS3VectorsIndex)).Methods(http.MethodDelete) + filesApi := api.PathPrefix("/files").Subrouter() filesApi.Handle("/delete", wrapWrite(h.fileBrowserHandlers.DeleteFile)).Methods(http.MethodDelete) filesApi.Handle("/delete-multiple", wrapWrite(h.fileBrowserHandlers.DeleteMultipleFiles)).Methods(http.MethodDelete) @@ -373,6 +384,45 @@ func (h *AdminHandlers) ShowS3TablesBuckets(w http.ResponseWriter, r *http.Reque } } +// ShowS3VectorsBuckets renders the vector buckets page +func (h *AdminHandlers) ShowS3VectorsBuckets(w http.ResponseWriter, r *http.Request) { + username := h.getUsername(r) + data, err := h.adminServer.GetS3VectorsBucketsData(r.Context()) + if err != nil { + writeJSONError(w, http.StatusInternalServerError, "Failed to get vector buckets: "+err.Error()) + return + } + data.Username = username + + w.Header().Set("Content-Type", "text/html") + viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context())) + if err := layout.Layout(viewCtx, app.S3VectorsBuckets(data)).Render(r.Context(), w); err != nil { + writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error()) + } +} + +// ShowS3VectorsBucketDetails renders one vector bucket with its indexes and policy +func (h *AdminHandlers) ShowS3VectorsBucketDetails(w http.ResponseWriter, r *http.Request) { + username := h.getUsername(r) + data, err := h.adminServer.GetS3VectorsBucketData(r.Context(), mux.Vars(r)["bucket"]) + if err != nil { + status := http.StatusInternalServerError + var e *s3vectors.Error + if errors.As(err, &e) && e.Code == "NotFoundException" { + status = http.StatusNotFound + } + writeJSONError(w, status, "Failed to get vector bucket: "+err.Error()) + return + } + data.Username = username + + w.Header().Set("Content-Type", "text/html") + viewCtx := layout.NewViewContext(r, username, dash.CSRFTokenFromContext(r.Context())) + if err := layout.Layout(viewCtx, app.S3VectorsBucketDetails(data)).Render(r.Context(), w); err != nil { + writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error()) + } +} + // ShowS3TablesNamespaces renders namespaces for a table bucket func (h *AdminHandlers) ShowS3TablesNamespaces(w http.ResponseWriter, r *http.Request) { username := h.getUsername(r) diff --git a/weed/admin/view/app/s3tables_buckets.templ b/weed/admin/view/app/s3tables_buckets.templ index 6533e32545..04ee616219 100644 --- a/weed/admin/view/app/s3tables_buckets.templ +++ b/weed/admin/view/app/s3tables_buckets.templ @@ -6,6 +6,7 @@ import ( "github.com/seaweedfs/seaweedfs/weed/admin/dash" "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3vectors" ) templ S3TablesBuckets(data dash.S3TablesBucketsData) { @@ -160,6 +161,9 @@ templ S3TablesBuckets(data dash.S3TablesBucketsData) { { bucket.Name } { formatLabel(bucket.Format) } + if s3vectors.IsVectorBucketFormat(bucket.Format) { + vector bucket + } { bucket.OwnerAccountID } { bucket.ARN } diff --git a/weed/admin/view/app/s3tables_buckets_templ.go b/weed/admin/view/app/s3tables_buckets_templ.go index f469bf8481..1829d8083a 100644 --- a/weed/admin/view/app/s3tables_buckets_templ.go +++ b/weed/admin/view/app/s3tables_buckets_templ.go @@ -15,6 +15,7 @@ import ( "github.com/seaweedfs/seaweedfs/weed/admin/dash" "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3vectors" ) func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { @@ -46,7 +47,7 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templruntime.NewAttributeExpression(templ.SafeURL(fmt.Sprintf("//localhost:%d/v1/config", data.IcebergPort))).ResolveURL(ctx) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 24, Col: 119} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 25, Col: 119} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2) if templ_7745c5c3_Err != nil { @@ -65,7 +66,7 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templruntime.NewAttributeExpression(templ.SafeURL(fmt.Sprintf("//localhost:%d/v1/table", data.LancePort))).ResolveURL(ctx) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 31, Col: 114} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 32, Col: 114} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3) if templ_7745c5c3_Err != nil { @@ -83,7 +84,7 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templruntime.NewAttributeExpression(fmt.Sprintf("%d", data.IcebergPort)).Resolve(ctx) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 38, Col: 91} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 39, Col: 91} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4) if templ_7745c5c3_Err != nil { @@ -96,7 +97,7 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templruntime.NewAttributeExpression(fmt.Sprintf("%d", data.LancePort)).Resolve(ctx) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 38, Col: 145} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 39, Col: 145} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5) if templ_7745c5c3_Err != nil { @@ -119,7 +120,7 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templruntime.NewAttributeExpression(fmt.Sprintf("%d", data.IcebergPort)).Resolve(ctx) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 52, Col: 105} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 53, Col: 105} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) if templ_7745c5c3_Err != nil { @@ -132,7 +133,7 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("http://localhost:%d", data.IcebergPort)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 52, Col: 162} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 53, Col: 162} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -151,7 +152,7 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templruntime.NewAttributeExpression(fmt.Sprintf("%d", data.LancePort)).Resolve(ctx) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 58, Col: 103} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 59, Col: 103} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8) if templ_7745c5c3_Err != nil { @@ -164,7 +165,7 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("http://localhost:%d", data.LancePort)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 58, Col: 158} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 59, Col: 158} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { @@ -193,7 +194,7 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalBuckets)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 81, Col: 47} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 82, Col: 47} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -206,7 +207,7 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var11 string templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 100, Col: 54} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 101, Col: 54} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { @@ -220,7 +221,7 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.IcebergPort)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 120, Col: 47} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 121, Col: 47} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -244,7 +245,7 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 160, Col: 28} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 161, Col: 28} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -283,7 +284,7 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var16 string templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.NewAttributeExpression(formatBadgeTitle(bucket.Format)).Resolve(ctx) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 162, Col: 99} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 163, Col: 99} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16) if templ_7745c5c3_Err != nil { @@ -310,220 +311,243 @@ func S3TablesBuckets(data dash.S3TablesBucketsData) templ.Component { var templ_7745c5c3_Var17 string templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(formatLabel(bucket.Format)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 162, Col: 130} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 163, Col: 130} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.OwnerAccountID) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 164, Col: 38} + if s3vectors.IsVectorBucketFormat(bucket.Format) { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "vector bucket") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var19 string - templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.ARN) + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.OwnerAccountID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 165, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 168, Col: 38} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var20 string - templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(bucketCatalogPath(bucket.Format, bucket.Name)) + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.ARN) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 166, Col: 82} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 169, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.CreatedAt.Format("2006-01-02 15:04")) + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(bucketCatalogPath(bucket.Format, bucket.Name)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 167, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 170, Col: 82} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.CreatedAt.Format("2006-01-02 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 171, Col: 60} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if bucket.PolicyStatementCount > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "Not configured") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "Not configured") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } bucketName, parseErr := s3tables.ParseBucketNameFromARN(bucket.ARN) if parseErr == nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\"> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\" data-bucket-name=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var29 string + templ_7745c5c3_Var29, templ_7745c5c3_Err = templruntime.NewAttributeExpression(bucket.Name).Resolve(ctx) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 204, Col: 161} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\" title=\"Delete\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if len(data.Buckets) == 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
No table buckets found

Create your first S3 Tables bucket to get started.

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
No table buckets found

Create your first S3 Tables bucket to get started.

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.IcebergPort > 0 || data.LancePort > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
Client Examples
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
Client Examples
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.IcebergPort > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
ICEBERGDuckDB
")
+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
ICEBERGDuckDB
")
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				var templ_7745c5c3_Var29 string
-				templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(`INSTALL iceberg;
+				var templ_7745c5c3_Var30 string
+				templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(`INSTALL iceberg;
 LOAD iceberg;
 
 CREATE SECRET (
@@ -534,18 +558,18 @@ CREATE SECRET (
 
 SELECT * FROM iceberg_scan('s3://my-table-bucket/my-namespace/my-table');`)
 				if templ_7745c5c3_Err != nil {
-					return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 251, Col: 74}
+					return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 255, Col: 74}
 				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
ICEBERGPython (PyIceberg)
")
+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
ICEBERGPython (PyIceberg)
")
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				var templ_7745c5c3_Var30 string
-				templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(`from pyiceberg.catalog import load_catalog
+				var templ_7745c5c3_Var31 string
+				templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(`from pyiceberg.catalog import load_catalog
 
 catalog = load_catalog(
     name="seaweedfs",
@@ -558,13 +582,13 @@ catalog = load_catalog(
 
 namespaces = catalog.list_namespaces()`)
 				if templ_7745c5c3_Err != nil {
-					return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 268, Col: 39}
+					return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 272, Col: 39}
 				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -574,25 +598,25 @@ namespaces = catalog.list_namespaces()`) templ_7745c5c3_ElementWriter := templ_7745c5c3_Buffer templ_7745c5c3_Buffer := templ.GetBuffer() var templ_7745c5c3_ElementScripts []templ.ComponentScript - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
0)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_ElementWriter, templ_7745c5c3_Var31...) + var templ_7745c5c3_Var32 = []any{templ.KV("mt-4", data.IcebergPort > 0)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_ElementWriter, templ_7745c5c3_Var32...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var32 string - templ_7745c5c3_Var32, templ_7745c5c3_Err = templruntime.NewAttributeExpression(templ.CSSClasses(templ_7745c5c3_Var31).String()).Resolve(ctx) + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, templ_7745c5c3_Err = templruntime.NewAttributeExpression(templ.CSSClasses(templ_7745c5c3_Var32).String()).Resolve(ctx) if templ_7745c5c3_Err != nil { return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 1, Col: 0} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -606,30 +630,30 @@ namespaces = catalog.list_namespaces()`) } templ.ReleaseBuffer(templ_7745c5c3_Buffer) } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, ">LANCEPython (lance-namespace)
")
+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, ">LANCEPython (lance-namespace)
")
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				var templ_7745c5c3_Var33 string
-				templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(`from lance_namespace import connect
+				var templ_7745c5c3_Var34 string
+				templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(`from lance_namespace import connect
 
 ns = connect("rest", {"uri": "http://localhost:` + fmt.Sprintf("%d", data.LancePort) + `"})
 
 ns.list_namespaces(id=["my-table-bucket"])
 ns.describe_table(id=["my-table-bucket", "my-namespace", "my-table"])`)
 				if templ_7745c5c3_Err != nil {
-					return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 281, Col: 70}
+					return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 285, Col: 70}
 				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "
LANCEPython (pylance)
")
+				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "
LANCEPython (pylance)
")
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				var templ_7745c5c3_Var34 string
-				templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(`import lance
+				var templ_7745c5c3_Var35 string
+				templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(`import lance
 from lance_namespace import connect
 
 ns = connect("rest", {"uri": "http://localhost:` + fmt.Sprintf("%d", data.LancePort) + `"})
@@ -638,84 +662,84 @@ table = ns.describe_table(id=["my-table-bucket", "my-namespace", "my-table"])
 # The namespace vends both the location and the credentials to read it.
 dataset = lance.dataset(table.location, storage_options=table.storage_options)`)
 				if templ_7745c5c3_Err != nil {
-					return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 294, Col: 79}
+					return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 298, Col: 79}
 				}
-				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
+				_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
 				if templ_7745c5c3_Err != nil {
 					return templ_7745c5c3_Err
 				}
-				templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "
Create Table Bucket
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "
Create Table Bucket
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.IcebergPort > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if data.LancePort > 0 && data.IcebergPort <= 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else if data.LancePort > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "
Lance Served over the Lance Namespace API. pylance, LanceDB, Ray.
A bucket holds one format. Tables of the other are refused.
The S3 identity that owns this table bucket. Non-admin users can only access table buckets they own.
Optional tags in key=value format.
Delete Table Bucket

Are you sure you want to delete the table bucket ?

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\" data-lance-port=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var37 string + templ_7745c5c3_Var37, templ_7745c5c3_Err = templruntime.NewAttributeExpression(fmt.Sprintf("%d", data.LancePort)).Resolve(ctx) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3tables_buckets.templ`, Line: 367, Col: 169} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\">A bucket holds one format. Tables of the other are refused.
The S3 identity that owns this table bucket. Non-admin users can only access table buckets they own.
Optional tags in key=value format.
Delete Table Bucket

Are you sure you want to delete the table bucket ?

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -723,7 +747,7 @@ dataset = lance.dataset(table.location, storage_options=table.storage_options)`) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "
Table Bucket Policy
Provide a policy JSON; use Delete Policy to remove the policy.
Resource Tags
Loading...
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "
Table Bucket Policy
Provide a policy JSON; use Delete Policy to remove the policy.
Resource Tags
Loading...
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/admin/view/app/s3vectors_bucket_details.templ b/weed/admin/view/app/s3vectors_bucket_details.templ new file mode 100644 index 0000000000..8a6bf1c21d --- /dev/null +++ b/weed/admin/view/app/s3vectors_bucket_details.templ @@ -0,0 +1,210 @@ +package app + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + "github.com/seaweedfs/seaweedfs/weed/admin/dash" +) + +templ S3VectorsBucketDetails(data dash.S3VectorsBucketData) { +
+
+

+ { data.Bucket.Name } +

+ +
+
+
+
+
+
ARN
+
{ data.Bucket.ARN }
+
Created
+
{ data.Bucket.CreatedAt.Format("2006-01-02 15:04:05") }
+
Encryption
+
+ if data.Bucket.Encryption != "" { + { data.Bucket.Encryption } + } else { + None + } +
+
+
+
+
+
+
+ { fmt.Sprintf("%d indexes", data.TotalIndexes) } + if data.Truncated { + { fmt.Sprintf("showing the first %d; list the rest with an S3 Vectors client", data.TotalIndexes) } + } +
+
+
+
+ + + + + + + + + + + + + + for _, index := range data.Indexes { + + + + + + + + + + } + if len(data.Indexes) == 0 { + + + + } + +
NameData TypeDimensionDistance MetricNon-filterable Metadata KeysCreatedActions
+ { index.IndexName } +
{ index.IndexArn }
+
{ index.DataType }{ fmt.Sprintf("%d", index.Dimension) }{ index.DistanceMetric } + if index.MetadataConfiguration != nil && len(index.MetadataConfiguration.NonFilterableMetadataKeys) > 0 { + { strings.Join(index.MetadataConfiguration.NonFilterableMetadataKeys, ", ") } + } else { + None + } + { index.CreationTime.Format("2006-01-02 15:04") } + +
+ No indexes. Create one with an S3 Vectors client, e.g. aws s3vectors create-index. +
+
+
+
+
+
+
+ Bucket Policy +
+
+
+ if data.Policy != "" { +
{ indentJSON(data.Policy) }
+ } else { + No policy. Set one with PutVectorBucketPolicy. + } +
+
+ +
+ +} + +const maxIndentedJSONDepth = 32 + +func indentJSON(s string) string { + if jsonDepth(s) > maxIndentedJSONDepth { + return s + } + var out bytes.Buffer + if json.Indent(&out, []byte(s), "", " ") != nil { + return s + } + return out.String() +} + +func jsonDepth(s string) int { + depth, maxDepth := 0, 0 + inString, escaped := false, false + for i := 0; i < len(s); i++ { + c := s[i] + switch { + case escaped: + escaped = false + case inString: + if c == '\\' { + escaped = true + } else if c == '"' { + inString = false + } + case c == '"': + inString = true + case c == '{' || c == '[': + depth++ + if depth > maxDepth { + maxDepth = depth + } + case c == '}' || c == ']': + depth-- + } + } + return maxDepth +} diff --git a/weed/admin/view/app/s3vectors_bucket_details_templ.go b/weed/admin/view/app/s3vectors_bucket_details_templ.go new file mode 100644 index 0000000000..c74e739b65 --- /dev/null +++ b/weed/admin/view/app/s3vectors_bucket_details_templ.go @@ -0,0 +1,392 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1100 + +package app + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + "github.com/seaweedfs/seaweedfs/weed/admin/dash" +) + +func S3VectorsBucketDetails(data dash.S3VectorsBucketData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Bucket.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 16, Col: 69} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "

ARN
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Bucket.ARN) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 30, Col: 49} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
Created
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.Bucket.CreatedAt.Format("2006-01-02 15:04:05")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 32, Col: 79} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
Encryption
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Bucket.Encryption != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.Bucket.Encryption) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 36, Col: 61} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "None") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d indexes", data.TotalIndexes)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 47, Col: 84} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Truncated { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("showing the first %d; list the rest with an S3 Vectors client", data.TotalIndexes)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 49, Col: 138} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, index := range data.Indexes { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.Indexes) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
NameData TypeDimensionDistance MetricNon-filterable Metadata KeysCreatedActions
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(index.IndexName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 71, Col: 26} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(index.IndexArn) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 72, Col: 55} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(index.DataType) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 74, Col: 28} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", index.Dimension)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 75, Col: 48} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(index.DistanceMetric) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 76, Col: 34} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if index.MetadataConfiguration != nil && len(index.MetadataConfiguration.NonFilterableMetadataKeys) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(index.MetadataConfiguration.NonFilterableMetadataKeys, ", ")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 79, Col: 91} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "None") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(index.CreationTime.Format("2006-01-02 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 84, Col: 59} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
No indexes. Create one with an S3 Vectors client, e.g. aws s3vectors create-index.
Bucket Policy
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Policy != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
")
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			var templ_7745c5c3_Var18 string
+			templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(indentJSON(data.Policy))
+			if templ_7745c5c3_Err != nil {
+				return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_bucket_details.templ`, Line: 112, Col: 102}
+			}
+			_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
+			if templ_7745c5c3_Err != nil {
+				return templ_7745c5c3_Err
+			}
+			templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "No policy. Set one with PutVectorBucketPolicy.") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
Delete Index

Delete the index and all its vectors?

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +const maxIndentedJSONDepth = 32 + +func indentJSON(s string) string { + if jsonDepth(s) > maxIndentedJSONDepth { + return s + } + var out bytes.Buffer + if json.Indent(&out, []byte(s), "", " ") != nil { + return s + } + return out.String() +} + +func jsonDepth(s string) int { + depth, maxDepth := 0, 0 + inString, escaped := false, false + for i := 0; i < len(s); i++ { + c := s[i] + switch { + case escaped: + escaped = false + case inString: + if c == '\\' { + escaped = true + } else if c == '"' { + inString = false + } + case c == '"': + inString = true + case c == '{' || c == '[': + depth++ + if depth > maxDepth { + maxDepth = depth + } + case c == '}' || c == ']': + depth-- + } + } + return maxDepth +} + +var _ = templruntime.GeneratedTemplate diff --git a/weed/admin/view/app/s3vectors_buckets.templ b/weed/admin/view/app/s3vectors_buckets.templ new file mode 100644 index 0000000000..626212dd01 --- /dev/null +++ b/weed/admin/view/app/s3vectors_buckets.templ @@ -0,0 +1,189 @@ +package app + +import ( + "fmt" + "net/url" + + "github.com/seaweedfs/seaweedfs/weed/admin/dash" +) + +templ S3VectorsBuckets(data dash.S3VectorsBucketsData) { +
+

+ Vector Buckets +

+
+
+ +
+
+
+
+ + Vector buckets serve the Amazon S3 Vectors API on the S3 port. Each one is a LANCE table bucket that keeps its indexes in its vector namespace. + Indexes are created and filled with an S3 Vectors client, which needs a running weed-vector engine. +
+
+
+
+ { fmt.Sprintf("%d vector buckets", data.TotalBuckets) } + if data.Truncated { + { fmt.Sprintf("showing the first %d; list the rest with an S3 Vectors client", data.TotalBuckets) } + } +
+
+
+
+ + + + + + + + + + + + for _, bucket := range data.Buckets { + + + + + + + + } + if len(data.Buckets) == 0 { + + + + } + +
NameARNEncryptionCreatedActions
+ { bucket.Name } + { bucket.ARN } + if bucket.Encryption != "" { + { bucket.Encryption } + } else { + None + } + { bucket.CreatedAt.Format("2006-01-02 15:04") } +
+ + + + +
+
+ +
+
No vector buckets found
+
+
+
+
+
+ + + +} diff --git a/weed/admin/view/app/s3vectors_buckets_templ.go b/weed/admin/view/app/s3vectors_buckets_templ.go new file mode 100644 index 0000000000..8b486b043a --- /dev/null +++ b/weed/admin/view/app/s3vectors_buckets_templ.go @@ -0,0 +1,202 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.1100 + +package app + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "fmt" + "net/url" + + "github.com/seaweedfs/seaweedfs/weed/admin/dash" +) + +func S3VectorsBuckets(data dash.S3VectorsBucketsData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

Vector Buckets

Vector buckets serve the Amazon S3 Vectors API on the S3 port. Each one is a LANCE table bucket that keeps its indexes in its vector namespace. Indexes are created and filled with an S3 Vectors client, which needs a running weed-vector engine.
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d vector buckets", data.TotalBuckets)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_buckets.templ`, Line: 31, Col: 104} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if data.Truncated { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("showing the first %d; list the rest with an S3 Vectors client", data.TotalBuckets)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_buckets.templ`, Line: 33, Col: 138} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, bucket := range data.Buckets { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if len(data.Buckets) == 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
NameARNEncryptionCreatedActions
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_buckets.templ`, Line: 53, Col: 110} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.ARN) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_buckets.templ`, Line: 55, Col: 49} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if bucket.Encryption != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Encryption) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_buckets.templ`, Line: 58, Col: 60} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "None") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.CreatedAt.Format("2006-01-02 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3vectors_buckets.templ`, Line: 63, Col: 57} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
No vector buckets found
Create Vector Bucket
3 to 63 lowercase letters, digits and hyphens, starting and ending with a letter or digit.
The bucket is created without encryption. To encrypt it, create it with an S3 Vectors client and encryptionConfiguration, which the S3 gateway checks against its SSE-S3 key.
Delete Vector Bucket

Delete the vector bucket ? A bucket that still has indexes is not deleted.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/weed/admin/view/app/s3vectors_buckets_test.go b/weed/admin/view/app/s3vectors_buckets_test.go new file mode 100644 index 0000000000..bae344eb2d --- /dev/null +++ b/weed/admin/view/app/s3vectors_buckets_test.go @@ -0,0 +1,80 @@ +package app + +import ( + "bytes" + "strings" + "testing" + "time" + + "github.com/seaweedfs/seaweedfs/weed/admin/dash" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3vectors" + "github.com/stretchr/testify/require" +) + +func TestS3VectorsBucketsRendersBuckets(t *testing.T) { + data := dash.S3VectorsBucketsData{TotalBuckets: 2, Buckets: []dash.S3VectorsBucketSummary{ + {Name: "media", ARN: "arn:aws:s3vectors:us-east-1:000000000000:bucket/media", CreatedAt: time.Unix(0, 0)}, + {Name: "sealed", ARN: "arn:aws:s3vectors:us-east-1:000000000000:bucket/sealed", Encryption: "AES256", CreatedAt: time.Unix(0, 0)}, + }} + var out bytes.Buffer + require.NoError(t, S3VectorsBuckets(data).Render(dash.WithURLPrefix(t.Context(), "/admin"), &out)) + page := out.String() + require.Contains(t, page, `href="/admin/object-store/vector-buckets/media"`) + require.Contains(t, page, `data-bucket-name="sealed"`) + require.Contains(t, page, `AES256`) + require.Contains(t, page, "2 vector buckets") + require.NotContains(t, page, "No vector buckets found") + require.NotContains(t, page, "showing the first") + + data.Truncated = true + out.Reset() + require.NoError(t, S3VectorsBuckets(data).Render(t.Context(), &out)) + require.Contains(t, out.String(), "showing the first 2; list the rest with an S3 Vectors client") +} + +func TestS3VectorsBucketDetailsRendersIndexesAndPolicy(t *testing.T) { + data := dash.S3VectorsBucketData{ + Bucket: dash.S3VectorsBucketSummary{Name: "media", ARN: "arn:aws:s3vectors:us-east-1:000000000000:bucket/media"}, + Policy: `{"Version":"2012-10-17","Statement":[]}`, + TotalIndexes: 1, + Indexes: []*s3vectors.Index{{ + IndexName: "movies", IndexArn: "arn:aws:s3vectors:us-east-1:000000000000:bucket/media/index/movies", + DataType: "float32", Dimension: 384, DistanceMetric: "cosine", + MetadataConfiguration: &s3vectors.MetadataConfiguration{NonFilterableMetadataKeys: []string{"summary", "body"}}, + }}, + } + var out bytes.Buffer + require.NoError(t, S3VectorsBucketDetails(data).Render(t.Context(), &out)) + page := out.String() + for _, want := range []string{"movies", ">384<", ">cosine<", ">float32<", "summary, body", `data-index-name="movies"`, "{\n "Version": "2012-10-17",\n "Statement": []\n}"} { + require.Contains(t, page, want) + } + require.False(t, strings.Contains(page, "No indexes."), "an index is listed") + + data.Policy = "not json" + out.Reset() + require.NoError(t, S3VectorsBucketDetails(data).Render(t.Context(), &out)) + require.Contains(t, out.String(), ">not json") + + data.Policy = `{"Version":"2012-10-17","Extra":` + strings.Repeat("[", 5000) + strings.Repeat("]", 5000) + "}" + out.Reset() + require.NoError(t, S3VectorsBucketDetails(data).Render(t.Context(), &out)) + require.Contains(t, out.String(), `"Extra":[[[[`, "a deeply nested policy is shown as stored") +} + +func TestJSONDepthSkipsStrings(t *testing.T) { + require.Equal(t, 3, jsonDepth(`{"a":["[{\"[",{}]}`)) + require.Equal(t, 0, jsonDepth(`"[[["`)) +} + +func TestS3TablesBucketsLinksVectorBuckets(t *testing.T) { + data := dash.S3TablesBucketsData{Buckets: []dash.S3TablesBucketSummary{ + {Name: "vectors", ARN: "arn:aws:s3tables:us-east-1:000000000000:bucket/vectors", Format: "LANCE"}, + {Name: "warehouse", ARN: "arn:aws:s3tables:us-east-1:000000000000:bucket/warehouse", Format: "ICEBERG"}, + }} + var out bytes.Buffer + require.NoError(t, S3TablesBuckets(data).Render(t.Context(), &out)) + page := out.String() + require.Contains(t, page, `href="/object-store/vector-buckets/vectors"`) + require.NotContains(t, page, `href="/object-store/vector-buckets/warehouse"`) +} diff --git a/weed/admin/view/layout/layout.templ b/weed/admin/view/layout/layout.templ index 26f228ba4e..3e10120a89 100644 --- a/weed/admin/view/layout/layout.templ +++ b/weed/admin/view/layout/layout.templ @@ -189,6 +189,11 @@ templ Layout(view ViewContext, content templ.Component) { Table Buckets +
  • Vector Buckets
  • Groups
  • Users
  • Service Accounts
  • Groups
  • Policies
  • MANAGEMENT
    MANAGEMENT
    • File Browser
    • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if isMQPage { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "Message Queue ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "Message Queue ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "Message Queue ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "Message Queue ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if isMQPage { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
      • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
        • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if currentPath == "/mq/brokers" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "Brokers") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "Brokers") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\">Brokers") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
        • ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if currentPath == "/mq/topics" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "Topics") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">Brokers") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
        • ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if currentPath == "/mq/topics" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "Topics") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">Topics") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "Topics") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
        ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
      ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "
    WORKERS
    • ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if isDefaultWorkerPage { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "Brokers
    • Default") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\">Topics
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "
    WORKERS
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -900,108 +913,95 @@ func Layout(view ViewContext, content templ.Component) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "
    © ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var51 string - templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", time.Now().Year())) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 351, Col: 60} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, " SeaweedFS Admin v") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "
    © ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var52 string - templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(version.VERSION_NUMBER) + templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", time.Now().Year())) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 351, Col: 102} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 356, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, " SeaweedFS Admin v") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var53 string + templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(version.VERSION_NUMBER) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 356, Col: 102} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if version.COMMIT != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "(") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "(") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var53 string - templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinStringErrs(version.COMMIT) + var templ_7745c5c3_Var54 string + templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinStringErrs(version.COMMIT) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 353, Col: 55} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 358, Col: 55} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, ") ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, ") ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if !strings.Contains(version.VERSION, "enterprise") { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, "• Enterprise Version Available") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "• Enterprise Version Available") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1059,134 +1072,134 @@ func LoginForm(title string, errorMessage string, csrfToken string) templ.Compon ctx = templ.InitializeContext(ctx) ctx = templ.ClearChildren(ctx) prefix := dash.URLPrefixFromContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var61 string - templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.JoinStringErrs(title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 386, Col: 17} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, " - Login") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var62 string - templ_7745c5c3_Var62, templ_7745c5c3_Err = templruntime.NewAttributeExpression(prefix + "/static/favicon.ico").ResolveURL(ctx) + templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 387, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 391, Col: 17} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var62) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, "\" type=\"image/x-icon\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, " - Login

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "\" rel=\"stylesheet\">

    Please sign in to continue

    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, "\" rel=\"stylesheet\">

    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var66 string + templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(title) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 405, Col: 57} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "

    Please sign in to continue

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if errorMessage != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var66 string - templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage) + var templ_7745c5c3_Var67 string + templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 407, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 412, Col: 45} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var66)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var67)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "\">
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/weed/s3api/s3vectors/execute.go b/weed/s3api/s3vectors/execute.go new file mode 100644 index 0000000000..9a2a2e6872 --- /dev/null +++ b/weed/s3api/s3vectors/execute.go @@ -0,0 +1,54 @@ +package s3vectors + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" +) + +// Error is an S3 Vectors error response, as Execute returns it. +type Error struct { + Code string `json:"code"` + Message string `json:"message"` +} + +func (e *Error) Error() string { return e.Code + ": " + e.Message } + +// Execute runs one operation as identity for trusted local tooling such as +// the admin console, like s3tables.Manager.Execute: nothing is signed or +// authenticated, and identity's grants decide what the operation may do. +// Operations that reach the vector engine need the server's VectorServiceClient. +func (s *Server) Execute(ctx context.Context, operation string, req, resp interface{}, identity string) error { + handler, ok := handlers[operation] + if !ok { + return &Error{Code: "ValidationException", Message: "unknown operation " + operation} + } + body, err := json.Marshal(req) + if err != nil { + return err + } + r, err := http.NewRequestWithContext(ctx, http.MethodPost, "/"+operation, bytes.NewReader(body)) + if err != nil { + return err + } + if identity != "" { + r = r.WithContext(s3_constants.SetIdentityNameInContext(r.Context(), identity)) + } + w := httptest.NewRecorder() + handler(s, w, r) + if w.Code >= http.StatusBadRequest { + e := &Error{} + if json.Unmarshal(w.Body.Bytes(), e) != nil || e.Code == "" { + e = &Error{Code: http.StatusText(w.Code), Message: w.Body.String()} + } + return e + } + if resp == nil || w.Body.Len() == 0 { + return nil + } + return json.Unmarshal(w.Body.Bytes(), resp) +} diff --git a/weed/s3api/s3vectors/execute_test.go b/weed/s3api/s3vectors/execute_test.go new file mode 100644 index 0000000000..38f7508aa3 --- /dev/null +++ b/weed/s3api/s3vectors/execute_test.go @@ -0,0 +1,66 @@ +package s3vectors + +import ( + "context" + "errors" + "testing" + + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3tables/s3tablestest" +) + +func TestExecuteRunsOperationsAsTheIdentity(t *testing.T) { + fs := s3tablestest.Start(t) + s := NewServer(s3tables.NewManagerClient(fs.Client), nil, nil) + s.SetRegion(s3tables.DefaultRegion) + s.SetAccountID(s3tables.DefaultAccountID) + ctx := context.Background() + admin := s3_constants.AccountAdminId + + var created CreateVectorBucketResponse + if err := s.Execute(ctx, "CreateVectorBucket", &CreateVectorBucketRequest{VectorBucketName: "media"}, &created, admin); err != nil { + t.Fatalf("create: %v", err) + } + if want := "arn:aws:s3vectors:us-east-1:000000000000:bucket/media"; created.VectorBucketArn != want { + t.Fatalf("arn %q, want %q", created.VectorBucketArn, want) + } + var list ListVectorBucketsResponse + if err := s.Execute(ctx, "ListVectorBuckets", &ListVectorBucketsRequest{}, &list, admin); err != nil { + t.Fatalf("list: %v", err) + } + if len(list.VectorBuckets) != 1 || list.VectorBuckets[0].VectorBucketName != "media" { + t.Fatalf("list = %+v", list.VectorBuckets) + } + + var e *Error + err := s.Execute(ctx, "GetVectorBucketPolicy", &GetVectorBucketPolicyRequest{VectorBucketName: "media"}, nil, admin) + if !errors.As(err, &e) || e.Code != "NotFoundException" { + t.Fatalf("policy of a bucket without one: %v, want NotFoundException", err) + } + if err := s.Execute(ctx, "DeleteVectorBucket", &DeleteVectorBucketRequest{VectorBucketName: "media"}, nil, admin); err != nil { + t.Fatalf("delete: %v", err) + } + err = s.Execute(ctx, "GetVectorBucket", &GetVectorBucketRequest{VectorBucketName: "media"}, nil, admin) + if !errors.As(err, &e) || e.Code != "NotFoundException" { + t.Fatalf("get after delete: %v, want NotFoundException", err) + } + err = s.Execute(ctx, "TagResource", struct{}{}, nil, admin) + if !errors.As(err, &e) || e.Code != "ValidationException" { + t.Fatalf("unknown operation: %v, want ValidationException", err) + } +} + +func TestExecuteChecksTheIdentitysGrants(t *testing.T) { + fs := s3tablestest.Start(t) + s := NewServer(s3tables.NewManagerClient(fs.Client), nil, nil) + ctx := context.Background() + if err := s.Execute(ctx, "CreateVectorBucket", &CreateVectorBucketRequest{VectorBucketName: "media"}, nil, s3_constants.AccountAdminId); err != nil { + t.Fatalf("create: %v", err) + } + var e *Error + err := s.Execute(ctx, "DeleteVectorBucket", &DeleteVectorBucketRequest{VectorBucketName: "media"}, nil, "someone-else") + if !errors.As(err, &e) || e.Code != "AccessDeniedException" { + t.Fatalf("delete by a stranger: %v, want AccessDeniedException", err) + } +} diff --git a/weed/s3api/s3vectors/handlers.go b/weed/s3api/s3vectors/handlers.go index 83018dca6b..0f629d2cc1 100644 --- a/weed/s3api/s3vectors/handlers.go +++ b/weed/s3api/s3vectors/handlers.go @@ -382,7 +382,7 @@ func (s *Server) handleListVectorBuckets(w http.ResponseWriter, r *http.Request) // of the listing, Lance and format-less legacy buckets stay. out := &ListVectorBucketsResponse{NextToken: resp.ContinuationToken, VectorBuckets: []*VectorBucket{}} for _, b := range resp.TableBuckets { - if b.Format != "" && b.Format != s3tables.FormatLance { + if !IsVectorBucketFormat(b.Format) { continue } owner, policy, err := s.vectorPolicyView(r.Context(), b.Name) @@ -758,11 +758,15 @@ func (s *Server) mutateVectorPolicy(ctx context.Context, bucket string, mutate f }) } -// isVectorBucket reports whether the catalog record describes a vector -// bucket: an explicit ICEBERG bucket is not one, while a bucket declared -// before formats existed reads as Lance. +// IsVectorBucketFormat reports whether a table bucket of this format is a +// vector bucket: an explicit ICEBERG bucket is not one, while a bucket +// declared before formats existed reads as Lance. +func IsVectorBucketFormat(format string) bool { + return format == "" || format == s3tables.FormatLance +} + func isVectorBucket(record *s3tables.GetTableBucketResponse) bool { - return record.Format == "" || record.Format == s3tables.FormatLance + return IsVectorBucketFormat(record.Format) } // errVectorDenied marks a failed vector permission check inside a policy