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 }