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

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