k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kubeadm/app/util/version_test.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     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 util
    18  
    19  import (
    20  	"fmt"
    21  	"os"
    22  	"path"
    23  	"strings"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/pkg/errors"
    28  
    29  	"k8s.io/kubernetes/cmd/kubeadm/app/constants"
    30  )
    31  
    32  func TestMain(m *testing.M) {
    33  	KubernetesReleaseVersion = kubernetesReleaseVersionTest
    34  	os.Exit(m.Run())
    35  }
    36  
    37  func kubernetesReleaseVersionTest(version string) (string, error) {
    38  	fetcher := func(string, time.Duration) (string, error) {
    39  		return constants.DefaultKubernetesPlaceholderVersion.String(), nil
    40  	}
    41  	return kubernetesReleaseVersion(version, fetcher)
    42  }
    43  
    44  func TestKubernetesReleaseVersion(t *testing.T) {
    45  	tests := []struct {
    46  		name           string
    47  		input          string
    48  		expectedOutput string
    49  		expectedError  bool
    50  	}{
    51  		{
    52  			name:           "empty input",
    53  			input:          "",
    54  			expectedOutput: "",
    55  			expectedError:  true,
    56  		},
    57  		{
    58  			name:           "label as input",
    59  			input:          "stable",
    60  			expectedOutput: normalizedBuildVersion(constants.DefaultKubernetesPlaceholderVersion.String()),
    61  			expectedError:  false,
    62  		},
    63  	}
    64  
    65  	for _, tc := range tests {
    66  		t.Run(tc.name, func(t *testing.T) {
    67  			output, err := KubernetesReleaseVersion(tc.input)
    68  			if (err != nil) != tc.expectedError {
    69  				t.Errorf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
    70  			}
    71  			if output != tc.expectedOutput {
    72  				t.Errorf("expected output: %s, got: %s", tc.expectedOutput, output)
    73  			}
    74  		})
    75  	}
    76  }
    77  
    78  func TestValidVersion(t *testing.T) {
    79  	validVersions := []string{
    80  		"v1.3.0",
    81  		"v1.4.0-alpha.0",
    82  		"v1.4.5",
    83  		"v1.4.0-beta.0",
    84  		"v2.0.0",
    85  		"v1.6.0-alpha.0.536+d60d9f3269288f",
    86  		"v1.5.0-alpha.0.1078+1044b6822497da-pull",
    87  		"v1.5.0-alpha.1.822+49b9e32fad9f32-pull-gke-gci",
    88  		"v1.6.1+coreos.0",
    89  		"1.7.1",
    90  	}
    91  	for _, s := range validVersions {
    92  		t.Run(s, func(t *testing.T) {
    93  			ver, err := kubernetesReleaseVersion(s, errorFetcher)
    94  			t.Log("Valid: ", s, ver, err)
    95  			if err != nil {
    96  				t.Errorf("kubernetesReleaseVersion unexpected error for version %q: %v", s, err)
    97  			}
    98  			if ver != s && ver != "v"+s {
    99  				t.Errorf("kubernetesReleaseVersion should return same valid version string. %q != %q", s, ver)
   100  			}
   101  		})
   102  	}
   103  }
   104  
   105  func TestInvalidVersion(t *testing.T) {
   106  	invalidVersions := []string{
   107  		"v1.3",
   108  		"1.4",
   109  		"b1.4.0",
   110  		"c1.4.5+git",
   111  		"something1.2",
   112  	}
   113  	for _, s := range invalidVersions {
   114  		t.Run(s, func(t *testing.T) {
   115  			ver, err := kubernetesReleaseVersion(s, errorFetcher)
   116  			t.Log("Invalid: ", s, ver, err)
   117  			if err == nil {
   118  				t.Errorf("kubernetesReleaseVersion error expected for version %q, but returned successfully", s)
   119  			}
   120  			if ver != "" {
   121  				t.Errorf("kubernetesReleaseVersion should return empty string in case of error. Returned %q for version %q", ver, s)
   122  			}
   123  		})
   124  	}
   125  }
   126  
   127  func TestValidConvenientForUserVersion(t *testing.T) {
   128  	validVersions := []string{
   129  		"1.4.0",
   130  		"1.4.5+git",
   131  		"1.6.1_coreos.0",
   132  	}
   133  	for _, s := range validVersions {
   134  		t.Run(s, func(t *testing.T) {
   135  			ver, err := kubernetesReleaseVersion(s, errorFetcher)
   136  			t.Log("Valid: ", s, ver, err)
   137  			if err != nil {
   138  				t.Errorf("kubernetesReleaseVersion unexpected error for version %q: %v", s, err)
   139  			}
   140  			if ver != "v"+s {
   141  				t.Errorf("kubernetesReleaseVersion should return semantic version string. %q vs. %q", s, ver)
   142  			}
   143  		})
   144  	}
   145  }
   146  
   147  func TestVersionFromNetwork(t *testing.T) {
   148  	type T struct {
   149  		Content              string
   150  		Expected             string
   151  		FetcherErrorExpected bool
   152  		ErrorExpected        bool
   153  	}
   154  
   155  	currentVersion := normalizedBuildVersion(constants.CurrentKubernetesVersion.String())
   156  
   157  	cases := map[string]T{
   158  		"stable":          {"stable-1", "v1.4.6", false, false}, // recursive pointer to stable-1
   159  		"stable-1":        {"v1.4.6", "v1.4.6", false, false},
   160  		"stable-1.3":      {"v1.3.10", "v1.3.10", false, false},
   161  		"latest":          {"v1.6.0-alpha.0", "v1.6.0-alpha.0", false, false},
   162  		"latest-1.3":      {"v1.3.11-beta.0", "v1.3.11-beta.0", false, false},
   163  		"latest-1.5":      {"", currentVersion, true, false}, // fallback to currentVersion on fetcher error
   164  		"invalid-version": {"", "", false, true},             // invalid version cannot be parsed
   165  	}
   166  
   167  	for k, v := range cases {
   168  		t.Run(k, func(t *testing.T) {
   169  
   170  			fileFetcher := func(url string, timeout time.Duration) (string, error) {
   171  				key := strings.TrimSuffix(path.Base(url), ".txt")
   172  				res, found := cases[key]
   173  				if found {
   174  					if v.FetcherErrorExpected {
   175  						return "error", errors.New("expected error")
   176  					}
   177  					return res.Content, nil
   178  				}
   179  				return "Unknown test case key!", errors.New("unknown test case key")
   180  			}
   181  
   182  			ver, err := kubernetesReleaseVersion(k, fileFetcher)
   183  			t.Logf("Key: %q. Result: %q, Error: %v", k, ver, err)
   184  			switch {
   185  			case err != nil && !v.ErrorExpected:
   186  				t.Errorf("kubernetesReleaseVersion: unexpected error for %q. Error: %+v", k, err)
   187  			case err == nil && v.ErrorExpected:
   188  				t.Errorf("kubernetesReleaseVersion: error expected for key %q, but result is %q", k, ver)
   189  			case ver != v.Expected:
   190  				t.Errorf("kubernetesReleaseVersion: unexpected result for key %q. Expected: %q Actual: %q", k, v.Expected, ver)
   191  			}
   192  		})
   193  	}
   194  }
   195  
   196  func TestVersionToTag(t *testing.T) {
   197  	type T struct {
   198  		input    string
   199  		expected string
   200  	}
   201  	cases := []T{
   202  		// NOP
   203  		{"", ""},
   204  		// Official releases
   205  		{"v1.0.0", "v1.0.0"},
   206  		// CI or custom builds
   207  		{"v10.1.2-alpha.1.100+0123456789abcdef+SOMETHING", "v10.1.2-alpha.1.100_0123456789abcdef_SOMETHING"},
   208  		// random and invalid input: should return safe value
   209  		{"v1,0!0+üñµ", "v1_0_0____"},
   210  	}
   211  
   212  	for _, tc := range cases {
   213  		t.Run(fmt.Sprintf("input:%s/expected:%s", tc.input, tc.expected), func(t *testing.T) {
   214  			tag := KubernetesVersionToImageTag(tc.input)
   215  			t.Logf("kubernetesVersionToImageTag: Input: %q. Result: %q. Expected: %q", tc.input, tag, tc.expected)
   216  			if tag != tc.expected {
   217  				t.Errorf("failed KubernetesVersionToImageTag: Input: %q. Result: %q. Expected: %q", tc.input, tag, tc.expected)
   218  			}
   219  		})
   220  	}
   221  }
   222  
   223  func TestSplitVersion(t *testing.T) {
   224  	type T struct {
   225  		input  string
   226  		bucket string
   227  		label  string
   228  		valid  bool
   229  	}
   230  	cases := []T{
   231  		// Release area
   232  		{"v1.7.0", "https://dl.k8s.io/release", "v1.7.0", true},
   233  		{"v1.8.0-alpha.2.1231+afabd012389d53a", "https://dl.k8s.io/release", "v1.8.0-alpha.2.1231+afabd012389d53a", true},
   234  		{"release/v1.7.0", "https://dl.k8s.io/release", "v1.7.0", true},
   235  		{"release/latest-1.7", "https://dl.k8s.io/release", "latest-1.7", true},
   236  		// CI builds area
   237  		{"ci/latest", "https://storage.googleapis.com/k8s-release-dev/ci", "latest", true},
   238  		{"ci/latest-1.7", "https://storage.googleapis.com/k8s-release-dev/ci", "latest-1.7", true},
   239  		// unknown label in default (release) area: splitVersion validate only areas.
   240  		{"unknown-1", "https://dl.k8s.io/release", "unknown-1", true},
   241  		// unknown area, not valid input.
   242  		{"unknown/latest-1", "", "", false},
   243  		// invalid input
   244  		{"", "", "", false},
   245  		{"ci/", "", "", false},
   246  	}
   247  
   248  	for _, tc := range cases {
   249  		t.Run(fmt.Sprintf("input:%s/label:%s", tc.input, tc.label), func(t *testing.T) {
   250  			bucket, label, err := splitVersion(tc.input)
   251  			switch {
   252  			case err != nil && tc.valid:
   253  				t.Errorf("splitVersion: unexpected error for %q. Error: %v", tc.input, err)
   254  			case err == nil && !tc.valid:
   255  				t.Errorf("splitVersion: error expected for key %q, but result is %q, %q", tc.input, bucket, label)
   256  			case bucket != tc.bucket:
   257  				t.Errorf("splitVersion: unexpected bucket result for key %q. Expected: %q Actual: %q", tc.input, tc.bucket, bucket)
   258  			case label != tc.label:
   259  				t.Errorf("splitVersion: unexpected label result for key %q. Expected: %q Actual: %q", tc.input, tc.label, label)
   260  			}
   261  		})
   262  	}
   263  }
   264  
   265  func TestKubernetesIsCIVersion(t *testing.T) {
   266  	type T struct {
   267  		input    string
   268  		expected bool
   269  	}
   270  	cases := []T{
   271  		{"", false},
   272  		// Official releases
   273  		{"v1.0.0", false},
   274  		{"release/v1.0.0", false},
   275  		// CI builds
   276  		{"ci/latest-1", true},
   277  		{"ci/v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
   278  		{"ci/", false},
   279  	}
   280  
   281  	for _, tc := range cases {
   282  		t.Run(fmt.Sprintf("input:%s/expected:%t", tc.input, tc.expected), func(t *testing.T) {
   283  			result := KubernetesIsCIVersion(tc.input)
   284  			t.Logf("kubernetesIsCIVersion: Input: %q. Result: %v. Expected: %v", tc.input, result, tc.expected)
   285  			if result != tc.expected {
   286  				t.Errorf("failed KubernetesIsCIVersion: Input: %q. Result: %v. Expected: %v", tc.input, result, tc.expected)
   287  			}
   288  		})
   289  	}
   290  }
   291  
   292  // Validate kubernetesReleaseVersion but with bucket prefixes
   293  func TestCIBuildVersion(t *testing.T) {
   294  	type T struct {
   295  		input    string
   296  		expected string
   297  		valid    bool
   298  	}
   299  	cases := []T{
   300  		// Official releases
   301  		{"v1.7.0", "v1.7.0", true},
   302  		{"release/v1.8.0", "v1.8.0", true},
   303  		{"1.4.0-beta.0", "v1.4.0-beta.0", true},
   304  		{"release/0invalid", "", false},
   305  		// CI or custom builds
   306  		{"ci/v1.9.0-alpha.1.123+acbcbfd53bfa0a", "v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
   307  		{"ci/1.9.0-alpha.1.123+acbcbfd53bfa0a", "v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
   308  		{"ci/0invalid", "", false},
   309  		{"0invalid", "", false},
   310  	}
   311  
   312  	for _, tc := range cases {
   313  		t.Run(fmt.Sprintf("input:%s/expected:%s", tc.input, tc.expected), func(t *testing.T) {
   314  
   315  			fileFetcher := func(url string, timeout time.Duration) (string, error) {
   316  				if tc.valid {
   317  					return tc.expected, nil
   318  				}
   319  				return "Unknown test case key!", errors.New("unknown test case key")
   320  			}
   321  
   322  			ver, err := kubernetesReleaseVersion(tc.input, fileFetcher)
   323  			t.Logf("Input: %q. Result: %q, Error: %v", tc.input, ver, err)
   324  			switch {
   325  			case err != nil && tc.valid:
   326  				t.Errorf("kubernetesReleaseVersion: unexpected error for input %q. Error: %v", tc.input, err)
   327  			case err == nil && !tc.valid:
   328  				t.Errorf("kubernetesReleaseVersion: error expected for input %q, but result is %q", tc.input, ver)
   329  			case ver != tc.expected:
   330  				t.Errorf("kubernetesReleaseVersion: unexpected result for input %q. Expected: %q Actual: %q", tc.input, tc.expected, ver)
   331  			}
   332  		})
   333  	}
   334  }
   335  
   336  func TestNormalizedBuildVersionVersion(t *testing.T) {
   337  	type T struct {
   338  		input    string
   339  		expected string
   340  	}
   341  	cases := []T{
   342  		{"v1.7.0", "v1.7.0"},
   343  		{"v1.8.0-alpha.2.1231+afabd012389d53a", "v1.8.0-alpha.2.1231+afabd012389d53a"},
   344  		{"1.7.0", "v1.7.0"},
   345  		{"unknown-1", ""},
   346  	}
   347  
   348  	for _, tc := range cases {
   349  		t.Run(fmt.Sprintf("input:%s/expected:%s", tc.input, tc.expected), func(t *testing.T) {
   350  			output := normalizedBuildVersion(tc.input)
   351  			if output != tc.expected {
   352  				t.Errorf("normalizedBuildVersion: unexpected output %q for input %q. Expected: %q", output, tc.input, tc.expected)
   353  			}
   354  		})
   355  	}
   356  }
   357  
   358  func TestKubeadmVersion(t *testing.T) {
   359  	type T struct {
   360  		name         string
   361  		input        string
   362  		output       string
   363  		outputError  bool
   364  		parsingError bool
   365  	}
   366  	cases := []T{
   367  		{
   368  			name:   "valid version with label and metadata",
   369  			input:  "v1.8.0-alpha.2.1231+afabd012389d53a",
   370  			output: "v1.8.0-alpha.2",
   371  		},
   372  		{
   373  			name:   "valid version with label and extra metadata",
   374  			input:  "v1.8.0-alpha.2.1231+afabd012389d53a.extra",
   375  			output: "v1.8.0-alpha.2",
   376  		},
   377  		{
   378  			name:   "valid patch version with label and extra metadata",
   379  			input:  "v1.11.3-beta.0.38+135cc4c1f47994",
   380  			output: "v1.11.2",
   381  		},
   382  		{
   383  			name:   "valid version with label extra",
   384  			input:  "v1.8.0-alpha.2.1231",
   385  			output: "v1.8.0-alpha.2",
   386  		},
   387  		{
   388  			name:   "valid patch version with label",
   389  			input:  "v1.9.11-beta.0",
   390  			output: "v1.9.10",
   391  		},
   392  		{
   393  			name:   "handle version with partial label",
   394  			input:  "v1.8.0-alpha",
   395  			output: "v1.8.0-alpha.0",
   396  		},
   397  		{
   398  			name:   "handle version missing 'v'",
   399  			input:  "1.11.0",
   400  			output: "v1.11.0",
   401  		},
   402  		{
   403  			name:   "valid version without label and metadata",
   404  			input:  "v1.8.0",
   405  			output: "v1.8.0",
   406  		},
   407  		{
   408  			name:   "valid patch version without label and metadata",
   409  			input:  "v1.8.2",
   410  			output: "v1.8.2",
   411  		},
   412  		{
   413  			name:         "invalid version",
   414  			input:        "foo",
   415  			parsingError: true,
   416  		},
   417  		{
   418  			name:         "invalid version with stray dash",
   419  			input:        "v1.9.11-",
   420  			parsingError: true,
   421  		},
   422  		{
   423  			name:         "invalid version without patch release",
   424  			input:        "v1.9",
   425  			parsingError: true,
   426  		},
   427  		{
   428  			name:         "invalid version with label and stray dot",
   429  			input:        "v1.8.0-alpha.2.",
   430  			parsingError: true,
   431  		},
   432  		{
   433  			name:        "invalid version with label and metadata",
   434  			input:       "v1.8.0-alpha.2.1231+afabd012389d53a",
   435  			output:      "v1.8.0-alpha.3",
   436  			outputError: true,
   437  		},
   438  	}
   439  
   440  	for _, tc := range cases {
   441  		t.Run(tc.name, func(t *testing.T) {
   442  			output, err := kubeadmVersion(tc.input)
   443  			if (err != nil) != tc.parsingError {
   444  				t.Fatalf("expected error: %v, got: %v", tc.parsingError, err != nil)
   445  			}
   446  			if (output != tc.output) != tc.outputError {
   447  				t.Fatalf("expected output: %s, got: %s, for input: %s", tc.output, output, tc.input)
   448  			}
   449  		})
   450  	}
   451  }
   452  
   453  func TestValidateStableVersion(t *testing.T) {
   454  	type T struct {
   455  		name          string
   456  		remoteVersion string
   457  		clientVersion string
   458  		output        string
   459  		expectedError bool
   460  	}
   461  	cases := []T{
   462  		{
   463  			name:          "valid: remote version is newer; return stable label [1]",
   464  			remoteVersion: "v1.12.0",
   465  			clientVersion: "v1.11.0",
   466  			output:        "stable-1.11",
   467  		},
   468  		{
   469  			name:          "valid: remote version is newer; return stable label [2]",
   470  			remoteVersion: "v2.0.0",
   471  			clientVersion: "v1.11.0",
   472  			output:        "stable-1.11",
   473  		},
   474  		{
   475  			name:          "valid: remote version is newer; return stable label [3]",
   476  			remoteVersion: "v2.1.5",
   477  			clientVersion: "v1.11.5",
   478  			output:        "stable-1.11",
   479  		},
   480  		{
   481  			name:          "valid: return the remote version as it is part of the same release",
   482  			remoteVersion: "v1.11.5",
   483  			clientVersion: "v1.11.0",
   484  			output:        "v1.11.5",
   485  		},
   486  		{
   487  			name:          "valid: return the same version",
   488  			remoteVersion: "v1.11.0",
   489  			clientVersion: "v1.11.0",
   490  			output:        "v1.11.0",
   491  		},
   492  		{
   493  			name:          "invalid: client version is empty",
   494  			remoteVersion: "v1.12.1",
   495  			clientVersion: "",
   496  			expectedError: true,
   497  		},
   498  		{
   499  			name:          "invalid: error parsing the remote version",
   500  			remoteVersion: "invalid-version",
   501  			clientVersion: "v1.12.0",
   502  			expectedError: true,
   503  		},
   504  		{
   505  			name:          "invalid: error parsing the client version",
   506  			remoteVersion: "v1.12.0",
   507  			clientVersion: "invalid-version",
   508  			expectedError: true,
   509  		},
   510  	}
   511  
   512  	for _, tc := range cases {
   513  		t.Run(tc.name, func(t *testing.T) {
   514  			output, err := validateStableVersion(tc.remoteVersion, tc.clientVersion)
   515  			if (err != nil) != tc.expectedError {
   516  				t.Fatalf("expected error: %v, got: %v", tc.expectedError, err != nil)
   517  			}
   518  			if output != tc.output {
   519  				t.Fatalf("expected output: %s, got: %s", tc.output, output)
   520  			}
   521  		})
   522  	}
   523  }
   524  
   525  func errorFetcher(url string, timeout time.Duration) (string, error) {
   526  	return "should not make internet calls", errors.Errorf("should not make internet calls, tried to request url: %s", url)
   527  }