github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/endpoint-ellipses_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 "fmt" 22 "reflect" 23 "testing" 24 25 "github.com/minio/pkg/v2/ellipses" 26 ) 27 28 // Tests create endpoints with ellipses and without. 29 func TestCreateServerEndpoints(t *testing.T) { 30 testCases := []struct { 31 serverAddr string 32 args []string 33 success bool 34 }{ 35 // Invalid input. 36 {"", []string{}, false}, 37 // Range cannot be negative. 38 {":9000", []string{"/export1{-1...1}"}, false}, 39 // Range cannot start bigger than end. 40 {":9000", []string{"/export1{64...1}"}, false}, 41 // Range can only be numeric. 42 {":9000", []string{"/export1{a...z}"}, false}, 43 // Duplicate disks not allowed. 44 {":9000", []string{"/export1{1...32}", "/export1{1...32}"}, false}, 45 // Same host cannot export same disk on two ports - special case localhost. 46 {":9001", []string{"http://localhost:900{1...2}/export{1...64}"}, false}, 47 // Valid inputs. 48 {":9000", []string{"/export1"}, true}, 49 {":9000", []string{"/export1", "/export2", "/export3", "/export4"}, true}, 50 {":9000", []string{"/export1{1...64}"}, true}, 51 {":9000", []string{"/export1{01...64}"}, true}, 52 {":9000", []string{"/export1{1...32}", "/export1{33...64}"}, true}, 53 {":9001", []string{"http://localhost:9001/export{1...64}"}, true}, 54 {":9001", []string{"http://localhost:9001/export{01...64}"}, true}, 55 } 56 57 for i, testCase := range testCases { 58 testCase := testCase 59 t.Run("", func(t *testing.T) { 60 srvCtxt := serverCtxt{} 61 err := mergeDisksLayoutFromArgs(testCase.args, &srvCtxt) 62 if err != nil && testCase.success { 63 t.Fatalf("Test %d: unexpected error: %v", i+1, err) 64 } 65 _, _, err = createServerEndpoints(testCase.serverAddr, srvCtxt.Layout.pools, srvCtxt.Layout.legacy) 66 if err != nil && testCase.success { 67 t.Errorf("Test %d: Expected success but failed instead %s", i+1, err) 68 } 69 if err == nil && !testCase.success { 70 t.Errorf("Test %d: Expected failure but passed instead", i+1) 71 } 72 }) 73 } 74 } 75 76 func TestGetDivisibleSize(t *testing.T) { 77 testCases := []struct { 78 totalSizes []uint64 79 result uint64 80 }{ 81 {[]uint64{24, 32, 16}, 8}, 82 {[]uint64{32, 8, 4}, 4}, 83 {[]uint64{8, 8, 8}, 8}, 84 {[]uint64{24}, 24}, 85 } 86 87 for _, testCase := range testCases { 88 testCase := testCase 89 t.Run("", func(t *testing.T) { 90 gotGCD := getDivisibleSize(testCase.totalSizes) 91 if testCase.result != gotGCD { 92 t.Errorf("Expected %v, got %v", testCase.result, gotGCD) 93 } 94 }) 95 } 96 } 97 98 // Test tests calculating set indexes with ENV override for drive count. 99 func TestGetSetIndexesEnvOverride(t *testing.T) { 100 testCases := []struct { 101 args []string 102 totalSizes []uint64 103 indexes [][]uint64 104 envOverride uint64 105 success bool 106 }{ 107 { 108 []string{"data{1...64}"}, 109 []uint64{64}, 110 [][]uint64{{8, 8, 8, 8, 8, 8, 8, 8}}, 111 8, 112 true, 113 }, 114 { 115 []string{"http://host{1...2}/data{1...180}"}, 116 []uint64{360}, 117 [][]uint64{{15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15}}, 118 15, 119 true, 120 }, 121 { 122 []string{"http://host{1...12}/data{1...12}"}, 123 []uint64{144}, 124 [][]uint64{{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12}}, 125 12, 126 true, 127 }, 128 { 129 []string{"http://host{0...5}/data{1...28}"}, 130 []uint64{168}, 131 [][]uint64{{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12}}, 132 12, 133 true, 134 }, 135 // Incorrect custom set drive count. 136 { 137 []string{"http://host{0...5}/data{1...28}"}, 138 []uint64{168}, 139 nil, 140 10, 141 false, 142 }, 143 // Failure not divisible number of disks. 144 { 145 []string{"http://host{1...11}/data{1...11}"}, 146 []uint64{121}, 147 [][]uint64{{11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11}}, 148 11, 149 true, 150 }, 151 { 152 []string{"data{1...60}"}, 153 nil, 154 nil, 155 8, 156 false, 157 }, 158 { 159 []string{"data{1...64}"}, 160 nil, 161 nil, 162 64, 163 false, 164 }, 165 { 166 []string{"data{1...64}"}, 167 nil, 168 nil, 169 2, 170 false, 171 }, 172 } 173 174 for _, testCase := range testCases { 175 testCase := testCase 176 t.Run("", func(t *testing.T) { 177 argPatterns := make([]ellipses.ArgPattern, len(testCase.args)) 178 for i, arg := range testCase.args { 179 patterns, err := ellipses.FindEllipsesPatterns(arg) 180 if err != nil { 181 t.Fatalf("Unexpected failure %s", err) 182 } 183 argPatterns[i] = patterns 184 } 185 186 gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes, testCase.envOverride, argPatterns) 187 if err != nil && testCase.success { 188 t.Errorf("Expected success but failed instead %s", err) 189 } 190 if err == nil && !testCase.success { 191 t.Errorf("Expected failure but passed instead") 192 } 193 if !reflect.DeepEqual(testCase.indexes, gotIndexes) { 194 t.Errorf("Expected %v, got %v", testCase.indexes, gotIndexes) 195 } 196 }) 197 } 198 } 199 200 // Test tests calculating set indexes. 201 func TestGetSetIndexes(t *testing.T) { 202 testCases := []struct { 203 args []string 204 totalSizes []uint64 205 indexes [][]uint64 206 success bool 207 }{ 208 // Invalid inputs. 209 { 210 []string{"data{1...17}/export{1...52}"}, 211 []uint64{14144}, 212 nil, 213 false, 214 }, 215 // Valid inputs. 216 { 217 []string{"data{1...3}"}, 218 []uint64{3}, 219 [][]uint64{{3}}, 220 true, 221 }, 222 { 223 []string{"data/controller1/export{1...2}, data/controller2/export{1...4}, data/controller3/export{1...8}"}, 224 []uint64{2, 4, 8}, 225 [][]uint64{{2}, {2, 2}, {2, 2, 2, 2}}, 226 true, 227 }, 228 { 229 []string{"data{1...27}"}, 230 []uint64{27}, 231 [][]uint64{{9, 9, 9}}, 232 true, 233 }, 234 { 235 []string{"http://host{1...3}/data{1...180}"}, 236 []uint64{540}, 237 [][]uint64{{15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15}}, 238 true, 239 }, 240 { 241 []string{"http://host{1...2}.rack{1...4}/data{1...180}"}, 242 []uint64{1440}, 243 [][]uint64{{16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16}}, 244 true, 245 }, 246 { 247 []string{"http://host{1...2}/data{1...180}"}, 248 []uint64{360}, 249 [][]uint64{{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12}}, 250 true, 251 }, 252 { 253 []string{"data/controller1/export{1...4}, data/controller2/export{1...8}, data/controller3/export{1...12}"}, 254 []uint64{4, 8, 12}, 255 [][]uint64{{4}, {4, 4}, {4, 4, 4}}, 256 true, 257 }, 258 { 259 []string{"data{1...64}"}, 260 []uint64{64}, 261 [][]uint64{{16, 16, 16, 16}}, 262 true, 263 }, 264 { 265 []string{"data{1...24}"}, 266 []uint64{24}, 267 [][]uint64{{12, 12}}, 268 true, 269 }, 270 { 271 []string{"data/controller{1...11}/export{1...8}"}, 272 []uint64{88}, 273 [][]uint64{{11, 11, 11, 11, 11, 11, 11, 11}}, 274 true, 275 }, 276 { 277 []string{"data{1...4}"}, 278 []uint64{4}, 279 [][]uint64{{4}}, 280 true, 281 }, 282 { 283 []string{"data/controller1/export{1...10}, data/controller2/export{1...10}, data/controller3/export{1...10}"}, 284 []uint64{10, 10, 10}, 285 [][]uint64{{10}, {10}, {10}}, 286 true, 287 }, 288 { 289 []string{"data{1...16}/export{1...52}"}, 290 []uint64{832}, 291 [][]uint64{{16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16}}, 292 true, 293 }, 294 } 295 296 for _, testCase := range testCases { 297 testCase := testCase 298 t.Run("", func(t *testing.T) { 299 argPatterns := make([]ellipses.ArgPattern, len(testCase.args)) 300 for i, arg := range testCase.args { 301 patterns, err := ellipses.FindEllipsesPatterns(arg) 302 if err != nil { 303 t.Fatalf("Unexpected failure %s", err) 304 } 305 argPatterns[i] = patterns 306 } 307 gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes, 0, argPatterns) 308 if err != nil && testCase.success { 309 t.Errorf("Expected success but failed instead %s", err) 310 } 311 if err == nil && !testCase.success { 312 t.Errorf("Expected failure but passed instead") 313 } 314 if !reflect.DeepEqual(testCase.indexes, gotIndexes) { 315 t.Errorf("Expected %v, got %v", testCase.indexes, gotIndexes) 316 } 317 }) 318 } 319 } 320 321 func getHexSequences(start int, number int, paddinglen int) (seq []string) { 322 for i := start; i <= number; i++ { 323 if paddinglen == 0 { 324 seq = append(seq, fmt.Sprintf("%x", i)) 325 } else { 326 seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dx", paddinglen), i)) 327 } 328 } 329 return seq 330 } 331 332 func getSequences(start int, number int, paddinglen int) (seq []string) { 333 for i := start; i <= number; i++ { 334 if paddinglen == 0 { 335 seq = append(seq, fmt.Sprintf("%d", i)) 336 } else { 337 seq = append(seq, fmt.Sprintf(fmt.Sprintf("%%0%dd", paddinglen), i)) 338 } 339 } 340 return seq 341 } 342 343 // Test tests parses endpoint ellipses input pattern. 344 func TestParseEndpointSet(t *testing.T) { 345 testCases := []struct { 346 arg string 347 es endpointSet 348 success bool 349 }{ 350 // Tests invalid inputs. 351 { 352 "...", 353 endpointSet{}, 354 false, 355 }, 356 // No range specified. 357 { 358 "{...}", 359 endpointSet{}, 360 false, 361 }, 362 // Invalid range. 363 { 364 "http://minio{2...3}/export/set{1...0}", 365 endpointSet{}, 366 false, 367 }, 368 // Range cannot be smaller than 4 minimum. 369 { 370 "/export{1..2}", 371 endpointSet{}, 372 false, 373 }, 374 // Unsupported characters. 375 { 376 "/export/test{1...2O}", 377 endpointSet{}, 378 false, 379 }, 380 // Tests valid inputs. 381 { 382 "{1...27}", 383 endpointSet{ 384 []ellipses.ArgPattern{ 385 []ellipses.Pattern{ 386 { 387 Prefix: "", 388 Suffix: "", 389 Seq: getSequences(1, 27, 0), 390 }, 391 }, 392 }, 393 nil, 394 [][]uint64{{9, 9, 9}}, 395 }, 396 true, 397 }, 398 { 399 "/export/set{1...64}", 400 endpointSet{ 401 []ellipses.ArgPattern{ 402 []ellipses.Pattern{ 403 { 404 Prefix: "/export/set", 405 Suffix: "", 406 Seq: getSequences(1, 64, 0), 407 }, 408 }, 409 }, 410 nil, 411 [][]uint64{{16, 16, 16, 16}}, 412 }, 413 true, 414 }, 415 // Valid input for distributed setup. 416 { 417 "http://minio{2...3}/export/set{1...64}", 418 endpointSet{ 419 []ellipses.ArgPattern{ 420 []ellipses.Pattern{ 421 { 422 Prefix: "", 423 Suffix: "", 424 Seq: getSequences(1, 64, 0), 425 }, 426 { 427 Prefix: "http://minio", 428 Suffix: "/export/set", 429 Seq: getSequences(2, 3, 0), 430 }, 431 }, 432 }, 433 nil, 434 [][]uint64{{16, 16, 16, 16, 16, 16, 16, 16}}, 435 }, 436 true, 437 }, 438 // Supporting some advanced cases. 439 { 440 "http://minio{1...64}.mydomain.net/data", 441 endpointSet{ 442 []ellipses.ArgPattern{ 443 []ellipses.Pattern{ 444 { 445 Prefix: "http://minio", 446 Suffix: ".mydomain.net/data", 447 Seq: getSequences(1, 64, 0), 448 }, 449 }, 450 }, 451 nil, 452 [][]uint64{{16, 16, 16, 16}}, 453 }, 454 true, 455 }, 456 { 457 "http://rack{1...4}.mydomain.minio{1...16}/data", 458 endpointSet{ 459 []ellipses.ArgPattern{ 460 []ellipses.Pattern{ 461 { 462 Prefix: "", 463 Suffix: "/data", 464 Seq: getSequences(1, 16, 0), 465 }, 466 { 467 Prefix: "http://rack", 468 Suffix: ".mydomain.minio", 469 Seq: getSequences(1, 4, 0), 470 }, 471 }, 472 }, 473 nil, 474 [][]uint64{{16, 16, 16, 16}}, 475 }, 476 true, 477 }, 478 // Supporting kubernetes cases. 479 { 480 "http://minio{0...15}.mydomain.net/data{0...1}", 481 endpointSet{ 482 []ellipses.ArgPattern{ 483 []ellipses.Pattern{ 484 { 485 Prefix: "", 486 Suffix: "", 487 Seq: getSequences(0, 1, 0), 488 }, 489 { 490 Prefix: "http://minio", 491 Suffix: ".mydomain.net/data", 492 Seq: getSequences(0, 15, 0), 493 }, 494 }, 495 }, 496 nil, 497 [][]uint64{{16, 16}}, 498 }, 499 true, 500 }, 501 // No host regex, just disks. 502 { 503 "http://server1/data{1...32}", 504 endpointSet{ 505 []ellipses.ArgPattern{ 506 []ellipses.Pattern{ 507 { 508 Prefix: "http://server1/data", 509 Suffix: "", 510 Seq: getSequences(1, 32, 0), 511 }, 512 }, 513 }, 514 nil, 515 [][]uint64{{16, 16}}, 516 }, 517 true, 518 }, 519 // No host regex, just disks with two position numerics. 520 { 521 "http://server1/data{01...32}", 522 endpointSet{ 523 []ellipses.ArgPattern{ 524 []ellipses.Pattern{ 525 { 526 Prefix: "http://server1/data", 527 Suffix: "", 528 Seq: getSequences(1, 32, 2), 529 }, 530 }, 531 }, 532 nil, 533 [][]uint64{{16, 16}}, 534 }, 535 true, 536 }, 537 // More than 2 ellipses are supported as well. 538 { 539 "http://minio{2...3}/export/set{1...64}/test{1...2}", 540 endpointSet{ 541 []ellipses.ArgPattern{ 542 []ellipses.Pattern{ 543 { 544 Prefix: "", 545 Suffix: "", 546 Seq: getSequences(1, 2, 0), 547 }, 548 { 549 Prefix: "", 550 Suffix: "/test", 551 Seq: getSequences(1, 64, 0), 552 }, 553 { 554 Prefix: "http://minio", 555 Suffix: "/export/set", 556 Seq: getSequences(2, 3, 0), 557 }, 558 }, 559 }, 560 nil, 561 [][]uint64{{ 562 16, 16, 16, 16, 16, 16, 16, 16, 563 16, 16, 16, 16, 16, 16, 16, 16, 564 }}, 565 }, 566 true, 567 }, 568 // More than 1 ellipses per argument for standalone setup. 569 { 570 "/export{1...10}/disk{1...10}", 571 endpointSet{ 572 []ellipses.ArgPattern{ 573 []ellipses.Pattern{ 574 { 575 Prefix: "", 576 Suffix: "", 577 Seq: getSequences(1, 10, 0), 578 }, 579 { 580 Prefix: "/export", 581 Suffix: "/disk", 582 Seq: getSequences(1, 10, 0), 583 }, 584 }, 585 }, 586 nil, 587 [][]uint64{{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}}, 588 }, 589 true, 590 }, 591 // IPv6 ellipses with hexadecimal expansion 592 { 593 "http://[2001:3984:3989::{1...a}]/disk{1...10}", 594 endpointSet{ 595 []ellipses.ArgPattern{ 596 []ellipses.Pattern{ 597 { 598 Prefix: "", 599 Suffix: "", 600 Seq: getSequences(1, 10, 0), 601 }, 602 { 603 Prefix: "http://[2001:3984:3989::", 604 Suffix: "]/disk", 605 Seq: getHexSequences(1, 10, 0), 606 }, 607 }, 608 }, 609 nil, 610 [][]uint64{{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}}, 611 }, 612 true, 613 }, 614 // IPv6 ellipses with hexadecimal expansion with 3 position numerics. 615 { 616 "http://[2001:3984:3989::{001...00a}]/disk{1...10}", 617 endpointSet{ 618 []ellipses.ArgPattern{ 619 []ellipses.Pattern{ 620 { 621 Prefix: "", 622 Suffix: "", 623 Seq: getSequences(1, 10, 0), 624 }, 625 { 626 Prefix: "http://[2001:3984:3989::", 627 Suffix: "]/disk", 628 Seq: getHexSequences(1, 10, 3), 629 }, 630 }, 631 }, 632 nil, 633 [][]uint64{{10, 10, 10, 10, 10, 10, 10, 10, 10, 10}}, 634 }, 635 true, 636 }, 637 } 638 639 for _, testCase := range testCases { 640 testCase := testCase 641 t.Run("", func(t *testing.T) { 642 gotEs, err := parseEndpointSet(0, testCase.arg) 643 if err != nil && testCase.success { 644 t.Errorf("Expected success but failed instead %s", err) 645 } 646 if err == nil && !testCase.success { 647 t.Errorf("Expected failure but passed instead") 648 } 649 if !reflect.DeepEqual(testCase.es, gotEs) { 650 t.Errorf("Expected %v, got %v", testCase.es, gotEs) 651 } 652 }) 653 } 654 }