storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/admin-handlers_test.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2016-2019 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package cmd
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"io"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"net/http/httptest"
    27  	"net/url"
    28  	"sync"
    29  	"testing"
    30  
    31  	"github.com/gorilla/mux"
    32  
    33  	"storj.io/minio/pkg/auth"
    34  	"storj.io/minio/pkg/madmin"
    35  )
    36  
    37  // adminErasureTestBed - encapsulates subsystems that need to be setup for
    38  // admin-handler unit tests.
    39  type adminErasureTestBed struct {
    40  	erasureDirs []string
    41  	objLayer    ObjectLayer
    42  	router      *mux.Router
    43  }
    44  
    45  // prepareAdminErasureTestBed - helper function that setups a single-node
    46  // Erasure backend for admin-handler tests.
    47  func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, error) {
    48  
    49  	// reset global variables to start afresh.
    50  	resetTestGlobals()
    51  
    52  	// Set globalIsErasure to indicate that the setup uses an erasure
    53  	// code backend.
    54  	globalIsErasure = true
    55  
    56  	// Initializing objectLayer for HealFormatHandler.
    57  	objLayer, erasureDirs, xlErr := initTestErasureObjLayer(ctx)
    58  	if xlErr != nil {
    59  		return nil, xlErr
    60  	}
    61  
    62  	// Initialize minio server config.
    63  	if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
    64  		return nil, err
    65  	}
    66  
    67  	// Initialize boot time
    68  	globalBootTime = UTCNow()
    69  
    70  	globalEndpoints = mustGetPoolEndpoints(erasureDirs...)
    71  
    72  	newAllSubsystems()
    73  
    74  	initAllSubsystems(ctx, objLayer)
    75  
    76  	// Setup admin mgmt REST API handlers.
    77  	adminRouter := mux.NewRouter()
    78  	registerAdminRouter(adminRouter, true, true)
    79  
    80  	return &adminErasureTestBed{
    81  		erasureDirs: erasureDirs,
    82  		objLayer:    objLayer,
    83  		router:      adminRouter,
    84  	}, nil
    85  }
    86  
    87  // TearDown - method that resets the test bed for subsequent unit
    88  // tests to start afresh.
    89  func (atb *adminErasureTestBed) TearDown() {
    90  	removeRoots(atb.erasureDirs)
    91  	resetTestGlobals()
    92  }
    93  
    94  // initTestObjLayer - Helper function to initialize an Erasure-based object
    95  // layer and set globalObjectAPI.
    96  func initTestErasureObjLayer(ctx context.Context) (ObjectLayer, []string, error) {
    97  	erasureDirs, err := getRandomDisks(16)
    98  	if err != nil {
    99  		return nil, nil, err
   100  	}
   101  	endpoints := mustGetPoolEndpoints(erasureDirs...)
   102  	globalPolicySys = NewPolicySys()
   103  	objLayer, err := newErasureServerPools(ctx, endpoints)
   104  	if err != nil {
   105  		return nil, nil, err
   106  	}
   107  
   108  	// Make objLayer available to all internal services via globalObjectAPI.
   109  	globalObjLayerMutex.Lock()
   110  	globalObjectAPI = objLayer
   111  	globalObjLayerMutex.Unlock()
   112  	return objLayer, erasureDirs, nil
   113  }
   114  
   115  // cmdType - Represents different service subcomands like status, stop
   116  // and restart.
   117  type cmdType int
   118  
   119  const (
   120  	restartCmd cmdType = iota
   121  	stopCmd
   122  )
   123  
   124  // toServiceSignal - Helper function that translates a given cmdType
   125  // value to its corresponding serviceSignal value.
   126  func (c cmdType) toServiceSignal() serviceSignal {
   127  	switch c {
   128  	case restartCmd:
   129  		return serviceRestart
   130  	case stopCmd:
   131  		return serviceStop
   132  	}
   133  	return serviceRestart
   134  }
   135  
   136  func (c cmdType) toServiceAction() madmin.ServiceAction {
   137  	switch c {
   138  	case restartCmd:
   139  		return madmin.ServiceActionRestart
   140  	case stopCmd:
   141  		return madmin.ServiceActionStop
   142  	}
   143  	return madmin.ServiceActionRestart
   144  }
   145  
   146  // testServiceSignalReceiver - Helper function that simulates a
   147  // go-routine waiting on service signal.
   148  func testServiceSignalReceiver(cmd cmdType, t *testing.T) {
   149  	expectedCmd := cmd.toServiceSignal()
   150  	serviceCmd := <-globalServiceSignalCh
   151  	if serviceCmd != expectedCmd {
   152  		t.Errorf("Expected service command %v but received %v", expectedCmd, serviceCmd)
   153  	}
   154  }
   155  
   156  // getServiceCmdRequest - Constructs a management REST API request for service
   157  // subcommands for a given cmdType value.
   158  func getServiceCmdRequest(cmd cmdType, cred auth.Credentials) (*http.Request, error) {
   159  	queryVal := url.Values{}
   160  	queryVal.Set("action", string(cmd.toServiceAction()))
   161  	resource := adminPathPrefix + adminAPIVersionPrefix + "/service?" + queryVal.Encode()
   162  	req, err := newTestRequest(http.MethodPost, resource, 0, nil)
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	// management REST API uses signature V4 for authentication.
   168  	err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	return req, nil
   173  }
   174  
   175  // testServicesCmdHandler - parametrizes service subcommand tests on
   176  // cmdType value.
   177  func testServicesCmdHandler(cmd cmdType, t *testing.T) {
   178  	ctx, cancel := context.WithCancel(context.Background())
   179  	defer cancel()
   180  
   181  	adminTestBed, err := prepareAdminErasureTestBed(ctx)
   182  	if err != nil {
   183  		t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.")
   184  	}
   185  	defer adminTestBed.TearDown()
   186  
   187  	// Initialize admin peers to make admin RPC calls. Note: In a
   188  	// single node setup, this degenerates to a simple function
   189  	// call under the hood.
   190  	globalMinioAddr = "127.0.0.1:9000"
   191  
   192  	var wg sync.WaitGroup
   193  
   194  	// Setting up a go routine to simulate ServerRouter's
   195  	// handleServiceSignals for stop and restart commands.
   196  	if cmd == restartCmd {
   197  		wg.Add(1)
   198  		go func() {
   199  			defer wg.Done()
   200  			testServiceSignalReceiver(cmd, t)
   201  		}()
   202  	}
   203  	credentials := globalActiveCred
   204  
   205  	req, err := getServiceCmdRequest(cmd, credentials)
   206  	if err != nil {
   207  		t.Fatalf("Failed to build service status request %v", err)
   208  	}
   209  
   210  	rec := httptest.NewRecorder()
   211  	adminTestBed.router.ServeHTTP(rec, req)
   212  
   213  	if rec.Code != http.StatusOK {
   214  		resp, _ := ioutil.ReadAll(rec.Body)
   215  		t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
   216  			http.StatusOK, rec.Code, string(resp))
   217  	}
   218  
   219  	// Wait until testServiceSignalReceiver() called in a goroutine quits.
   220  	wg.Wait()
   221  }
   222  
   223  // Test for service restart management REST API.
   224  func TestServiceRestartHandler(t *testing.T) {
   225  	testServicesCmdHandler(restartCmd, t)
   226  }
   227  
   228  // buildAdminRequest - helper function to build an admin API request.
   229  func buildAdminRequest(queryVal url.Values, method, path string,
   230  	contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error) {
   231  
   232  	req, err := newTestRequest(method,
   233  		adminPathPrefix+adminAPIVersionPrefix+path+"?"+queryVal.Encode(),
   234  		contentLength, bodySeeker)
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  
   239  	cred := globalActiveCred
   240  	err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  
   245  	return req, nil
   246  }
   247  
   248  func TestAdminServerInfo(t *testing.T) {
   249  	ctx, cancel := context.WithCancel(context.Background())
   250  	defer cancel()
   251  
   252  	adminTestBed, err := prepareAdminErasureTestBed(ctx)
   253  	if err != nil {
   254  		t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.")
   255  	}
   256  
   257  	defer adminTestBed.TearDown()
   258  
   259  	// Initialize admin peers to make admin RPC calls.
   260  	globalMinioAddr = "127.0.0.1:9000"
   261  
   262  	// Prepare query params for set-config mgmt REST API.
   263  	queryVal := url.Values{}
   264  	queryVal.Set("info", "")
   265  
   266  	req, err := buildAdminRequest(queryVal, http.MethodGet, "/info", 0, nil)
   267  	if err != nil {
   268  		t.Fatalf("Failed to construct get-config object request - %v", err)
   269  	}
   270  
   271  	rec := httptest.NewRecorder()
   272  	adminTestBed.router.ServeHTTP(rec, req)
   273  	if rec.Code != http.StatusOK {
   274  		t.Errorf("Expected to succeed but failed with %d", rec.Code)
   275  	}
   276  
   277  	results := madmin.InfoMessage{}
   278  	err = json.NewDecoder(rec.Body).Decode(&results)
   279  	if err != nil {
   280  		t.Fatalf("Failed to decode set config result json %v", err)
   281  	}
   282  
   283  	if results.Region != globalMinioDefaultRegion {
   284  		t.Errorf("Expected %s, got %s", globalMinioDefaultRegion, results.Region)
   285  	}
   286  }
   287  
   288  // TestToAdminAPIErrCode - test for toAdminAPIErrCode helper function.
   289  func TestToAdminAPIErrCode(t *testing.T) {
   290  	testCases := []struct {
   291  		err            error
   292  		expectedAPIErr APIErrorCode
   293  	}{
   294  		// 1. Server not in quorum.
   295  		{
   296  			err:            errErasureWriteQuorum,
   297  			expectedAPIErr: ErrAdminConfigNoQuorum,
   298  		},
   299  		// 2. No error.
   300  		{
   301  			err:            nil,
   302  			expectedAPIErr: ErrNone,
   303  		},
   304  		// 3. Non-admin API specific error.
   305  		{
   306  			err:            errDiskNotFound,
   307  			expectedAPIErr: toAPIErrorCode(GlobalContext, errDiskNotFound),
   308  		},
   309  	}
   310  
   311  	for i, test := range testCases {
   312  		actualErr := toAdminAPIErrCode(GlobalContext, test.err)
   313  		if actualErr != test.expectedAPIErr {
   314  			t.Errorf("Test %d: Expected %v but received %v",
   315  				i+1, test.expectedAPIErr, actualErr)
   316  		}
   317  	}
   318  }
   319  
   320  func TestExtractHealInitParams(t *testing.T) {
   321  	mkParams := func(clientToken string, forceStart, forceStop bool) url.Values {
   322  		v := url.Values{}
   323  		if clientToken != "" {
   324  			v.Add(mgmtClientToken, clientToken)
   325  		}
   326  		if forceStart {
   327  			v.Add(mgmtForceStart, "")
   328  		}
   329  		if forceStop {
   330  			v.Add(mgmtForceStop, "")
   331  		}
   332  		return v
   333  	}
   334  	qParmsArr := []url.Values{
   335  		// Invalid cases
   336  		mkParams("", true, true),
   337  		mkParams("111", true, true),
   338  		mkParams("111", true, false),
   339  		mkParams("111", false, true),
   340  		// Valid cases follow
   341  		mkParams("", true, false),
   342  		mkParams("", false, true),
   343  		mkParams("", false, false),
   344  		mkParams("111", false, false),
   345  	}
   346  	varsArr := []map[string]string{
   347  		// Invalid cases
   348  		{mgmtPrefix: "objprefix"},
   349  		// Valid cases
   350  		{},
   351  		{mgmtBucket: "bucket"},
   352  		{mgmtBucket: "bucket", mgmtPrefix: "objprefix"},
   353  	}
   354  
   355  	// Body is always valid - we do not test JSON decoding.
   356  	body := `{"recursive": false, "dryRun": true, "remove": false, "scanMode": 0}`
   357  
   358  	// Test all combinations!
   359  	for pIdx, parms := range qParmsArr {
   360  		for vIdx, vars := range varsArr {
   361  			_, err := extractHealInitParams(vars, parms, bytes.NewReader([]byte(body)))
   362  			isErrCase := false
   363  			if pIdx < 4 || vIdx < 1 {
   364  				isErrCase = true
   365  			}
   366  
   367  			if err != ErrNone && !isErrCase {
   368  				t.Errorf("Got unexpected error: %v %v %v", pIdx, vIdx, err)
   369  			} else if err == ErrNone && isErrCase {
   370  				t.Errorf("Got no error but expected one: %v %v", pIdx, vIdx)
   371  			}
   372  		}
   373  	}
   374  
   375  }