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  }