mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-07 20:46:11 +00:00
5b962b6c58
* feat(heal): expose compatible canonical v3 outcomes Serialize the existing canonical heal outcome, retain the v3 summary vocabulary, and reject or conservatively adapt contradictory peer success responses. Preserve progress and bounded result cursors without treating legacy storage responses as repair proof. Add optional SDK outcome and cursor support with shared Rust fixtures, pinned legacy Go decoder and mc polling checks, and explicit compatibility limits. Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> * fix(madmin): box heal stop task status outcome Co-Authored-By: heihutu <heihutu@gmail.com> Co-Authored-By: zhi22915 <qiuzgang@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com> Co-authored-by: zhi22915 <qiuzgang@gmail.com>
138 lines
4.1 KiB
Go
138 lines
4.1 KiB
Go
// Copyright 2026 RustFS Team
|
|
// Licensed under the Apache License, Version 2.0.
|
|
|
|
package compat_test
|
|
|
|
import (
|
|
"context"
|
|
"debug/buildinfo"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
madmin "github.com/minio/madmin-go/v3"
|
|
)
|
|
|
|
type fixture struct {
|
|
Name string `json:"name"`
|
|
CLIExit int `json:"cliExit"`
|
|
Response json.RawMessage `json:"response"`
|
|
}
|
|
|
|
func fixtures(t *testing.T) []fixture {
|
|
t.Helper()
|
|
data, err := os.ReadFile("../../../crates/madmin/tests/fixtures/heal-outcome-v3.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cases []fixture
|
|
if err := json.Unmarshal(data, &cases); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(cases) != 8 {
|
|
t.Fatalf("expected eight owner/receiver-validated fixtures, got %d", len(cases))
|
|
}
|
|
return cases
|
|
}
|
|
|
|
func fixtureServer(t *testing.T, response []byte, polls *atomic.Int32) *httptest.Server {
|
|
t.Helper()
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost || !strings.HasPrefix(r.URL.Path, "/minio/admin/v3/heal/") {
|
|
t.Errorf("unexpected client request %s %s", r.Method, r.URL.Path)
|
|
http.Error(w, "unexpected request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if r.URL.Query().Get("clientToken") == "" {
|
|
fmt.Fprint(w, `{"clientToken":"fixture-token","clientAddress":"","startTime":"2026-01-01T00:00:00Z"}`)
|
|
return
|
|
}
|
|
polls.Add(1)
|
|
w.Write(response)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
return server
|
|
}
|
|
|
|
func TestLegacyMadminDecoder(t *testing.T) {
|
|
for _, f := range fixtures(t) {
|
|
t.Run(f.Name, func(t *testing.T) {
|
|
var polls atomic.Int32
|
|
server := fixtureServer(t, f.Response, &polls)
|
|
client, err := madmin.New(strings.TrimPrefix(server.URL, "http://"), "fixture-access", "fixture-secret", false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_, status, err := client.Heal(ctx, "bucket", "", madmin.HealOpts{}, "fixture-token", false, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var expected struct {
|
|
Summary string `json:"summary"`
|
|
Detail string `json:"detail"`
|
|
}
|
|
if err := json.Unmarshal(f.Response, &expected); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status.Summary != expected.Summary || status.FailureDetail != expected.Detail || polls.Load() != 1 {
|
|
t.Fatalf("decoder changed legacy fields: %+v, polls=%d", status, polls.Load())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLegacyMCPoll(t *testing.T) {
|
|
binary := os.Getenv("MC_BINARY")
|
|
if binary == "" {
|
|
t.Fatal("MC_BINARY must point to the pinned mc release; this check cannot be skipped")
|
|
}
|
|
info, err := buildinfo.ReadFile(binary)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Main.Path != "github.com/minio/mc" || !strings.Contains(info.Main.Version, "7394ce0dd2a8") {
|
|
t.Fatalf("expected mc RELEASE.2025-08-13T08-35-41Z (7394ce0dd2a8), got %+v", info.Main)
|
|
}
|
|
for _, f := range fixtures(t) {
|
|
t.Run(f.Name, func(t *testing.T) {
|
|
var polls atomic.Int32
|
|
server := fixtureServer(t, f.Response, &polls)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, binary, "--config-dir", filepath.Join(t.TempDir(), "mc"), "--json", "admin", "heal", "--recursive", "w23/bucket")
|
|
cmd.Env = append(os.Environ(), "MC_HOST_w23="+strings.Replace(server.URL, "http://", "http://fixture-access:fixture-secret@", 1), "MC_NO_COLOR=1")
|
|
output, err := cmd.CombinedOutput()
|
|
if ctx.Err() != nil {
|
|
t.Fatalf("legacy poll did not terminate: %s", output)
|
|
}
|
|
exit := 0
|
|
if err != nil {
|
|
var ok bool
|
|
var status *exec.ExitError
|
|
status, ok = err.(*exec.ExitError)
|
|
if !ok {
|
|
t.Fatal(err)
|
|
}
|
|
exit = status.ExitCode()
|
|
}
|
|
if exit != f.CLIExit || polls.Load() != 1 {
|
|
t.Fatalf("exit=%d expected=%d polls=%d output=%s", exit, f.CLIExit, polls.Load(), output)
|
|
}
|
|
if f.CLIExit != 0 && !strings.Contains(string(output), "Heal had an error") {
|
|
t.Fatalf("failure was not the expected legacy terminal result: %s", output)
|
|
}
|
|
})
|
|
}
|
|
}
|