github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/admin-handlers_test.go (about)

     1  // Copyright (c) 2015-2021 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"net/http"
    27  	"net/http/httptest"
    28  	"net/url"
    29  	"sort"
    30  	"sync"
    31  	"testing"
    32  	"time"
    33  
    34  	"github.com/minio/madmin-go/v3"
    35  	"github.com/minio/minio/internal/auth"
    36  	"github.com/minio/mux"
    37  )
    38  
    39  // adminErasureTestBed - encapsulates subsystems that need to be setup for
    40  // admin-handler unit tests.
    41  type adminErasureTestBed struct {
    42  	erasureDirs []string
    43  	objLayer    ObjectLayer
    44  	router      *mux.Router
    45  	done        context.CancelFunc
    46  }
    47  
    48  // prepareAdminErasureTestBed - helper function that setups a single-node
    49  // Erasure backend for admin-handler tests.
    50  func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, error) {
    51  	ctx, cancel := context.WithCancel(ctx)
    52  
    53  	// reset global variables to start afresh.
    54  	resetTestGlobals()
    55  
    56  	// Set globalIsErasure to indicate that the setup uses an erasure
    57  	// code backend.
    58  	globalIsErasure = true
    59  
    60  	// Initializing objectLayer for HealFormatHandler.
    61  	objLayer, erasureDirs, xlErr := initTestErasureObjLayer(ctx)
    62  	if xlErr != nil {
    63  		cancel()
    64  		return nil, xlErr
    65  	}
    66  
    67  	// Initialize minio server config.
    68  	if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
    69  		cancel()
    70  		return nil, err
    71  	}
    72  
    73  	// Initialize boot time
    74  	globalBootTime = UTCNow()
    75  
    76  	globalEndpoints = mustGetPoolEndpoints(0, erasureDirs...)
    77  
    78  	initAllSubsystems(ctx)
    79  
    80  	initConfigSubsystem(ctx, objLayer)
    81  
    82  	globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
    83  
    84  	// Setup admin mgmt REST API handlers.
    85  	adminRouter := mux.NewRouter()
    86  	registerAdminRouter(adminRouter, true)
    87  
    88  	return &adminErasureTestBed{
    89  		erasureDirs: erasureDirs,
    90  		objLayer:    objLayer,
    91  		router:      adminRouter,
    92  		done:        cancel,
    93  	}, nil
    94  }
    95  
    96  // TearDown - method that resets the test bed for subsequent unit
    97  // tests to start afresh.
    98  func (atb *adminErasureTestBed) TearDown() {
    99  	atb.done()
   100  	removeRoots(atb.erasureDirs)
   101  	resetTestGlobals()
   102  }
   103  
   104  // initTestObjLayer - Helper function to initialize an Erasure-based object
   105  // layer and set globalObjectAPI.
   106  func initTestErasureObjLayer(ctx context.Context) (ObjectLayer, []string, error) {
   107  	erasureDirs, err := getRandomDisks(16)
   108  	if err != nil {
   109  		return nil, nil, err
   110  	}
   111  	endpoints := mustGetPoolEndpoints(0, erasureDirs...)
   112  	globalPolicySys = NewPolicySys()
   113  	objLayer, err := newErasureServerPools(ctx, endpoints)
   114  	if err != nil {
   115  		return nil, nil, err
   116  	}
   117  
   118  	// Make objLayer available to all internal services via globalObjectAPI.
   119  	globalObjLayerMutex.Lock()
   120  	globalObjectAPI = objLayer
   121  	globalObjLayerMutex.Unlock()
   122  	return objLayer, erasureDirs, nil
   123  }
   124  
   125  // cmdType - Represents different service subcomands like status, stop
   126  // and restart.
   127  type cmdType int
   128  
   129  const (
   130  	restartCmd cmdType = iota
   131  	stopCmd
   132  )
   133  
   134  // toServiceSignal - Helper function that translates a given cmdType
   135  // value to its corresponding serviceSignal value.
   136  func (c cmdType) toServiceSignal() serviceSignal {
   137  	switch c {
   138  	case restartCmd:
   139  		return serviceRestart
   140  	case stopCmd:
   141  		return serviceStop
   142  	}
   143  	return serviceRestart
   144  }
   145  
   146  func (c cmdType) toServiceAction() madmin.ServiceAction {
   147  	switch c {
   148  	case restartCmd:
   149  		return madmin.ServiceActionRestart
   150  	case stopCmd:
   151  		return madmin.ServiceActionStop
   152  	}
   153  	return madmin.ServiceActionRestart
   154  }
   155  
   156  // testServiceSignalReceiver - Helper function that simulates a
   157  // go-routine waiting on service signal.
   158  func testServiceSignalReceiver(cmd cmdType, t *testing.T) {
   159  	expectedCmd := cmd.toServiceSignal()
   160  	serviceCmd := <-globalServiceSignalCh
   161  	if serviceCmd != expectedCmd {
   162  		t.Errorf("Expected service command %v but received %v", expectedCmd, serviceCmd)
   163  	}
   164  }
   165  
   166  // getServiceCmdRequest - Constructs a management REST API request for service
   167  // subcommands for a given cmdType value.
   168  func getServiceCmdRequest(cmd cmdType, cred auth.Credentials) (*http.Request, error) {
   169  	queryVal := url.Values{}
   170  	queryVal.Set("action", string(cmd.toServiceAction()))
   171  	queryVal.Set("type", "2")
   172  	resource := adminPathPrefix + adminAPIVersionPrefix + "/service?" + queryVal.Encode()
   173  	req, err := newTestRequest(http.MethodPost, resource, 0, nil)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	// management REST API uses signature V4 for authentication.
   179  	err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  	return req, nil
   184  }
   185  
   186  // testServicesCmdHandler - parametrizes service subcommand tests on
   187  // cmdType value.
   188  func testServicesCmdHandler(cmd cmdType, t *testing.T) {
   189  	ctx, cancel := context.WithCancel(context.Background())
   190  	defer cancel()
   191  
   192  	adminTestBed, err := prepareAdminErasureTestBed(ctx)
   193  	if err != nil {
   194  		t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.", err)
   195  	}
   196  	defer adminTestBed.TearDown()
   197  
   198  	// Initialize admin peers to make admin RPC calls. Note: In a
   199  	// single node setup, this degenerates to a simple function
   200  	// call under the hood.
   201  	globalMinioAddr = "127.0.0.1:9000"
   202  
   203  	var wg sync.WaitGroup
   204  
   205  	// Setting up a go routine to simulate ServerRouter's
   206  	// handleServiceSignals for stop and restart commands.
   207  	if cmd == restartCmd {
   208  		wg.Add(1)
   209  		go func() {
   210  			defer wg.Done()
   211  			testServiceSignalReceiver(cmd, t)
   212  		}()
   213  	}
   214  	credentials := globalActiveCred
   215  
   216  	req, err := getServiceCmdRequest(cmd, credentials)
   217  	if err != nil {
   218  		t.Fatalf("Failed to build service status request %v", err)
   219  	}
   220  
   221  	rec := httptest.NewRecorder()
   222  	adminTestBed.router.ServeHTTP(rec, req)
   223  
   224  	resp, _ := io.ReadAll(rec.Body)
   225  	if rec.Code != http.StatusOK {
   226  		t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
   227  			http.StatusOK, rec.Code, string(resp))
   228  	}
   229  
   230  	result := &serviceResult{}
   231  	if err := json.Unmarshal(resp, result); err != nil {
   232  		t.Error(err)
   233  	}
   234  	_ = result
   235  
   236  	// Wait until testServiceSignalReceiver() called in a goroutine quits.
   237  	wg.Wait()
   238  }
   239  
   240  // Test for service restart management REST API.
   241  func TestServiceRestartHandler(t *testing.T) {
   242  	testServicesCmdHandler(restartCmd, t)
   243  }
   244  
   245  // buildAdminRequest - helper function to build an admin API request.
   246  func buildAdminRequest(queryVal url.Values, method, path string,
   247  	contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error,
   248  ) {
   249  	req, err := newTestRequest(method,
   250  		adminPathPrefix+adminAPIVersionPrefix+path+"?"+queryVal.Encode(),
   251  		contentLength, bodySeeker)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	cred := globalActiveCred
   257  	err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	return req, nil
   263  }
   264  
   265  func TestAdminServerInfo(t *testing.T) {
   266  	ctx, cancel := context.WithCancel(context.Background())
   267  	defer cancel()
   268  
   269  	adminTestBed, err := prepareAdminErasureTestBed(ctx)
   270  	if err != nil {
   271  		t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.", err)
   272  	}
   273  
   274  	defer adminTestBed.TearDown()
   275  
   276  	// Initialize admin peers to make admin RPC calls.
   277  	globalMinioAddr = "127.0.0.1:9000"
   278  
   279  	// Prepare query params for set-config mgmt REST API.
   280  	queryVal := url.Values{}
   281  	queryVal.Set("info", "")
   282  
   283  	req, err := buildAdminRequest(queryVal, http.MethodGet, "/info", 0, nil)
   284  	if err != nil {
   285  		t.Fatalf("Failed to construct get-config object request - %v", err)
   286  	}
   287  
   288  	rec := httptest.NewRecorder()
   289  	adminTestBed.router.ServeHTTP(rec, req)
   290  	if rec.Code != http.StatusOK {
   291  		t.Errorf("Expected to succeed but failed with %d", rec.Code)
   292  	}
   293  
   294  	results := madmin.InfoMessage{}
   295  	err = json.NewDecoder(rec.Body).Decode(&results)
   296  	if err != nil {
   297  		t.Fatalf("Failed to decode set config result json %v", err)
   298  	}
   299  
   300  	if results.Region != globalMinioDefaultRegion {
   301  		t.Errorf("Expected %s, got %s", globalMinioDefaultRegion, results.Region)
   302  	}
   303  }
   304  
   305  // TestToAdminAPIErrCode - test for toAdminAPIErrCode helper function.
   306  func TestToAdminAPIErrCode(t *testing.T) {
   307  	testCases := []struct {
   308  		err            error
   309  		expectedAPIErr APIErrorCode
   310  	}{
   311  		// 1. Server not in quorum.
   312  		{
   313  			err:            errErasureWriteQuorum,
   314  			expectedAPIErr: ErrAdminConfigNoQuorum,
   315  		},
   316  		// 2. No error.
   317  		{
   318  			err:            nil,
   319  			expectedAPIErr: ErrNone,
   320  		},
   321  		// 3. Non-admin API specific error.
   322  		{
   323  			err:            errDiskNotFound,
   324  			expectedAPIErr: toAPIErrorCode(GlobalContext, errDiskNotFound),
   325  		},
   326  	}
   327  
   328  	for i, test := range testCases {
   329  		actualErr := toAdminAPIErrCode(GlobalContext, test.err)
   330  		if actualErr != test.expectedAPIErr {
   331  			t.Errorf("Test %d: Expected %v but received %v",
   332  				i+1, test.expectedAPIErr, actualErr)
   333  		}
   334  	}
   335  }
   336  
   337  func TestExtractHealInitParams(t *testing.T) {
   338  	mkParams := func(clientToken string, forceStart, forceStop bool) url.Values {
   339  		v := url.Values{}
   340  		if clientToken != "" {
   341  			v.Add(mgmtClientToken, clientToken)
   342  		}
   343  		if forceStart {
   344  			v.Add(mgmtForceStart, "")
   345  		}
   346  		if forceStop {
   347  			v.Add(mgmtForceStop, "")
   348  		}
   349  		return v
   350  	}
   351  	qParamsArr := []url.Values{
   352  		// Invalid cases
   353  		mkParams("", true, true),
   354  		mkParams("111", true, true),
   355  		mkParams("111", true, false),
   356  		mkParams("111", false, true),
   357  		// Valid cases follow
   358  		mkParams("", true, false),
   359  		mkParams("", false, true),
   360  		mkParams("", false, false),
   361  		mkParams("111", false, false),
   362  	}
   363  	varsArr := []map[string]string{
   364  		// Invalid cases
   365  		{mgmtPrefix: "objprefix"},
   366  		// Valid cases
   367  		{},
   368  		{mgmtBucket: "bucket"},
   369  		{mgmtBucket: "bucket", mgmtPrefix: "objprefix"},
   370  	}
   371  
   372  	// Body is always valid - we do not test JSON decoding.
   373  	body := `{"recursive": false, "dryRun": true, "remove": false, "scanMode": 0}`
   374  
   375  	// Test all combinations!
   376  	for pIdx, params := range qParamsArr {
   377  		for vIdx, vars := range varsArr {
   378  			_, err := extractHealInitParams(vars, params, bytes.NewReader([]byte(body)))
   379  			isErrCase := false
   380  			if pIdx < 4 || vIdx < 1 {
   381  				isErrCase = true
   382  			}
   383  
   384  			if err != ErrNone && !isErrCase {
   385  				t.Errorf("Got unexpected error: %v %v %v", pIdx, vIdx, err)
   386  			} else if err == ErrNone && isErrCase {
   387  				t.Errorf("Got no error but expected one: %v %v", pIdx, vIdx)
   388  			}
   389  		}
   390  	}
   391  }
   392  
   393  type byResourceUID struct{ madmin.LockEntries }
   394  
   395  func (b byResourceUID) Less(i, j int) bool {
   396  	toUniqLock := func(entry madmin.LockEntry) string {
   397  		return fmt.Sprintf("%s/%s", entry.Resource, entry.ID)
   398  	}
   399  	return toUniqLock(b.LockEntries[i]) < toUniqLock(b.LockEntries[j])
   400  }
   401  
   402  func TestTopLockEntries(t *testing.T) {
   403  	locksHeld := make(map[string][]lockRequesterInfo)
   404  	var owners []string
   405  	for i := 0; i < 4; i++ {
   406  		owners = append(owners, fmt.Sprintf("node-%d", i))
   407  	}
   408  
   409  	// Simulate DeleteObjects of 10 objects in a single request. i.e same lock
   410  	// request UID, but 10 different resource names associated with it.
   411  	var lris []lockRequesterInfo
   412  	uuid := mustGetUUID()
   413  	for i := 0; i < 10; i++ {
   414  		resource := fmt.Sprintf("bucket/delete-object-%d", i)
   415  		lri := lockRequesterInfo{
   416  			Name:   resource,
   417  			Writer: true,
   418  			UID:    uuid,
   419  			Owner:  owners[i%len(owners)],
   420  			Group:  true,
   421  			Quorum: 3,
   422  		}
   423  		lris = append(lris, lri)
   424  		locksHeld[resource] = []lockRequesterInfo{lri}
   425  	}
   426  
   427  	// Add a few concurrent read locks to the mix
   428  	for i := 0; i < 50; i++ {
   429  		resource := fmt.Sprintf("bucket/get-object-%d", i)
   430  		lri := lockRequesterInfo{
   431  			Name:   resource,
   432  			UID:    mustGetUUID(),
   433  			Owner:  owners[i%len(owners)],
   434  			Quorum: 2,
   435  		}
   436  		lris = append(lris, lri)
   437  		locksHeld[resource] = append(locksHeld[resource], lri)
   438  		// concurrent read lock, same resource different uid
   439  		lri.UID = mustGetUUID()
   440  		lris = append(lris, lri)
   441  		locksHeld[resource] = append(locksHeld[resource], lri)
   442  	}
   443  
   444  	var peerLocks []*PeerLocks
   445  	for _, owner := range owners {
   446  		peerLocks = append(peerLocks, &PeerLocks{
   447  			Addr:  owner,
   448  			Locks: locksHeld,
   449  		})
   450  	}
   451  	var exp madmin.LockEntries
   452  	for _, lri := range lris {
   453  		lockType := func(lri lockRequesterInfo) string {
   454  			if lri.Writer {
   455  				return "WRITE"
   456  			}
   457  			return "READ"
   458  		}
   459  		exp = append(exp, madmin.LockEntry{
   460  			Resource:   lri.Name,
   461  			Type:       lockType(lri),
   462  			ServerList: owners,
   463  			Owner:      lri.Owner,
   464  			ID:         lri.UID,
   465  			Quorum:     lri.Quorum,
   466  		})
   467  	}
   468  
   469  	testCases := []struct {
   470  		peerLocks []*PeerLocks
   471  		expected  madmin.LockEntries
   472  	}{
   473  		{
   474  			peerLocks: peerLocks,
   475  			expected:  exp,
   476  		},
   477  	}
   478  
   479  	// printEntries := func(entries madmin.LockEntries) {
   480  	// 	for i, entry := range entries {
   481  	// 		fmt.Printf("%d: %s %s %s %s %v %d\n", i, entry.Resource, entry.ID, entry.Owner, entry.Type, entry.ServerList, entry.Elapsed)
   482  	// 	}
   483  	// }
   484  
   485  	check := func(exp, got madmin.LockEntries) (int, bool) {
   486  		if len(exp) != len(got) {
   487  			return 0, false
   488  		}
   489  		sort.Slice(exp, byResourceUID{exp}.Less)
   490  		sort.Slice(got, byResourceUID{got}.Less)
   491  		// printEntries(exp)
   492  		// printEntries(got)
   493  		for i, e := range exp {
   494  			if !e.Timestamp.Equal(got[i].Timestamp) {
   495  				return i, false
   496  			}
   497  			// Skip checking elapsed since it's time sensitive.
   498  			// if e.Elapsed != got[i].Elapsed {
   499  			// 	return false
   500  			// }
   501  			if e.Resource != got[i].Resource {
   502  				return i, false
   503  			}
   504  			if e.Type != got[i].Type {
   505  				return i, false
   506  			}
   507  			if e.Source != got[i].Source {
   508  				return i, false
   509  			}
   510  			if e.Owner != got[i].Owner {
   511  				return i, false
   512  			}
   513  			if e.ID != got[i].ID {
   514  				return i, false
   515  			}
   516  			if len(e.ServerList) != len(got[i].ServerList) {
   517  				return i, false
   518  			}
   519  			for j := range e.ServerList {
   520  				if e.ServerList[j] != got[i].ServerList[j] {
   521  					return i, false
   522  				}
   523  			}
   524  		}
   525  		return 0, true
   526  	}
   527  
   528  	for i, tc := range testCases {
   529  		got := topLockEntries(tc.peerLocks, false)
   530  		if idx, ok := check(tc.expected, got); !ok {
   531  			t.Fatalf("%d: mismatch at %d \n expected %#v but got %#v", i, idx, tc.expected[idx], got[idx])
   532  		}
   533  	}
   534  }