mirror of https://github.com/minio/minio.git
535 lines
14 KiB
Go
535 lines
14 KiB
Go
// Copyright (c) 2015-2021 MinIO, Inc.
|
|
//
|
|
// This file is part of MinIO Object Storage stack
|
|
//
|
|
// 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 <http://www.gnu.org/licenses/>.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"sort"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/minio/madmin-go/v3"
|
|
"github.com/minio/minio/internal/auth"
|
|
"github.com/minio/mux"
|
|
)
|
|
|
|
// adminErasureTestBed - encapsulates subsystems that need to be setup for
|
|
// admin-handler unit tests.
|
|
type adminErasureTestBed struct {
|
|
erasureDirs []string
|
|
objLayer ObjectLayer
|
|
router *mux.Router
|
|
done context.CancelFunc
|
|
}
|
|
|
|
// prepareAdminErasureTestBed - helper function that setups a single-node
|
|
// Erasure backend for admin-handler tests.
|
|
func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, error) {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
// reset global variables to start afresh.
|
|
resetTestGlobals()
|
|
|
|
// Set globalIsErasure to indicate that the setup uses an erasure
|
|
// code backend.
|
|
globalIsErasure = true
|
|
|
|
// Initializing objectLayer for HealFormatHandler.
|
|
objLayer, erasureDirs, xlErr := initTestErasureObjLayer(ctx)
|
|
if xlErr != nil {
|
|
cancel()
|
|
return nil, xlErr
|
|
}
|
|
|
|
// Initialize minio server config.
|
|
if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
|
cancel()
|
|
return nil, err
|
|
}
|
|
|
|
// Initialize boot time
|
|
globalBootTime = UTCNow()
|
|
|
|
globalEndpoints = mustGetPoolEndpoints(0, erasureDirs...)
|
|
|
|
initAllSubsystems(ctx)
|
|
|
|
initConfigSubsystem(ctx, objLayer)
|
|
|
|
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
|
|
|
// Setup admin mgmt REST API handlers.
|
|
adminRouter := mux.NewRouter()
|
|
registerAdminRouter(adminRouter, true)
|
|
|
|
return &adminErasureTestBed{
|
|
erasureDirs: erasureDirs,
|
|
objLayer: objLayer,
|
|
router: adminRouter,
|
|
done: cancel,
|
|
}, nil
|
|
}
|
|
|
|
// TearDown - method that resets the test bed for subsequent unit
|
|
// tests to start afresh.
|
|
func (atb *adminErasureTestBed) TearDown() {
|
|
atb.done()
|
|
removeRoots(atb.erasureDirs)
|
|
resetTestGlobals()
|
|
}
|
|
|
|
// initTestObjLayer - Helper function to initialize an Erasure-based object
|
|
// layer and set globalObjectAPI.
|
|
func initTestErasureObjLayer(ctx context.Context) (ObjectLayer, []string, error) {
|
|
erasureDirs, err := getRandomDisks(16)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
endpoints := mustGetPoolEndpoints(0, erasureDirs...)
|
|
globalPolicySys = NewPolicySys()
|
|
objLayer, err := newErasureServerPools(ctx, endpoints)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Make objLayer available to all internal services via globalObjectAPI.
|
|
globalObjLayerMutex.Lock()
|
|
globalObjectAPI = objLayer
|
|
globalObjLayerMutex.Unlock()
|
|
return objLayer, erasureDirs, nil
|
|
}
|
|
|
|
// cmdType - Represents different service subcomands like status, stop
|
|
// and restart.
|
|
type cmdType int
|
|
|
|
const (
|
|
restartCmd cmdType = iota
|
|
stopCmd
|
|
)
|
|
|
|
// toServiceSignal - Helper function that translates a given cmdType
|
|
// value to its corresponding serviceSignal value.
|
|
func (c cmdType) toServiceSignal() serviceSignal {
|
|
switch c {
|
|
case restartCmd:
|
|
return serviceRestart
|
|
case stopCmd:
|
|
return serviceStop
|
|
}
|
|
return serviceRestart
|
|
}
|
|
|
|
func (c cmdType) toServiceAction() madmin.ServiceAction {
|
|
switch c {
|
|
case restartCmd:
|
|
return madmin.ServiceActionRestart
|
|
case stopCmd:
|
|
return madmin.ServiceActionStop
|
|
}
|
|
return madmin.ServiceActionRestart
|
|
}
|
|
|
|
// testServiceSignalReceiver - Helper function that simulates a
|
|
// go-routine waiting on service signal.
|
|
func testServiceSignalReceiver(cmd cmdType, t *testing.T) {
|
|
expectedCmd := cmd.toServiceSignal()
|
|
serviceCmd := <-globalServiceSignalCh
|
|
if serviceCmd != expectedCmd {
|
|
t.Errorf("Expected service command %v but received %v", expectedCmd, serviceCmd)
|
|
}
|
|
}
|
|
|
|
// getServiceCmdRequest - Constructs a management REST API request for service
|
|
// subcommands for a given cmdType value.
|
|
func getServiceCmdRequest(cmd cmdType, cred auth.Credentials) (*http.Request, error) {
|
|
queryVal := url.Values{}
|
|
queryVal.Set("action", string(cmd.toServiceAction()))
|
|
queryVal.Set("type", "2")
|
|
resource := adminPathPrefix + adminAPIVersionPrefix + "/service?" + queryVal.Encode()
|
|
req, err := newTestRequest(http.MethodPost, resource, 0, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// management REST API uses signature V4 for authentication.
|
|
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
// testServicesCmdHandler - parametrizes service subcommand tests on
|
|
// cmdType value.
|
|
func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
adminTestBed, err := prepareAdminErasureTestBed(ctx)
|
|
if err != nil {
|
|
t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.", err)
|
|
}
|
|
defer adminTestBed.TearDown()
|
|
|
|
// Initialize admin peers to make admin RPC calls. Note: In a
|
|
// single node setup, this degenerates to a simple function
|
|
// call under the hood.
|
|
globalMinioAddr = "127.0.0.1:9000"
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Setting up a go routine to simulate ServerRouter's
|
|
// handleServiceSignals for stop and restart commands.
|
|
if cmd == restartCmd {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
testServiceSignalReceiver(cmd, t)
|
|
}()
|
|
}
|
|
credentials := globalActiveCred
|
|
|
|
req, err := getServiceCmdRequest(cmd, credentials)
|
|
if err != nil {
|
|
t.Fatalf("Failed to build service status request %v", err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
|
|
resp, _ := io.ReadAll(rec.Body)
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
|
|
http.StatusOK, rec.Code, string(resp))
|
|
}
|
|
|
|
result := &serviceResult{}
|
|
if err := json.Unmarshal(resp, result); err != nil {
|
|
t.Error(err)
|
|
}
|
|
_ = result
|
|
|
|
// Wait until testServiceSignalReceiver() called in a goroutine quits.
|
|
wg.Wait()
|
|
}
|
|
|
|
// Test for service restart management REST API.
|
|
func TestServiceRestartHandler(t *testing.T) {
|
|
testServicesCmdHandler(restartCmd, t)
|
|
}
|
|
|
|
// buildAdminRequest - helper function to build an admin API request.
|
|
func buildAdminRequest(queryVal url.Values, method, path string,
|
|
contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error,
|
|
) {
|
|
req, err := newTestRequest(method,
|
|
adminPathPrefix+adminAPIVersionPrefix+path+"?"+queryVal.Encode(),
|
|
contentLength, bodySeeker)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cred := globalActiveCred
|
|
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
func TestAdminServerInfo(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
adminTestBed, err := prepareAdminErasureTestBed(ctx)
|
|
if err != nil {
|
|
t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.", err)
|
|
}
|
|
|
|
defer adminTestBed.TearDown()
|
|
|
|
// Initialize admin peers to make admin RPC calls.
|
|
globalMinioAddr = "127.0.0.1:9000"
|
|
|
|
// Prepare query params for set-config mgmt REST API.
|
|
queryVal := url.Values{}
|
|
queryVal.Set("info", "")
|
|
|
|
req, err := buildAdminRequest(queryVal, http.MethodGet, "/info", 0, nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to construct get-config object request - %v", err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
adminTestBed.router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Errorf("Expected to succeed but failed with %d", rec.Code)
|
|
}
|
|
|
|
results := madmin.InfoMessage{}
|
|
err = json.NewDecoder(rec.Body).Decode(&results)
|
|
if err != nil {
|
|
t.Fatalf("Failed to decode set config result json %v", err)
|
|
}
|
|
|
|
if results.Region != globalMinioDefaultRegion {
|
|
t.Errorf("Expected %s, got %s", globalMinioDefaultRegion, results.Region)
|
|
}
|
|
}
|
|
|
|
// TestToAdminAPIErrCode - test for toAdminAPIErrCode helper function.
|
|
func TestToAdminAPIErrCode(t *testing.T) {
|
|
testCases := []struct {
|
|
err error
|
|
expectedAPIErr APIErrorCode
|
|
}{
|
|
// 1. Server not in quorum.
|
|
{
|
|
err: errErasureWriteQuorum,
|
|
expectedAPIErr: ErrAdminConfigNoQuorum,
|
|
},
|
|
// 2. No error.
|
|
{
|
|
err: nil,
|
|
expectedAPIErr: ErrNone,
|
|
},
|
|
// 3. Non-admin API specific error.
|
|
{
|
|
err: errDiskNotFound,
|
|
expectedAPIErr: toAPIErrorCode(GlobalContext, errDiskNotFound),
|
|
},
|
|
}
|
|
|
|
for i, test := range testCases {
|
|
actualErr := toAdminAPIErrCode(GlobalContext, test.err)
|
|
if actualErr != test.expectedAPIErr {
|
|
t.Errorf("Test %d: Expected %v but received %v",
|
|
i+1, test.expectedAPIErr, actualErr)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractHealInitParams(t *testing.T) {
|
|
mkParams := func(clientToken string, forceStart, forceStop bool) url.Values {
|
|
v := url.Values{}
|
|
if clientToken != "" {
|
|
v.Add(mgmtClientToken, clientToken)
|
|
}
|
|
if forceStart {
|
|
v.Add(mgmtForceStart, "")
|
|
}
|
|
if forceStop {
|
|
v.Add(mgmtForceStop, "")
|
|
}
|
|
return v
|
|
}
|
|
qParmsArr := []url.Values{
|
|
// Invalid cases
|
|
mkParams("", true, true),
|
|
mkParams("111", true, true),
|
|
mkParams("111", true, false),
|
|
mkParams("111", false, true),
|
|
// Valid cases follow
|
|
mkParams("", true, false),
|
|
mkParams("", false, true),
|
|
mkParams("", false, false),
|
|
mkParams("111", false, false),
|
|
}
|
|
varsArr := []map[string]string{
|
|
// Invalid cases
|
|
{mgmtPrefix: "objprefix"},
|
|
// Valid cases
|
|
{},
|
|
{mgmtBucket: "bucket"},
|
|
{mgmtBucket: "bucket", mgmtPrefix: "objprefix"},
|
|
}
|
|
|
|
// Body is always valid - we do not test JSON decoding.
|
|
body := `{"recursive": false, "dryRun": true, "remove": false, "scanMode": 0}`
|
|
|
|
// Test all combinations!
|
|
for pIdx, parms := range qParmsArr {
|
|
for vIdx, vars := range varsArr {
|
|
_, err := extractHealInitParams(vars, parms, bytes.NewReader([]byte(body)))
|
|
isErrCase := false
|
|
if pIdx < 4 || vIdx < 1 {
|
|
isErrCase = true
|
|
}
|
|
|
|
if err != ErrNone && !isErrCase {
|
|
t.Errorf("Got unexpected error: %v %v %v", pIdx, vIdx, err)
|
|
} else if err == ErrNone && isErrCase {
|
|
t.Errorf("Got no error but expected one: %v %v", pIdx, vIdx)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type byResourceUID struct{ madmin.LockEntries }
|
|
|
|
func (b byResourceUID) Less(i, j int) bool {
|
|
toUniqLock := func(entry madmin.LockEntry) string {
|
|
return fmt.Sprintf("%s/%s", entry.Resource, entry.ID)
|
|
}
|
|
return toUniqLock(b.LockEntries[i]) < toUniqLock(b.LockEntries[j])
|
|
}
|
|
|
|
func TestTopLockEntries(t *testing.T) {
|
|
locksHeld := make(map[string][]lockRequesterInfo)
|
|
var owners []string
|
|
for i := 0; i < 4; i++ {
|
|
owners = append(owners, fmt.Sprintf("node-%d", i))
|
|
}
|
|
|
|
// Simulate DeleteObjects of 10 objects in a single request. i.e same lock
|
|
// request UID, but 10 different resource names associated with it.
|
|
var lris []lockRequesterInfo
|
|
uuid := mustGetUUID()
|
|
for i := 0; i < 10; i++ {
|
|
resource := fmt.Sprintf("bucket/delete-object-%d", i)
|
|
lri := lockRequesterInfo{
|
|
Name: resource,
|
|
Writer: true,
|
|
UID: uuid,
|
|
Owner: owners[i%len(owners)],
|
|
Group: true,
|
|
Quorum: 3,
|
|
}
|
|
lris = append(lris, lri)
|
|
locksHeld[resource] = []lockRequesterInfo{lri}
|
|
}
|
|
|
|
// Add a few concurrent read locks to the mix
|
|
for i := 0; i < 50; i++ {
|
|
resource := fmt.Sprintf("bucket/get-object-%d", i)
|
|
lri := lockRequesterInfo{
|
|
Name: resource,
|
|
UID: mustGetUUID(),
|
|
Owner: owners[i%len(owners)],
|
|
Quorum: 2,
|
|
}
|
|
lris = append(lris, lri)
|
|
locksHeld[resource] = append(locksHeld[resource], lri)
|
|
// concurrent read lock, same resource different uid
|
|
lri.UID = mustGetUUID()
|
|
lris = append(lris, lri)
|
|
locksHeld[resource] = append(locksHeld[resource], lri)
|
|
}
|
|
|
|
var peerLocks []*PeerLocks
|
|
for _, owner := range owners {
|
|
peerLocks = append(peerLocks, &PeerLocks{
|
|
Addr: owner,
|
|
Locks: locksHeld,
|
|
})
|
|
}
|
|
var exp madmin.LockEntries
|
|
for _, lri := range lris {
|
|
lockType := func(lri lockRequesterInfo) string {
|
|
if lri.Writer {
|
|
return "WRITE"
|
|
}
|
|
return "READ"
|
|
}
|
|
exp = append(exp, madmin.LockEntry{
|
|
Resource: lri.Name,
|
|
Type: lockType(lri),
|
|
ServerList: owners,
|
|
Owner: lri.Owner,
|
|
ID: lri.UID,
|
|
Quorum: lri.Quorum,
|
|
})
|
|
}
|
|
|
|
testCases := []struct {
|
|
peerLocks []*PeerLocks
|
|
expected madmin.LockEntries
|
|
}{
|
|
{
|
|
peerLocks: peerLocks,
|
|
expected: exp,
|
|
},
|
|
}
|
|
|
|
// printEntries := func(entries madmin.LockEntries) {
|
|
// for i, entry := range entries {
|
|
// fmt.Printf("%d: %s %s %s %s %v %d\n", i, entry.Resource, entry.ID, entry.Owner, entry.Type, entry.ServerList, entry.Elapsed)
|
|
// }
|
|
// }
|
|
|
|
check := func(exp, got madmin.LockEntries) (int, bool) {
|
|
if len(exp) != len(got) {
|
|
return 0, false
|
|
}
|
|
sort.Slice(exp, byResourceUID{exp}.Less)
|
|
sort.Slice(got, byResourceUID{got}.Less)
|
|
// printEntries(exp)
|
|
// printEntries(got)
|
|
for i, e := range exp {
|
|
if !e.Timestamp.Equal(got[i].Timestamp) {
|
|
return i, false
|
|
}
|
|
// Skip checking elapsed since it's time sensitive.
|
|
// if e.Elapsed != got[i].Elapsed {
|
|
// return false
|
|
// }
|
|
if e.Resource != got[i].Resource {
|
|
return i, false
|
|
}
|
|
if e.Type != got[i].Type {
|
|
return i, false
|
|
}
|
|
if e.Source != got[i].Source {
|
|
return i, false
|
|
}
|
|
if e.Owner != got[i].Owner {
|
|
return i, false
|
|
}
|
|
if e.ID != got[i].ID {
|
|
return i, false
|
|
}
|
|
if len(e.ServerList) != len(got[i].ServerList) {
|
|
return i, false
|
|
}
|
|
for j := range e.ServerList {
|
|
if e.ServerList[j] != got[i].ServerList[j] {
|
|
return i, false
|
|
}
|
|
}
|
|
}
|
|
return 0, true
|
|
}
|
|
|
|
for i, tc := range testCases {
|
|
got := topLockEntries(tc.peerLocks, false)
|
|
if idx, ok := check(tc.expected, got); !ok {
|
|
t.Fatalf("%d: mismatch at %d \n expected %#v but got %#v", i, idx, tc.expected[idx], got[idx])
|
|
}
|
|
}
|
|
}
|