mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
* iceberg: support namespace property updates
Add POST /v1/namespaces/{namespace}/properties to the REST catalog. It
applies the request's removals and updates and returns the removed/updated/
missing summary the spec defines. A new UpdateNamespace op on the S3 Tables
manager rewrites the stored namespace properties; AWS S3 Tables namespaces
have no properties, so this is the SeaweedFS-side backing for the catalog.
* iceberg: dedup namespace property removals
A key repeated in removals was deleted on its first occurrence, then
reported as missing on the next — landing in both removed and missing.
Skip keys already processed.
* iceberg: map namespace-update backend errors to REST statuses
UpdateNamespaceProperties returned 500 for every manager failure, masking
the namespace being dropped between read and write, or a denied caller.
Inspect the typed S3TablesError and answer 404/403 accordingly, 500 only
for the rest. Also replaces the GetNamespace not-found string match.
* iceberg: test the namespace-properties conflict path
Cover the 422 returned when a key appears in both removals and updates.
The check runs before any backend call, so it needs no filer.
88 lines
2.8 KiB
Go
88 lines
2.8 KiB
Go
package iceberg
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
func TestApplyNamespacePropertyUpdates(t *testing.T) {
|
|
current := map[string]string{"foo": "bar", "prop": "yes"}
|
|
|
|
props, summary := applyNamespacePropertyUpdates(current, []string{"abc"}, map[string]string{"prop": "no"})
|
|
|
|
// Removal of a missing key reports missing, not removed.
|
|
if !reflect.DeepEqual(summary.Removed, []string{}) {
|
|
t.Errorf("Removed = %v, want []", summary.Removed)
|
|
}
|
|
if !reflect.DeepEqual(summary.Updated, []string{"prop"}) {
|
|
t.Errorf("Updated = %v, want [prop]", summary.Updated)
|
|
}
|
|
if !reflect.DeepEqual(summary.Missing, []string{"abc"}) {
|
|
t.Errorf("Missing = %v, want [abc]", summary.Missing)
|
|
}
|
|
if props["prop"] != "no" || props["foo"] != "bar" {
|
|
t.Errorf("merged properties = %v, want foo=bar prop=no", props)
|
|
}
|
|
|
|
// Input map must not be mutated.
|
|
if current["prop"] != "yes" {
|
|
t.Errorf("source map mutated: prop = %q, want yes", current["prop"])
|
|
}
|
|
}
|
|
|
|
func TestApplyNamespacePropertyUpdatesRemoveExisting(t *testing.T) {
|
|
props, summary := applyNamespacePropertyUpdates(
|
|
map[string]string{"a": "1", "b": "2"}, []string{"a"}, nil)
|
|
|
|
if !reflect.DeepEqual(summary.Removed, []string{"a"}) {
|
|
t.Errorf("Removed = %v, want [a]", summary.Removed)
|
|
}
|
|
if _, ok := props["a"]; ok {
|
|
t.Errorf("key a should have been removed: %v", props)
|
|
}
|
|
if len(summary.Missing) != 0 || len(summary.Updated) != 0 {
|
|
t.Errorf("unexpected summary: %+v", summary)
|
|
}
|
|
}
|
|
|
|
func TestApplyNamespacePropertyUpdatesDuplicateRemovals(t *testing.T) {
|
|
// A repeated removal key must not be reported as both removed and missing.
|
|
_, summary := applyNamespacePropertyUpdates(
|
|
map[string]string{"a": "1"}, []string{"a", "a"}, nil)
|
|
|
|
if !reflect.DeepEqual(summary.Removed, []string{"a"}) {
|
|
t.Errorf("Removed = %v, want [a]", summary.Removed)
|
|
}
|
|
if len(summary.Missing) != 0 {
|
|
t.Errorf("Missing = %v, want []", summary.Missing)
|
|
}
|
|
}
|
|
|
|
// A key in both removals and updates is rejected before any backend call, so
|
|
// the conflict path is reachable without a filer.
|
|
func TestHandleUpdateNamespacePropertiesConflict(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/namespaces/ns/properties",
|
|
strings.NewReader(`{"removals":["x"],"updates":{"x":"1"}}`))
|
|
req = mux.SetURLVars(req, map[string]string{"namespace": "ns"})
|
|
rec := httptest.NewRecorder()
|
|
|
|
(&Server{}).handleUpdateNamespaceProperties(rec, req)
|
|
|
|
if rec.Code != http.StatusUnprocessableEntity {
|
|
t.Fatalf("status = %d, want 422", rec.Code)
|
|
}
|
|
var resp ErrorResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Error.Type != "UnprocessableEntityException" {
|
|
t.Fatalf("error type = %q, want UnprocessableEntityException", resp.Error.Type)
|
|
}
|
|
}
|