github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/generic-autobumper/main_test.go (about) 1 /* 2 Copyright 2021 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 main 18 19 import ( 20 "errors" 21 "fmt" 22 "net/http" 23 "net/http/httptest" 24 "os" 25 "path" 26 "reflect" 27 "regexp" 28 "strings" 29 "testing" 30 31 "github.com/google/go-cmp/cmp/cmpopts" 32 33 "github.com/google/go-cmp/cmp" 34 ) 35 36 func TestGetAssignment(t *testing.T) { 37 cases := []struct { 38 description string 39 oncallURL string 40 oncallGroup string 41 oncallServerResponse string 42 skipOncallAssignment bool 43 selfAssign bool 44 expectResKeyword string 45 expectOncallActive bool 46 }{ 47 { 48 description: "empty oncall URL will return an empty string", 49 oncallURL: "", 50 oncallGroup: defaultOncallGroup, 51 oncallServerResponse: "", 52 expectResKeyword: "", 53 }, 54 { 55 description: "an invalid oncall URL will return an error message", 56 oncallURL: "whatever-url", 57 oncallGroup: defaultOncallGroup, 58 oncallServerResponse: "", 59 expectResKeyword: "error", 60 }, 61 { 62 description: "an invalid response will return an error message", 63 oncallURL: "auto", 64 oncallGroup: defaultOncallGroup, 65 oncallServerResponse: "whatever-malformed-response", 66 expectResKeyword: "error", 67 }, 68 { 69 description: "a valid response will return the oncaller from default group", 70 oncallURL: "auto", 71 oncallGroup: defaultOncallGroup, 72 oncallServerResponse: `{"Oncall":{"testinfra":"fake-oncall-name"},"Active":{"testinfra":false}}`, 73 expectResKeyword: "fake-oncall-name", 74 }, 75 { 76 description: "a valid response will return the oncaller from non-default group", 77 oncallURL: "auto", 78 oncallGroup: "another-group", 79 oncallServerResponse: `{"Oncall":{"testinfra":"fake-oncall-name","another-group":"fake-oncall-name2"},"Active":{"another-group":false}}`, 80 expectResKeyword: "fake-oncall-name2", 81 }, 82 { 83 description: "a valid response without expected oncall group", 84 oncallURL: "auto", 85 oncallGroup: "group-not-exist", 86 oncallServerResponse: `{"Oncall":{"testinfra":"fake-oncall-name","another-group":"fake-oncall-name2"}}`, 87 expectResKeyword: "error", 88 }, 89 { 90 description: "a valid response with empty oncall will return on oncall message", 91 oncallURL: "auto", 92 oncallGroup: defaultOncallGroup, 93 oncallServerResponse: `{"Oncall":{"testinfra":""},"Active":{"testinfra":false}}`, 94 expectResKeyword: "Nobody", 95 }, 96 { 97 description: "oncall active", 98 oncallURL: "auto", 99 oncallGroup: defaultOncallGroup, 100 oncallServerResponse: `{"Oncall":{"testinfra":"fake-oncall-name"},"Active":{"testinfra":true}}`, 101 expectResKeyword: "fake-oncall-name", 102 expectOncallActive: true, 103 }, 104 { 105 description: "skip", 106 oncallURL: "auto", 107 oncallGroup: defaultOncallGroup, 108 oncallServerResponse: `{"Oncall":{"testinfra":"fake-oncall-name"},"Active":{"testinfra":true}}`, 109 skipOncallAssignment: true, 110 expectResKeyword: "", 111 expectOncallActive: true, 112 }, 113 { 114 description: "self-assign-with-oncall", 115 oncallURL: "auto", 116 oncallGroup: defaultOncallGroup, 117 oncallServerResponse: `{"Oncall":{"testinfra":"fake-oncall-name"},"Active":{"testinfra":true}}`, 118 skipOncallAssignment: true, 119 selfAssign: true, 120 expectResKeyword: "/cc", 121 expectOncallActive: true, 122 }, 123 { 124 description: "self-assign-without-oncall", 125 oncallURL: "auto", 126 oncallGroup: defaultOncallGroup, 127 oncallServerResponse: `{"Oncall":{"testinfra":""},"Active":{"testinfra":false}}`, 128 skipOncallAssignment: true, 129 selfAssign: true, 130 expectResKeyword: "/cc", 131 expectOncallActive: false, 132 }, 133 } 134 135 for _, tc := range cases { 136 t.Run(tc.description, func(t *testing.T) { 137 if tc.oncallURL == "auto" { 138 // generate a test server so we can capture and inspect the request 139 testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 140 res.Write([]byte(tc.oncallServerResponse)) 141 })) 142 defer func() { testServer.Close() }() 143 tc.oncallURL = testServer.URL 144 } 145 146 res := getAssignment(tc.oncallURL, tc.oncallGroup, tc.skipOncallAssignment, tc.selfAssign) 147 if !strings.Contains(res, tc.expectResKeyword) { 148 t.Errorf("Expect the result %q contains keyword %q but it does not", res, tc.expectResKeyword) 149 } 150 if got, want := isOncallActive(tc.oncallURL, tc.oncallGroup), tc.expectOncallActive; got != want { 151 t.Errorf("Expect oncall active. Want: %v, got: %v", want, got) 152 } 153 }) 154 } 155 } 156 157 func TestValidateOptions(t *testing.T) { 158 emptyStr := "" 159 whateverStr := "whatever" 160 emptyArr := make([]string, 0) 161 emptyPrefixes := make([]prefix, 0) 162 validExceptionPrefixes := []prefix{{ 163 Name: "test", 164 Prefix: "gcr.io/test/", 165 ConsistentImages: true, 166 ConsistentImageExceptions: []string{"gcr.io/test/foo", "gcr.io/test/bar"}, 167 RefConfigFile: "", 168 StagingRefConfigFile: "", 169 }} 170 invalidExceptionPrefixes := []prefix{{ 171 Name: "test", 172 Prefix: "gcr.io/test/", 173 ConsistentImages: false, 174 ConsistentImageExceptions: []string{"gcr.io/test/foo", "gcr.io/test/bar"}, 175 RefConfigFile: "", 176 StagingRefConfigFile: "", 177 }} 178 latestPrefixes := []prefix{{ 179 Name: "test", 180 Prefix: "gcr.io/test/", 181 RefConfigFile: "", 182 StagingRefConfigFile: "", 183 }} 184 upstreamPrefixes := []prefix{{ 185 Name: "test", 186 Prefix: "gcr.io/test/", 187 RefConfigFile: "ref", 188 StagingRefConfigFile: "stagingRef", 189 }} 190 upstreamVersion := "upstream" 191 stagingVersion := "upstream-staging" 192 cases := []struct { 193 name string 194 targetVersion *string 195 includeConfigPaths *[]string 196 prefixes *[]prefix 197 upstreamURLBase *string 198 err bool 199 upstreamBaseChanged bool 200 }{ 201 { 202 name: "Everything correct", 203 err: false, 204 }, 205 { 206 name: "unformatted TargetVersion is also allowed", 207 targetVersion: &whateverStr, 208 err: false, 209 }, 210 { 211 name: "must include at least one config path", 212 includeConfigPaths: &emptyArr, 213 err: true, 214 }, 215 { 216 name: "must include upstreamURLBase if target version is upstream", 217 upstreamURLBase: &emptyStr, 218 targetVersion: &upstreamVersion, 219 prefixes: &upstreamPrefixes, 220 err: false, 221 upstreamBaseChanged: true, 222 }, 223 { 224 name: "must include upstreamURLBase if target version is upstreamStaging", 225 upstreamURLBase: &emptyStr, 226 targetVersion: &stagingVersion, 227 prefixes: &upstreamPrefixes, 228 err: false, 229 upstreamBaseChanged: true, 230 }, 231 { 232 name: "must include at least one prefix", 233 prefixes: &emptyPrefixes, 234 err: true, 235 }, 236 { 237 name: "can enable consistentImageExceptions with consistentImages", 238 prefixes: &validExceptionPrefixes, 239 err: false, 240 }, 241 { 242 name: "cannot enable consistentImageExceptions without consistentImages", 243 prefixes: &invalidExceptionPrefixes, 244 err: true, 245 }, 246 { 247 name: "must have ref files for upstream version", 248 targetVersion: &upstreamVersion, 249 prefixes: &latestPrefixes, 250 err: true, 251 }, 252 { 253 name: "must have stagingRef files for Stagingupstream version", 254 targetVersion: &stagingVersion, 255 prefixes: &latestPrefixes, 256 err: true, 257 }, 258 { 259 name: "don't use default upstreamURLbase if not needed for upstream", 260 upstreamURLBase: &whateverStr, 261 targetVersion: &upstreamVersion, 262 prefixes: &upstreamPrefixes, 263 err: false, 264 upstreamBaseChanged: false, 265 }, 266 { 267 name: "don't use default upstreamURLbase if not neededfor upstreamStaging", 268 upstreamURLBase: &whateverStr, 269 targetVersion: &stagingVersion, 270 prefixes: &upstreamPrefixes, 271 err: false, 272 upstreamBaseChanged: false, 273 }, 274 } 275 for _, tc := range cases { 276 t.Run(tc.name, func(t *testing.T) { 277 defaultOption := &options{ 278 UpstreamURLBase: "whatever-URLBase", 279 Prefixes: latestPrefixes, 280 TargetVersion: latestVersion, 281 IncludedConfigPaths: []string{"whatever-config-path1", "whatever-config-path2"}, 282 } 283 284 if tc.targetVersion != nil { 285 defaultOption.TargetVersion = *tc.targetVersion 286 } 287 if tc.includeConfigPaths != nil { 288 defaultOption.IncludedConfigPaths = *tc.includeConfigPaths 289 } 290 if tc.prefixes != nil { 291 defaultOption.Prefixes = *tc.prefixes 292 } 293 if tc.upstreamURLBase != nil { 294 defaultOption.UpstreamURLBase = *tc.upstreamURLBase 295 } 296 297 err := validateOptions(defaultOption) 298 t.Logf("err is: %v", err) 299 if err == nil && tc.err { 300 t.Errorf("Expected to get an error for %#v but got nil", defaultOption) 301 } 302 if err != nil && !tc.err { 303 t.Errorf("Expected to not get an error for %#v but got %v", defaultOption, err) 304 } 305 if tc.upstreamBaseChanged && defaultOption.UpstreamURLBase != defaultUpstreamURLBase { 306 t.Errorf("UpstreamURLBase should have been changed to %q, but was %q", defaultOption.UpstreamURLBase, defaultUpstreamURLBase) 307 } 308 if !tc.upstreamBaseChanged && defaultOption.UpstreamURLBase == defaultUpstreamURLBase { 309 t.Errorf("UpstreamURLBase should not have been changed to default, but was") 310 } 311 }) 312 } 313 } 314 315 type fakeImageBumperCli struct { 316 replacements map[string]string 317 tagCache map[string]string 318 } 319 320 func (c *fakeImageBumperCli) FindLatestTag(imageHost, imageName, currentTag string) (string, error) { 321 return "fake-latest", nil 322 } 323 324 func (c *fakeImageBumperCli) UpdateFile(tagPicker func(imageHost, imageName, currentTag string) (string, error), 325 path string, imageFilter *regexp.Regexp) error { 326 targetTag, _ := tagPicker("", "", "") 327 c.replacements[path] = targetTag 328 return nil 329 } 330 331 func (c *fakeImageBumperCli) GetReplacements() map[string]string { 332 return c.replacements 333 } 334 335 func (c *fakeImageBumperCli) AddToCache(image, newTag string) { 336 c.tagCache[image] = newTag 337 } 338 339 func (cli *fakeImageBumperCli) TagExists(imageHost, imageName, currentTag string) (bool, error) { 340 if currentTag == "DNE" { 341 return false, nil 342 } 343 return true, nil 344 } 345 346 func TestUpdateReferences(t *testing.T) { 347 tmpDir := t.TempDir() 348 for dir, fps := range map[string][]string{ 349 "testdata/dir/subdir1": {"test1-1.yaml", "test1-2.yaml"}, 350 "testdata/dir/subdir2": {"test2-1.yaml"}, 351 "testdata/dir/subdir3": {"test3-1.yaml"}, 352 "testdata/dir": {"extra-file"}, 353 } { 354 if err := os.MkdirAll(path.Join(tmpDir, dir), 0755); err != nil { 355 t.Fatalf("Faile creating dir %q: %v", dir, err) 356 } 357 for _, f := range fps { 358 if _, err := os.Create(path.Join(tmpDir, dir, f)); err != nil { 359 t.Fatalf("Faile creating file %q: %v", f, err) 360 } 361 } 362 } 363 364 cases := []struct { 365 description string 366 targetVersion string 367 includeConfigPaths []string 368 excludeConfigPaths []string 369 extraFiles []string 370 expectedRes map[string]string 371 expectError bool 372 }{ 373 { 374 description: "update the images to the latest version", 375 targetVersion: latestVersion, 376 includeConfigPaths: []string{ 377 path.Join(tmpDir, "testdata/dir/subdir1"), 378 path.Join(tmpDir, "testdata/dir/subdir2"), 379 }, 380 expectedRes: map[string]string{ 381 path.Join(tmpDir, "testdata/dir/subdir1/test1-1.yaml"): "fake-latest", 382 path.Join(tmpDir, "testdata/dir/subdir1/test1-2.yaml"): "fake-latest", 383 path.Join(tmpDir, "testdata/dir/subdir2/test2-1.yaml"): "fake-latest", 384 }, 385 expectError: false, 386 }, 387 { 388 description: "update the images to a specific version", 389 targetVersion: "v20200101-livebull", 390 includeConfigPaths: []string{ 391 path.Join(tmpDir, "testdata/dir/subdir2"), 392 }, 393 expectedRes: map[string]string{ 394 path.Join(tmpDir, "testdata/dir/subdir2/test2-1.yaml"): "v20200101-livebull", 395 }, 396 expectError: false, 397 }, 398 { 399 description: "by default only yaml files will be updated", 400 targetVersion: latestVersion, 401 includeConfigPaths: []string{ 402 path.Join(tmpDir, "testdata/dir/subdir3"), 403 }, 404 expectedRes: map[string]string{ 405 path.Join(tmpDir, "testdata/dir/subdir3/test3-1.yaml"): "fake-latest", 406 }, 407 expectError: false, 408 }, 409 { 410 description: "files under the excluded paths will not be updated", 411 targetVersion: latestVersion, 412 includeConfigPaths: []string{ 413 path.Join(tmpDir, "testdata/dir"), 414 }, 415 excludeConfigPaths: []string{ 416 path.Join(tmpDir, "testdata/dir/subdir1"), 417 path.Join(tmpDir, "testdata/dir/subdir2"), 418 }, 419 expectedRes: map[string]string{ 420 path.Join(tmpDir, "testdata/dir/subdir3/test3-1.yaml"): "fake-latest", 421 }, 422 expectError: false, 423 }, 424 { 425 description: "non YAML files could be configured by specifying extraFiles", 426 targetVersion: latestVersion, 427 includeConfigPaths: []string{ 428 path.Join(tmpDir, "testdata/dir/subdir3"), 429 }, 430 extraFiles: []string{ 431 path.Join(tmpDir, "testdata/dir/extra-file"), 432 path.Join(tmpDir, "testdata/dir/subdir3/test3-2"), 433 }, 434 expectedRes: map[string]string{ 435 path.Join(tmpDir, "testdata/dir/subdir3/test3-1.yaml"): "fake-latest", 436 path.Join(tmpDir, "testdata/dir/extra-file"): "fake-latest", 437 path.Join(tmpDir, "testdata/dir/subdir3/test3-2"): "fake-latest", 438 }, 439 expectError: false, 440 }, 441 { 442 description: "updating non-existed files will return an error", 443 targetVersion: latestVersion, 444 includeConfigPaths: []string{ 445 path.Join(tmpDir, "testdata/dir/whatever-subdir"), 446 }, 447 expectError: true, 448 }, 449 } 450 451 for _, tc := range cases { 452 t.Run(tc.description, func(t *testing.T) { 453 option := &options{ 454 TargetVersion: tc.targetVersion, 455 IncludedConfigPaths: tc.includeConfigPaths, 456 ExtraFiles: tc.extraFiles, 457 ExcludedConfigPaths: tc.excludeConfigPaths, 458 } 459 cli := &fakeImageBumperCli{replacements: map[string]string{}} 460 res, err := updateReferences(cli, nil, option) 461 if tc.expectError && err == nil { 462 t.Errorf("Expected to get an error but the result is nil") 463 } 464 if !tc.expectError && err != nil { 465 t.Errorf("Expected to not get an error but got one: %v", err) 466 } 467 468 if !reflect.DeepEqual(res, tc.expectedRes) { 469 t.Errorf("Expected to get the result map as %v but got %v", tc.expectedRes, res) 470 } 471 }) 472 } 473 } 474 475 func TestParseUpstreamImageVersion(t *testing.T) { 476 cases := []struct { 477 description string 478 upstreamURL string 479 upstreamServerResponse string 480 expectedRes string 481 expectError bool 482 prefix string 483 }{ 484 { 485 description: "empty upstream URL will return an error", 486 upstreamURL: "", 487 upstreamServerResponse: "", 488 expectedRes: "", 489 expectError: true, 490 prefix: "gcr.io/k8s-prow/", 491 }, 492 { 493 description: "an invalid upstream URL will return an error", 494 upstreamURL: "whatever-url", 495 upstreamServerResponse: "", 496 expectedRes: "", 497 expectError: true, 498 prefix: "gcr.io/k8s-prow/", 499 }, 500 { 501 description: "an invalid response will return an error", 502 upstreamURL: "auto", 503 upstreamServerResponse: "whatever-response", 504 expectedRes: "", 505 expectError: true, 506 prefix: "gcr.io/k8s-prow/", 507 }, 508 { 509 description: "a valid response will return the correct tag", 510 upstreamURL: "auto", 511 upstreamServerResponse: " image: gcr.io/k8s-prow/deck:v20200717-cf288082e1", 512 expectedRes: "v20200717-cf288082e1", 513 expectError: false, 514 prefix: "gcr.io/k8s-prow/", 515 }, 516 { 517 description: "a valid response will return the correct tag with other prefixes in the same file", 518 upstreamURL: "auto", 519 upstreamServerResponse: "other random garbage\n image: gcr.io/k8s-other/deck:v22222222-cf288082e1\n image: gcr.io/k8s-prow/deck:v20200717-cf288082e1\n image: gcr.io/k8s-another/deck:v11111111-cf288082e1", 520 expectedRes: "v20200717-cf288082e1", 521 expectError: false, 522 prefix: "gcr.io/k8s-prow/", 523 }, 524 } 525 526 for _, tc := range cases { 527 t.Run(tc.description, func(t *testing.T) { 528 if tc.upstreamURL == "auto" { 529 // generate a test server so we can capture and inspect the request 530 testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 531 res.Write([]byte(tc.upstreamServerResponse)) 532 })) 533 defer func() { testServer.Close() }() 534 tc.upstreamURL = testServer.URL 535 } 536 537 res, err := parseUpstreamImageVersion(tc.upstreamURL, tc.prefix) 538 if res != tc.expectedRes { 539 t.Errorf("The expected result %q != the actual result %q", tc.expectedRes, res) 540 } 541 if tc.expectError && err == nil { 542 t.Errorf("Expected to get an error but the result is nil") 543 } 544 if !tc.expectError && err != nil { 545 t.Errorf("Expected to not get an error but got one: %v", err) 546 } 547 }) 548 } 549 } 550 551 func TestUpstreamImageVersionResolver(t *testing.T) { 552 const ( 553 prowProdFakeVersion = "v-prow-prod-version" 554 prowStagingFakeVersion = "v-prow-staging-version" 555 boskosProdFakeVersion = "v-boskos-prod-version" 556 boskosStagingFakeVersion = "v-boskos-staging-version" 557 prowRefConfigFile = "prow-prod" 558 boskosRefConfigFile = "boskos-prod" 559 prowStagingRefConfigFile = "prow-staging" 560 boskosStagingRefConfigFile = "boskos-staging" 561 fakeUpstreamURLBase = "test.com" 562 prowPrefix = "gcr.io/k8s-prow/" 563 boskosPrefix = "gcr.io/k8s-boskos/" 564 doesNotExistPrefix = "gcr.io/dne" 565 doesNotExist = "DNE" 566 ) 567 prowPrefixStruct := prefix{ 568 Prefix: prowPrefix, 569 RefConfigFile: prowRefConfigFile, 570 StagingRefConfigFile: prowStagingRefConfigFile, 571 } 572 boskosPrefixStruct := prefix{ 573 Prefix: boskosPrefix, 574 RefConfigFile: boskosRefConfigFile, 575 StagingRefConfigFile: boskosStagingRefConfigFile, 576 } 577 // prefix used to test when a tag does not exist. This is used to have parser return a tag that will make TagExists return false 578 tagDoesNotExistPrefix := prefix{ 579 Prefix: doesNotExistPrefix, 580 RefConfigFile: doesNotExist, 581 StagingRefConfigFile: doesNotExist, 582 } 583 584 cases := []struct { 585 description string 586 parser func(string, string) (string, error) 587 upstreamVersionType string 588 imageHost string 589 imageName string 590 currentTag string 591 expectedTargetTag string 592 expectError bool 593 resolverError bool 594 prefixes []prefix 595 }{ 596 { 597 description: "resolve image version with an invalid version type", 598 parser: func(upAddr, pref string) (string, error) { 599 switch strings.TrimPrefix(upAddr, fakeUpstreamURLBase+"/") { 600 case prowRefConfigFile: 601 return prowProdFakeVersion, nil 602 case boskosRefConfigFile: 603 return boskosProdFakeVersion, nil 604 default: 605 return "", errors.New("not supported") 606 } 607 }, 608 upstreamVersionType: "whatever-version-type", 609 expectError: true, 610 prefixes: []prefix{prowPrefixStruct, boskosPrefixStruct}, 611 }, 612 { 613 description: "resolve image with two prefixes possible and upstreamVersion", 614 parser: func(upAddr, pref string) (string, error) { 615 switch strings.TrimPrefix(upAddr, fakeUpstreamURLBase+"/") { 616 case prowRefConfigFile: 617 return prowProdFakeVersion, nil 618 case boskosRefConfigFile: 619 return boskosProdFakeVersion, nil 620 default: 621 return "", errors.New("not supported") 622 } 623 }, 624 upstreamVersionType: upstreamVersion, 625 expectError: false, 626 prefixes: []prefix{prowPrefixStruct, boskosPrefixStruct}, 627 imageHost: prowPrefix, 628 currentTag: "whatever-current-tag", 629 expectedTargetTag: prowProdFakeVersion, 630 }, 631 { 632 description: "resolve image with two prefixes possible and staging version", 633 parser: func(upAddr, pref string) (string, error) { 634 switch strings.TrimPrefix(upAddr, fakeUpstreamURLBase+"/") { 635 case prowStagingRefConfigFile: 636 return prowStagingFakeVersion, nil 637 case boskosStagingRefConfigFile: 638 return boskosStagingFakeVersion, nil 639 default: 640 return "", errors.New("not supported") 641 } 642 }, 643 upstreamVersionType: upstreamStagingVersion, 644 expectError: false, 645 prefixes: []prefix{prowPrefixStruct, boskosPrefixStruct}, 646 imageHost: boskosPrefix, 647 currentTag: "whatever-current-tag", 648 expectedTargetTag: boskosStagingFakeVersion, 649 }, 650 { 651 description: "resolve image when unknown prefix", 652 parser: func(upAddr, pref string) (string, error) { 653 switch strings.TrimPrefix(upAddr, fakeUpstreamURLBase+"/") { 654 case boskosRefConfigFile: 655 return boskosProdFakeVersion, nil 656 default: 657 return "", errors.New("not supported") 658 } 659 }, 660 upstreamVersionType: upstreamVersion, 661 expectError: false, 662 prefixes: []prefix{boskosPrefixStruct}, 663 imageHost: prowPrefix, 664 currentTag: "whatever-current-tag", 665 expectedTargetTag: "whatever-current-tag", 666 }, 667 { 668 description: "tag does not exist", 669 parser: func(upAddr, pref string) (string, error) { 670 switch strings.TrimPrefix(upAddr, fakeUpstreamURLBase+"/") { 671 case doesNotExist: 672 return doesNotExist, nil 673 default: 674 return "", errors.New("not supported") 675 } 676 }, 677 upstreamVersionType: upstreamVersion, 678 expectError: false, 679 prefixes: []prefix{tagDoesNotExistPrefix}, 680 imageHost: doesNotExistPrefix, 681 currentTag: "doesNotExist", 682 expectedTargetTag: "", 683 resolverError: true, 684 }, 685 } 686 for _, tc := range cases { 687 t.Run(tc.description, func(t *testing.T) { 688 option := &options{ 689 UpstreamURLBase: fakeUpstreamURLBase, 690 Prefixes: tc.prefixes, 691 } 692 cli := &fakeImageBumperCli{replacements: map[string]string{}, tagCache: map[string]string{}} 693 resolver, err := upstreamImageVersionResolver(option, tc.upstreamVersionType, tc.parser, cli) 694 if tc.expectError && err == nil { 695 t.Errorf("Expected to get an error but the result is nil") 696 return 697 } 698 if !tc.expectError && err != nil { 699 t.Errorf("Expected to not get an error but got one: %v", err) 700 return 701 } 702 703 if err == nil && resolver == nil { 704 t.Error("Expected to get an image resolver but got nil") 705 return 706 } 707 708 if resolver != nil { 709 res, resErr := resolver(tc.imageHost, tc.imageName, tc.currentTag) 710 if !tc.resolverError && resErr != nil { 711 t.Errorf("Expected resolver to return without error, but received error: %v", resErr) 712 } 713 if tc.resolverError && resErr == nil { 714 t.Error("Expected resolver to return with error, but did not receive one") 715 } 716 if tc.expectedTargetTag != res { 717 t.Errorf("Expected to get target tag %q but got %q", tc.expectedTargetTag, res) 718 } 719 720 } 721 722 }) 723 } 724 } 725 726 func TestUpstreamConfigVersions(t *testing.T) { 727 prowProdFakeVersion := "v-prow-prod-version" 728 prowStagingFakeVersion := "v-prow-staging-version" 729 boskosProdFakeVersion := "v-boskos-prod-version" 730 boskosStagingFakeVersion := "v-boskos-staging-version" 731 prowRefConfigFile := "prow-prod" 732 boskosRefConfigFile := "boskos-prod" 733 prowStagingRefConfigFile := "prow-staging" 734 boskosStagingRefConfigFile := "boskos-staging" 735 fakeUpstreamURLBase := "test.com" 736 prowPrefix := "gcr.io/k8s-prow/" 737 boskosPrefix := "gcr.io/k8s-boskos/" 738 739 prowPrefixStruct := prefix{ 740 Prefix: prowPrefix, 741 RefConfigFile: prowRefConfigFile, 742 StagingRefConfigFile: prowStagingRefConfigFile, 743 } 744 boskosPrefixStruct := prefix{ 745 Prefix: boskosPrefix, 746 RefConfigFile: boskosRefConfigFile, 747 StagingRefConfigFile: boskosStagingRefConfigFile, 748 } 749 750 fakeImageVersionParser := func(upstreamAddress, prefix string) (string, error) { 751 switch upstreamAddress { 752 case fakeUpstreamURLBase + "/" + prowRefConfigFile: 753 return prowProdFakeVersion, nil 754 case fakeUpstreamURLBase + "/" + prowStagingRefConfigFile: 755 return prowStagingFakeVersion, nil 756 case fakeUpstreamURLBase + "/" + boskosRefConfigFile: 757 return boskosProdFakeVersion, nil 758 case fakeUpstreamURLBase + "/" + boskosStagingRefConfigFile: 759 return boskosStagingFakeVersion, nil 760 default: 761 return "", fmt.Errorf("unsupported upstream address %q for parsing the image version", upstreamAddress) 762 } 763 } 764 cases := []struct { 765 description string 766 upstreamVersionType string 767 expectedResult map[string]string 768 expectError bool 769 prefixes []prefix 770 }{ 771 { 772 description: "resolve image version with an invalid version type", 773 upstreamVersionType: "whatever-version-type", 774 expectError: true, 775 prefixes: []prefix{prowPrefixStruct, boskosPrefixStruct}, 776 }, 777 { 778 description: "correct versions map for production", 779 upstreamVersionType: upstreamVersion, 780 expectError: false, 781 prefixes: []prefix{prowPrefixStruct, boskosPrefixStruct}, 782 expectedResult: map[string]string{prowPrefix: prowProdFakeVersion, boskosPrefix: boskosProdFakeVersion}, 783 }, 784 { 785 description: "correct versions map for staging", 786 upstreamVersionType: upstreamStagingVersion, 787 expectError: false, 788 prefixes: []prefix{prowPrefixStruct, boskosPrefixStruct}, 789 expectedResult: map[string]string{prowPrefix: prowStagingFakeVersion, boskosPrefix: boskosStagingFakeVersion}, 790 }, 791 } 792 for _, tc := range cases { 793 t.Run(tc.description, func(t *testing.T) { 794 option := &options{ 795 UpstreamURLBase: fakeUpstreamURLBase, 796 Prefixes: tc.prefixes, 797 } 798 versions, err := upstreamConfigVersions(tc.upstreamVersionType, option, fakeImageVersionParser) 799 if tc.expectError && err == nil { 800 t.Errorf("Expected to get an error but the result is nil") 801 return 802 } 803 if !tc.expectError && err != nil { 804 t.Errorf("Expected to not get an error but got one: %v", err) 805 return 806 } 807 if err == nil && versions == nil { 808 t.Error("Expected to get an versions but did not") 809 return 810 } 811 }) 812 } 813 814 } 815 816 func TestGetVersionsAndCheckConsistency(t *testing.T) { 817 prowPrefix := prefix{Prefix: "gcr.io/k8s-prow/", ConsistentImages: true} 818 boskosPrefix := prefix{Prefix: "gcr.io/k8s-boskos/", ConsistentImages: true} 819 inconsistentPrefix := prefix{Prefix: "inconsistent/", ConsistentImages: false} 820 consistentPrefixWithExceptions := prefix{ 821 Prefix: "consistent/", 822 ConsistentImages: true, 823 ConsistentImageExceptions: []string{"consistent/foo", "consistent/bar"}, 824 } 825 testCases := []struct { 826 name string 827 images map[string]string 828 prefixes []prefix 829 expectedVersions map[string][]string 830 err bool 831 }{ 832 { 833 name: "two prefixes being bumped with consistent tags", 834 prefixes: []prefix{prowPrefix, boskosPrefix}, 835 images: map[string]string{"gcr.io/k8s-prow/test:tag1": "newtag1", "gcr.io/k8s-prow/test2:tag1": "newtag1"}, 836 err: false, 837 expectedVersions: map[string][]string{"newtag1": {"gcr.io/k8s-prow/test:tag1", "gcr.io/k8s-prow/test2:tag1"}}, 838 }, 839 { 840 name: "two prefixes being bumped with inconsistent tags", 841 prefixes: []prefix{prowPrefix, boskosPrefix}, 842 images: map[string]string{"gcr.io/k8s-prow/test:tag1": "newtag1", "gcr.io/k8s-prow/test2:tag1": "tag1"}, 843 err: true, 844 }, 845 { 846 name: "two prefixes being bumped with no bumps", 847 prefixes: []prefix{prowPrefix, boskosPrefix}, 848 images: map[string]string{}, 849 err: false, 850 expectedVersions: map[string][]string{}, 851 }, 852 { 853 name: "Prefix being bumped with inconsistent tags", 854 prefixes: []prefix{inconsistentPrefix}, 855 images: map[string]string{"inconsistent/test:tag1": "newtag1", "inconsistent/test2:tag2": "newtag2"}, 856 err: false, 857 expectedVersions: map[string][]string{"newtag1": {"inconsistent/test:tag1"}, "newtag2": {"inconsistent/test2:tag2"}}, 858 }, 859 { 860 name: "One of the image types wasn't bumped. Do not include in versions.", 861 prefixes: []prefix{prowPrefix, boskosPrefix}, 862 images: map[string]string{"gcr.io/k8s-prow/test:tag1": "newtag1", "gcr.io/k8s-prow/test2:tag1": "newtag1", "gcr.io/k8s-boskos/nobumped:tag1": "tag1"}, 863 err: false, 864 expectedVersions: map[string][]string{"newtag1": {"gcr.io/k8s-prow/test:tag1", "gcr.io/k8s-prow/test2:tag1"}}, 865 }, 866 { 867 name: "Two of the images in one type wasn't bumped. Do not include in versions. Do not error", 868 prefixes: []prefix{prowPrefix, boskosPrefix}, 869 images: map[string]string{"gcr.io/k8s-prow/test:tag1": "newtag1", "gcr.io/k8s-prow/test2:tag1": "newtag1", "gcr.io/k8s-boskos/nobumped:tag1": "tag1", "gcr.io/k8s-boskos/nobumped2:tag1": "tag1"}, 870 err: false, 871 expectedVersions: map[string][]string{"newtag1": {"gcr.io/k8s-prow/test:tag1", "gcr.io/k8s-prow/test2:tag1"}}, 872 }, 873 { 874 name: "prefix was not consistent before bump and now is", 875 prefixes: []prefix{prowPrefix}, 876 images: map[string]string{"gcr.io/k8s-prow/test:tag1": "newtag1", "gcr.io/k8s-prow/test2:tag2": "newtag1"}, 877 err: false, 878 expectedVersions: map[string][]string{"newtag1": {"gcr.io/k8s-prow/test:tag1", "gcr.io/k8s-prow/test2:tag2"}}, 879 }, 880 { 881 name: "prefix was not consistent before bump one was bumped ahead manually", 882 prefixes: []prefix{prowPrefix}, 883 images: map[string]string{"gcr.io/k8s-prow/test:tag1": "newtag1", "gcr.io/k8s-prow/test2:newtag1": "newtag1"}, 884 err: false, 885 expectedVersions: map[string][]string{"newtag1": {"gcr.io/k8s-prow/test:tag1"}}, 886 }, 887 { 888 name: "prefix is not consistent but all images are excepted", 889 prefixes: []prefix{consistentPrefixWithExceptions}, 890 images: map[string]string{"consistent/foo:tag1": "newtag1", "consistent/bar:tag2": "newtag2"}, 891 err: false, 892 expectedVersions: map[string][]string{"newtag1": {"consistent/foo:tag1"}, "newtag2": {"consistent/bar:tag2"}}, 893 }, 894 { 895 name: "prefix is not consistent but inconsistent images are excepted", 896 prefixes: []prefix{consistentPrefixWithExceptions}, 897 images: map[string]string{"consistent/banana:tag1": "newtag3", "consistent/apple:tag2": "newtag3", "consistent/foo:tag1": "newtag1", "consistent/bar:tag2": "newtag2", "consistent/orange:tag0": "newtag3"}, 898 err: false, 899 expectedVersions: map[string][]string{"newtag1": {"consistent/foo:tag1"}, "newtag2": {"consistent/bar:tag2"}, "newtag3": {"consistent/banana:tag1", "consistent/apple:tag2", "consistent/orange:tag0"}}, 900 }, 901 { 902 name: "prefix is not consistent and not all inconsistent images are excepted", 903 prefixes: []prefix{consistentPrefixWithExceptions}, 904 images: map[string]string{"consistent/banana:tag1": "newtag4", "consistent/apple:tag2": "newtag4", "consistent/foo:tag1": "newtag1", "consistent/bar:tag2": "newtag2", "consistent/orange:tag0": "newtag3"}, 905 err: true, 906 }, 907 } 908 for _, tc := range testCases { 909 t.Run(tc.name, func(t *testing.T) { 910 versions, err := getVersionsAndCheckConsistency(tc.prefixes, tc.images) 911 if tc.err && err == nil { 912 t.Errorf("expected error but did not get one") 913 } 914 if !tc.err && err != nil { 915 t.Errorf("expected no error, but got one: %v", err) 916 } 917 if diff := cmp.Diff(tc.expectedVersions, versions, cmpopts.SortSlices(func(x, y string) bool { return strings.Compare(x, y) > 0 })); diff != "" { 918 t.Errorf("versions returned unexpected value (-want +got):\n%s", diff) 919 } 920 }) 921 } 922 } 923 924 func TestMakeCommitSummary(t *testing.T) { 925 prowPrefix := prefix{Name: "Prow", Prefix: "gcr.io/k8s-prow/", ConsistentImages: true} 926 boskosPrefix := prefix{Name: "Boskos", Prefix: "gcr.io/k8s-boskos/", ConsistentImages: true} 927 inconsistentPrefix := prefix{Name: "Inconsistent", Prefix: "gcr.io/inconsistent/", ConsistentImages: false} 928 testCases := []struct { 929 name string 930 prefixes []prefix 931 versions map[string][]string 932 consistency bool 933 expectedResult string 934 }{ 935 { 936 name: "Two prefixes, but only one bumped", 937 prefixes: []prefix{prowPrefix, boskosPrefix}, 938 versions: map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1"}}, 939 expectedResult: "Update Prow to tag1", 940 }, 941 { 942 name: "Two prefixes, both bumped", 943 prefixes: []prefix{prowPrefix, boskosPrefix}, 944 versions: map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1"}, "tag2": {"gcr.io/k8s-boskos/test:tag2"}}, 945 expectedResult: "Update Prow to tag1, Boskos to tag2", 946 }, 947 { 948 name: "Empty versions", 949 prefixes: []prefix{prowPrefix, boskosPrefix}, 950 versions: map[string][]string{}, 951 expectedResult: "Update Prow, Boskos images as necessary", 952 }, 953 { 954 name: "One bumped inconsistently", 955 prefixes: []prefix{prowPrefix, boskosPrefix, inconsistentPrefix}, 956 versions: map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1"}, "tag2": {"gcr.io/k8s-boskos/test:tag2"}, "tag3": {"gcr.io/inconsistent/test:tag3"}}, 957 expectedResult: "Update Prow to tag1, Boskos to tag2 and Inconsistent as needed", 958 }, 959 { 960 name: "inconsistent tag was not bumped, do not include in result", 961 prefixes: []prefix{prowPrefix, boskosPrefix, inconsistentPrefix}, 962 versions: map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1"}, "tag2": {"gcr.io/k8s-boskos/test:tag2"}}, 963 expectedResult: "Update Prow to tag1, Boskos to tag2", 964 }, 965 { 966 name: "Two images bumped to same version", 967 prefixes: []prefix{prowPrefix, boskosPrefix, inconsistentPrefix}, 968 versions: map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1", "gcr.io/inconsistent/test:tag3"}, "tag2": {"gcr.io/k8s-boskos/test:tag2"}}, 969 expectedResult: "Update Prow to tag1, Boskos to tag2 and Inconsistent as needed", 970 }, 971 { 972 name: "only bump inconsistent", 973 prefixes: []prefix{inconsistentPrefix}, 974 versions: map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1", "gcr.io/inconsistent/test:tag3"}, "tag2": {"gcr.io/k8s-boskos/test:tag2"}}, 975 expectedResult: "Update Inconsistent as needed", 976 }, 977 } 978 for _, tc := range testCases { 979 t.Run(tc.name, func(t *testing.T) { 980 res := makeCommitSummary(tc.prefixes, tc.versions) 981 if res != tc.expectedResult { 982 t.Errorf("expected commit string to be %q, but was %q", tc.expectedResult, res) 983 } 984 }) 985 } 986 } 987 988 func TestGenerateSummary(t *testing.T) { 989 beforeCommit := "2b1234567" 990 afterCommit := "3a1234567" 991 beforeDate := "20210128" 992 afterDate := "20210129" 993 beforeCommit2 := "1c1234567" 994 afterCommit2 := "4f1234567" 995 beforeDate2 := "20210125" 996 afterDate2 := "20210126" 997 unsummarizedOutHeader := `Multiple distinct %s changes: 998 999 Commits | Dates | Images 1000 --- | --- | ---` 1001 1002 unsummarizedOutLine := "github.com/test/repo/compare/%s...%s | %s → %s | %s" 1003 1004 sampleImages := map[string]string{ 1005 fmt.Sprintf("gcr.io/bumped/bumpName:v%s-%s", beforeDate, beforeCommit): fmt.Sprintf("v%s-%s", afterDate, afterCommit), 1006 fmt.Sprintf("gcr.io/variant/name:v%s-%s-first", beforeDate, beforeCommit): fmt.Sprintf("v%s-%s", afterDate, afterCommit), 1007 fmt.Sprintf("gcr.io/variant/name:v%s-%s-second", beforeDate, beforeCommit): fmt.Sprintf("v%s-%s", afterDate, afterCommit), 1008 fmt.Sprintf("gcr.io/inconsistent/first:v%s-%s", beforeDate2, beforeCommit2): fmt.Sprintf("v%s-%s", afterDate2, afterCommit2), 1009 fmt.Sprintf("gcr.io/inconsistent/second:v%s-%s", beforeDate, beforeCommit): fmt.Sprintf("v%s-%s", afterDate, afterCommit), 1010 } 1011 testCases := []struct { 1012 testName string 1013 name string 1014 repo string 1015 prefix string 1016 summarize bool 1017 images map[string]string 1018 expected string 1019 }{ 1020 { 1021 testName: "Image not bumped unsummarized", 1022 name: "Test", 1023 repo: "repo", 1024 prefix: "gcr.io/none", 1025 summarize: true, 1026 images: sampleImages, 1027 expected: "No gcr.io/none changes.", 1028 }, 1029 { 1030 testName: "Image not bumped summarized", 1031 name: "Test", 1032 repo: "repo", 1033 prefix: "gcr.io/none", 1034 summarize: true, 1035 images: sampleImages, 1036 expected: "No gcr.io/none changes.", 1037 }, 1038 { 1039 testName: "Image bumped: summarized", 1040 name: "Test", 1041 repo: "github.com/test/repo", 1042 prefix: "gcr.io/bumped", 1043 summarize: true, 1044 images: sampleImages, 1045 expected: fmt.Sprintf("gcr.io/bumped changes: github.com/test/repo/compare/%s...%s (%s → %s)", beforeCommit, afterCommit, formatTagDate(beforeDate), formatTagDate(afterDate)), 1046 }, 1047 { 1048 testName: "Image bumped: not summarized", 1049 name: "Test", 1050 repo: "github.com/test/repo", 1051 prefix: "gcr.io/bumped", 1052 summarize: false, 1053 images: sampleImages, 1054 expected: fmt.Sprintf("%s\n%s\n", fmt.Sprintf(unsummarizedOutHeader, "gcr.io/bumped"), fmt.Sprintf(unsummarizedOutLine, beforeCommit, afterCommit, formatTagDate(beforeDate), formatTagDate(afterDate), "bumpName")), 1055 }, 1056 { 1057 testName: "Image bumped: not summarized", 1058 name: "Test", 1059 repo: "github.com/test/repo", 1060 prefix: "gcr.io/variant", 1061 summarize: false, 1062 images: sampleImages, 1063 expected: fmt.Sprintf("%s\n%s\n", fmt.Sprintf(unsummarizedOutHeader, "gcr.io/variant"), fmt.Sprintf(unsummarizedOutLine, beforeCommit, afterCommit, formatTagDate(beforeDate), formatTagDate(afterDate), "name(first), name(second)")), 1064 }, 1065 { 1066 testName: "Image bumped, inconsistent: not summarized", 1067 name: "Test", 1068 repo: "github.com/test/repo", 1069 prefix: "gcr.io/inconsistent", 1070 summarize: false, 1071 images: sampleImages, 1072 expected: fmt.Sprintf("%s\n%s\n%s\n", fmt.Sprintf(unsummarizedOutHeader, "gcr.io/inconsistent"), fmt.Sprintf(unsummarizedOutLine, beforeCommit2, afterCommit2, formatTagDate(beforeDate2), formatTagDate(afterDate2), "first"), fmt.Sprintf(unsummarizedOutLine, beforeCommit, afterCommit, formatTagDate(beforeDate), formatTagDate(afterDate), "second")), 1073 }, 1074 } 1075 for _, tc := range testCases { 1076 tc := tc 1077 t.Run(tc.testName, func(t *testing.T) { 1078 want, got := tc.expected, generateSummary(tc.name, tc.repo, tc.prefix, tc.summarize, tc.images) 1079 if diff := cmp.Diff(want, got); diff != "" { 1080 t.Errorf("generateSummary returned unexpected value (-want +got):\n%s", diff) 1081 } 1082 }) 1083 1084 } 1085 } 1086 1087 func TestPRTitleBody(t *testing.T) { 1088 prowPrefix := prefix{Name: "Prow", Prefix: "gcr.io/k8s-prow/", ConsistentImages: true} 1089 beforeCommit := "2b1234567" 1090 afterCommit := "3a1234567" 1091 beforeDate := "20210128" 1092 afterDate := "20210129" 1093 prowImages := map[string]string{ 1094 fmt.Sprintf("gcr.io/k8s-prow/bumpName:v%s-%s", beforeDate, beforeCommit): fmt.Sprintf("v%s-%s", afterDate, afterCommit), 1095 } 1096 testCases := []struct { 1097 name string 1098 options options 1099 versions map[string][]string 1100 images map[string]string 1101 expectedSummary string 1102 expectedBody string 1103 }{ 1104 { 1105 name: "prow bumped", 1106 options: options{ 1107 Prefixes: []prefix{prowPrefix}, 1108 }, 1109 versions: map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1"}}, 1110 images: prowImages, 1111 expectedSummary: "Update Prow to tag1", 1112 expectedBody: "Multiple distinct gcr.io/k8s-prow/ changes:\n\nCommits | Dates | Images\n--- | --- | ---\n/compare/2b1234567...3a1234567 | 2021‑01‑28 → 2021‑01‑29 | bumpName\n\n\n\nNobody is currently oncall, so falling back to Blunderbuss.\n", 1113 }, 1114 { 1115 name: "contains additional PR body", 1116 options: options{ 1117 Prefixes: []prefix{prowPrefix}, 1118 AdditionalPRBody: "/some-other-command", 1119 }, 1120 versions: map[string][]string{"tag1": {"gcr.io/k8s-prow/test:tag1"}}, 1121 images: prowImages, 1122 expectedSummary: "Update Prow to tag1", 1123 expectedBody: "Multiple distinct gcr.io/k8s-prow/ changes:\n\nCommits | Dates | Images\n--- | --- | ---\n/compare/2b1234567...3a1234567 | 2021‑01‑28 → 2021‑01‑29 | bumpName\n\n\n\nNobody is currently oncall, so falling back to Blunderbuss.\n/some-other-command\n", 1124 }, 1125 } 1126 for _, tc := range testCases { 1127 t.Run(tc.name, func(t *testing.T) { 1128 c := client{ 1129 o: &tc.options, 1130 images: tc.images, 1131 versions: tc.versions, 1132 } 1133 summary, body := c.PRTitleBody() 1134 if diff := cmp.Diff(tc.expectedSummary, summary); diff != "" { 1135 t.Fatalf("summary doesn't match expected, diff: %s", diff) 1136 } 1137 if diff := cmp.Diff(tc.expectedBody, body); diff != "" { 1138 t.Fatalf("body doesn't match expected, diff: %s", diff) 1139 } 1140 1141 }) 1142 } 1143 }