github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/repository/repository_test.go (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 17 package repository 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/config" 25 "io" 26 "io/fs" 27 "net/http" 28 "net/http/httptest" 29 "os" 30 "os/exec" 31 "path" 32 "path/filepath" 33 "strings" 34 "testing" 35 "time" 36 37 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository/testutil" 38 "go.uber.org/zap" 39 "golang.org/x/sync/errgroup" 40 41 "github.com/freiheit-com/kuberpult/pkg/setup" 42 43 api "github.com/freiheit-com/kuberpult/pkg/api/v1" 44 "github.com/freiheit-com/kuberpult/services/cd-service/pkg/repository/testssh" 45 46 "github.com/cenkalti/backoff/v4" 47 "github.com/go-git/go-billy/v5/util" 48 "github.com/google/go-cmp/cmp" 49 "github.com/google/go-cmp/cmp/cmpopts" 50 git "github.com/libgit2/git2go/v34" 51 "google.golang.org/grpc/codes" 52 "google.golang.org/grpc/status" 53 ) 54 55 // Used to compare two error message strings, needed because errors.Is(fmt.Errorf(text),fmt.Errorf(text)) == false 56 type errMatcher struct { 57 msg string 58 } 59 60 func (e errMatcher) Error() string { 61 return e.msg 62 } 63 64 func (e errMatcher) Is(err error) bool { 65 return e.Error() == err.Error() 66 } 67 68 func TestNew(t *testing.T) { 69 tcs := []struct { 70 Name string 71 Branch string 72 Setup func(t *testing.T, remoteDir, localDir string) 73 Test func(t *testing.T, repo Repository, remoteDir string) 74 }{ 75 { 76 Name: "new in empty directory works", 77 Setup: func(_ *testing.T, _, _ string) {}, 78 }, 79 { 80 Name: "new in initialized repository works", 81 Setup: func(t *testing.T, remoteDir, localDir string) { 82 // run the initialization code once 83 _, err := New( 84 testutil.MakeTestContext(), 85 RepositoryConfig{ 86 URL: "file://" + remoteDir, 87 Path: localDir, 88 }, 89 ) 90 if err != nil { 91 t.Fatal(err) 92 } 93 }, 94 Test: func(t *testing.T, repo Repository, remoteDir string) { 95 state := repo.State() 96 entries, err := state.Filesystem.ReadDir("") 97 if err != nil { 98 t.Fatal(err) 99 } 100 if len(entries) > 0 { 101 t.Errorf("repository is not empty but contains %d entries", len(entries)) 102 } 103 }, 104 }, 105 { 106 Name: "new in initialized repository with data works", 107 Setup: func(t *testing.T, remoteDir, localDir string) { 108 // run the initialization code once 109 repo, err := New( 110 testutil.MakeTestContext(), 111 RepositoryConfig{ 112 URL: remoteDir, 113 Path: localDir, 114 }, 115 ) 116 if err != nil { 117 t.Fatal(err) 118 } 119 err = repo.Apply(testutil.MakeTestContext(), &CreateApplicationVersion{ 120 Application: "foo", 121 Manifests: map[string]string{ 122 "development": "foo", 123 }, 124 }) 125 if err != nil { 126 t.Fatal(err) 127 } 128 }, 129 Test: func(t *testing.T, repo Repository, remoteDir string) { 130 state := repo.State() 131 entries, err := state.Filesystem.ReadDir("applications/foo/releases") 132 if err != nil { 133 t.Fatal(err) 134 } 135 if len(entries) != 1 { 136 t.Errorf("applications/foo/releases doesn't contain 1 but %d entries", len(entries)) 137 } 138 }, 139 }, 140 { 141 Name: "new with empty repository but non-empty remote works", 142 Setup: func(t *testing.T, remoteDir, localDir string) { 143 // run the initialization code once 144 repo, err := New( 145 testutil.MakeTestContext(), 146 RepositoryConfig{ 147 URL: remoteDir, 148 Path: t.TempDir(), 149 }, 150 ) 151 if err != nil { 152 t.Fatal(err) 153 } 154 err = repo.Apply(testutil.MakeTestContext(), &CreateApplicationVersion{ 155 Application: "foo", 156 Manifests: map[string]string{ 157 "development": "foo", 158 }, 159 }) 160 if err != nil { 161 t.Fatal(err) 162 } 163 }, 164 Test: func(t *testing.T, repo Repository, remoteDir string) { 165 state := repo.State() 166 entries, err := state.Filesystem.ReadDir("applications/foo/releases") 167 if err != nil { 168 t.Fatal(err) 169 } 170 if len(entries) != 1 { 171 t.Errorf("applications/foo/releases doesn't contain 1 but %d entries", len(entries)) 172 } 173 }, 174 }, 175 { 176 Name: "new with changed branch works", 177 Branch: "not-master", 178 Setup: func(t *testing.T, remoteDir, localDir string) {}, 179 Test: func(t *testing.T, repo Repository, remoteDir string) { 180 err := repo.Apply(testutil.MakeTestContext(), &CreateApplicationVersion{ 181 Application: "foo", 182 Manifests: map[string]string{ 183 "development": "foo", 184 }, 185 }) 186 if err != nil { 187 t.Fatal(err) 188 } 189 cmd := exec.Command("git", "--git-dir="+remoteDir, "rev-parse", "not-master") 190 out, err := cmd.Output() 191 if err != nil { 192 if exitErr, ok := err.(*exec.ExitError); ok { 193 t.Logf("stderr: %s\n", exitErr.Stderr) 194 } 195 t.Fatal(err) 196 } 197 state := repo.State() 198 localRev := state.Commit.Id().String() 199 if diff := cmp.Diff(localRev, strings.TrimSpace(string(out))); diff != "" { 200 t.Errorf("mismatched revision (-want, +got):\n%s", diff) 201 } 202 }, 203 }, 204 { 205 Name: "old with changed branch works", 206 Branch: "master", 207 Setup: func(t *testing.T, remoteDir, localDir string) {}, 208 Test: func(t *testing.T, repo Repository, remoteDir string) { 209 workdir := t.TempDir() 210 cmd := exec.Command("git", "clone", remoteDir, workdir) // Clone git dir 211 out, err := cmd.Output() 212 if err != nil { 213 if exitErr, ok := err.(*exec.ExitError); ok { 214 t.Logf("stderr: %s\n", exitErr.Stderr) 215 } 216 t.Fatal(err) 217 } 218 219 if err := os.WriteFile(filepath.Join(workdir, "hello.txt"), []byte("hello"), 0666); err != nil { 220 t.Fatal(err) 221 } 222 cmd = exec.Command("git", "add", "hello.txt") // Add a new file to git 223 cmd.Dir = workdir 224 out, err = cmd.Output() 225 if err != nil { 226 if exitErr, ok := err.(*exec.ExitError); ok { 227 t.Logf("stderr: %s\n", exitErr.Stderr) 228 } 229 t.Fatal(err) 230 } 231 cmd = exec.Command("git", "commit", "-m", "new-file") // commit the new file 232 cmd.Dir = workdir 233 cmd.Env = []string{ 234 "GIT_AUTHOR_NAME=kuberpult", 235 "GIT_COMMITTER_NAME=kuberpult", 236 "EMAIL=test@kuberpult.com", 237 } 238 out, err = cmd.Output() 239 if err != nil { 240 if exitErr, ok := err.(*exec.ExitError); ok { 241 t.Logf("stderr: %s\n", exitErr.Stderr) 242 } 243 t.Fatal(err) 244 } 245 cmd = exec.Command("git", "push", "origin", "HEAD") // push the new commit 246 cmd.Dir = workdir 247 out, err = cmd.Output() 248 if err != nil { 249 if exitErr, ok := err.(*exec.ExitError); ok { 250 t.Logf("stderr: %s\n", exitErr.Stderr) 251 } 252 t.Fatal(err) 253 } 254 err = repo.Apply(testutil.MakeTestContext(), &CreateApplicationVersion{ 255 Application: "foo", 256 Manifests: map[string]string{ 257 "development": "foo", 258 }, 259 }) 260 if err != nil { 261 t.Fatal(err) 262 } 263 cmd = exec.Command("git", "--git-dir="+remoteDir, "rev-parse", "master") 264 out, err = cmd.Output() 265 if err != nil { 266 if exitErr, ok := err.(*exec.ExitError); ok { 267 t.Logf("stderr: %s\n", exitErr.Stderr) 268 } 269 t.Fatal(err) 270 } 271 state := repo.State() 272 localRev := state.Commit.Id().String() 273 if diff := cmp.Diff(localRev, strings.TrimSpace(string(out))); diff != "" { 274 t.Errorf("mismatched revision (-want, +got):\n%s", diff) 275 } 276 277 content, err := util.ReadFile(state.Filesystem, "hello.txt") 278 if err != nil { 279 t.Fatal(err) 280 } 281 if diff := cmp.Diff("hello", string(content)); diff != "" { 282 t.Errorf("mismatched file content (-want, +got):\n%s", diff) 283 } 284 }, 285 }, 286 } 287 for _, tc := range tcs { 288 tc := tc 289 t.Run(tc.Name, func(t *testing.T) { 290 t.Parallel() 291 // create a remote 292 dir := t.TempDir() 293 remoteDir := path.Join(dir, "remote") 294 localDir := path.Join(dir, "local") 295 cmd := exec.Command("git", "init", "--bare", remoteDir) 296 cmd.Start() 297 cmd.Wait() 298 tc.Setup(t, remoteDir, localDir) 299 repo, err := New( 300 testutil.MakeTestContext(), 301 RepositoryConfig{ 302 URL: "file://" + remoteDir, 303 Path: localDir, 304 Branch: tc.Branch, 305 }, 306 ) 307 if err != nil { 308 t.Fatalf("new: expected no error, got '%e'", err) 309 } 310 if tc.Test != nil { 311 tc.Test(t, repo, remoteDir) 312 } 313 }) 314 } 315 } 316 317 func TestGetTagsNoTags(t *testing.T) { 318 name := "No tags to be returned at all" 319 320 t.Run(name, func(t *testing.T) { 321 t.Parallel() 322 dir := t.TempDir() 323 remoteDir := path.Join(dir, "remote") 324 localDir := path.Join(dir, "local") 325 repoConfig := RepositoryConfig{ 326 StorageBackend: 0, 327 URL: "file://" + remoteDir, 328 Path: localDir, 329 Branch: "master", 330 } 331 cmd := exec.Command("git", "init", "--bare", remoteDir) 332 cmd.Start() 333 cmd.Wait() 334 _, err := New( 335 testutil.MakeTestContext(), 336 repoConfig, 337 ) 338 339 if err != nil { 340 t.Fatal(err) 341 } 342 tags, err := GetTags( 343 repoConfig, 344 localDir, 345 testutil.MakeTestContext(), 346 ) 347 if err != nil { 348 t.Fatalf("new: expected no error, got '%e'", err) 349 } 350 if len(tags) != 0 { 351 t.Fatalf("expected %v tags but got %v", 0, len(tags)) 352 } 353 }) 354 355 } 356 357 func TestGetTags(t *testing.T) { 358 tcs := []struct { 359 Name string 360 expectedTags []api.TagData 361 tagsToAdd []string 362 }{ 363 { 364 Name: "Tags added to be returned", 365 tagsToAdd: []string{"v1.0.0"}, 366 expectedTags: []api.TagData{{Tag: "refs/tags/v1.0.0", CommitId: ""}}, 367 }, 368 { 369 Name: "Tags added in opposite order and are sorted", 370 tagsToAdd: []string{"v1.0.1", "v0.0.1"}, 371 expectedTags: []api.TagData{{Tag: "refs/tags/v0.0.1", CommitId: ""}, {Tag: "refs/tags/v1.0.1", CommitId: ""}}, 372 }, 373 } 374 for _, tc := range tcs { 375 tc := tc 376 t.Run(tc.Name, func(t *testing.T) { 377 t.Parallel() 378 dir := t.TempDir() 379 remoteDir := path.Join(dir, "remote") 380 localDir := path.Join(dir, "local") 381 repoConfig := RepositoryConfig{ 382 StorageBackend: 0, 383 URL: "file://" + remoteDir, 384 Path: localDir, 385 Branch: "master", 386 } 387 cmd := exec.Command("git", "init", "--bare", remoteDir) 388 cmd.Start() 389 cmd.Wait() 390 _, err := New( 391 testutil.MakeTestContext(), 392 repoConfig, 393 ) 394 if err != nil { 395 t.Fatal(err) 396 } 397 repo, err := git.OpenRepository(localDir) 398 if err != nil { 399 t.Fatal(err) 400 } 401 idx, err := repo.Index() 402 if err != nil { 403 t.Fatal(err) 404 } 405 treeId, err := idx.WriteTree() 406 if err != nil { 407 t.Fatal(err) 408 } 409 410 tree, err := repo.LookupTree(treeId) 411 if err != nil { 412 t.Fatal(err) 413 } 414 oid, err := repo.CreateCommit("HEAD", &git.Signature{Name: "SRE", Email: "testing@gmail"}, &git.Signature{Name: "SRE", Email: "testing@gmail"}, "testing", tree) 415 if err != nil { 416 t.Fatal(err) 417 } 418 commit, err := repo.LookupCommit(oid) 419 if err != nil { 420 t.Fatal(err) 421 } 422 var expectedCommits []api.TagData 423 for addTag := range tc.tagsToAdd { 424 _, err := repo.Tags.Create(tc.tagsToAdd[addTag], commit, &git.Signature{Name: "SRE", Email: "testing@gmail"}, "testing") 425 expectedCommits = append(expectedCommits, api.TagData{Tag: tc.tagsToAdd[addTag], CommitId: commit.Id().String()}) 426 if err != nil { 427 t.Fatal(err) 428 } 429 } 430 tags, err := GetTags( 431 repoConfig, 432 localDir, 433 testutil.MakeTestContext(), 434 ) 435 if err != nil { 436 t.Fatalf("new: expected no error, got '%e'", err) 437 } 438 if len(tags) != len(tc.expectedTags) { 439 t.Fatalf("expected %v tags but got %v", len(tc.expectedTags), len(tags)) 440 } 441 442 iter := 0 443 for _, tagData := range tags { 444 for commit := range expectedCommits { 445 if tagData.Tag != expectedCommits[commit].Tag { 446 if tagData.CommitId == expectedCommits[commit].CommitId { 447 t.Fatalf("expected [%v] for TagList commit but got [%v]", expectedCommits[commit].CommitId, tagData.CommitId) 448 } 449 } 450 } 451 if tagData.Tag != tc.expectedTags[iter].Tag { 452 t.Fatalf("expected [%v] for TagList tag but got [%v] with tagList %v", tc.expectedTags[iter].Tag, tagData.Tag, tags) 453 } 454 iter += 1 455 } 456 }) 457 } 458 } 459 460 func TestBootstrapModeNew(t *testing.T) { 461 tcs := []struct { 462 Name string 463 PreInitialize bool 464 }{ 465 { 466 Name: "New in empty repo", 467 PreInitialize: false, 468 }, 469 { 470 Name: "New in existing repo", 471 PreInitialize: true, 472 }, 473 } 474 for _, tc := range tcs { 475 tc := tc 476 t.Run(tc.Name, func(t *testing.T) { 477 t.Parallel() 478 // create a remote 479 dir := t.TempDir() 480 remoteDir := path.Join(dir, "remote") 481 localDir := path.Join(dir, "local") 482 483 cmd := exec.Command("git", "init", "--bare", remoteDir) 484 cmd.Start() 485 cmd.Wait() 486 487 if tc.PreInitialize { 488 _, err := New( 489 testutil.MakeTestContext(), 490 RepositoryConfig{ 491 URL: "file://" + remoteDir, 492 Path: localDir, 493 }, 494 ) 495 if err != nil { 496 t.Fatal(err) 497 } 498 } 499 500 environmentConfigsPath := filepath.Join(remoteDir, "..", "environment_configs.json") 501 502 repo, err := New( 503 testutil.MakeTestContext(), 504 RepositoryConfig{ 505 URL: "file://" + remoteDir, 506 Path: localDir, 507 BootstrapMode: true, 508 EnvironmentConfigsPath: environmentConfigsPath, 509 }, 510 ) 511 if err != nil { 512 t.Fatalf("New: Expected no error, error %e was thrown", err) 513 } 514 515 state := repo.State() 516 if !state.BootstrapMode { 517 t.Fatalf("Bootstrap mode not preserved") 518 } 519 }) 520 } 521 } 522 523 func TestBootstrapModeReadConfig(t *testing.T) { 524 tcs := []struct { 525 Name string 526 }{ 527 { 528 Name: "Config read correctly", 529 }, 530 } 531 for _, tc := range tcs { 532 tc := tc 533 t.Run(tc.Name, func(t *testing.T) { 534 t.Parallel() 535 // create a remote 536 dir := t.TempDir() 537 remoteDir := path.Join(dir, "remote") 538 localDir := path.Join(dir, "local") 539 540 cmd := exec.Command("git", "init", "--bare", remoteDir) 541 cmd.Start() 542 cmd.Wait() 543 544 environmentConfigsPath := filepath.Join(remoteDir, "..", "environment_configs.json") 545 if err := os.WriteFile(environmentConfigsPath, []byte(`{"uniqueEnv": {"environmentGroup": "testgroup321", "upstream": {"latest": true}}}`), fs.FileMode(0644)); err != nil { 546 t.Fatal(err) 547 } 548 549 repo, err := New( 550 testutil.MakeTestContext(), 551 RepositoryConfig{ 552 URL: "file://" + remoteDir, 553 Path: localDir, 554 BootstrapMode: true, 555 EnvironmentConfigsPath: environmentConfigsPath, 556 }, 557 ) 558 if err != nil { 559 t.Fatalf("New: Expected no error, error %e was thrown", err) 560 } 561 562 state := repo.State() 563 if !state.BootstrapMode { 564 t.Fatalf("Bootstrap mode not preserved") 565 } 566 configs, err := state.GetEnvironmentConfigs() 567 if err != nil { 568 t.Fatal(err) 569 } 570 if len(configs) != 1 { 571 t.Fatal("Configuration not read properly") 572 } 573 if configs["uniqueEnv"].Upstream.Latest != true { 574 t.Fatal("Configuration not read properly") 575 } 576 if configs["uniqueEnv"].EnvironmentGroup == nil { 577 t.Fatalf("EnvironmentGroup not read, found nil") 578 } 579 if *configs["uniqueEnv"].EnvironmentGroup != "testgroup321" { 580 t.Fatalf("EnvironmentGroup not read, found '%s' instead", *configs["uniqueEnv"].EnvironmentGroup) 581 } 582 }) 583 } 584 } 585 586 func TestBootstrapError(t *testing.T) { 587 tcs := []struct { 588 Name string 589 ConfigContent string 590 }{ 591 { 592 Name: "Invalid json in bootstrap configuration", 593 ConfigContent: `{"development": "upstream": {"latest": true}}}`, 594 }, 595 } 596 597 for _, tc := range tcs { 598 tc := tc 599 t.Run(tc.Name, func(t *testing.T) { 600 t.Parallel() 601 // create a remote 602 dir := t.TempDir() 603 remoteDir := path.Join(dir, "remote") 604 localDir := path.Join(dir, "local") 605 cmd := exec.Command("git", "init", "--bare", remoteDir) 606 cmd.Start() 607 cmd.Wait() 608 609 environmentConfigsPath := filepath.Join(remoteDir, "..", "environment_configs.json") 610 if err := os.WriteFile(environmentConfigsPath, []byte(tc.ConfigContent), fs.FileMode(0644)); err != nil { 611 t.Fatal(err) 612 } 613 614 _, err := New( 615 testutil.MakeTestContext(), 616 RepositoryConfig{ 617 URL: "file://" + remoteDir, 618 Path: localDir, 619 BootstrapMode: true, 620 EnvironmentConfigsPath: environmentConfigsPath, 621 }, 622 ) 623 if err == nil { 624 t.Fatalf("New: Expected error but no error was thrown") 625 } 626 }) 627 } 628 } 629 630 func TestConfigReload(t *testing.T) { 631 configFiles := []struct { 632 ConfigContent string 633 ErrorExpected bool 634 }{ 635 { 636 ConfigContent: "{\"upstream\": {\"latest\": true }}", 637 ErrorExpected: false, 638 }, 639 { 640 ConfigContent: "{\"upstream\": \"latest\": true }}", 641 ErrorExpected: true, 642 }, 643 { 644 ConfigContent: "{\"upstream\": {\"latest\": true }}", 645 ErrorExpected: false, 646 }, 647 } 648 t.Run("Config file reload on change", func(t *testing.T) { 649 t.Parallel() 650 // create a remote 651 workdir := t.TempDir() 652 remoteDir := path.Join(workdir, "remote") 653 cmd := exec.Command("git", "init", "--bare", remoteDir) 654 cmd.Start() 655 cmd.Wait() 656 657 workdir = t.TempDir() 658 cmd = exec.Command("git", "clone", remoteDir, workdir) // Clone git dir 659 _, err := cmd.Output() 660 if err != nil { 661 if exitErr, ok := err.(*exec.ExitError); ok { 662 t.Logf("stderr: %s\n", exitErr.Stderr) 663 } 664 t.Fatal(err) 665 } 666 cmd = exec.Command("git", "config", "pull.rebase", "false") // Add a new file to git 667 cmd.Dir = workdir 668 _, err = cmd.Output() 669 if err != nil { 670 if exitErr, ok := err.(*exec.ExitError); ok { 671 t.Logf("stderr: %s\n", exitErr.Stderr) 672 } 673 t.Fatal(err) 674 } 675 676 if err := os.MkdirAll(path.Join(workdir, "environments", "development"), 0700); err != nil { 677 t.Fatal(err) 678 } 679 680 updateConfigFile := func(configFileContent string) error { 681 configFilePath := path.Join(workdir, "environments", "development", "config.json") 682 if err := os.WriteFile(configFilePath, []byte(configFileContent), 0666); err != nil { 683 return err 684 } 685 cmd = exec.Command("git", "add", configFilePath) // Add a new file to git 686 cmd.Dir = workdir 687 _, err = cmd.Output() 688 if err != nil { 689 if exitErr, ok := err.(*exec.ExitError); ok { 690 t.Logf("stderr: %s\n", exitErr.Stderr) 691 } 692 return err 693 } 694 cmd = exec.Command("git", "commit", "-m", "valid config") // commit the new file 695 cmd.Dir = workdir 696 cmd.Env = []string{ 697 "GIT_AUTHOR_NAME=kuberpult", 698 "GIT_COMMITTER_NAME=kuberpult", 699 "EMAIL=test@kuberpult.com", 700 } 701 out, err := cmd.Output() 702 fmt.Println(string(out)) 703 if err != nil { 704 if exitErr, ok := err.(*exec.ExitError); ok { 705 t.Logf("stderr: %s\n", exitErr.Stderr) 706 t.Logf("stderr: %s\n", err) 707 } 708 return err 709 } 710 cmd = exec.Command("git", "push", "origin", "HEAD") // push the new commit 711 cmd.Dir = workdir 712 _, err = cmd.Output() 713 if err != nil { 714 if exitErr, ok := err.(*exec.ExitError); ok { 715 t.Logf("stderr: %s\n", exitErr.Stderr) 716 } 717 return err 718 } 719 return nil 720 } 721 722 repo, err := New( 723 testutil.MakeTestContext(), 724 RepositoryConfig{ 725 URL: remoteDir, 726 Path: t.TempDir(), 727 }, 728 ) 729 730 if err != nil { 731 t.Fatal(err) 732 } 733 734 for _, configFile := range configFiles { 735 err = updateConfigFile(configFile.ConfigContent) 736 if err != nil { 737 t.Fatal(err) 738 } 739 err := repo.Apply(testutil.MakeTestContext(), &CreateApplicationVersion{ 740 Application: "foo", 741 Manifests: map[string]string{ 742 "development": "foo", 743 }, 744 }) 745 if configFile.ErrorExpected { 746 if err == nil { 747 t.Errorf("Apply gave no error even though config.json was incorrect") 748 } 749 } else { 750 if err != nil { 751 t.Errorf("Initialization failed with valid config.json: %s", err.Error()) 752 } 753 cmd = exec.Command("git", "pull") // Add a new file to git 754 cmd.Dir = workdir 755 _, err = cmd.Output() 756 if err != nil { 757 if exitErr, ok := err.(*exec.ExitError); ok { 758 t.Logf("stderr: %s\n", exitErr.Stderr) 759 } 760 t.Fatal(err) 761 } 762 } 763 } 764 }) 765 } 766 func TestConfigValidity(t *testing.T) { 767 tcs := []struct { 768 Name string 769 ConfigContent string 770 ErrorExpected bool 771 }{ 772 { 773 Name: "Initialization with valid config.json file works", 774 ConfigContent: "{\"upstream\": {\"latest\": true }}", 775 ErrorExpected: false, 776 }, 777 { 778 Name: "Initialization with invalid config.json file throws error", 779 ConfigContent: "{\"upstream\": \"latest\": true }}", 780 ErrorExpected: true, 781 }, 782 } 783 for _, tc := range tcs { 784 tc := tc 785 t.Run(tc.Name, func(t *testing.T) { 786 t.Parallel() 787 // create a remote 788 workdir := t.TempDir() 789 remoteDir := path.Join(workdir, "remote") 790 cmd := exec.Command("git", "init", "--bare", remoteDir) 791 cmd.Start() 792 cmd.Wait() 793 794 workdir = t.TempDir() 795 cmd = exec.Command("git", "clone", remoteDir, workdir) // Clone git dir 796 _, err := cmd.Output() 797 if err != nil { 798 if exitErr, ok := err.(*exec.ExitError); ok { 799 t.Logf("stderr: %s\n", exitErr.Stderr) 800 } 801 t.Fatal(err) 802 } 803 804 if err := os.MkdirAll(path.Join(workdir, "environments", "development"), 0700); err != nil { 805 t.Fatal(err) 806 } 807 808 configFilePath := path.Join(workdir, "environments", "development", "config.json") 809 if err := os.WriteFile(configFilePath, []byte(tc.ConfigContent), 0666); err != nil { 810 t.Fatal(err) 811 } 812 cmd = exec.Command("git", "add", configFilePath) // Add a new file to git 813 cmd.Dir = workdir 814 _, err = cmd.Output() 815 if err != nil { 816 if exitErr, ok := err.(*exec.ExitError); ok { 817 t.Logf("stderr: %s\n", exitErr.Stderr) 818 } 819 t.Fatal(err) 820 } 821 cmd = exec.Command("git", "commit", "-m", "valid config") // commit the new file 822 cmd.Dir = workdir 823 cmd.Env = []string{ 824 "GIT_AUTHOR_NAME=kuberpult", 825 "GIT_COMMITTER_NAME=kuberpult", 826 "EMAIL=test@kuberpult.com", 827 } 828 _, err = cmd.Output() 829 if err != nil { 830 if exitErr, ok := err.(*exec.ExitError); ok { 831 t.Logf("stderr: %s\n", exitErr.Stderr) 832 } 833 t.Fatal(err) 834 } 835 cmd = exec.Command("git", "push", "origin", "HEAD") // push the new commit 836 cmd.Dir = workdir 837 _, err = cmd.Output() 838 if err != nil { 839 if exitErr, ok := err.(*exec.ExitError); ok { 840 t.Logf("stderr: %s\n", exitErr.Stderr) 841 } 842 t.Fatal(err) 843 } 844 845 _, err = New( 846 testutil.MakeTestContext(), 847 RepositoryConfig{ 848 URL: remoteDir, 849 Path: t.TempDir(), 850 }, 851 ) 852 853 if tc.ErrorExpected { 854 if err == nil { 855 t.Errorf("Initialized even though config.json was incorrect") 856 } 857 } else { 858 if err != nil { 859 t.Errorf("Initialization failed with valid config.json") 860 } 861 } 862 863 }) 864 } 865 } 866 867 func TestGc(t *testing.T) { 868 tcs := []struct { 869 Name string 870 GcFrequency uint 871 StorageBackend StorageBackend 872 ExpectedGarbageMin uint64 873 ExpectedGarbageMax uint64 874 }{ 875 { 876 // 0 disables GC entirely 877 // we are reasonably expecting some additional files around 878 Name: "gc disabled", 879 GcFrequency: 0, 880 StorageBackend: GitBackend, 881 ExpectedGarbageMin: 906, 882 ExpectedGarbageMax: 1500, 883 }, 884 { 885 // we are going to perform 101 requests, that should trigger a gc 886 // the number of additional files should be lower than in the case above 887 Name: "gc enabled", 888 GcFrequency: 100, 889 StorageBackend: GitBackend, 890 ExpectedGarbageMin: 9, 891 ExpectedGarbageMax: 10, 892 }, 893 { 894 // enabling sqlite should bring the number of loose files down to 0 895 Name: "sqlite", 896 GcFrequency: 0, // the actual number here doesn't matter. GC is not run when sqlite is in use 897 StorageBackend: SqliteBackend, 898 ExpectedGarbageMin: 0, 899 ExpectedGarbageMax: 0, 900 }, 901 } 902 903 for _, tc := range tcs { 904 tc := tc 905 t.Run(tc.Name, func(t *testing.T) { 906 t.Parallel() 907 // create a remote 908 dir := t.TempDir() 909 remoteDir := path.Join(dir, "remote") 910 localDir := path.Join(dir, "local") 911 cmd := exec.Command("git", "init", "--bare", remoteDir) 912 cmd.Start() 913 cmd.Wait() 914 ctx := testutil.MakeTestContext() 915 repo, err := New( 916 ctx, 917 RepositoryConfig{ 918 URL: "file://" + remoteDir, 919 Path: localDir, 920 GcFrequency: tc.GcFrequency, 921 StorageBackend: tc.StorageBackend, 922 }, 923 ) 924 if err != nil { 925 t.Fatalf("new: expected no error, got '%e'", err) 926 } 927 928 err = repo.Apply(ctx, &CreateEnvironment{ 929 Environment: "test", 930 }) 931 if err != nil { 932 t.Fatal(err) 933 } 934 for i := 0; i < 100; i++ { 935 err := repo.Apply(ctx, &CreateApplicationVersion{ 936 Application: "test", 937 Manifests: map[string]string{ 938 "test": fmt.Sprintf("test%d", i), 939 }, 940 }) 941 if err != nil { 942 t.Fatal(err) 943 } 944 } 945 stats, err := repo.(*repository).countObjects(ctx) 946 if err != nil { 947 t.Fatal(err) 948 } 949 if stats.Count > tc.ExpectedGarbageMax { 950 t.Errorf("expected object count to be lower than %d, but got %d", tc.ExpectedGarbageMax, stats.Count) 951 } 952 if stats.Count < tc.ExpectedGarbageMin { 953 t.Errorf("expected object count to be higher than %d, but got %d", tc.ExpectedGarbageMin, stats.Count) 954 } 955 }) 956 } 957 } 958 959 func TestRetrySsh(t *testing.T) { 960 tcs := []struct { 961 Name string 962 NumOfFailures int 963 ExpectedNumOfCall int 964 ExpectedResponse error 965 CustomResponse error 966 }{ 967 { 968 Name: "No retries success from 1st try", 969 NumOfFailures: 0, 970 ExpectedNumOfCall: 1, 971 ExpectedResponse: nil, 972 CustomResponse: nil, 973 }, { 974 Name: "Success after the 4th attempt", 975 NumOfFailures: 4, 976 ExpectedNumOfCall: 5, 977 ExpectedResponse: nil, 978 CustomResponse: &git.GitError{Message: "mock error"}, 979 }, { 980 Name: "Fail after the 6th attempt", 981 NumOfFailures: 6, 982 ExpectedNumOfCall: 6, 983 ExpectedResponse: &git.GitError{Message: "max number of retries exceeded error"}, 984 CustomResponse: &git.GitError{Message: "max number of retries exceeded error"}, 985 }, { 986 Name: "Do not retry after a permanent error", 987 NumOfFailures: 1, 988 ExpectedNumOfCall: 1, 989 ExpectedResponse: &git.GitError{Message: "permanent error"}, 990 CustomResponse: &git.GitError{Message: "permanent error", Code: git.ErrorCodeNonFastForward}, 991 }, { 992 Name: "Fail after the 6th attempt = Max number of retries ", 993 NumOfFailures: 12, 994 ExpectedNumOfCall: 6, 995 ExpectedResponse: &git.GitError{Message: "max number of retries exceeded error"}, 996 CustomResponse: nil, 997 }, 998 } 999 for _, tc := range tcs { 1000 tc := tc 1001 t.Run(tc.Name, func(t *testing.T) { 1002 t.Parallel() 1003 repo := &repository{} 1004 counter := 0 1005 repo.backOffProvider = func() backoff.BackOff { 1006 return backoff.WithMaxRetries(&backoff.ZeroBackOff{}, 5) 1007 } 1008 resp := repo.Push(testutil.MakeTestContext(), func() error { 1009 counter++ 1010 if counter > tc.NumOfFailures { 1011 return nil 1012 } 1013 if counter == tc.NumOfFailures { // Custom response 1014 return tc.CustomResponse 1015 } 1016 if counter == 6 { // max number of retries 1017 return &git.GitError{Message: "max number of retries exceeded error"} 1018 } 1019 return &git.GitError{Message: fmt.Sprintf("mock error %d", counter)} 1020 }) 1021 1022 if resp == nil || tc.ExpectedResponse == nil { 1023 if resp != tc.ExpectedResponse { 1024 t.Fatalf("new: expected '%v', got '%v'", tc.ExpectedResponse, resp) 1025 } 1026 } else if resp.Error() != tc.ExpectedResponse.Error() { 1027 t.Fatalf("new: expected '%v', got '%v'", tc.ExpectedResponse.Error(), resp.Error()) 1028 } 1029 if counter != tc.ExpectedNumOfCall { 1030 t.Fatalf("new: expected number of calls '%d', got '%d'", tc.ExpectedNumOfCall, counter) 1031 } 1032 1033 }) 1034 } 1035 } 1036 1037 type SlowTransformer struct { 1038 finished chan struct{} 1039 started chan struct{} 1040 } 1041 1042 func (s *SlowTransformer) Transform(ctx context.Context, state *State, t TransformerContext) (string, error) { 1043 s.started <- struct{}{} 1044 <-s.finished 1045 return "ok", nil 1046 } 1047 1048 type EmptyTransformer struct{} 1049 1050 func (p *EmptyTransformer) Transform(ctx context.Context, state *State, t TransformerContext) (string, error) { 1051 return "nothing happened", nil 1052 } 1053 1054 type PanicTransformer struct{} 1055 1056 func (p *PanicTransformer) Transform(ctx context.Context, state *State, t TransformerContext) (string, error) { 1057 panic("panic tranformer") 1058 } 1059 1060 var TransformerError = errors.New("error transformer") 1061 1062 type ErrorTransformer struct{} 1063 1064 func (p *ErrorTransformer) Transform(ctx context.Context, state *State, t TransformerContext) (string, error) { 1065 return "error", TransformerError 1066 } 1067 1068 type InvalidJsonTransformer struct{} 1069 1070 func (p *InvalidJsonTransformer) Transform(ctx context.Context, state *State, t TransformerContext) (string, error) { 1071 return "error", InvalidJson 1072 } 1073 1074 func convertToSet(list []uint64) map[int]bool { 1075 set := make(map[int]bool) 1076 for _, i := range list { 1077 set[int(i)] = true 1078 } 1079 return set 1080 } 1081 1082 func TestApplyQueuePanic(t *testing.T) { 1083 type action struct { 1084 Transformer Transformer 1085 // Tests 1086 ExpectedError error 1087 } 1088 tcs := []struct { 1089 Name string 1090 Actions []action 1091 }{ 1092 { 1093 Name: "panic at the start", 1094 Actions: []action{ 1095 { 1096 Transformer: &PanicTransformer{}, 1097 ExpectedError: panicError, 1098 }, { 1099 ExpectedError: panicError, 1100 }, { 1101 ExpectedError: panicError, 1102 }, 1103 }, 1104 }, 1105 { 1106 Name: "panic at the middle", 1107 Actions: []action{ 1108 { 1109 ExpectedError: panicError, 1110 }, { 1111 Transformer: &PanicTransformer{}, 1112 ExpectedError: panicError, 1113 }, { 1114 ExpectedError: panicError, 1115 }, 1116 }, 1117 }, 1118 { 1119 Name: "panic at the end", 1120 Actions: []action{ 1121 { 1122 ExpectedError: panicError, 1123 }, { 1124 ExpectedError: panicError, 1125 }, { 1126 Transformer: &PanicTransformer{}, 1127 ExpectedError: panicError, 1128 }, 1129 }, 1130 }, 1131 } 1132 for _, tc := range tcs { 1133 tc := tc 1134 t.Run(tc.Name, func(t *testing.T) { 1135 t.Parallel() 1136 // create a remote 1137 dir := t.TempDir() 1138 remoteDir := path.Join(dir, "remote") 1139 localDir := path.Join(dir, "local") 1140 cmd := exec.Command("git", "init", "--bare", remoteDir) 1141 cmd.Start() 1142 cmd.Wait() 1143 repo, processQueue, err := New2( 1144 testutil.MakeTestContext(), 1145 RepositoryConfig{ 1146 URL: "file://" + remoteDir, 1147 Path: localDir, 1148 MaximumCommitsPerPush: 3, 1149 }, 1150 ) 1151 if err != nil { 1152 t.Fatalf("new: expected no error, got '%e'", err) 1153 } 1154 // The worker go routine is not started. We can move some items into the queue now. 1155 results := make([]<-chan error, len(tc.Actions)) 1156 for i, action := range tc.Actions { 1157 // We are using the internal interface here as an optimization to avoid spinning up one go-routine per action 1158 t := action.Transformer 1159 if t == nil { 1160 t = &EmptyTransformer{} 1161 } 1162 results[i] = repo.(*repository).applyDeferred(testutil.MakeTestContext(), t) 1163 } 1164 defer func() { 1165 r := recover() 1166 if r == nil { 1167 t.Errorf("The code did not panic") 1168 } else if r != "panic tranformer" { 1169 t.Logf("The code did not panic with the correct string but %#v", r) 1170 panic(r) 1171 } 1172 // Check for the correct errors 1173 for i, action := range tc.Actions { 1174 if err := <-results[i]; err != action.ExpectedError { 1175 t.Errorf("result[%d] error is not \"%v\" but got \"%v\"", i, action.ExpectedError, err) 1176 } 1177 } 1178 }() 1179 ctx, cancel := context.WithTimeout(testutil.MakeTestContext(), 10*time.Second) 1180 defer cancel() 1181 processQueue(ctx, nil) 1182 }) 1183 } 1184 } 1185 1186 type mockClock struct { 1187 t time.Time 1188 } 1189 1190 func (m *mockClock) now() time.Time { 1191 return m.t 1192 } 1193 1194 func (m *mockClock) sleep(d time.Duration) { 1195 m.t = m.t.Add(d) 1196 } 1197 1198 func TestApplyQueueTtlForHealth(t *testing.T) { 1199 // we set the networkTimeout to something low, so that it doesn't interfere with other processes e.g like once per second: 1200 networkTimeout := 1 * time.Second 1201 1202 tcs := []struct { 1203 Name string 1204 }{ 1205 { 1206 Name: "sleeps way too long, so health should fail", 1207 }, 1208 } 1209 for _, tc := range tcs { 1210 tc := tc 1211 t.Run(tc.Name, func(t *testing.T) { 1212 dir := t.TempDir() 1213 remoteDir := path.Join(dir, "remote") 1214 localDir := path.Join(dir, "local") 1215 cmd := exec.Command("git", "init", "--bare", remoteDir) 1216 cmd.Start() 1217 cmd.Wait() 1218 repo, processQueue, err := New2( 1219 testutil.MakeTestContext(), 1220 RepositoryConfig{ 1221 URL: "file://" + remoteDir, 1222 Path: localDir, 1223 NetworkTimeout: networkTimeout, 1224 }, 1225 ) 1226 if err != nil { 1227 t.Fatalf("new: expected no error, got '%e'", err) 1228 } 1229 ctx, cancel := context.WithTimeout(testutil.MakeTestContext(), 10*time.Second) 1230 1231 mc := mockClock{} 1232 hlth := &setup.HealthServer{} 1233 hlth.BackOffFactory = func() backoff.BackOff { return backoff.NewConstantBackOff(0) } 1234 hlth.Clock = mc.now 1235 reporterName := "ClarkKent" 1236 reporter := hlth.Reporter(reporterName) 1237 isReady := func() bool { 1238 return hlth.IsReady(reporterName) 1239 } 1240 errChan := make(chan error) 1241 go func() { 1242 err = processQueue(ctx, reporter) 1243 errChan <- err 1244 }() 1245 defer func() { 1246 cancel() 1247 chanError := <-errChan 1248 if chanError != nil { 1249 t.Errorf("Expected no error in processQueue but got: %v", chanError) 1250 } 1251 }() 1252 1253 finished := make(chan struct{}) 1254 started := make(chan struct{}) 1255 var transformer Transformer = &SlowTransformer{ 1256 finished: finished, 1257 started: started, 1258 } 1259 1260 go repo.Apply(ctx, transformer) 1261 1262 // first, wait, until the transformer has started: 1263 <-started 1264 // health should be reporting as ready now 1265 if !isReady() { 1266 t.Error("Expected health to be ready after transformer was started, but it was not") 1267 } 1268 // now advance the clock time 1269 mc.sleep(4 * networkTimeout) 1270 1271 // now that the transformer is started, we should get a failed health check immediately, because the networkTimeout is tiny: 1272 if isReady() { 1273 t.Error("Expected health to be not ready after transformer took too long, but it was") 1274 } 1275 1276 // let the transformer finish: 1277 finished <- struct{}{} 1278 1279 }) 1280 } 1281 } 1282 1283 func TestApplyQueue(t *testing.T) { 1284 type action struct { 1285 CancelBeforeAdd bool 1286 CancelAfterAdd bool 1287 Transformer Transformer 1288 // Tests 1289 ExpectedError error 1290 } 1291 tcs := []struct { 1292 Name string 1293 Actions []action 1294 ExpectedReleases []uint64 1295 }{ 1296 { 1297 Name: "simple", 1298 Actions: []action{ 1299 {}, {}, {}, 1300 }, 1301 ExpectedReleases: []uint64{ 1302 1, 2, 3, 1303 }, 1304 }, 1305 { 1306 Name: "cancellation in the middle (after)", 1307 Actions: []action{ 1308 {}, { 1309 CancelAfterAdd: true, 1310 ExpectedError: context.Canceled, 1311 }, {}, 1312 }, 1313 ExpectedReleases: []uint64{ 1314 1, 3, 1315 }, 1316 }, 1317 { 1318 Name: "cancellation at the start (after)", 1319 Actions: []action{ 1320 { 1321 CancelAfterAdd: true, 1322 ExpectedError: context.Canceled, 1323 }, {}, {}, 1324 }, 1325 ExpectedReleases: []uint64{ 1326 2, 3, 1327 }, 1328 }, 1329 { 1330 Name: "cancellation at the end (after)", 1331 Actions: []action{ 1332 {}, {}, 1333 { 1334 CancelAfterAdd: true, 1335 ExpectedError: context.Canceled, 1336 }, 1337 }, 1338 ExpectedReleases: []uint64{ 1339 1, 2, 1340 }, 1341 }, 1342 { 1343 Name: "cancellation in the middle (before)", 1344 Actions: []action{ 1345 {}, { 1346 CancelBeforeAdd: true, 1347 ExpectedError: context.Canceled, 1348 }, {}, 1349 }, 1350 ExpectedReleases: []uint64{ 1351 1, 3, 1352 }, 1353 }, 1354 { 1355 Name: "cancellation at the start (before)", 1356 Actions: []action{ 1357 { 1358 CancelBeforeAdd: true, 1359 ExpectedError: context.Canceled, 1360 }, {}, {}, 1361 }, 1362 ExpectedReleases: []uint64{ 1363 2, 3, 1364 }, 1365 }, 1366 { 1367 Name: "cancellation at the end (before)", 1368 Actions: []action{ 1369 {}, {}, 1370 { 1371 CancelBeforeAdd: true, 1372 ExpectedError: context.Canceled, 1373 }, 1374 }, 1375 ExpectedReleases: []uint64{ 1376 1, 2, 1377 }, 1378 }, 1379 { 1380 Name: "error at the start", 1381 Actions: []action{ 1382 { 1383 ExpectedError: &TransformerBatchApplyError{TransformerError: TransformerError, Index: 0}, 1384 Transformer: &ErrorTransformer{}, 1385 }, {}, {}, 1386 }, 1387 ExpectedReleases: []uint64{ 1388 2, 3, 1389 }, 1390 }, 1391 { 1392 Name: "error at the middle", 1393 Actions: []action{ 1394 {}, 1395 { 1396 ExpectedError: &TransformerBatchApplyError{TransformerError: TransformerError, Index: 0}, 1397 Transformer: &ErrorTransformer{}, 1398 }, {}, 1399 }, 1400 ExpectedReleases: []uint64{ 1401 1, 3, 1402 }, 1403 }, 1404 { 1405 Name: "error at the end", 1406 Actions: []action{ 1407 {}, {}, 1408 { 1409 ExpectedError: &TransformerBatchApplyError{TransformerError: TransformerError, Index: 0}, 1410 Transformer: &ErrorTransformer{}, 1411 }, 1412 }, 1413 ExpectedReleases: []uint64{ 1414 1, 2, 1415 }, 1416 }, 1417 { 1418 Name: "Invalid json error at start", 1419 Actions: []action{ 1420 { 1421 ExpectedError: &TransformerBatchApplyError{TransformerError: InvalidJson, Index: 0}, 1422 Transformer: &InvalidJsonTransformer{}, 1423 }, 1424 {}, {}, 1425 }, 1426 ExpectedReleases: []uint64{ 1427 2, 3, 1428 }, 1429 }, 1430 { 1431 Name: "Invalid json error at middle", 1432 Actions: []action{ 1433 {}, 1434 { 1435 ExpectedError: &TransformerBatchApplyError{TransformerError: InvalidJson, Index: 0}, 1436 Transformer: &InvalidJsonTransformer{}, 1437 }, 1438 {}, 1439 }, 1440 ExpectedReleases: []uint64{ 1441 1, 3, 1442 }, 1443 }, 1444 { 1445 Name: "Invalid json error at end", 1446 Actions: []action{ 1447 {}, {}, 1448 { 1449 ExpectedError: &TransformerBatchApplyError{TransformerError: InvalidJson, Index: 0}, 1450 Transformer: &InvalidJsonTransformer{}, 1451 }, 1452 }, 1453 ExpectedReleases: []uint64{ 1454 1, 2, 1455 }, 1456 }, 1457 } 1458 for _, tc := range tcs { 1459 tc := tc 1460 t.Run(tc.Name, func(t *testing.T) { 1461 t.Parallel() 1462 // create a remote 1463 dir := t.TempDir() 1464 remoteDir := path.Join(dir, "remote") 1465 localDir := path.Join(dir, "local") 1466 cmd := exec.Command("git", "init", "--bare", remoteDir) 1467 cmd.Start() 1468 cmd.Wait() 1469 repo, err := New( 1470 testutil.MakeTestContext(), 1471 RepositoryConfig{ 1472 URL: "file://" + remoteDir, 1473 Path: localDir, 1474 MaximumCommitsPerPush: 10, 1475 }, 1476 ) 1477 if err != nil { 1478 t.Fatalf("new: expected no error, got '%e'", err) 1479 } 1480 repoInternal := repo.(*repository) 1481 // Block the worker so that we have multiple items in the queue 1482 finished := make(chan struct{}) 1483 started := make(chan struct{}, 1) 1484 go func() { 1485 repo.Apply(testutil.MakeTestContext(), &SlowTransformer{finished: finished, started: started}) 1486 }() 1487 <-started 1488 // The worker go routine is now blocked. We can move some items into the queue now. 1489 results := make([]<-chan error, len(tc.Actions)) 1490 for i, action := range tc.Actions { 1491 ctx, cancel := context.WithCancel(testutil.MakeTestContext()) 1492 if action.CancelBeforeAdd { 1493 cancel() 1494 } 1495 if action.Transformer != nil { 1496 results[i] = repoInternal.applyDeferred(ctx, action.Transformer) 1497 } else { 1498 tf := &CreateApplicationVersion{ 1499 Application: "foo", 1500 Manifests: map[string]string{ 1501 "development": fmt.Sprintf("%d", i), 1502 }, 1503 Version: uint64(i + 1), 1504 } 1505 results[i] = repoInternal.applyDeferred(ctx, tf) 1506 } 1507 if action.CancelAfterAdd { 1508 cancel() 1509 } 1510 } 1511 // Now release the slow transformer 1512 finished <- struct{}{} 1513 // Check for the correct errors 1514 for i, action := range tc.Actions { 1515 if err := <-results[i]; err != nil && err.Error() != action.ExpectedError.Error() { 1516 t.Errorf("result[%d] error is not \"%v\" but got \"%v\"", i, action.ExpectedError, err) 1517 } 1518 } 1519 releases, _ := repo.State().Releases("foo") 1520 if !cmp.Equal(convertToSet(tc.ExpectedReleases), convertToSet(releases)) { 1521 t.Fatal("Output mismatch (-want +got):\n", cmp.Diff(tc.ExpectedReleases, releases)) 1522 } 1523 1524 }) 1525 } 1526 } 1527 1528 func TestMaximumCommitsPerPush(t *testing.T) { 1529 tcs := []struct { 1530 NumberOfCommits uint 1531 MaximumCommitsPerPush uint 1532 ExpectedAtLeastPushes uint 1533 }{ 1534 { 1535 NumberOfCommits: 7, 1536 MaximumCommitsPerPush: 5, 1537 ExpectedAtLeastPushes: 2, 1538 }, 1539 { 1540 NumberOfCommits: 5, 1541 MaximumCommitsPerPush: 0, 1542 ExpectedAtLeastPushes: 5, 1543 }, 1544 { 1545 NumberOfCommits: 5, 1546 MaximumCommitsPerPush: 10, 1547 ExpectedAtLeastPushes: 1, 1548 }, 1549 } 1550 1551 for _, tc := range tcs { 1552 tc := tc 1553 t.Run(fmt.Sprintf("with %d commits and %d per push", tc.NumberOfCommits, tc.MaximumCommitsPerPush), func(t *testing.T) { 1554 // create a remote 1555 dir := t.TempDir() 1556 remoteDir := path.Join(dir, "remote") 1557 localDir := path.Join(dir, "local") 1558 cmd := exec.Command("git", "init", "--bare", remoteDir) 1559 cmd.Run() 1560 ts := testssh.New(remoteDir) 1561 defer ts.Close() 1562 repo, processor, err := New2( 1563 testutil.MakeTestContext(), 1564 RepositoryConfig{ 1565 URL: ts.Url, 1566 Path: localDir, 1567 Certificates: Certificates{ 1568 KnownHostsFile: ts.KnownHosts, 1569 }, 1570 Credentials: Credentials{ 1571 SshKey: ts.ClientKey, 1572 }, 1573 1574 MaximumCommitsPerPush: tc.MaximumCommitsPerPush, 1575 }, 1576 ) 1577 if err != nil { 1578 t.Fatalf("new: expected no error, got '%e'", err) 1579 } 1580 var eg errgroup.Group 1581 for i := uint(0); i < tc.NumberOfCommits; i++ { 1582 eg.Go(func() error { 1583 return repo.Apply(testutil.MakeTestContext(), &CreateApplicationVersion{ 1584 Application: "foo", 1585 Manifests: map[string]string{"development": "foo"}, 1586 }) 1587 }) 1588 } 1589 ctx, cancel := context.WithCancel(context.Background()) 1590 defer cancel() 1591 go func() { 1592 processor(ctx, nil) 1593 }() 1594 eg.Wait() 1595 if ts.Pushes < tc.ExpectedAtLeastPushes { 1596 t.Errorf("expected at least %d pushes, but %d happened", tc.ExpectedAtLeastPushes, ts.Pushes) 1597 } 1598 1599 }) 1600 } 1601 } 1602 1603 func getTransformer(i int) (Transformer, error) { 1604 transformerType := i % 5 1605 switch transformerType { 1606 case 0: 1607 case 1: 1608 case 2: 1609 return &CreateApplicationVersion{ 1610 Application: "foo", 1611 Manifests: map[string]string{ 1612 "development": fmt.Sprintf("%d", i), 1613 }, 1614 Version: uint64(i + 1), 1615 }, nil 1616 case 3: 1617 return &ErrorTransformer{}, TransformerError 1618 case 4: 1619 return &InvalidJsonTransformer{}, InvalidJson 1620 } 1621 return &ErrorTransformer{}, TransformerError 1622 } 1623 1624 func createGitWithCommit(remote string, local string, t *testing.B) { 1625 cmd := exec.Command("git", "init", "--bare", remote) 1626 cmd.Start() 1627 cmd.Wait() 1628 1629 cmd = exec.Command("git", "clone", remote, local) // Clone git dir 1630 _, err := cmd.Output() 1631 if err != nil { 1632 t.Fatal(err) 1633 } 1634 cmd = exec.Command("touch", "a") // Add a new file to git 1635 cmd.Dir = local 1636 _, err = cmd.Output() 1637 if err != nil { 1638 t.Fatal(err) 1639 } 1640 cmd = exec.Command("git", "add", "a") // Add a new file to git 1641 cmd.Dir = local 1642 _, err = cmd.Output() 1643 if err != nil { 1644 t.Fatal(err) 1645 } 1646 1647 cmd = exec.Command("git", "commit", "-m", "adding") // commit the new file 1648 cmd.Dir = local 1649 cmd.Env = []string{ 1650 "GIT_AUTHOR_NAME=kuberpult", 1651 "GIT_COMMITTER_NAME=kuberpult", 1652 "EMAIL=test@kuberpult.com", 1653 } 1654 _, err = cmd.Output() 1655 if err != nil { 1656 if exitErr, ok := err.(*exec.ExitError); ok { 1657 t.Logf("stderr: %s\n", exitErr.Stderr) 1658 t.Logf("stderr: %s\n", err) 1659 } 1660 t.Fatal(err) 1661 } 1662 cmd = exec.Command("git", "push", "origin", "HEAD") // push the new commit 1663 cmd.Dir = local 1664 _, err = cmd.Output() 1665 if err != nil { 1666 t.Fatal(err) 1667 } 1668 } 1669 1670 func BenchmarkApplyQueue(t *testing.B) { 1671 t.StopTimer() 1672 dir := t.TempDir() 1673 remoteDir := path.Join(dir, "remote") 1674 localDir := path.Join(dir, "local") 1675 createGitWithCommit(remoteDir, localDir, t) 1676 1677 repo, err := New( 1678 testutil.MakeTestContext(), 1679 RepositoryConfig{ 1680 URL: "file://" + remoteDir, 1681 Path: localDir, 1682 }, 1683 ) 1684 if err != nil { 1685 t.Fatalf("new: expected no error, got '%e'", err) 1686 } 1687 repoInternal := repo.(*repository) 1688 // The worker go routine is now blocked. We can move some items into the queue now. 1689 results := make([]<-chan error, t.N) 1690 expectedResults := make([]error, t.N) 1691 expectedReleases := make(map[int]bool, t.N) 1692 tf, _ := getTransformer(0) 1693 repoInternal.Apply(testutil.MakeTestContext(), tf) 1694 1695 t.StartTimer() 1696 for i := 0; i < t.N; i++ { 1697 tf, expectedResult := getTransformer(i) 1698 results[i] = repoInternal.applyDeferred(testutil.MakeTestContext(), tf) 1699 expectedResults[i] = expectedResult 1700 if expectedResult == nil { 1701 expectedReleases[i+1] = true 1702 } 1703 } 1704 1705 for i := 0; i < t.N; i++ { 1706 if err := <-results[i]; err != expectedResults[i] { 1707 t.Errorf("result[%d] expected error \"%v\" but got \"%v\"", i, expectedResults[i], err) 1708 } 1709 } 1710 releases, _ := repo.State().Releases("foo") 1711 if !cmp.Equal(expectedReleases, convertToSet(releases)) { 1712 t.Fatal("Output mismatch (-want +got):\n", cmp.Diff(expectedReleases, convertToSet(releases))) 1713 } 1714 } 1715 1716 func TestPushUpdate(t *testing.T) { 1717 tcs := []struct { 1718 Name string 1719 InputBranch string 1720 InputRefName string 1721 InputStatus string 1722 ExpectedSuccess bool 1723 }{ 1724 { 1725 Name: "Should succeed", 1726 InputBranch: "main", 1727 InputRefName: "refs/heads/main", 1728 InputStatus: "", 1729 ExpectedSuccess: true, 1730 }, 1731 { 1732 Name: "Should fail because wrong branch", 1733 InputBranch: "main", 1734 InputRefName: "refs/heads/master", 1735 InputStatus: "", 1736 ExpectedSuccess: false, 1737 }, 1738 { 1739 Name: "Should fail because status not empty", 1740 InputBranch: "master", 1741 InputRefName: "refs/heads/master", 1742 InputStatus: "i am the status, stopping this from working", 1743 ExpectedSuccess: false, 1744 }, 1745 } 1746 for _, tc := range tcs { 1747 tc := tc 1748 t.Run(tc.Name, func(t *testing.T) { 1749 t.Parallel() 1750 var success = false 1751 actualError := defaultPushUpdate(tc.InputBranch, &success)(tc.InputRefName, tc.InputStatus) 1752 if success != tc.ExpectedSuccess { 1753 t.Fatal(fmt.Sprintf("expected sucess=%t but got %t", tc.ExpectedSuccess, success)) 1754 } 1755 if actualError != nil { 1756 t.Fatal(fmt.Sprintf("expected no error but got %s but got none", actualError)) 1757 } 1758 }) 1759 } 1760 } 1761 1762 func TestDeleteDirIfEmpty(t *testing.T) { 1763 tcs := []struct { 1764 Name string 1765 CreateThisDir string 1766 DeleteThisDir string 1767 ExpectedError error 1768 ExpectedReason SuccessReason 1769 }{ 1770 { 1771 Name: "Should succeed: dir exists and is empty", 1772 CreateThisDir: "foo/bar", 1773 DeleteThisDir: "foo/bar", 1774 ExpectedReason: NoReason, 1775 }, 1776 { 1777 Name: "Should succeed: dir does not exist", 1778 CreateThisDir: "foo/bar", 1779 DeleteThisDir: "foo/bar/pow", 1780 ExpectedReason: DirDoesNotExist, 1781 }, 1782 { 1783 Name: "Should succeed: dir does not exist", 1784 CreateThisDir: "foo/bar/pow", 1785 DeleteThisDir: "foo/bar", 1786 ExpectedReason: DirNotEmpty, 1787 }, 1788 } 1789 for _, tc := range tcs { 1790 tc := tc 1791 t.Run(tc.Name, func(t *testing.T) { 1792 t.Parallel() 1793 repo := setupRepositoryTest(t) 1794 state := repo.State() 1795 err := state.Filesystem.MkdirAll(tc.CreateThisDir, 0777) 1796 if err != nil { 1797 t.Fatalf("error in mkdir: %v", err) 1798 return 1799 } 1800 successReason, err := state.DeleteDirIfEmpty(tc.DeleteThisDir) 1801 if diff := cmp.Diff(tc.ExpectedError, err, cmpopts.EquateErrors()); diff != "" { 1802 t.Errorf("error mismatch (-want, +got):\n%s", diff) 1803 } 1804 if successReason != tc.ExpectedReason { 1805 t.Fatal("Output mismatch (-want +got):\n", cmp.Diff(tc.ExpectedReason, successReason)) 1806 } 1807 }) 1808 } 1809 } 1810 1811 func TestProcessQueueOnce(t *testing.T) { 1812 tcs := []struct { 1813 Name string 1814 Element transformerBatch 1815 PushUpdateFunc PushUpdateFunc 1816 PushActionFunc PushActionCallbackFunc 1817 ExpectedError error 1818 }{ 1819 { 1820 Name: "success", 1821 PushUpdateFunc: defaultPushUpdate, 1822 PushActionFunc: DefaultPushActionCallback, 1823 Element: transformerBatch{ 1824 ctx: testutil.MakeTestContext(), 1825 transformers: []Transformer{ 1826 &EmptyTransformer{}, 1827 }, 1828 result: make(chan error, 1), 1829 }, 1830 ExpectedError: nil, 1831 }, 1832 { 1833 Name: "failure because DefaultPushUpdate is wrong (branch protection)", 1834 PushUpdateFunc: func(s string, success *bool) git.PushUpdateReferenceCallback { 1835 *success = false 1836 return nil 1837 }, 1838 PushActionFunc: DefaultPushActionCallback, 1839 Element: transformerBatch{ 1840 ctx: testutil.MakeTestContext(), 1841 transformers: []Transformer{ 1842 &EmptyTransformer{}, 1843 }, 1844 result: make(chan error, 1), 1845 }, 1846 ExpectedError: errMatcher{"failed to push - this indicates that branch protection is enabled in 'file://$DIR/remote' on branch 'master'"}, 1847 }, 1848 { 1849 Name: "failure because error is returned in push (ssh key has read only access)", 1850 PushUpdateFunc: func(s string, success *bool) git.PushUpdateReferenceCallback { 1851 return nil 1852 }, 1853 PushActionFunc: func(options git.PushOptions, r *repository) PushActionFunc { 1854 return func() error { 1855 return git.MakeGitError(1) 1856 } 1857 }, 1858 Element: transformerBatch{ 1859 ctx: testutil.MakeTestContext(), 1860 transformers: []Transformer{ 1861 &EmptyTransformer{}, 1862 }, 1863 result: make(chan error, 1), 1864 }, 1865 ExpectedError: errMatcher{"rpc error: code = InvalidArgument desc = error: could not push to manifest repository 'file://$DIR/remote' on branch 'master' - this indicates that the ssh key does not have write access"}, 1866 }, 1867 } 1868 for _, tc := range tcs { 1869 tc := tc 1870 t.Run(tc.Name, func(t *testing.T) { 1871 t.Parallel() 1872 1873 // create a remote 1874 dir := t.TempDir() 1875 remoteDir := path.Join(dir, "remote") 1876 localDir := path.Join(dir, "local") 1877 cmd := exec.Command("git", "init", "--bare", remoteDir) 1878 cmd.Start() 1879 cmd.Wait() 1880 repo, actualError := New( 1881 testutil.MakeTestContext(), 1882 RepositoryConfig{ 1883 URL: "file://" + remoteDir, 1884 Path: localDir, 1885 }, 1886 ) 1887 if actualError != nil { 1888 t.Fatalf("new: expected no error, got '%e'", actualError) 1889 } 1890 repoInternal := repo.(*repository) 1891 repoInternal.ProcessQueueOnce(testutil.MakeTestContext(), tc.Element, tc.PushUpdateFunc, tc.PushActionFunc) 1892 1893 result := tc.Element.result 1894 actualError = <-result 1895 1896 var expectedError error 1897 if tc.ExpectedError != nil { 1898 expectedError = errMatcher{strings.ReplaceAll(tc.ExpectedError.Error(), "$DIR", dir)} 1899 } 1900 if diff := cmp.Diff(expectedError, actualError, cmpopts.EquateErrors()); diff != "" { 1901 t.Errorf("error mismatch (-want, +got):\n%s", diff) 1902 } 1903 }) 1904 } 1905 } 1906 1907 func TestGitPushDoesntGetStuck(t *testing.T) { 1908 tcs := []struct { 1909 Name string 1910 }{ 1911 { 1912 Name: "it doesnt get stuck", 1913 }, 1914 } 1915 for _, tc := range tcs { 1916 tc := tc 1917 t.Run(tc.Name, func(t *testing.T) { 1918 t.Parallel() 1919 ctx, cancel := context.WithCancel(context.Background()) 1920 defer cancel() 1921 // create a remote 1922 dir := t.TempDir() 1923 remoteDir := path.Join(dir, "remote") 1924 localDir := path.Join(dir, "local") 1925 cmd := exec.Command("git", "init", "--bare", remoteDir) 1926 cmd.Run() 1927 ts := testssh.New(remoteDir) 1928 defer ts.Close() 1929 repo, err := New( 1930 ctx, 1931 RepositoryConfig{ 1932 URL: ts.Url, 1933 Certificates: Certificates{ 1934 KnownHostsFile: ts.KnownHosts, 1935 }, 1936 Credentials: Credentials{ 1937 SshKey: ts.ClientKey, 1938 }, 1939 Path: localDir, 1940 NetworkTimeout: time.Second, 1941 }, 1942 ) 1943 if err != nil { 1944 t.Errorf("expected no error, got %q ( %#v )", err, err) 1945 } 1946 err = repo.Apply(testutil.MakeTestContext(), 1947 &CreateEnvironment{Environment: "dev"}, 1948 ) 1949 if err != nil { 1950 t.Errorf("expected no error, got %q ( %#v )", err, err) 1951 } 1952 // This will prevent the next push from working 1953 ts.DelayExecs(15 * time.Second) 1954 err = repo.Apply(testutil.MakeTestContext(), 1955 &CreateEnvironment{Environment: "stg"}, 1956 ) 1957 if err == nil { 1958 t.Errorf("expected an error, but didn't get one") 1959 } 1960 if status.Code(err) != codes.Canceled { 1961 t.Errorf("expected status code cancelled, but got %q", status.Code(err)) 1962 } 1963 // This will make the next push work 1964 ts.DelayExecs(0 * time.Second) 1965 err = repo.Apply(testutil.MakeTestContext(), 1966 &CreateEnvironment{Environment: "stg"}, 1967 ) 1968 if err != nil { 1969 t.Errorf("expected no error, got %q ( %#v )", err, err) 1970 } 1971 }) 1972 } 1973 } 1974 1975 type TestWebhookResolver struct { 1976 t *testing.T 1977 rec *httptest.ResponseRecorder 1978 requests chan *http.Request 1979 } 1980 1981 func (resolver TestWebhookResolver) Resolve(insecure bool, req *http.Request) (*http.Response, error) { 1982 testhandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1983 resolver.t.Logf("called with request: %v", *r) 1984 resolver.requests <- req 1985 close(resolver.requests) 1986 }) 1987 testhandler.ServeHTTP(resolver.rec, req) 1988 response := resolver.rec.Result() 1989 resolver.t.Logf("responded with: %v", response) 1990 return response, nil 1991 } 1992 1993 func TestSendWebhookToArgoCd(t *testing.T) { 1994 tcs := []struct { 1995 Name string 1996 Changes TransformerResult 1997 webUrl string 1998 branch string 1999 }{ 2000 { 2001 Name: "webhook", 2002 Changes: TransformerResult{ 2003 2004 Commits: &CommitIds{ 2005 Current: git.NewOidFromBytes([]byte{'C', 'U', 'R', 'R', 'E', 'N', 'T', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), 2006 Previous: git.NewOidFromBytes([]byte{'P', 'R', 'E', 'V', 'I', 'O', 'U', 'S', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), 2007 }, 2008 }, 2009 webUrl: "http://example.com", 2010 branch: "examplebranch", 2011 }, 2012 } 2013 for _, tc := range tcs { 2014 tc := tc 2015 ctx, cancel := context.WithCancel(context.Background()) 2016 defer cancel() 2017 t.Run(tc.Name, func(t *testing.T) { 2018 t.Parallel() 2019 2020 // given 2021 logger, err := zap.NewDevelopment() 2022 if err != nil { 2023 t.Fatalf("error creating logger: %v", err) 2024 } 2025 dir := t.TempDir() 2026 path := path.Join(dir, "repo") 2027 repo, _, err := New2( 2028 ctx, 2029 RepositoryConfig{ 2030 URL: fmt.Sprintf("file://%s", path), 2031 Path: path, 2032 }, 2033 ) 2034 if err != nil { 2035 t.Fatalf("new: expected no error, got '%v'", err) 2036 } 2037 repoInternal := repo.(*repository) 2038 repoInternal.config.ArgoWebhookUrl = "http://argo.example.com" 2039 rec := httptest.NewRecorder() 2040 resolver := TestWebhookResolver{ 2041 t: t, 2042 rec: rec, 2043 requests: make(chan *http.Request, 1), 2044 } 2045 repoInternal.config.WebhookResolver = resolver 2046 2047 // when 2048 repoInternal.config.WebURL = tc.webUrl 2049 repoInternal.config.Branch = tc.branch 2050 repoInternal.sendWebhookToArgoCd(ctx, logger, &tc.Changes) 2051 2052 // then 2053 req := <-resolver.requests 2054 buf := make([]byte, req.ContentLength) 2055 if _, err = io.ReadFull(req.Body, buf); err != nil { 2056 t.Errorf("error reading request body: %v", err) 2057 } 2058 var jsonRequest map[string]any 2059 if err = json.Unmarshal(buf, &jsonRequest); err != nil { 2060 t.Errorf("Error parsing request body '%s' as json: %v", string(buf), err) 2061 } 2062 after := jsonRequest["after"].(string) 2063 if after != tc.Changes.Commits.Current.String() { 2064 t.Fatalf("after '%s' does not match current '%s'", after, tc.Changes.Commits.Current) 2065 } 2066 before := jsonRequest["before"].(string) 2067 if before != tc.Changes.Commits.Previous.String() { 2068 t.Fatalf("before '%s' does not match previous '%s'", before, tc.Changes.Commits.Previous) 2069 } 2070 ref := jsonRequest["ref"].(string) 2071 if ref != fmt.Sprintf("refs/heads/%s", tc.branch) { 2072 t.Fatalf("refs '%s' does not match expected for branch given as '%s'", ref, tc.branch) 2073 } 2074 repository := jsonRequest["repository"].(map[string]any) 2075 htmlUrl := repository["html_url"].(string) 2076 if htmlUrl != tc.webUrl { 2077 t.Fatalf("repository/html_url '%s' does not match expected for webUrl given as '%s'", htmlUrl, tc.webUrl) 2078 } 2079 }) 2080 } 2081 } 2082 func TestLimit(t *testing.T) { 2083 transformers := []Transformer{ 2084 &CreateEnvironment{ 2085 Environment: "production", 2086 Config: config.EnvironmentConfig{Upstream: &config.EnvironmentConfigUpstream{Latest: true}}, 2087 }, 2088 &CreateApplicationVersion{ 2089 Application: "test", 2090 Manifests: map[string]string{ 2091 "production": "manifest", 2092 }, 2093 }, 2094 &CreateApplicationVersion{ 2095 Application: "test", 2096 Manifests: map[string]string{ 2097 "production": "manifest2", 2098 }, 2099 }, 2100 } 2101 tcs := []struct { 2102 Name string 2103 numberBatchActions int 2104 ShouldSucceed bool 2105 limit int 2106 Setup []Transformer 2107 ExpectedError error 2108 }{ 2109 { 2110 Name: "less than maximum number of requests", 2111 ShouldSucceed: true, 2112 limit: 5, 2113 numberBatchActions: 1, 2114 Setup: transformers, 2115 ExpectedError: nil, 2116 }, 2117 { 2118 Name: "more than the maximum number of requests", 2119 numberBatchActions: 10, 2120 limit: 5, 2121 ShouldSucceed: false, 2122 Setup: transformers, 2123 ExpectedError: errMatcher{"queue is full. Queue Capacity: 5."}, 2124 }, 2125 } 2126 for _, tc := range tcs { 2127 tc := tc 2128 t.Run(tc.Name, func(t *testing.T) { 2129 2130 repo, err := setupRepositoryTestAux(t, 3) 2131 ctx := testutil.MakeTestContext() 2132 if err != nil { 2133 t.Fatal(err) 2134 } 2135 for _, tr := range tc.Setup { 2136 errCh := repo.(*repository).applyDeferred(ctx, tr) 2137 select { 2138 case e := <-repo.(*repository).queue.transformerBatches: 2139 dummyPushUpdateFunction := func(string, *bool) git.PushUpdateReferenceCallback { return nil } 2140 dummyPushActionFunction := func(options git.PushOptions, r *repository) PushActionFunc { 2141 return func() error { 2142 return nil 2143 } 2144 } 2145 repo.(*repository).ProcessQueueOnce(ctx, e, dummyPushUpdateFunction, dummyPushActionFunction) 2146 default: 2147 } 2148 select { 2149 case err := <-errCh: 2150 if err != nil { 2151 t.Fatal(err) 2152 } 2153 default: 2154 } 2155 } 2156 2157 expectedErrorNumber := tc.numberBatchActions - tc.limit 2158 actualErrorNumber := 0 2159 for i := 0; i < tc.numberBatchActions; i++ { 2160 errCh := repo.(*repository).applyDeferred(ctx, transformers[0]) 2161 select { 2162 case err := <-errCh: 2163 if tc.ShouldSucceed { 2164 t.Fatalf("Got an error at iteration %d and was not expecting it %v\n", i, err) 2165 } 2166 //Should get some errors, check if they are the ones we expect 2167 if diff := cmp.Diff(tc.ExpectedError, err, cmpopts.EquateErrors()); diff != "" { 2168 t.Errorf("error mismatch (-want, +got):\n%s", diff) 2169 } 2170 actualErrorNumber += 1 2171 default: 2172 // If there is no error, 2173 } 2174 } 2175 if expectedErrorNumber > 0 && expectedErrorNumber != actualErrorNumber { 2176 t.Errorf("error number mismatch expected: %d, got %d", expectedErrorNumber, actualErrorNumber) 2177 } 2178 }) 2179 } 2180 } 2181 2182 func setupRepositoryTestAux(t *testing.T, commits uint) (Repository, error) { 2183 t.Parallel() 2184 dir := t.TempDir() 2185 remoteDir := path.Join(dir, "remote") 2186 localDir := path.Join(dir, "local") 2187 cmd := exec.Command("git", "init", "--bare", remoteDir) 2188 cmd.Start() 2189 cmd.Wait() 2190 t.Logf("test created dir: %s", localDir) 2191 repo, _, err := New2( 2192 testutil.MakeTestContext(), 2193 RepositoryConfig{ 2194 URL: remoteDir, 2195 Path: localDir, 2196 CommitterEmail: "kuberpult@freiheit.com", 2197 CommitterName: "kuberpult", 2198 EnvironmentConfigsPath: filepath.Join(remoteDir, "..", "environment_configs.json"), 2199 MaximumCommitsPerPush: commits, 2200 }, 2201 ) 2202 if err != nil { 2203 t.Fatal(err) 2204 } 2205 return repo, nil 2206 }