github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/deck/main_test.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "bufio" 21 "bytes" 22 "context" 23 "encoding/json" 24 "errors" 25 "flag" 26 "fmt" 27 "io" 28 "net/http" 29 "net/http/httptest" 30 "net/url" 31 "os" 32 "reflect" 33 "strconv" 34 "testing" 35 "time" 36 37 "github.com/google/go-cmp/cmp" 38 "github.com/sirupsen/logrus" 39 coreapi "k8s.io/api/core/v1" 40 "k8s.io/apimachinery/pkg/api/equality" 41 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 42 "k8s.io/apimachinery/pkg/util/diff" 43 "k8s.io/apimachinery/pkg/util/sets" 44 ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" 45 "sigs.k8s.io/yaml" 46 47 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 48 "sigs.k8s.io/prow/pkg/client/clientset/versioned/fake" 49 "sigs.k8s.io/prow/pkg/config" 50 "sigs.k8s.io/prow/pkg/deck/jobs" 51 "sigs.k8s.io/prow/pkg/flagutil" 52 configflagutil "sigs.k8s.io/prow/pkg/flagutil/config" 53 pluginsflagutil "sigs.k8s.io/prow/pkg/flagutil/plugins" 54 "sigs.k8s.io/prow/pkg/pluginhelp" 55 "sigs.k8s.io/prow/pkg/plugins" 56 _ "sigs.k8s.io/prow/pkg/spyglass/lenses/buildlog" 57 "sigs.k8s.io/prow/pkg/spyglass/lenses/common" 58 _ "sigs.k8s.io/prow/pkg/spyglass/lenses/junit" 59 _ "sigs.k8s.io/prow/pkg/spyglass/lenses/metadata" 60 "sigs.k8s.io/prow/pkg/tide" 61 "sigs.k8s.io/prow/pkg/tide/history" 62 ) 63 64 type fkc []prowapi.ProwJob 65 66 func (f fkc) List(ctx context.Context, pjs *prowapi.ProwJobList, _ ...ctrlruntimeclient.ListOption) error { 67 pjs.Items = f 68 return nil 69 } 70 71 type fca struct { 72 c config.Config 73 } 74 75 func (ca fca) Config() *config.Config { 76 return &ca.c 77 } 78 79 func TestOptions_Validate(t *testing.T) { 80 setTenantIDs := flagutil.Strings{} 81 setTenantIDs.Set("Test") 82 var testCases = []struct { 83 name string 84 input options 85 expectedErr bool 86 }{ 87 { 88 name: "minimal set ok", 89 input: options{ 90 config: configflagutil.ConfigOptions{ConfigPath: "test"}, 91 controllerManager: flagutil.ControllerManagerOptions{ 92 TimeoutListingProwJobsDefault: 30 * time.Second, 93 }, 94 }, 95 expectedErr: false, 96 }, 97 { 98 name: "missing configpath", 99 input: options{}, 100 expectedErr: true, 101 }, 102 { 103 name: "ok with oauth", 104 input: options{ 105 config: configflagutil.ConfigOptions{ConfigPath: "test"}, 106 controllerManager: flagutil.ControllerManagerOptions{ 107 TimeoutListingProwJobsDefault: 30 * time.Second, 108 }, 109 oauthURL: "website", 110 githubOAuthConfigFile: "something", 111 cookieSecretFile: "yum", 112 }, 113 expectedErr: false, 114 }, 115 { 116 name: "missing github config with oauth", 117 input: options{ 118 config: configflagutil.ConfigOptions{ConfigPath: "test"}, 119 controllerManager: flagutil.ControllerManagerOptions{ 120 TimeoutListingProwJobsDefault: 30 * time.Second, 121 }, 122 oauthURL: "website", 123 cookieSecretFile: "yum", 124 }, 125 expectedErr: true, 126 }, 127 { 128 name: "missing cookie with oauth", 129 input: options{ 130 config: configflagutil.ConfigOptions{ConfigPath: "test"}, 131 controllerManager: flagutil.ControllerManagerOptions{ 132 TimeoutListingProwJobsDefault: 30 * time.Second, 133 }, 134 oauthURL: "website", 135 githubOAuthConfigFile: "something", 136 }, 137 expectedErr: true, 138 }, 139 { 140 name: "hidden only and show hidden are mutually exclusive", 141 input: options{ 142 config: configflagutil.ConfigOptions{ConfigPath: "test"}, 143 controllerManager: flagutil.ControllerManagerOptions{ 144 TimeoutListingProwJobsDefault: 30 * time.Second, 145 }, 146 hiddenOnly: true, 147 showHidden: true, 148 }, 149 expectedErr: true, 150 }, 151 { 152 name: "show hidden and tenantIds are mutually exclusive", 153 input: options{ 154 config: configflagutil.ConfigOptions{ConfigPath: "test"}, 155 controllerManager: flagutil.ControllerManagerOptions{ 156 TimeoutListingProwJobsDefault: 30 * time.Second, 157 }, 158 hiddenOnly: false, 159 showHidden: true, 160 tenantIDs: setTenantIDs, 161 }, 162 expectedErr: true, 163 }, 164 { 165 name: "hiddenOnly and tenantIds are mutually exclusive", 166 input: options{ 167 config: configflagutil.ConfigOptions{ConfigPath: "test"}, 168 controllerManager: flagutil.ControllerManagerOptions{ 169 TimeoutListingProwJobsDefault: 30 * time.Second, 170 }, 171 hiddenOnly: true, 172 showHidden: false, 173 tenantIDs: setTenantIDs, 174 }, 175 expectedErr: true, 176 }, 177 } 178 179 for _, testCase := range testCases { 180 err := testCase.input.Validate() 181 if testCase.expectedErr && err == nil { 182 t.Errorf("%s: expected an error but got none", testCase.name) 183 } 184 if !testCase.expectedErr && err != nil { 185 t.Errorf("%s: expected no error but got one: %v", testCase.name, err) 186 } 187 } 188 } 189 190 type flc int 191 192 func (f flc) GetJobLog(job, id, container string) ([]byte, error) { 193 if job == "job" && id == "123" { 194 return []byte("hello"), nil 195 } 196 return nil, errors.New("muahaha") 197 } 198 199 func TestHandleLog(t *testing.T) { 200 var testcases = []struct { 201 name string 202 path string 203 code int 204 }{ 205 { 206 name: "no job name", 207 path: "", 208 code: http.StatusBadRequest, 209 }, 210 { 211 name: "job but no id", 212 path: "?job=job", 213 code: http.StatusBadRequest, 214 }, 215 { 216 name: "id but no job", 217 path: "?id=123", 218 code: http.StatusBadRequest, 219 }, 220 { 221 name: "id and job, found", 222 path: "?job=job&id=123", 223 code: http.StatusOK, 224 }, 225 { 226 name: "id and job, not found", 227 path: "?job=ohno&id=123", 228 code: http.StatusNotFound, 229 }, 230 } 231 handler := handleLog(flc(0), logrus.WithField("handler", "/log")) 232 for _, tc := range testcases { 233 req, err := http.NewRequest(http.MethodGet, "", nil) 234 if err != nil { 235 t.Fatalf("Error making request: %v", err) 236 } 237 u, err := url.Parse(tc.path) 238 if err != nil { 239 t.Fatalf("Error parsing URL: %v", err) 240 } 241 var follow = false 242 if ok, _ := strconv.ParseBool(u.Query().Get("follow")); ok { 243 follow = true 244 } 245 req.URL = u 246 rr := httptest.NewRecorder() 247 handler.ServeHTTP(rr, req) 248 if rr.Code != tc.code { 249 t.Errorf("Wrong error code. Got %v, want %v", rr.Code, tc.code) 250 } else if rr.Code == http.StatusOK { 251 if follow { 252 //wait a little to get the chunks 253 time.Sleep(2 * time.Millisecond) 254 reader := bufio.NewReader(rr.Body) 255 var buf bytes.Buffer 256 for { 257 line, err := reader.ReadBytes('\n') 258 if err == io.EOF { 259 break 260 } 261 if err != nil { 262 t.Fatalf("Expecting reply with content but got error: %v", err) 263 } 264 buf.Write(line) 265 } 266 if !bytes.Contains(buf.Bytes(), []byte("hello")) { 267 t.Errorf("Unexpected body: got %s.", buf.String()) 268 } 269 } else { 270 resp := rr.Result() 271 defer resp.Body.Close() 272 if body, err := io.ReadAll(resp.Body); err != nil { 273 t.Errorf("Error reading response body: %v", err) 274 } else if string(body) != "hello" { 275 t.Errorf("Unexpected body: got %s.", string(body)) 276 } 277 } 278 } 279 } 280 } 281 282 // TestHandleProwJobs just checks that the results can be unmarshaled properly, have the same 283 func TestHandleProwJobs(t *testing.T) { 284 kc := fkc{ 285 prowapi.ProwJob{ 286 ObjectMeta: metav1.ObjectMeta{ 287 Annotations: map[string]string{ 288 "hello": "world", 289 }, 290 Labels: map[string]string{ 291 "goodbye": "world", 292 }, 293 }, 294 Spec: prowapi.ProwJobSpec{ 295 Agent: prowapi.KubernetesAgent, 296 Job: "job", 297 DecorationConfig: &prowapi.DecorationConfig{}, 298 PodSpec: &coreapi.PodSpec{ 299 Containers: []coreapi.Container{ 300 { 301 Name: "test-1", 302 Image: "tester1", 303 }, 304 { 305 Name: "test-2", 306 Image: "tester2", 307 }, 308 }, 309 }, 310 }, 311 }, 312 prowapi.ProwJob{ 313 ObjectMeta: metav1.ObjectMeta{ 314 Annotations: map[string]string{ 315 "hello": "world", 316 }, 317 Labels: map[string]string{ 318 "goodbye": "world", 319 }, 320 }, 321 Spec: prowapi.ProwJobSpec{ 322 Agent: prowapi.KubernetesAgent, 323 Job: "missing-podspec-job", 324 DecorationConfig: &prowapi.DecorationConfig{}, 325 }, 326 }, 327 } 328 329 fakeJa := jobs.NewJobAgent(context.Background(), kc, false, true, []string{}, map[string]jobs.PodLogClient{}, fca{}.Config) 330 fakeJa.Start() 331 332 handler := handleProwJobs(fakeJa, logrus.WithField("handler", "/prowjobs.js")) 333 req, err := http.NewRequest(http.MethodGet, "/prowjobs.js?omit=annotations,labels,decoration_config,pod_spec", nil) 334 if err != nil { 335 t.Fatalf("Error making request: %v", err) 336 } 337 rr := httptest.NewRecorder() 338 handler.ServeHTTP(rr, req) 339 if rr.Code != http.StatusOK { 340 t.Fatalf("Bad error code: %d", rr.Code) 341 } 342 resp := rr.Result() 343 defer resp.Body.Close() 344 body, err := io.ReadAll(resp.Body) 345 if err != nil { 346 t.Fatalf("Error reading response body: %v", err) 347 } 348 type prowjobItems struct { 349 Items []prowapi.ProwJob `json:"items"` 350 } 351 var res prowjobItems 352 if err := json.Unmarshal(body, &res); err != nil { 353 t.Fatalf("Error unmarshaling: %v", err) 354 } 355 if res.Items[0].Annotations != nil { 356 t.Errorf("Failed to omit annotations correctly, expected: nil, got %v", res.Items[0].Annotations) 357 } 358 if res.Items[0].Labels != nil { 359 t.Errorf("Failed to omit labels correctly, expected: nil, got %v", res.Items[0].Labels) 360 } 361 if res.Items[0].Spec.DecorationConfig != nil { 362 t.Errorf("Failed to omit decoration config correctly, expected: nil, got %v", res.Items[0].Spec.DecorationConfig) 363 } 364 365 // this tests the behavior for filling a podspec with empty containers when asked to omit it 366 emptyPodspec := &coreapi.PodSpec{ 367 Containers: []coreapi.Container{{}, {}}, 368 } 369 if !equality.Semantic.DeepEqual(res.Items[0].Spec.PodSpec, emptyPodspec) { 370 t.Errorf("Failed to omit podspec correctly\n%s", diff.ObjectReflectDiff(res.Items[0].Spec.PodSpec, emptyPodspec)) 371 } 372 373 if res.Items[1].Spec.PodSpec != nil { 374 t.Errorf("Failed to omit podspec correctly, expected: nil, got %v", res.Items[0].Spec.PodSpec) 375 } 376 } 377 378 // TestProwJob just checks that the result can be unmarshaled properly, has 379 // the same status, and has equal spec. 380 func TestProwJob(t *testing.T) { 381 fakeProwJobClient := fake.NewSimpleClientset(&prowapi.ProwJob{ 382 ObjectMeta: metav1.ObjectMeta{ 383 Name: "wowsuch", 384 Namespace: "prowjobs", 385 }, 386 Spec: prowapi.ProwJobSpec{ 387 Job: "whoa", 388 Type: prowapi.PresubmitJob, 389 Refs: &prowapi.Refs{ 390 Org: "org", 391 Repo: "repo", 392 Pulls: []prowapi.Pull{ 393 {Number: 1}, 394 }, 395 }, 396 }, 397 Status: prowapi.ProwJobStatus{ 398 State: prowapi.PendingState, 399 }, 400 }) 401 handler := handleProwJob(fakeProwJobClient.ProwV1().ProwJobs("prowjobs"), logrus.WithField("handler", "/prowjob")) 402 req, err := http.NewRequest(http.MethodGet, "/prowjob?prowjob=wowsuch", nil) 403 if err != nil { 404 t.Fatalf("Error making request: %v", err) 405 } 406 rr := httptest.NewRecorder() 407 handler.ServeHTTP(rr, req) 408 if rr.Code != http.StatusOK { 409 t.Fatalf("Bad error code: %d", rr.Code) 410 } 411 resp := rr.Result() 412 defer resp.Body.Close() 413 body, err := io.ReadAll(resp.Body) 414 if err != nil { 415 t.Fatalf("Error reading response body: %v", err) 416 } 417 var res prowapi.ProwJob 418 if err := yaml.Unmarshal(body, &res); err != nil { 419 t.Fatalf("Error unmarshaling: %v", err) 420 } 421 if res.Spec.Job != "whoa" { 422 t.Errorf("Wrong job, expected \"whoa\", got \"%s\"", res.Spec.Job) 423 } 424 if res.Status.State != prowapi.PendingState { 425 t.Errorf("Wrong state, expected \"%v\", got \"%v\"", prowapi.PendingState, res.Status.State) 426 } 427 } 428 429 type fakeAuthenticatedUserIdentifier struct { 430 login string 431 } 432 433 func (a *fakeAuthenticatedUserIdentifier) LoginForRequester(requester, token string) (string, error) { 434 return a.login, nil 435 } 436 437 func TestTide(t *testing.T) { 438 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 439 pools := []tide.Pool{ 440 { 441 Org: "o", 442 }, 443 } 444 b, err := json.Marshal(pools) 445 if err != nil { 446 t.Fatalf("Marshaling: %v", err) 447 } 448 fmt.Fprint(w, string(b)) 449 })) 450 ca := &config.Agent{} 451 ca.Set(&config.Config{ 452 ProwConfig: config.ProwConfig{ 453 Tide: config.Tide{ 454 TideGitHubConfig: config.TideGitHubConfig{ 455 Queries: []config.TideQuery{ 456 {Repos: []string{"prowapi.netes/test-infra"}}, 457 }, 458 }, 459 }, 460 }, 461 }) 462 ta := tideAgent{ 463 path: s.URL, 464 hiddenRepos: func() []string { 465 return []string{} 466 }, 467 updatePeriod: func() time.Duration { return time.Minute }, 468 cfg: func() *config.Config { return &config.Config{} }, 469 } 470 if err := ta.updatePools(); err != nil { 471 t.Fatalf("Updating: %v", err) 472 } 473 if len(ta.pools) != 1 { 474 t.Fatalf("Wrong number of pools. Got %d, expected 1 in %v", len(ta.pools), ta.pools) 475 } 476 if ta.pools[0].Org != "o" { 477 t.Errorf("Wrong org in pool. Got %s, expected o in %v", ta.pools[0].Org, ta.pools) 478 } 479 handler := handleTidePools(ca.Config, &ta, logrus.WithField("handler", "/tide.js")) 480 req, err := http.NewRequest(http.MethodGet, "/tide.js", nil) 481 if err != nil { 482 t.Fatalf("Error making request: %v", err) 483 } 484 rr := httptest.NewRecorder() 485 handler.ServeHTTP(rr, req) 486 if rr.Code != http.StatusOK { 487 t.Fatalf("Bad error code: %d", rr.Code) 488 } 489 resp := rr.Result() 490 defer resp.Body.Close() 491 body, err := io.ReadAll(resp.Body) 492 if err != nil { 493 t.Fatalf("Error reading response body: %v", err) 494 } 495 res := tidePools{} 496 if err := json.Unmarshal(body, &res); err != nil { 497 t.Fatalf("Error unmarshaling: %v", err) 498 } 499 if len(res.Pools) != 1 { 500 t.Fatalf("Wrong number of pools. Got %d, expected 1 in %v", len(res.Pools), res.Pools) 501 } 502 if res.Pools[0].Org != "o" { 503 t.Errorf("Wrong org in pool. Got %s, expected o in %v", res.Pools[0].Org, res.Pools) 504 } 505 if len(res.Queries) != 1 { 506 t.Fatalf("Wrong number of pools. Got %d, expected 1 in %v", len(res.Queries), res.Queries) 507 } 508 if expected := "is:pr state:open archived:false repo:\"prowapi.netes/test-infra\""; res.Queries[0] != expected { 509 t.Errorf("Wrong query. Got %s, expected %s", res.Queries[0], expected) 510 } 511 } 512 513 func TestTideHistory(t *testing.T) { 514 testHist := map[string][]history.Record{ 515 "o/r:b": { 516 {Action: "MERGE"}, {Action: "TRIGGER"}, 517 }, 518 } 519 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 520 b, err := json.Marshal(testHist) 521 if err != nil { 522 t.Fatalf("Marshaling: %v", err) 523 } 524 fmt.Fprint(w, string(b)) 525 })) 526 527 ta := tideAgent{ 528 path: s.URL, 529 hiddenRepos: func() []string { 530 return []string{} 531 }, 532 updatePeriod: func() time.Duration { return time.Minute }, 533 cfg: func() *config.Config { return &config.Config{} }, 534 } 535 if err := ta.updateHistory(); err != nil { 536 t.Fatalf("Updating: %v", err) 537 } 538 if !reflect.DeepEqual(ta.history, testHist) { 539 t.Fatalf("Expected tideAgent history:\n%#v\n,but got:\n%#v\n", testHist, ta.history) 540 } 541 542 handler := handleTideHistory(&ta, logrus.WithField("handler", "/tide-history.js")) 543 req, err := http.NewRequest(http.MethodGet, "/tide-history.js", nil) 544 if err != nil { 545 t.Fatalf("Error making request: %v", err) 546 } 547 rr := httptest.NewRecorder() 548 handler.ServeHTTP(rr, req) 549 if rr.Code != http.StatusOK { 550 t.Fatalf("Bad error code: %d", rr.Code) 551 } 552 resp := rr.Result() 553 defer resp.Body.Close() 554 body, err := io.ReadAll(resp.Body) 555 if err != nil { 556 t.Fatalf("Error reading response body: %v", err) 557 } 558 var res tideHistory 559 if err := json.Unmarshal(body, &res); err != nil { 560 t.Fatalf("Error unmarshaling: %v", err) 561 } 562 if !reflect.DeepEqual(res.History, testHist) { 563 t.Fatalf("Expected /tide-history.js:\n%#v\n,but got:\n%#v\n", testHist, res.History) 564 } 565 } 566 567 func TestHelp(t *testing.T) { 568 hitCount := 0 569 help := pluginhelp.Help{ 570 AllRepos: []string{"org/repo"}, 571 RepoPlugins: map[string][]string{"org": {"plugin"}}, 572 RepoExternalPlugins: map[string][]string{"org/repo": {"external-plugin"}}, 573 PluginHelp: map[string]pluginhelp.PluginHelp{"plugin": {Description: "plugin"}}, 574 ExternalPluginHelp: map[string]pluginhelp.PluginHelp{"external-plugin": {Description: "external-plugin"}}, 575 } 576 s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 577 hitCount++ 578 b, err := json.Marshal(help) 579 if err != nil { 580 t.Fatalf("Marshaling: %v", err) 581 } 582 fmt.Fprint(w, string(b)) 583 })) 584 ha := &helpAgent{ 585 path: s.URL, 586 } 587 handler := handlePluginHelp(ha, logrus.WithField("handler", "/plugin-help.js")) 588 handleAndCheck := func() { 589 req, err := http.NewRequest(http.MethodGet, "/plugin-help.js", nil) 590 if err != nil { 591 t.Fatalf("Error making request: %v", err) 592 } 593 rr := httptest.NewRecorder() 594 handler.ServeHTTP(rr, req) 595 if rr.Code != http.StatusOK { 596 t.Fatalf("Bad error code: %d", rr.Code) 597 } 598 resp := rr.Result() 599 defer resp.Body.Close() 600 body, err := io.ReadAll(resp.Body) 601 if err != nil { 602 t.Fatalf("Error reading response body: %v", err) 603 } 604 var res pluginhelp.Help 605 if err := yaml.Unmarshal(body, &res); err != nil { 606 t.Fatalf("Error unmarshaling: %v", err) 607 } 608 if !reflect.DeepEqual(help, res) { 609 t.Errorf("Invalid plugin help. Got %v, expected %v", res, help) 610 } 611 if hitCount != 1 { 612 t.Errorf("Expected fake hook endpoint to be hit once, but endpoint was hit %d times.", hitCount) 613 } 614 } 615 handleAndCheck() 616 handleAndCheck() 617 } 618 619 func Test_gatherOptions(t *testing.T) { 620 cases := []struct { 621 name string 622 args map[string]string 623 del sets.Set[string] 624 koDataPath string 625 expected func(*options) 626 err bool 627 }{ 628 { 629 name: "minimal flags work", 630 expected: func(o *options) { 631 o.controllerManager.TimeoutListingProwJobs = 30 * time.Second 632 o.controllerManager.TimeoutListingProwJobsDefault = 30 * time.Second 633 }, 634 }, 635 { 636 name: "default static files location", 637 expected: func(o *options) { 638 o.controllerManager.TimeoutListingProwJobs = 30 * time.Second 639 o.controllerManager.TimeoutListingProwJobsDefault = 30 * time.Second 640 o.spyglassFilesLocation = "/lenses" 641 o.staticFilesLocation = "/static" 642 o.templateFilesLocation = "/template" 643 }, 644 }, 645 { 646 name: "ko data path", 647 koDataPath: "ko-data", 648 expected: func(o *options) { 649 o.controllerManager.TimeoutListingProwJobs = 30 * time.Second 650 o.controllerManager.TimeoutListingProwJobsDefault = 30 * time.Second 651 o.spyglassFilesLocation = "ko-data/lenses" 652 o.staticFilesLocation = "ko-data/static" 653 o.templateFilesLocation = "ko-data/template" 654 }, 655 }, 656 { 657 name: "explicitly set --config-path", 658 args: map[string]string{ 659 "--config-path": "/random/value", 660 }, 661 expected: func(o *options) { 662 o.config.ConfigPath = "/random/value" 663 o.controllerManager.TimeoutListingProwJobs = 30 * time.Second 664 o.controllerManager.TimeoutListingProwJobsDefault = 30 * time.Second 665 }, 666 }, 667 { 668 name: "explicitly set both --hidden-only and --show-hidden to true", 669 args: map[string]string{ 670 "--hidden-only": "true", 671 "--show-hidden": "true", 672 }, 673 err: true, 674 }, 675 { 676 name: "explicitly set --plugin-config", 677 args: map[string]string{ 678 "--hidden-only": "true", 679 "--show-hidden": "true", 680 }, 681 err: true, 682 }, 683 } 684 for _, tc := range cases { 685 fs := flag.NewFlagSet("fake-flags", flag.PanicOnError) 686 ghoptions := flagutil.GitHubOptions{} 687 ghoptions.AddFlags(fs) 688 ghoptions.AllowAnonymous = true 689 ghoptions.AllowDirectAccess = true 690 t.Run(tc.name, func(t *testing.T) { 691 oldKoDataPath := os.Getenv("KO_DATA_PATH") 692 if err := os.Setenv("KO_DATA_PATH", tc.koDataPath); err != nil { 693 t.Fatalf("Failed set env var KO_DATA_PATH: %v", err) 694 } 695 defer os.Setenv("KO_DATA_PATH", oldKoDataPath) 696 697 expected := &options{ 698 config: configflagutil.ConfigOptions{ 699 ConfigPathFlagName: "config-path", 700 JobConfigPathFlagName: "job-config-path", 701 ConfigPath: "yo", 702 SupplementalProwConfigsFileNameSuffix: "_prowconfig.yaml", 703 InRepoConfigCacheSize: 200, 704 }, 705 pluginsConfig: pluginsflagutil.PluginOptions{ 706 SupplementalPluginsConfigsFileNameSuffix: "_pluginconfig.yaml", 707 }, 708 githubOAuthConfigFile: "/etc/github/secret", 709 cookieSecretFile: "", 710 staticFilesLocation: "/static", 711 templateFilesLocation: "/template", 712 spyglassFilesLocation: "/lenses", 713 github: ghoptions, 714 instrumentation: flagutil.DefaultInstrumentationOptions(), 715 } 716 if tc.expected != nil { 717 tc.expected(expected) 718 } 719 720 argMap := map[string]string{ 721 "--config-path": "yo", 722 } 723 for k, v := range tc.args { 724 argMap[k] = v 725 } 726 for k := range tc.del { 727 delete(argMap, k) 728 } 729 730 var args []string 731 for k, v := range argMap { 732 args = append(args, k+"="+v) 733 } 734 fs := flag.NewFlagSet("fake-flags", flag.PanicOnError) 735 actual := gatherOptions(fs, args...) 736 switch err := actual.Validate(); { 737 case err != nil: 738 if !tc.err { 739 t.Errorf("unexpected error: %v", err) 740 } 741 case tc.err: 742 t.Errorf("failed to receive expected error") 743 case !reflect.DeepEqual(*expected, actual): 744 t.Errorf("actual differs from expected: %s", cmp.Diff(actual, *expected, cmp.Exporter(func(_ reflect.Type) bool { return true }))) 745 } 746 }) 747 } 748 749 } 750 751 func TestHandleConfig(t *testing.T) { 752 trueVal := true 753 c := config.Config{ 754 JobConfig: config.JobConfig{ 755 PresubmitsStatic: map[string][]config.Presubmit{ 756 "org/repo": { 757 { 758 Reporter: config.Reporter{ 759 Context: "gce", 760 }, 761 AlwaysRun: true, 762 }, 763 { 764 Reporter: config.Reporter{ 765 Context: "unit", 766 }, 767 AlwaysRun: true, 768 }, 769 }, 770 }, 771 }, 772 ProwConfig: config.ProwConfig{ 773 BranchProtection: config.BranchProtection{ 774 Orgs: map[string]config.Org{ 775 "kubernetes": { 776 Policy: config.Policy{ 777 Protect: &trueVal, 778 RequiredStatusChecks: &config.ContextPolicy{ 779 Strict: &trueVal, 780 }, 781 }, 782 }, 783 }, 784 }, 785 Tide: config.Tide{ 786 TideGitHubConfig: config.TideGitHubConfig{ 787 Queries: []config.TideQuery{ 788 {Repos: []string{"prowapi.netes/test-infra"}}, 789 }, 790 }, 791 }, 792 }, 793 } 794 cWithDisabledCluster := config.Config{ 795 ProwConfig: config.ProwConfig{ 796 DisabledClusters: []string{"build08", "build08", "build01"}, 797 }, 798 } 799 dataC, err := yaml.Marshal(c) 800 if err != nil { 801 t.Fatalf("Error unmarshaling: %v", err) 802 } 803 804 testcases := []struct { 805 name string 806 config config.Config 807 url string 808 expectedBody []byte 809 expectedStatus int 810 expectedContentType string 811 }{ 812 { 813 name: "general case", 814 config: c, 815 url: "/config", 816 expectedBody: dataC, 817 expectedStatus: http.StatusOK, 818 expectedContentType: "text/plain", 819 }, 820 { 821 name: "unsupported key", 822 config: c, 823 url: "/config?key=some", 824 expectedBody: []byte("getting config for key some is not supported\n"), 825 expectedStatus: http.StatusInternalServerError, 826 expectedContentType: `text/plain; charset=utf-8`, 827 }, 828 { 829 name: "no disabled clusters", 830 config: c, 831 url: "/config?key=disabled-clusters", 832 expectedBody: []byte(`[] 833 `), 834 expectedStatus: http.StatusOK, 835 expectedContentType: `text/plain`, 836 }, 837 { 838 name: "disabled clusters", 839 config: cWithDisabledCluster, 840 url: "/config?key=disabled-clusters", 841 expectedBody: []byte(`- build01 842 - build08 843 `), 844 expectedStatus: http.StatusOK, 845 expectedContentType: `text/plain`, 846 }, 847 } 848 849 for _, tc := range testcases { 850 t.Run(tc.name, func(t *testing.T) { 851 configGetter := func() *config.Config { 852 return &tc.config 853 } 854 handler := handleConfig(configGetter, logrus.WithField("handler", "/config")) 855 req, err := http.NewRequest(http.MethodGet, tc.url, nil) 856 if err != nil { 857 t.Fatalf("Error making request: %v", err) 858 } 859 rr := httptest.NewRecorder() 860 handler.ServeHTTP(rr, req) 861 862 if rr.Code != tc.expectedStatus { 863 t.Fatalf("Bad error code: %d", rr.Code) 864 } 865 if h := rr.Header().Get("Content-Type"); h != tc.expectedContentType { 866 t.Fatalf("Bad Content-Type, expected: 'text/plain', got: %v", h) 867 } 868 resp := rr.Result() 869 defer resp.Body.Close() 870 body, err := io.ReadAll(resp.Body) 871 if err != nil { 872 t.Fatalf("Error reading response body: %v", err) 873 } 874 if diff := cmp.Diff(string(tc.expectedBody), string(body)); diff != "" { 875 t.Errorf("Error differs from expected:\n%s", diff) 876 } 877 }) 878 } 879 880 } 881 882 func TestHandlePluginConfig(t *testing.T) { 883 c := plugins.Configuration{ 884 Plugins: plugins.Plugins{ 885 "org/repo": {Plugins: []string{ 886 "approve", 887 "lgtm", 888 }}, 889 }, 890 Blunderbuss: plugins.Blunderbuss{ 891 ExcludeApprovers: true, 892 }, 893 } 894 pluginAgent := &plugins.ConfigAgent{} 895 pluginAgent.Set(&c) 896 handler := handlePluginConfig(pluginAgent, logrus.WithField("handler", "/plugin-config")) 897 req, err := http.NewRequest(http.MethodGet, "/config", nil) 898 if err != nil { 899 t.Fatalf("Error making request: %v", err) 900 } 901 rr := httptest.NewRecorder() 902 handler.ServeHTTP(rr, req) 903 if rr.Code != http.StatusOK { 904 t.Fatalf("Bad error code: %d", rr.Code) 905 } 906 if h := rr.Header().Get("Content-Type"); h != "text/plain" { 907 t.Fatalf("Bad Content-Type, expected: 'text/plain', got: %v", h) 908 } 909 resp := rr.Result() 910 defer resp.Body.Close() 911 body, err := io.ReadAll(resp.Body) 912 if err != nil { 913 t.Fatalf("Error reading response body: %v", err) 914 } 915 var res plugins.Configuration 916 if err := yaml.Unmarshal(body, &res); err != nil { 917 t.Fatalf("Error unmarshaling: %v", err) 918 } 919 if !reflect.DeepEqual(c, res) { 920 t.Errorf("Invalid config. Got %v, expected %v", res, c) 921 } 922 } 923 924 func cfgWithLensNamed(lensName string) *config.Config { 925 return &config.Config{ 926 ProwConfig: config.ProwConfig{ 927 Deck: config.Deck{ 928 Spyglass: config.Spyglass{ 929 Lenses: []config.LensFileConfig{{ 930 Lens: config.LensConfig{ 931 Name: lensName, 932 }, 933 }}, 934 }, 935 }, 936 }, 937 } 938 } 939 940 func verifyCfgHasRemoteForLens(lensName string) func(*config.Config, error) error { 941 return func(c *config.Config, err error) error { 942 if err != nil { 943 return fmt.Errorf("got unexpected error: %w", err) 944 } 945 946 var found bool 947 for _, lens := range c.Deck.Spyglass.Lenses { 948 if lens.Lens.Name != lensName { 949 continue 950 } 951 found = true 952 953 if lens.RemoteConfig == nil { 954 return errors.New("remoteConfig for lens was nil") 955 } 956 957 if lens.RemoteConfig.Endpoint == "" { 958 return errors.New("endpoint was unset") 959 } 960 961 if lens.RemoteConfig.ParsedEndpoint == nil { 962 return errors.New("parsedEndpoint was nil") 963 } 964 if expected := common.DyanmicPathForLens(lensName); lens.RemoteConfig.ParsedEndpoint.Path != expected { 965 return fmt.Errorf("expected parsedEndpoint.Path to be %q, was %q", expected, lens.RemoteConfig.ParsedEndpoint.Path) 966 } 967 if lens.RemoteConfig.ParsedEndpoint.Scheme != "http" { 968 return fmt.Errorf("expected parsedEndpoint.scheme to be 'http', was %q", lens.RemoteConfig.ParsedEndpoint.Scheme) 969 } 970 if lens.RemoteConfig.ParsedEndpoint.Host != spyglassLocalLensListenerAddr { 971 return fmt.Errorf("expected parsedEndpoint.Host to be %q, was %q", spyglassLocalLensListenerAddr, lens.RemoteConfig.ParsedEndpoint.Host) 972 } 973 if lens.RemoteConfig.Title == "" { 974 return errors.New("expected title to be set") 975 } 976 if lens.RemoteConfig.Priority == nil { 977 return errors.New("expected priority to be set") 978 } 979 if lens.RemoteConfig.HideTitle == nil { 980 return errors.New("expected HideTitle to be set") 981 } 982 } 983 984 if !found { 985 return fmt.Errorf("no config found for lens %q", lensName) 986 } 987 988 return nil 989 } 990 991 } 992 993 func TestSpyglassConfigDefaulting(t *testing.T) { 994 t.Parallel() 995 996 testCases := []struct { 997 name string 998 in *config.Config 999 verify func(*config.Config, error) error 1000 }{ 1001 { 1002 name: "buildlog lens gets defaulted", 1003 in: cfgWithLensNamed("buildlog"), 1004 verify: verifyCfgHasRemoteForLens("buildlog"), 1005 }, 1006 { 1007 name: "coverage lens gets defaulted", 1008 in: cfgWithLensNamed("coverage"), 1009 verify: verifyCfgHasRemoteForLens("coverage"), 1010 }, 1011 { 1012 name: "junit lens gets defaulted", 1013 in: cfgWithLensNamed("junit"), 1014 verify: verifyCfgHasRemoteForLens("junit"), 1015 }, 1016 { 1017 name: "metadata lens gets defaulted", 1018 in: cfgWithLensNamed("metadata"), 1019 verify: verifyCfgHasRemoteForLens("metadata"), 1020 }, 1021 { 1022 name: "podinfo lens gets defaulted", 1023 in: cfgWithLensNamed("podinfo"), 1024 verify: verifyCfgHasRemoteForLens("podinfo"), 1025 }, 1026 { 1027 name: "restcoverage lens gets defaulted", 1028 in: cfgWithLensNamed("restcoverage"), 1029 verify: verifyCfgHasRemoteForLens("restcoverage"), 1030 }, 1031 { 1032 name: "undef lens defaulting fails", 1033 in: cfgWithLensNamed("undef"), 1034 verify: func(_ *config.Config, err error) error { 1035 expectedErrMsg := `lens "undef" has no remote_config and could not get default: invalid lens name` 1036 if err == nil || err.Error() != expectedErrMsg { 1037 return fmt.Errorf("expected err to be %q, was %w", expectedErrMsg, err) 1038 } 1039 return nil 1040 }, 1041 }, 1042 } 1043 1044 for _, tc := range testCases { 1045 t.Run(tc.name, func(t *testing.T) { 1046 if err := tc.verify(tc.in, spglassConfigDefaulting(tc.in)); err != nil { 1047 t.Error(err) 1048 } 1049 }) 1050 } 1051 } 1052 1053 func TestHandleGitHubLink(t *testing.T) { 1054 ghoptions := flagutil.GitHubOptions{Host: "github.mycompany.com"} 1055 org, repo := "org", "repo" 1056 handler := HandleGitHubLink(ghoptions.Host, true) 1057 req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/github-link?dest=%s/%s", org, repo), nil) 1058 if err != nil { 1059 t.Fatalf("Error making request: %v", err) 1060 } 1061 rr := httptest.NewRecorder() 1062 handler.ServeHTTP(rr, req) 1063 if rr.Code != http.StatusFound { 1064 t.Fatalf("Bad error code: %d", rr.Code) 1065 } 1066 resp := rr.Result() 1067 defer resp.Body.Close() 1068 actual := resp.Header.Get("Location") 1069 expected := fmt.Sprintf("https://%s/%s/%s", ghoptions.Host, org, repo) 1070 if expected != actual { 1071 t.Fatalf("%v", actual) 1072 } 1073 } 1074 1075 func TestHandleGitProviderLink(t *testing.T) { 1076 tests := []struct { 1077 name string 1078 query string 1079 want string 1080 }{ 1081 { 1082 name: "github-commit", 1083 query: "target=commit&repo=bar&commit=abc123", 1084 want: "https://github.mycompany.com/bar/commit/abc123", 1085 }, 1086 { 1087 name: "github-branch", 1088 query: "target=branch&repo=bar&branch=main", 1089 want: "https://github.mycompany.com/bar/tree/main", 1090 }, 1091 { 1092 name: "github-pr", 1093 query: "target=pr&repo=bar&number=2", 1094 want: "https://github.mycompany.com/bar/pull/2", 1095 }, 1096 { 1097 name: "github-pr-with-quote", 1098 query: "target=pr&repo='bar'&number=2", 1099 want: "https://github.mycompany.com/bar/pull/2", 1100 }, 1101 { 1102 name: "github-author", 1103 query: "target=author&author=chaodaiG", 1104 want: "https://github.mycompany.com/chaodaiG", 1105 }, 1106 { 1107 name: "github-author-withquote", 1108 query: "target=author&repo='bar'&author=chaodaiG", 1109 want: "https://github.mycompany.com/chaodaiG", 1110 }, 1111 { 1112 name: "github-invalid", 1113 query: "target=invalid&repo=bar&commit=abc123", 1114 want: "/", 1115 }, 1116 { 1117 name: "gerrit-commit", 1118 query: "target=commit&repo='https://foo-review.abc/bar'&commit=abc123", 1119 want: "https://foo.abc/bar/+/abc123", 1120 }, 1121 { 1122 name: "gerrit-commit", 1123 query: "target=prcommit&repo='https://foo-review.abc/bar'&commit=abc123", 1124 want: "https://foo.abc/bar/+/abc123", 1125 }, 1126 { 1127 name: "gerrit-branch", 1128 query: "target=branch&repo='https://foo-review.abc/bar'&branch=main", 1129 want: "https://foo.abc/bar/+/refs/heads/main", 1130 }, 1131 { 1132 name: "gerrit-pr", 1133 query: "target=pr&repo='https://foo-review.abc/bar'&number=2", 1134 want: "https://foo-review.abc/c/bar/+/2", 1135 }, 1136 { 1137 name: "gerrit-invalid", 1138 query: "target=invalid&repo='https://foo-review.abc/bar'&commit=abc123", 1139 want: "/", 1140 }, 1141 } 1142 1143 ghoptions := flagutil.GitHubOptions{Host: "github.mycompany.com"} 1144 1145 for _, tc := range tests { 1146 t.Run(tc.name, func(t *testing.T) { 1147 url := fmt.Sprintf("/git-provider-link?%s", tc.query) 1148 req, err := http.NewRequest(http.MethodGet, url, nil) 1149 if err != nil { 1150 t.Fatalf("Error making request: %v", err) 1151 } 1152 1153 handler := HandleGitProviderLink(ghoptions.Host, true) 1154 rr := httptest.NewRecorder() 1155 handler.ServeHTTP(rr, req) 1156 if rr.Code != http.StatusFound { 1157 t.Fatalf("Bad error code: %d", rr.Code) 1158 } 1159 resp := rr.Result() 1160 defer resp.Body.Close() 1161 if want, got := tc.want, resp.Header.Get("Location"); want != got { 1162 t.Fatalf("Wrong URL. Want: %s, got: %s", want, got) 1163 } 1164 }) 1165 } 1166 } 1167 1168 func TestHttpStatusForError(t *testing.T) { 1169 testCases := []struct { 1170 name string 1171 input error 1172 expectedStatus int 1173 }{ 1174 { 1175 name: "normal_error", 1176 input: errors.New("some error message"), 1177 expectedStatus: http.StatusInternalServerError, 1178 }, 1179 { 1180 name: "httpError", 1181 input: httpError{ 1182 error: errors.New("some error message"), 1183 statusCode: http.StatusGone, 1184 }, 1185 expectedStatus: http.StatusGone, 1186 }, 1187 { 1188 name: "httpError_wrapped", 1189 input: fmt.Errorf("wrapped error: %w", httpError{ 1190 error: errors.New("some error message"), 1191 statusCode: http.StatusGone, 1192 }), 1193 expectedStatus: http.StatusGone, 1194 }, 1195 } 1196 for _, tc := range testCases { 1197 t.Run(tc.name, func(nested *testing.T) { 1198 actual := httpStatusForError(tc.input) 1199 if actual != tc.expectedStatus { 1200 t.Fatalf("unexpected HTTP status (expected=%v, actual=%v) for error: %v", tc.expectedStatus, actual, tc.input) 1201 } 1202 }) 1203 } 1204 } 1205 1206 func TestPRHistLink(t *testing.T) { 1207 tests := []struct { 1208 name string 1209 tmpl string 1210 org string 1211 repo string 1212 number int 1213 want string 1214 wantErr bool 1215 }{ 1216 { 1217 name: "default", 1218 tmpl: defaultPRHistLinkTemplate, 1219 org: "org", 1220 repo: "repo", 1221 number: 0, 1222 want: "/pr-history?org=org&repo=repo&pr=0", 1223 wantErr: false, 1224 }, 1225 { 1226 name: "different-template", 1227 tmpl: "/pull={{.Number}}", 1228 org: "org", 1229 repo: "repo", 1230 number: 0, 1231 want: "/pull=0", 1232 wantErr: false, 1233 }, 1234 { 1235 name: "invalid-template", 1236 tmpl: "doesn't matter {{.NotExist}}", 1237 org: "org", 1238 repo: "repo", 1239 number: 0, 1240 want: "", 1241 wantErr: true, 1242 }, 1243 } 1244 1245 for _, tc := range tests { 1246 t.Run(tc.name, func(t *testing.T) { 1247 got, gotErr := prHistLinkFromTemplate(tc.tmpl, tc.org, tc.repo, tc.number) 1248 if (tc.wantErr && (gotErr == nil)) || (!tc.wantErr && (gotErr != nil)) { 1249 t.Fatalf("Error mismatch. Want: %v, got: %v", tc.wantErr, gotErr) 1250 } 1251 if diff := cmp.Diff(tc.want, got); diff != "" { 1252 t.Fatalf("Template mismatch. Want: (-), got: (+). \n%s", diff) 1253 } 1254 }) 1255 } 1256 }