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 }