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 }