From eece638c6094c939fdd4350d0230124a3c2b3746 Mon Sep 17 00:00:00 2001
From: Georg Mangold <67909897+georgmangold@users.noreply.github.com>
Date: Fri, 27 Jun 2025 15:58:00 +0200
Subject: [PATCH 1/3] revert: "Removed heal backend (#3188)"
This reverts commit 343ff575e615f0cf393845e91676f1c9fa4d9aa1.
---
api/admin_heal.go | 375 +++++++++++++++++++++++++++++++
api/admin_heal_test.go | 270 ++++++++++++++++++++++
api/ws_handle.go | 30 +++
web-app/tests/policies/heal.json | 16 ++
web-app/tests/scripts/common.sh | 1 +
5 files changed, 692 insertions(+)
create mode 100644 api/admin_heal.go
create mode 100644 api/admin_heal_test.go
create mode 100644 web-app/tests/policies/heal.json
diff --git a/api/admin_heal.go b/api/admin_heal.go
new file mode 100644
index 0000000000..2c574aeacc
--- /dev/null
+++ b/api/admin_heal.go
@@ -0,0 +1,375 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2021 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/minio/websocket"
+)
+
+// An alias of string to represent the health color code of an object
+type col string
+
+const (
+ colGrey col = "Grey"
+ colRed col = "Red"
+ colYellow col = "Yellow"
+ colGreen col = "Green"
+)
+
+var (
+ hColOrder = []col{colRed, colYellow, colGreen}
+ hColTable = map[int][]int{
+ 1: {0, -1, 1},
+ 2: {0, 1, 2},
+ 3: {1, 2, 3},
+ 4: {1, 2, 4},
+ 5: {1, 3, 5},
+ 6: {2, 4, 6},
+ 7: {2, 4, 7},
+ 8: {2, 5, 8},
+ }
+)
+
+type healItemStatus struct {
+ Status string `json:"status"`
+ Error string `json:"errors,omitempty"`
+ Type string `json:"type"`
+ Name string `json:"name"`
+ Before struct {
+ Color string `json:"color"`
+ Offline int `json:"offline"`
+ Online int `json:"online"`
+ Missing int `json:"missing"`
+ Corrupted int `json:"corrupted"`
+ Drives []madmin.HealDriveInfo `json:"drives"`
+ } `json:"before"`
+ After struct {
+ Color string `json:"color"`
+ Offline int `json:"offline"`
+ Online int `json:"online"`
+ Missing int `json:"missing"`
+ Corrupted int `json:"corrupted"`
+ Drives []madmin.HealDriveInfo `json:"drives"`
+ } `json:"after"`
+ Size int64 `json:"size"`
+}
+
+type healStatus struct {
+ // Total time since heal start in seconds
+ HealDuration float64 `json:"healDuration"`
+
+ // Accumulated statistics of heal result records
+ BytesScanned int64 `json:"bytesScanned"`
+
+ // Counter for objects, and another counter for all kinds of
+ // items
+ ObjectsScanned int64 `json:"objectsScanned"`
+ ItemsScanned int64 `json:"itemsScanned"`
+
+ // Counters for healed objects and all kinds of healed items
+ ObjectsHealed int64 `json:"objectsHealed"`
+ ItemsHealed int64 `json:"itemsHealed"`
+
+ ItemsHealthStatus []healItemStatus `json:"itemsHealthStatus"`
+ // Map of health color code to number of objects with that
+ // health color code.
+ HealthBeforeCols map[col]int64 `json:"healthBeforeCols"`
+ HealthAfterCols map[col]int64 `json:"healthAfterCols"`
+}
+
+type healOptions struct {
+ BucketName string
+ Prefix string
+ ForceStart bool
+ ForceStop bool
+ madmin.HealOpts
+}
+
+// startHeal starts healing of the servers based on heal options
+func startHeal(ctx context.Context, conn WSConn, client MinioAdmin, hOpts *healOptions) error {
+ // Initialize heal
+ healStart, _, err := client.heal(ctx, hOpts.BucketName, hOpts.Prefix, hOpts.HealOpts, "", hOpts.ForceStart, hOpts.ForceStop)
+ if err != nil {
+ LogError("error initializing healing: %v", err)
+ return err
+ }
+ if hOpts.ForceStop {
+ return nil
+ }
+ clientToken := healStart.ClientToken
+ hs := healStatus{
+ HealthBeforeCols: make(map[col]int64),
+ HealthAfterCols: make(map[col]int64),
+ }
+ for {
+ select {
+ case <-ctx.Done():
+ return nil
+ default:
+ _, res, err := client.heal(ctx, hOpts.BucketName, hOpts.Prefix, hOpts.HealOpts, clientToken, hOpts.ForceStart, hOpts.ForceStop)
+ if err != nil {
+ LogError("error on heal: %v", err)
+ return err
+ }
+
+ hs.writeStatus(&res, conn)
+
+ if res.Summary == "finished" {
+ return nil
+ }
+
+ if res.Summary == "stopped" {
+ return fmt.Errorf("heal had an errors - %s", res.FailureDetail)
+ }
+
+ time.Sleep(time.Second)
+ }
+ }
+}
+
+func (h *healStatus) writeStatus(s *madmin.HealTaskStatus, conn WSConn) error {
+ // Update state
+ h.updateDuration(s)
+ for _, item := range s.Items {
+ err := h.updateStats(item)
+ if err != nil {
+ LogError("error on updateStats: %v", err)
+ return err
+ }
+ }
+
+ // Serialize message to be sent
+ infoBytes, err := json.Marshal(h)
+ if err != nil {
+ LogError("error on json.Marshal: %v", err)
+ return err
+ }
+ // Send Message through websocket connection
+ err = conn.writeMessage(websocket.TextMessage, infoBytes)
+ if err != nil {
+ LogError("error writeMessage: %v", err)
+ return err
+ }
+ return nil
+}
+
+func (h *healStatus) updateDuration(s *madmin.HealTaskStatus) {
+ h.HealDuration = time.Now().UTC().Sub(s.StartTime).Round(time.Second).Seconds()
+}
+
+func (h *healStatus) updateStats(i madmin.HealResultItem) error {
+ // update general status
+ if i.Type == madmin.HealItemObject {
+ // Objects whose size could not be found have -1 size
+ // returned.
+ if i.ObjectSize >= 0 {
+ h.BytesScanned += i.ObjectSize
+ }
+ h.ObjectsScanned++
+ }
+ h.ItemsScanned++
+
+ beforeUp, afterUp := i.GetOnlineCounts()
+ if afterUp > beforeUp {
+ if i.Type == madmin.HealItemObject {
+ h.ObjectsHealed++
+ }
+ h.ItemsHealed++
+ }
+ // update per item status
+ itemStatus := healItemStatus{}
+ // get color health status
+ var beforeColor, afterColor col
+ var err error
+ switch i.Type {
+ case madmin.HealItemMetadata, madmin.HealItemBucket:
+ beforeColor, afterColor, err = getReplicatedFileHCCChange(i)
+ default:
+ if i.Type == madmin.HealItemObject {
+ itemStatus.Size = i.ObjectSize
+ }
+ beforeColor, afterColor, err = getObjectHCCChange(i)
+ }
+ if err != nil {
+ return err
+ }
+ itemStatus.Status = "success"
+ itemStatus.Before.Color = strings.ToLower(string(beforeColor))
+ itemStatus.After.Color = strings.ToLower(string(afterColor))
+ itemStatus.Type, itemStatus.Name = getHRITypeAndName(i)
+ itemStatus.Before.Online, itemStatus.After.Online = beforeUp, afterUp
+ itemStatus.Before.Missing, itemStatus.After.Missing = i.GetMissingCounts()
+ itemStatus.Before.Corrupted, itemStatus.After.Corrupted = i.GetCorruptedCounts()
+ itemStatus.Before.Offline, itemStatus.After.Offline = i.GetOfflineCounts()
+ itemStatus.Before.Drives = i.Before.Drives
+ itemStatus.After.Drives = i.After.Drives
+ h.ItemsHealthStatus = append(h.ItemsHealthStatus, itemStatus)
+ h.HealthBeforeCols[beforeColor]++
+ h.HealthAfterCols[afterColor]++
+ return nil
+}
+
+// getObjectHCCChange - returns before and after color change for
+// objects
+func getObjectHCCChange(h madmin.HealResultItem) (b, a col, err error) {
+ parityShards := h.ParityBlocks
+ dataShards := h.DataBlocks
+
+ onlineBefore, onlineAfter := h.GetOnlineCounts()
+ surplusShardsBeforeHeal := onlineBefore - dataShards
+ surplusShardsAfterHeal := onlineAfter - dataShards
+ b, err = getHColCode(surplusShardsBeforeHeal, parityShards)
+ if err != nil {
+ return
+ }
+ a, err = getHColCode(surplusShardsAfterHeal, parityShards)
+ return
+}
+
+// getReplicatedFileHCCChange - fetches health color code for metadata
+// files that are replicated.
+func getReplicatedFileHCCChange(h madmin.HealResultItem) (b, a col, err error) {
+ getColCode := func(numAvail int) (c col, err error) {
+ // calculate color code for replicated object similar
+ // to erasure coded objects
+ quorum := h.DiskCount/h.SetCount/2 + 1
+ surplus := numAvail/h.SetCount - quorum
+ parity := h.DiskCount/h.SetCount - quorum
+ c, err = getHColCode(surplus, parity)
+ return
+ }
+
+ onlineBefore, onlineAfter := h.GetOnlineCounts()
+ b, err = getColCode(onlineBefore)
+ if err != nil {
+ return
+ }
+ a, err = getColCode(onlineAfter)
+ return
+}
+
+func getHColCode(surplusShards, parityShards int) (c col, err error) {
+ if parityShards < 1 || parityShards > 8 || surplusShards > parityShards {
+ return c, fmt.Errorf("invalid parity shard count/surplus shard count given")
+ }
+ if surplusShards < 0 {
+ return colGrey, err
+ }
+ colRow := hColTable[parityShards]
+ for index, val := range colRow {
+ if val != -1 && surplusShards <= val {
+ return hColOrder[index], err
+ }
+ }
+ return c, fmt.Errorf("cannot get a heal color code")
+}
+
+func getHRITypeAndName(i madmin.HealResultItem) (typ, name string) {
+ name = fmt.Sprintf("%s/%s", i.Bucket, i.Object)
+ switch i.Type {
+ case madmin.HealItemMetadata:
+ typ = "system"
+ name = i.Detail
+ case madmin.HealItemBucketMetadata:
+ typ = "system"
+ name = "bucket-metadata:" + name
+ case madmin.HealItemBucket:
+ typ = "bucket"
+ case madmin.HealItemObject:
+ typ = "object"
+ default:
+ typ = fmt.Sprintf("!! Unknown heal result record %#v !!", i)
+ name = typ
+ }
+ return
+}
+
+// getHealOptionsFromReq return options from request for healing process
+// path come as : `/heal///bucket1`
+// and query params come on request form
+func getHealOptionsFromReq(req *http.Request) (*healOptions, error) {
+ hOptions := healOptions{}
+ re := regexp.MustCompile(`(/heal/)(.*?)(\?.*?$|$)`)
+ matches := re.FindAllSubmatch([]byte(req.URL.Path), -1)
+ // matches comes as e.g.
+ // [["...", "/heal/", "bucket1"]]
+ // [["/heal/" "/heal/" ""]]
+
+ if len(matches) == 0 || len(matches[0]) < 3 {
+ return nil, fmt.Errorf("invalid url: %s", req.URL.Path)
+ }
+ hOptions.BucketName = strings.TrimSpace(string(matches[0][2]))
+ hOptions.Prefix = req.FormValue("prefix")
+ hOptions.HealOpts.ScanMode = transformScanStr(req.FormValue("scan"))
+
+ if req.FormValue("force-start") != "" {
+ boolVal, err := strconv.ParseBool(req.FormValue("force-start"))
+ if err != nil {
+ return nil, err
+ }
+ hOptions.ForceStart = boolVal
+ }
+ if req.FormValue("force-stop") != "" {
+ boolVal, err := strconv.ParseBool(req.FormValue("force-stop"))
+ if err != nil {
+ return nil, err
+ }
+ hOptions.ForceStop = boolVal
+ }
+ // heal recursively
+ if req.FormValue("recursive") != "" {
+ boolVal, err := strconv.ParseBool(req.FormValue("recursive"))
+ if err != nil {
+ return nil, err
+ }
+ hOptions.HealOpts.Recursive = boolVal
+ }
+ // remove dangling objects in heal sequence
+ if req.FormValue("remove") != "" {
+ boolVal, err := strconv.ParseBool(req.FormValue("remove"))
+ if err != nil {
+ return nil, err
+ }
+ hOptions.HealOpts.Remove = boolVal
+ }
+ // only inspect data
+ if req.FormValue("dry-run") != "" {
+ boolVal, err := strconv.ParseBool(req.FormValue("dry-run"))
+ if err != nil {
+ return nil, err
+ }
+ hOptions.HealOpts.DryRun = boolVal
+ }
+ return &hOptions, nil
+}
+
+func transformScanStr(scanStr string) madmin.HealScanMode {
+ if scanStr == "deep" {
+ return madmin.HealDeepScan
+ }
+ return madmin.HealNormalScan
+}
diff --git a/api/admin_heal_test.go b/api/admin_heal_test.go
new file mode 100644
index 0000000000..0ca5476b36
--- /dev/null
+++ b/api/admin_heal_test.go
@@ -0,0 +1,270 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2021 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "net/http"
+ "net/url"
+ "testing"
+ "time"
+
+ "github.com/minio/madmin-go/v3"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestHeal(t *testing.T) {
+ assert := assert.New(t)
+
+ client := AdminClientMock{}
+ mockWSConn := mockConn{}
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ function := "startHeal()"
+ mockResultItem1 := madmin.HealResultItem{
+ Type: madmin.HealItemObject,
+ SetCount: 1,
+ DiskCount: 4,
+ ParityBlocks: 2,
+ DataBlocks: 2,
+ Before: struct {
+ Drives []madmin.HealDriveInfo `json:"drives"`
+ }{
+ Drives: []madmin.HealDriveInfo{
+ {
+ State: madmin.DriveStateOk,
+ },
+ {
+ State: madmin.DriveStateOk,
+ },
+ {
+ State: madmin.DriveStateOk,
+ },
+ {
+ State: madmin.DriveStateMissing,
+ },
+ },
+ },
+ After: struct {
+ Drives []madmin.HealDriveInfo `json:"drives"`
+ }{
+ Drives: []madmin.HealDriveInfo{
+ {
+ State: madmin.DriveStateOk,
+ },
+ {
+ State: madmin.DriveStateOk,
+ },
+ {
+ State: madmin.DriveStateOk,
+ },
+ {
+ State: madmin.DriveStateOk,
+ },
+ },
+ },
+ }
+ mockResultItem2 := madmin.HealResultItem{
+ Type: madmin.HealItemBucket,
+ SetCount: 1,
+ DiskCount: 4,
+ ParityBlocks: 2,
+ DataBlocks: 2,
+ Before: struct {
+ Drives []madmin.HealDriveInfo `json:"drives"`
+ }{
+ Drives: []madmin.HealDriveInfo{
+ {
+ State: madmin.DriveStateOk,
+ },
+ {
+ State: madmin.DriveStateOk,
+ },
+ {
+ State: madmin.DriveStateOk,
+ },
+ {
+ State: madmin.DriveStateMissing,
+ },
+ },
+ },
+ After: struct {
+ Drives []madmin.HealDriveInfo `json:"drives"`
+ }{
+ Drives: []madmin.HealDriveInfo{
+ {
+ State: madmin.DriveStateOk,
+ },
+ {
+ State: madmin.DriveStateOk,
+ },
+ {
+ State: madmin.DriveStateOk,
+ },
+ {
+ State: madmin.DriveStateOk,
+ },
+ },
+ },
+ }
+ mockHealTaskStatus := madmin.HealTaskStatus{
+ StartTime: time.Now().UTC().Truncate(time.Second * 2), // mock 2 sec duration
+ Items: []madmin.HealResultItem{
+ mockResultItem1,
+ mockResultItem2,
+ },
+ Summary: "finished",
+ }
+
+ testStreamSize := 1
+ testReceiver := make(chan healStatus, testStreamSize)
+ isClosed := false // testReceiver is closed?
+
+ testOptions := &healOptions{
+ BucketName: "testbucket",
+ Prefix: "",
+ ForceStart: false,
+ ForceStop: false,
+ }
+ // Test-1: startHeal send simple stream of data, no errors
+ minioHealMock = func(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
+ forceStart, forceStop bool,
+ ) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error) {
+ return healStart, mockHealTaskStatus, nil
+ }
+ writesCount := 1
+ // mock connection WriteMessage() no error
+ connWriteMessageMock = func(messageType int, data []byte) error {
+ // emulate that receiver gets the message written
+ var t healStatus
+ _ = json.Unmarshal(data, &t)
+ testReceiver <- t
+ if writesCount == testStreamSize {
+ // for testing we need to close the receiver channel
+ if !isClosed {
+ close(testReceiver)
+ isClosed = true
+ }
+ return nil
+ }
+ writesCount++
+ return nil
+ }
+ if err := startHeal(ctx, mockWSConn, client, testOptions); err != nil {
+ t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
+ }
+ // check that the TestReceiver got the same number of data from Console.
+ for i := range testReceiver {
+ assert.Equal(int64(1), i.ObjectsScanned)
+ assert.Equal(int64(1), i.ObjectsHealed)
+ assert.Equal(int64(2), i.ItemsScanned)
+ assert.Equal(int64(2), i.ItemsHealed)
+ assert.Equal(int64(0), i.HealthBeforeCols[colGreen])
+ assert.Equal(int64(1), i.HealthBeforeCols[colYellow])
+ assert.Equal(int64(1), i.HealthBeforeCols[colRed])
+ assert.Equal(int64(0), i.HealthBeforeCols[colGrey])
+ assert.Equal(int64(2), i.HealthAfterCols[colGreen])
+ assert.Equal(int64(0), i.HealthAfterCols[colYellow])
+ assert.Equal(int64(0), i.HealthAfterCols[colRed])
+ assert.Equal(int64(0), i.HealthAfterCols[colGrey])
+ }
+
+ // Test-2: startHeal error on init
+ minioHealMock = func(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
+ forceStart, forceStop bool,
+ ) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error) {
+ return healStart, mockHealTaskStatus, errors.New("error")
+ }
+
+ if err := startHeal(ctx, mockWSConn, client, testOptions); assert.Error(err) {
+ assert.Equal("error", err.Error())
+ }
+
+ // Test-3: getHealOptionsFromReq return heal options from request
+ u, _ := url.Parse("http://localhost/api/v1/heal/bucket1?prefix=file/&recursive=true&force-start=true&force-stop=true&remove=true&dry-run=true&scan=deep")
+ req := &http.Request{
+ URL: u,
+ }
+ opts, err := getHealOptionsFromReq(req)
+ if assert.NoError(err) {
+ expectedOptions := healOptions{
+ BucketName: "bucket1",
+ ForceStart: true,
+ ForceStop: true,
+ Prefix: "file/",
+ HealOpts: madmin.HealOpts{
+ Recursive: true,
+ DryRun: true,
+ ScanMode: madmin.HealDeepScan,
+ },
+ }
+ assert.Equal(expectedOptions.BucketName, opts.BucketName)
+ assert.Equal(expectedOptions.Prefix, opts.Prefix)
+ assert.Equal(expectedOptions.Recursive, opts.Recursive)
+ assert.Equal(expectedOptions.ForceStart, opts.ForceStart)
+ assert.Equal(expectedOptions.DryRun, opts.DryRun)
+ assert.Equal(expectedOptions.ScanMode, opts.ScanMode)
+ }
+
+ // Test-4: getHealOptionsFromReq return error if boolean value not valid
+ u, _ = url.Parse("http://localhost/api/v1/heal/bucket1?prefix=file/&recursive=nonbool&force-start=true&force-stop=true&remove=true&dry-run=true&scan=deep")
+ req = &http.Request{
+ URL: u,
+ }
+ _, err = getHealOptionsFromReq(req)
+ if assert.Error(err) {
+ assert.Equal("strconv.ParseBool: parsing \"nonbool\": invalid syntax", err.Error())
+ }
+ // Test-5: getHealOptionsFromReq return error if boolean value not valid
+ u, _ = url.Parse("http://localhost/api/v1/heal/bucket1?prefix=file/&recursive=true&force-start=true&force-stop=true&remove=nonbool&dry-run=true&scan=deep")
+ req = &http.Request{
+ URL: u,
+ }
+ _, err = getHealOptionsFromReq(req)
+ if assert.Error(err) {
+ assert.Equal("strconv.ParseBool: parsing \"nonbool\": invalid syntax", err.Error())
+ }
+ // Test-6: getHealOptionsFromReq return error if boolean value not valid
+ u, _ = url.Parse("http://localhost/api/v1/heal/bucket1?prefix=file/&recursive=true&force-start=nonbool&force-stop=true&remove=true&dry-run=true&scan=deep")
+ req = &http.Request{
+ URL: u,
+ }
+ _, err = getHealOptionsFromReq(req)
+ if assert.Error(err) {
+ assert.Equal("strconv.ParseBool: parsing \"nonbool\": invalid syntax", err.Error())
+ }
+ // Test-7: getHealOptionsFromReq return error if boolean value not valid
+ u, _ = url.Parse("http://localhost/api/v1/heal/bucket1?prefix=file/&recursive=true&force-start=true&force-stop=nonbool&remove=true&dry-run=true&scan=deep")
+ req = &http.Request{
+ URL: u,
+ }
+ _, err = getHealOptionsFromReq(req)
+ if assert.Error(err) {
+ assert.Equal("strconv.ParseBool: parsing \"nonbool\": invalid syntax", err.Error())
+ }
+ // Test-8: getHealOptionsFromReq return error if boolean value not valid
+ u, _ = url.Parse("http://localhost/api/v1/heal/bucket1?prefix=file/&recursive=true&force-start=true&force-stop=true&remove=true&dry-run=nonbool&scan=deep")
+ req = &http.Request{
+ URL: u,
+ }
+ _, err = getHealOptionsFromReq(req)
+ if assert.Error(err) {
+ assert.Equal("strconv.ParseBool: parsing \"nonbool\": invalid syntax", err.Error())
+ }
+}
diff --git a/api/ws_handle.go b/api/ws_handle.go
index d66c2b113a..65feec3004 100644
--- a/api/ws_handle.go
+++ b/api/ws_handle.go
@@ -63,6 +63,7 @@ type wsAdminClient struct {
// ConsoleWebsocket interface of a Websocket Client
type ConsoleWebsocket interface {
watch(options watchOptions)
+ heal(opts healOptions)
}
type wsS3Client struct {
@@ -254,6 +255,20 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
return
}
go wsAdminClient.healthInfo(ctx, deadline)
+ case strings.HasPrefix(wsPath, `/heal`):
+ hOptions, err := getHealOptionsFromReq(req)
+ if err != nil {
+ ErrorWithContext(ctx, fmt.Errorf("error getting heal options: %v", err))
+ closeWsConn(conn)
+ return
+ }
+ wsAdminClient, err := newWebSocketAdminClient(conn, session)
+ if err != nil {
+ ErrorWithContext(ctx, err)
+ closeWsConn(conn)
+ return
+ }
+ go wsAdminClient.heal(ctx, hOptions)
case strings.HasPrefix(wsPath, `/watch`):
wOptions, err := getWatchOptionsFromReq(req)
if err != nil {
@@ -487,6 +502,21 @@ func (wsc *wsS3Client) watch(ctx context.Context, params *watchOptions) {
sendWsCloseMessage(wsc.conn, err)
}
+func (wsc *wsAdminClient) heal(ctx context.Context, opts *healOptions) {
+ defer func() {
+ LogInfo("heal stopped")
+ // close connection after return
+ wsc.conn.close()
+ }()
+ LogInfo("heal started")
+
+ ctx = wsReadClientCtx(ctx, wsc.conn)
+
+ err := startHeal(ctx, wsc.conn, wsc.client, opts)
+
+ sendWsCloseMessage(wsc.conn, err)
+}
+
func (wsc *wsAdminClient) healthInfo(ctx context.Context, deadline *time.Duration) {
defer func() {
LogInfo("health info stopped")
diff --git a/web-app/tests/policies/heal.json b/web-app/tests/policies/heal.json
new file mode 100644
index 0000000000..2848bc8f9f
--- /dev/null
+++ b/web-app/tests/policies/heal.json
@@ -0,0 +1,16 @@
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Action": ["admin:Heal"],
+ "Effect": "Allow",
+ "Sid": ""
+ },
+ {
+ "Action": ["s3:ListBucket"],
+ "Effect": "Allow",
+ "Resource": ["arn:aws:s3:::*"],
+ "Sid": ""
+ }
+ ]
+}
diff --git a/web-app/tests/scripts/common.sh b/web-app/tests/scripts/common.sh
index e251df8679..d897ccc1fb 100755
--- a/web-app/tests/scripts/common.sh
+++ b/web-app/tests/scripts/common.sh
@@ -30,6 +30,7 @@ create_policies() {
mc admin policy create minio dashboard-$TIMESTAMP web-app/tests/policies/dashboard.json
mc admin policy create minio diagnostics-$TIMESTAMP web-app/tests/policies/diagnostics.json
mc admin policy create minio groups-$TIMESTAMP web-app/tests/policies/groups.json
+ mc admin policy create minio heal-$TIMESTAMP web-app/tests/policies/heal.json
mc admin policy create minio iampolicies-$TIMESTAMP web-app/tests/policies/iamPolicies.json
mc admin policy create minio logs-$TIMESTAMP web-app/tests/policies/logs.json
mc admin policy create minio notificationendpoints-$TIMESTAMP web-app/tests/policies/notificationEndpoints.json
From e8c4ba1d6e9c956c6f349912d6bf8b263c694010 Mon Sep 17 00:00:00 2001
From: Georg Mangold <67909897+georgmangold@users.noreply.github.com>
Date: Fri, 27 Jun 2025 16:04:11 +0200
Subject: [PATCH 2/3] revert: "Deprecated Heal (Drives) functionality (#3186)"
This reverts commit 08c922dca650402e2df7d030a246c94ef24e2348.
---
.../src/common/SecureComponent/permissions.ts | 2 +
web-app/src/screens/Console/Console.tsx | 5 +
web-app/src/screens/Console/Heal/Heal.tsx | 381 ++++++++++++++++++
web-app/src/screens/Console/Heal/types.ts | 65 +++
web-app/src/screens/Console/valid-routes.tsx | 7 +
web-app/tests/permissions-6/heal.ts | 78 ++++
.../deleteWithPrefixes.ts | 0
.../errorsVisibleOB.ts | 0
.../filePreview.ts | 0
.../resourceTesting.ts | 0
.../sameBucketPath.ts | 0
.../{permissions-7 => permissions-8}/users.ts | 0
.../rewind.ts | 0
13 files changed, 538 insertions(+)
create mode 100644 web-app/src/screens/Console/Heal/Heal.tsx
create mode 100644 web-app/src/screens/Console/Heal/types.ts
create mode 100644 web-app/tests/permissions-6/heal.ts
rename web-app/tests/{permissions-6 => permissions-7}/deleteWithPrefixes.ts (100%)
rename web-app/tests/{permissions-6 => permissions-7}/errorsVisibleOB.ts (100%)
rename web-app/tests/{permissions-6 => permissions-7}/filePreview.ts (100%)
rename web-app/tests/{permissions-6 => permissions-7}/resourceTesting.ts (100%)
rename web-app/tests/{permissions-6 => permissions-7}/sameBucketPath.ts (100%)
rename web-app/tests/{permissions-7 => permissions-8}/users.ts (100%)
rename web-app/tests/{permissions-8 => permissions-9}/rewind.ts (100%)
diff --git a/web-app/src/common/SecureComponent/permissions.ts b/web-app/src/common/SecureComponent/permissions.ts
index 087477a020..a2b619aaf5 100644
--- a/web-app/src/common/SecureComponent/permissions.ts
+++ b/web-app/src/common/SecureComponent/permissions.ts
@@ -175,6 +175,7 @@ export const IAM_PAGES = {
TOOLS_AUDITLOGS: "/tools/audit-logs",
TOOLS_TRACE: "/tools/trace",
DASHBOARD: "/tools/metrics",
+ TOOLS_HEAL: "/tools/heal",
TOOLS_WATCH: "/tools/watch",
/* KMS */
@@ -405,6 +406,7 @@ export const IAM_PAGES_PERMISSIONS = {
IAM_SCOPES.S3_LISTEN_BUCKET_NOTIFICATIONS, // display watch notifications
],
[IAM_PAGES.TOOLS_TRACE]: [IAM_SCOPES.ADMIN_SERVER_TRACE],
+ [IAM_PAGES.TOOLS_HEAL]: [IAM_SCOPES.ADMIN_HEAL],
[IAM_PAGES.TOOLS_DIAGNOSTICS]: [
IAM_SCOPES.ADMIN_HEALTH_INFO,
IAM_SCOPES.ADMIN_SERVER_INFO,
diff --git a/web-app/src/screens/Console/Console.tsx b/web-app/src/screens/Console/Console.tsx
index c4f9f2648d..d01229e5d5 100644
--- a/web-app/src/screens/Console/Console.tsx
+++ b/web-app/src/screens/Console/Console.tsx
@@ -50,6 +50,7 @@ import LoadingComponent from "../../common/LoadingComponent";
import ComponentsScreen from "./Common/ComponentsScreen";
const Trace = React.lazy(() => import("./Trace/Trace"));
+const Heal = React.lazy(() => import("./Heal/Heal"));
const Watch = React.lazy(() => import("./Watch/Watch"));
const HealthInfo = React.lazy(() => import("./HealthInfo/HealthInfo"));
@@ -336,6 +337,10 @@ const Console = () => {
component: IDPOpenIDConfigurationDetails,
path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS_VIEW,
},
+ {
+ component: Heal,
+ path: IAM_PAGES.TOOLS_HEAL,
+ },
{
component: Trace,
path: IAM_PAGES.TOOLS_TRACE,
diff --git a/web-app/src/screens/Console/Heal/Heal.tsx b/web-app/src/screens/Console/Heal/Heal.tsx
new file mode 100644
index 0000000000..48eab714c7
--- /dev/null
+++ b/web-app/src/screens/Console/Heal/Heal.tsx
@@ -0,0 +1,381 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2021 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import React, { Fragment, useEffect, useState } from "react";
+import { useSelector } from "react-redux";
+import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
+import {
+ Box,
+ Button,
+ Checkbox,
+ Grid,
+ HealIcon,
+ InputBox,
+ InputLabel,
+ PageLayout,
+ Select,
+} from "mds";
+import {
+ Bar,
+ BarChart,
+ CartesianGrid,
+ Legend,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+
+import { api } from "api";
+import { Bucket } from "api/consoleApi";
+import { errorToHandler } from "api/errors";
+import { wsProtocol } from "../../../utils/wsUtils";
+import { colorH, HealStatus } from "./types";
+import { niceBytes } from "../../../common/utils";
+import { modalStyleUtils } from "../Common/FormComponents/common/styleLibrary";
+import {
+ CONSOLE_UI_RESOURCE,
+ IAM_SCOPES,
+} from "../../../common/SecureComponent/permissions";
+import { selDistSet, setHelpName } from "../../../systemSlice";
+import { SecureComponent } from "../../../common/SecureComponent";
+import { useAppDispatch } from "../../../store";
+import DistributedOnly from "../Common/DistributedOnly/DistributedOnly";
+import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper";
+import HelpMenu from "../HelpMenu";
+
+const Heal = () => {
+ const distributedSetup = useSelector(selDistSet);
+
+ const [start, setStart] = useState(false);
+ const [bucketName, setBucketName] = useState("");
+ const [bucketList, setBucketList] = useState([]);
+ const [prefix, setPrefix] = useState("");
+ const [recursive, setRecursive] = useState(false);
+ const [forceStart, setForceStart] = useState(false);
+ const [forceStop, setForceStop] = useState(false);
+ // healStatus states
+ const [hStatus, setHStatus] = useState({
+ beforeHeal: [0, 0, 0, 0],
+ afterHeal: [0, 0, 0, 0],
+ objectsHealed: 0,
+ objectsScanned: 0,
+ healDuration: 0,
+ sizeScanned: "",
+ });
+
+ const fetchBucketList = () => {
+ api.buckets
+ .listBuckets()
+ .then((res) => {
+ let buckets: Bucket[] = [];
+ if (res.data.buckets) {
+ buckets = res.data.buckets;
+ }
+ setBucketList(buckets);
+ })
+ .catch((err) => {
+ console.error(errorToHandler(err.error));
+ });
+ };
+
+ useEffect(() => {
+ fetchBucketList();
+ }, []);
+
+ // forceStart and forceStop need to be mutually exclusive
+ useEffect(() => {
+ if (forceStart) {
+ setForceStop(false);
+ }
+ }, [forceStart]);
+
+ useEffect(() => {
+ if (forceStop) {
+ setForceStart(false);
+ }
+ }, [forceStop]);
+
+ const colorHealthArr = (color: colorH) => {
+ return [color.Green, color.Yellow, color.Red, color.Grey];
+ };
+
+ useEffect(() => {
+ // begin watch if bucketName in bucketList and start pressed
+ if (start) {
+ // values stored here to update chart
+ const cB: colorH = { Green: 0, Yellow: 0, Red: 0, Grey: 0 };
+ const cA: colorH = { Green: 0, Yellow: 0, Red: 0, Grey: 0 };
+
+ const url = new URL(window.location.toString());
+ const isDev = process.env.NODE_ENV === "development";
+ const port = isDev ? "9090" : url.port;
+
+ // check if we are using base path, if not this always is `/`
+ const baseLocation = new URL(document.baseURI);
+ const baseUrl = baseLocation.pathname;
+
+ const wsProt = wsProtocol(url.protocol);
+ const c = new W3CWebSocket(
+ `${wsProt}://${url.hostname}:${port}${baseUrl}ws/heal/${bucketName}?prefix=${prefix}&recursive=${recursive}&force-start=${forceStart}&force-stop=${forceStop}`,
+ );
+
+ if (c !== null) {
+ c.onopen = () => {
+ console.log("WebSocket Client Connected");
+ c.send("ok");
+ };
+ c.onmessage = (message: IMessageEvent) => {
+ let m: HealStatus = JSON.parse(message.data.toString());
+ // Store percentage per health color
+ for (const [key, value] of Object.entries(m.healthAfterCols)) {
+ cA[key] = (value * 100) / m.itemsScanned;
+ }
+ for (const [key, value] of Object.entries(m.healthBeforeCols)) {
+ cB[key] = (value * 100) / m.itemsScanned;
+ }
+ setHStatus({
+ beforeHeal: colorHealthArr(cB),
+ afterHeal: colorHealthArr(cA),
+ objectsHealed: m.objectsHealed,
+ objectsScanned: m.objectsScanned,
+ healDuration: m.healDuration,
+ sizeScanned: niceBytes(m.bytesScanned.toString()),
+ });
+ };
+ c.onclose = () => {
+ setStart(false);
+ console.log("connection closed by server");
+ };
+ return () => {
+ // close websocket on useEffect cleanup
+ c.close(1000);
+ console.log("closing websockets");
+ };
+ }
+ }
+ }, [start, bucketName, forceStart, forceStop, prefix, recursive]);
+
+ let data = [
+ {
+ name: "Green",
+ ah: hStatus.afterHeal[0],
+ bh: hStatus.beforeHeal[0],
+ amt: 100,
+ },
+ {
+ name: "Yellow",
+ ah: hStatus.afterHeal[1],
+ bh: hStatus.beforeHeal[1],
+ amt: 100,
+ },
+ {
+ name: "Red",
+ ah: hStatus.afterHeal[2],
+ bh: hStatus.beforeHeal[2],
+ amt: 100,
+ },
+ {
+ name: "Grey",
+ ah: hStatus.afterHeal[3],
+ bh: hStatus.beforeHeal[3],
+ amt: 100,
+ },
+ ];
+ const bucketNames = bucketList.map((bucketName) => ({
+ label: bucketName.name,
+ value: bucketName.name,
+ }));
+ const dispatch = useAppDispatch();
+ useEffect(() => {
+ dispatch(setHelpName("heal"));
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+ } />
+
+
+ {!distributedSetup ? (
+ } />
+ ) : (
+
+
+
+
+ Bucket
+
+
+ Prefix
+ {
+ setPrefix(e.target.value);
+ }}
+ />
+
+
+
+
+ {
+ setRecursive(!recursive);
+ }}
+ disabled={false}
+ label="Recursive"
+ />
+
+
+ {
+ setForceStart(!forceStart);
+ }}
+ disabled={false}
+ label="Force Start"
+ />
+
+
+ {
+ setForceStop(!forceStop);
+ }}
+ disabled={false}
+ label="Force Stop"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Size scanned: {hStatus.sizeScanned}
+
+
+ Objects healed: {hStatus.objectsHealed} /{" "}
+ {hStatus.objectsScanned}
+
+
+ Healing time: {hStatus.healDuration}s
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default Heal;
diff --git a/web-app/src/screens/Console/Heal/types.ts b/web-app/src/screens/Console/Heal/types.ts
new file mode 100644
index 0000000000..2a1a6cd53b
--- /dev/null
+++ b/web-app/src/screens/Console/Heal/types.ts
@@ -0,0 +1,65 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2021 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+export interface HealDriveInfo {
+ uuid: string;
+ endpoint: string;
+ state: string;
+}
+
+export interface MomentHealth {
+ color: string;
+ offline: number;
+ online: number;
+ missing: number;
+ corrupted: number;
+ drives: HealDriveInfo[];
+}
+
+export interface HealItemStatus {
+ status: string;
+ error: string;
+ type: string;
+ name: string;
+ before: MomentHealth;
+ after: MomentHealth;
+ size: number;
+}
+
+export interface HealStatus {
+ healDuration: number;
+ bytesScanned: number;
+ objectsScanned: number;
+ itemsScanned: number;
+ // Counters for healed objects and all kinds of healed items
+ objectsHealed: number;
+ itemsHealed: number;
+
+ itemsHealthStatus: HealItemStatus[];
+ // Map of health color code to number of objects with that
+ // health color code.
+ healthBeforeCols: Map;
+ healthAfterCols: Map;
+}
+
+// colorH used to save health's percentage per color
+export interface colorH {
+ Yellow: number;
+ Red: number;
+ Grey: number;
+
+ [Green: string]: number;
+}
diff --git a/web-app/src/screens/Console/valid-routes.tsx b/web-app/src/screens/Console/valid-routes.tsx
index 63fe7bc7ac..bcdfdd455d 100644
--- a/web-app/src/screens/Console/valid-routes.tsx
+++ b/web-app/src/screens/Console/valid-routes.tsx
@@ -30,6 +30,7 @@ import {
AuditLogsMenuIcon,
BucketsMenuIcon,
DocumentationIcon,
+ DrivesMenuIcon,
GroupsMenuIcon,
HealthMenuIcon,
IdentityMenuIcon,
@@ -211,6 +212,12 @@ export const validRoutes = (features: string[] | null | undefined) => {
icon: ,
path: IAM_PAGES.TOOLS_WATCH,
},
+ {
+ name: "Drives",
+ id: "monitorDrives",
+ path: IAM_PAGES.TOOLS_HEAL,
+ icon: ,
+ },
{
name: "Encryption",
id: "monitorEncryption",
diff --git a/web-app/tests/permissions-6/heal.ts b/web-app/tests/permissions-6/heal.ts
new file mode 100644
index 0000000000..fd99f28ad6
--- /dev/null
+++ b/web-app/tests/permissions-6/heal.ts
@@ -0,0 +1,78 @@
+// This file is part of MinIO Console Server
+// Copyright (c) 2022 MinIO, Inc.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import * as roles from "../utils/roles";
+import * as elements from "../utils/elements";
+import { bucketDropdownOptionFor } from "../utils/elements";
+import * as functions from "../utils/functions";
+import { drivesElement, monitoringElement } from "../utils/elements-menu";
+
+fixture("For user with Heal permissions")
+ .page("http://localhost:9090")
+ .beforeEach(async (t) => {
+ await t.useRole(roles.heal);
+ });
+
+test("Monitoring sidebar item exists", async (t) => {
+ await t.expect(monitoringElement.exists).ok();
+});
+
+test("Heal menu exists in Monitoring page", async (t) => {
+ await t
+ .expect(monitoringElement.exists)
+ .ok()
+ .click(monitoringElement)
+ .expect(drivesElement.exists)
+ .ok();
+});
+
+test("Heal page can be opened", async (t) => {
+ await t.navigateTo("http://localhost:9090/tools/heal");
+});
+
+test
+ .before(async (t) => {
+ // Create a bucket
+ await functions.setUpBucket(t, "heal");
+ })("Start button exists", async (t) => {
+ const startButtonExists = elements.startButton.exists;
+ await t
+ .useRole(roles.heal)
+ .navigateTo("http://localhost:9090/tools/heal")
+ .expect(startButtonExists)
+ .ok();
+ })
+ .after(async (t) => {
+ // Cleanup created bucket
+ await functions.cleanUpBucket(t, "heal");
+ });
+
+test
+ .before(async (t) => {
+ // Create a bucket
+ await functions.setUpBucket(t, "heal2");
+ })("Start button can be clicked", async (t) => {
+ await t
+ .useRole(roles.heal)
+ .navigateTo("http://localhost:9090/tools/heal")
+ .click(elements.bucketNameInput)
+ .click(bucketDropdownOptionFor("heal2"))
+ .click(elements.startButton);
+ })
+ .after(async (t) => {
+ // Cleanup created bucket
+ await functions.cleanUpBucket(t, "heal2");
+ });
diff --git a/web-app/tests/permissions-6/deleteWithPrefixes.ts b/web-app/tests/permissions-7/deleteWithPrefixes.ts
similarity index 100%
rename from web-app/tests/permissions-6/deleteWithPrefixes.ts
rename to web-app/tests/permissions-7/deleteWithPrefixes.ts
diff --git a/web-app/tests/permissions-6/errorsVisibleOB.ts b/web-app/tests/permissions-7/errorsVisibleOB.ts
similarity index 100%
rename from web-app/tests/permissions-6/errorsVisibleOB.ts
rename to web-app/tests/permissions-7/errorsVisibleOB.ts
diff --git a/web-app/tests/permissions-6/filePreview.ts b/web-app/tests/permissions-7/filePreview.ts
similarity index 100%
rename from web-app/tests/permissions-6/filePreview.ts
rename to web-app/tests/permissions-7/filePreview.ts
diff --git a/web-app/tests/permissions-6/resourceTesting.ts b/web-app/tests/permissions-7/resourceTesting.ts
similarity index 100%
rename from web-app/tests/permissions-6/resourceTesting.ts
rename to web-app/tests/permissions-7/resourceTesting.ts
diff --git a/web-app/tests/permissions-6/sameBucketPath.ts b/web-app/tests/permissions-7/sameBucketPath.ts
similarity index 100%
rename from web-app/tests/permissions-6/sameBucketPath.ts
rename to web-app/tests/permissions-7/sameBucketPath.ts
diff --git a/web-app/tests/permissions-7/users.ts b/web-app/tests/permissions-8/users.ts
similarity index 100%
rename from web-app/tests/permissions-7/users.ts
rename to web-app/tests/permissions-8/users.ts
diff --git a/web-app/tests/permissions-8/rewind.ts b/web-app/tests/permissions-9/rewind.ts
similarity index 100%
rename from web-app/tests/permissions-8/rewind.ts
rename to web-app/tests/permissions-9/rewind.ts
From f674cd4fd57338759275ec58ab9d4672bbb173c1 Mon Sep 17 00:00:00 2001
From: Georg Mangold <67909897+georgmangold@users.noreply.github.com>
Date: Fri, 27 Jun 2025 17:21:04 +0200
Subject: [PATCH 3/3] fix: Websocket, Renamed unused-parameter, Unused exported
types
---
api/admin_heal_test.go | 10 +++++-----
api/ws_handle.go | 2 +-
web-app/src/screens/Console/Heal/Heal.tsx | 5 ++---
web-app/src/screens/Console/Heal/types.ts | 6 +++---
4 files changed, 11 insertions(+), 12 deletions(-)
diff --git a/api/admin_heal_test.go b/api/admin_heal_test.go
index 0ca5476b36..d3cf80828c 100644
--- a/api/admin_heal_test.go
+++ b/api/admin_heal_test.go
@@ -143,14 +143,14 @@ func TestHeal(t *testing.T) {
ForceStop: false,
}
// Test-1: startHeal send simple stream of data, no errors
- minioHealMock = func(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
- forceStart, forceStop bool,
+ minioHealMock = func(_ context.Context, _ string, _ string, _ madmin.HealOpts, _ string,
+ _ bool, _ bool,
) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error) {
return healStart, mockHealTaskStatus, nil
}
writesCount := 1
// mock connection WriteMessage() no error
- connWriteMessageMock = func(messageType int, data []byte) error {
+ connWriteMessageMock = func(_ int, data []byte) error {
// emulate that receiver gets the message written
var t healStatus
_ = json.Unmarshal(data, &t)
@@ -186,8 +186,8 @@ func TestHeal(t *testing.T) {
}
// Test-2: startHeal error on init
- minioHealMock = func(ctx context.Context, bucket, prefix string, healOpts madmin.HealOpts, clientToken string,
- forceStart, forceStop bool,
+ minioHealMock = func(_ context.Context, _, _ string, _ madmin.HealOpts, _ string,
+ _ bool, _ bool,
) (healStart madmin.HealStartSuccess, healTaskStatus madmin.HealTaskStatus, err error) {
return healStart, mockHealTaskStatus, errors.New("error")
}
diff --git a/api/ws_handle.go b/api/ws_handle.go
index 65feec3004..02b7419e94 100644
--- a/api/ws_handle.go
+++ b/api/ws_handle.go
@@ -262,7 +262,7 @@ func serveWS(w http.ResponseWriter, req *http.Request) {
closeWsConn(conn)
return
}
- wsAdminClient, err := newWebSocketAdminClient(conn, session)
+ wsAdminClient, err := newWebSocketAdminClient(conn, session, clientIP)
if err != nil {
ErrorWithContext(ctx, err)
closeWsConn(conn)
diff --git a/web-app/src/screens/Console/Heal/Heal.tsx b/web-app/src/screens/Console/Heal/Heal.tsx
index 48eab714c7..10ef30c707 100644
--- a/web-app/src/screens/Console/Heal/Heal.tsx
+++ b/web-app/src/screens/Console/Heal/Heal.tsx
@@ -16,7 +16,6 @@
import React, { Fragment, useEffect, useState } from "react";
import { useSelector } from "react-redux";
-import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import {
Box,
Button,
@@ -129,7 +128,7 @@ const Heal = () => {
const baseUrl = baseLocation.pathname;
const wsProt = wsProtocol(url.protocol);
- const c = new W3CWebSocket(
+ const c = new WebSocket(
`${wsProt}://${url.hostname}:${port}${baseUrl}ws/heal/${bucketName}?prefix=${prefix}&recursive=${recursive}&force-start=${forceStart}&force-stop=${forceStop}`,
);
@@ -138,7 +137,7 @@ const Heal = () => {
console.log("WebSocket Client Connected");
c.send("ok");
};
- c.onmessage = (message: IMessageEvent) => {
+ c.onmessage = (message: MessageEvent) => {
let m: HealStatus = JSON.parse(message.data.toString());
// Store percentage per health color
for (const [key, value] of Object.entries(m.healthAfterCols)) {
diff --git a/web-app/src/screens/Console/Heal/types.ts b/web-app/src/screens/Console/Heal/types.ts
index 2a1a6cd53b..c4098d8b6b 100644
--- a/web-app/src/screens/Console/Heal/types.ts
+++ b/web-app/src/screens/Console/Heal/types.ts
@@ -14,13 +14,13 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-export interface HealDriveInfo {
+interface HealDriveInfo {
uuid: string;
endpoint: string;
state: string;
}
-export interface MomentHealth {
+interface MomentHealth {
color: string;
offline: number;
online: number;
@@ -29,7 +29,7 @@ export interface MomentHealth {
drives: HealDriveInfo[];
}
-export interface HealItemStatus {
+interface HealItemStatus {
status: string;
error: string;
type: string;