github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/spyglass/spyglass_test.go (about) 1 /* 2 Copyright 2018 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 spyglass 18 19 import ( 20 "fmt" 21 "os" 22 "strings" 23 "testing" 24 25 "github.com/fsouza/fake-gcs-server/fakestorage" 26 "github.com/sirupsen/logrus" 27 28 "k8s.io/test-infra/prow/config" 29 "k8s.io/test-infra/prow/deck/jobs" 30 "k8s.io/test-infra/prow/kube" 31 "k8s.io/test-infra/prow/spyglass/lenses" 32 ) 33 34 var ( 35 fakeJa *jobs.JobAgent 36 fakeGCSServer *fakestorage.Server 37 ) 38 39 const ( 40 testSrc = "gs://test-bucket/logs/example-ci-run/403" 41 ) 42 43 type fkc []kube.ProwJob 44 45 func (f fkc) GetLog(pod string) ([]byte, error) { 46 return nil, nil 47 } 48 49 func (f fkc) ListPods(selector string) ([]kube.Pod, error) { 50 return nil, nil 51 } 52 53 func (f fkc) ListProwJobs(s string) ([]kube.ProwJob, error) { 54 return f, nil 55 } 56 57 type fpkc string 58 59 func (f fpkc) GetLog(pod string) ([]byte, error) { 60 if pod == "wowowow" || pod == "powowow" { 61 return []byte(f), nil 62 } 63 return nil, fmt.Errorf("pod not found: %s", pod) 64 } 65 66 func (f fpkc) GetContainerLog(pod, container string) ([]byte, error) { 67 if pod == "wowowow" || pod == "powowow" { 68 return []byte(f), nil 69 } 70 return nil, fmt.Errorf("pod not found: %s", pod) 71 } 72 73 func (f fpkc) GetLogTail(pod, container string, n int64) ([]byte, error) { 74 if pod == "wowowow" || pod == "powowow" { 75 tailBytes := []byte(f) 76 lenTailBytes := int64(len(tailBytes)) 77 if lenTailBytes < n { 78 return tailBytes, nil 79 } 80 return tailBytes[lenTailBytes-n-1:], nil 81 } 82 return nil, fmt.Errorf("pod not found: %s", pod) 83 } 84 85 type fca struct { 86 c config.Config 87 } 88 89 func (ca fca) Config() *config.Config { 90 return &ca.c 91 } 92 93 func TestMain(m *testing.M) { 94 var longLog string 95 for i := 0; i < 300; i++ { 96 longLog += "here a log\nthere a log\neverywhere a log log\n" 97 } 98 fakeGCSServer = fakestorage.NewServer([]fakestorage.Object{ 99 { 100 BucketName: "test-bucket", 101 Name: "logs/example-ci-run/403/build-log.txt", 102 Content: []byte("Oh wow\nlogs\nthis is\ncrazy"), 103 }, 104 { 105 BucketName: "test-bucket", 106 Name: "logs/example-ci-run/403/long-log.txt", 107 Content: []byte(longLog), 108 }, 109 { 110 BucketName: "test-bucket", 111 Name: "logs/example-ci-run/403/junit_01.xml", 112 Content: []byte(`<testsuite tests="1017" failures="1017" time="0.016981535"> 113 <testcase name="BeforeSuite" classname="Kubernetes e2e suite" time="0.006343795"> 114 <failure type="Failure"> 115 test/e2e/e2e.go:137 BeforeSuite on Node 1 failed test/e2e/e2e.go:137 116 </failure> 117 </testcase> 118 </testsuite>`), 119 }, 120 { 121 BucketName: "test-bucket", 122 Name: "logs/example-ci-run/403/started.json", 123 Content: []byte(`{ 124 "node": "gke-prow-default-pool-3c8994a8-qfhg", 125 "repo-version": "v1.12.0-alpha.0.985+e6f64d0a79243c", 126 "timestamp": 1528742858, 127 "repos": { 128 "k8s.io/kubernetes": "master", 129 "k8s.io/release": "master" 130 }, 131 "version": "v1.12.0-alpha.0.985+e6f64d0a79243c", 132 "metadata": { 133 "pod": "cbc53d8e-6da7-11e8-a4ff-0a580a6c0269" 134 } 135 }`), 136 }, 137 { 138 BucketName: "test-bucket", 139 Name: "logs/example-ci-run/403/finished.json", 140 Content: []byte(`{ 141 "timestamp": 1528742943, 142 "version": "v1.12.0-alpha.0.985+e6f64d0a79243c", 143 "result": "SUCCESS", 144 "passed": true, 145 "job-version": "v1.12.0-alpha.0.985+e6f64d0a79243c", 146 "metadata": { 147 "repo": "k8s.io/kubernetes", 148 "repos": { 149 "k8s.io/kubernetes": "master", 150 "k8s.io/release": "master" 151 }, 152 "infra-commit": "260081852", 153 "pod": "cbc53d8e-6da7-11e8-a4ff-0a580a6c0269", 154 "repo-commit": "e6f64d0a79243c834babda494151fc5d66582240" 155 }, 156 },`), 157 }, 158 }) 159 defer fakeGCSServer.Stop() 160 kc := fkc{ 161 kube.ProwJob{ 162 Spec: kube.ProwJobSpec{ 163 Agent: kube.KubernetesAgent, 164 Job: "job", 165 }, 166 Status: kube.ProwJobStatus{ 167 PodName: "wowowow", 168 BuildID: "123", 169 }, 170 }, 171 kube.ProwJob{ 172 Spec: kube.ProwJobSpec{ 173 Agent: kube.KubernetesAgent, 174 Job: "jib", 175 Cluster: "trusted", 176 }, 177 Status: kube.ProwJobStatus{ 178 PodName: "powowow", 179 BuildID: "123", 180 }, 181 }, 182 } 183 fakeJa = jobs.NewJobAgent(kc, map[string]jobs.PodLogClient{kube.DefaultClusterAlias: fpkc("clusterA"), "trusted": fpkc("clusterB")}, &config.Agent{}) 184 fakeJa.Start() 185 os.Exit(m.Run()) 186 } 187 188 type dumpLens struct{} 189 190 func (dumpLens) Name() string { 191 return "dump" 192 } 193 194 func (dumpLens) Title() string { 195 return "Dump View" 196 } 197 198 func (dumpLens) Priority() int { 199 return 1 200 } 201 202 func (dumpLens) Header(artifacts []lenses.Artifact, resourceDir string) string { 203 return "" 204 } 205 206 func (dumpLens) Body(artifacts []lenses.Artifact, resourceDir, data string) string { 207 var view []byte 208 for _, a := range artifacts { 209 data, err := a.ReadAll() 210 if err != nil { 211 logrus.WithError(err).Error("Error reading artifact") 212 continue 213 } 214 view = append(view, data...) 215 } 216 return string(view) 217 } 218 219 func (dumpLens) Callback(artifacts []lenses.Artifact, resourceDir, data string) string { 220 return "" 221 } 222 223 func TestViews(t *testing.T) { 224 fakeGCSClient := fakeGCSServer.Client() 225 testCases := []struct { 226 name string 227 registeredViewers []lenses.Lens 228 matchCache map[string][]string 229 expectedLensTitles []string 230 }{ 231 { 232 name: "Spyglass basic test", 233 registeredViewers: []lenses.Lens{dumpLens{}}, 234 matchCache: map[string][]string{ 235 "dump": {"started.json"}, 236 }, 237 expectedLensTitles: []string{"Dump View"}, 238 }, 239 { 240 name: "Spyglass no matches", 241 registeredViewers: []lenses.Lens{dumpLens{}}, 242 matchCache: map[string][]string{ 243 "dump": {}, 244 }, 245 }, 246 } 247 248 for _, tc := range testCases { 249 t.Run(tc.name, func(t *testing.T) { 250 for _, l := range tc.registeredViewers { 251 lenses.RegisterLens(l) 252 } 253 sg := New(fakeJa, &config.Agent{}, fakeGCSClient) 254 lenses := sg.Lenses(tc.matchCache) 255 for _, l := range lenses { 256 var found bool 257 for _, title := range tc.expectedLensTitles { 258 if title == l.Title() { 259 found = true 260 } 261 } 262 if !found { 263 t.Errorf("lens title %s not found in expected titles.", l.Title()) 264 } 265 } 266 for _, title := range tc.expectedLensTitles { 267 var found bool 268 for _, l := range lenses { 269 if title == l.Title() { 270 found = true 271 } 272 } 273 if !found { 274 t.Errorf("expected title %s not found in produced lenses.", title) 275 } 276 } 277 }) 278 } 279 } 280 281 func TestSplitSrc(t *testing.T) { 282 testCases := []struct { 283 name string 284 src string 285 expKeyType string 286 expKey string 287 expError bool 288 }{ 289 { 290 name: "empty string", 291 src: "", 292 expError: true, 293 }, 294 { 295 name: "missing key", 296 src: "gcs", 297 expError: true, 298 }, 299 { 300 name: "prow key", 301 src: "prowjob/example-job-name/123456", 302 expKeyType: "prowjob", 303 expKey: "example-job-name/123456", 304 }, 305 { 306 name: "gcs key", 307 src: "gcs/kubernetes-jenkins/pr-logs/pull/test-infra/0000/example-job-name/314159/", 308 expKeyType: "gcs", 309 expKey: "kubernetes-jenkins/pr-logs/pull/test-infra/0000/example-job-name/314159/", 310 }, 311 } 312 for _, tc := range testCases { 313 keyType, key, err := splitSrc(tc.src) 314 if tc.expError && err == nil { 315 t.Errorf("test %q expected error", tc.name) 316 } 317 if !tc.expError && err != nil { 318 t.Errorf("test %q encountered unexpected error: %v", tc.name, err) 319 } 320 if keyType != tc.expKeyType || key != tc.expKey { 321 t.Errorf("test %q: splitting src %q: Expected <%q, %q>, got <%q, %q>", 322 tc.name, tc.src, tc.expKeyType, tc.expKey, keyType, key) 323 } 324 } 325 } 326 327 func TestJobPath(t *testing.T) { 328 kc := fkc{ 329 kube.ProwJob{ 330 Spec: kube.ProwJobSpec{ 331 Type: kube.PeriodicJob, 332 Job: "example-periodic-job", 333 DecorationConfig: &kube.DecorationConfig{ 334 GCSConfiguration: &kube.GCSConfiguration{ 335 Bucket: "chum-bucket", 336 }, 337 }, 338 }, 339 Status: kube.ProwJobStatus{ 340 PodName: "flying-whales", 341 BuildID: "1111", 342 }, 343 }, 344 kube.ProwJob{ 345 Spec: kube.ProwJobSpec{ 346 Type: kube.PresubmitJob, 347 Job: "example-presubmit-job", 348 DecorationConfig: &kube.DecorationConfig{ 349 GCSConfiguration: &kube.GCSConfiguration{ 350 Bucket: "chum-bucket", 351 }, 352 }, 353 }, 354 Status: kube.ProwJobStatus{ 355 PodName: "flying-whales", 356 BuildID: "2222", 357 }, 358 }, 359 kube.ProwJob{ 360 Spec: kube.ProwJobSpec{ 361 Type: kube.PresubmitJob, 362 Job: "undecorated-job", 363 }, 364 Status: kube.ProwJobStatus{ 365 PodName: "flying-whales", 366 BuildID: "1", 367 }, 368 }, 369 kube.ProwJob{ 370 Spec: kube.ProwJobSpec{ 371 Type: kube.PresubmitJob, 372 Job: "missing-gcs-job", 373 DecorationConfig: &kube.DecorationConfig{}, 374 }, 375 Status: kube.ProwJobStatus{ 376 PodName: "flying-whales", 377 BuildID: "1", 378 }, 379 }, 380 } 381 fakeJa = jobs.NewJobAgent(kc, map[string]jobs.PodLogClient{kube.DefaultClusterAlias: fpkc("clusterA"), "trusted": fpkc("clusterB")}, &config.Agent{}) 382 fakeJa.Start() 383 testCases := []struct { 384 name string 385 src string 386 expJobPath string 387 expError bool 388 }{ 389 { 390 name: "non-presubmit job in GCS with trailing /", 391 src: "gcs/kubernetes-jenkins/logs/example-job-name/123/", 392 expJobPath: "kubernetes-jenkins/logs/example-job-name", 393 }, 394 { 395 name: "non-presubmit job in GCS without trailing /", 396 src: "gcs/kubernetes-jenkins/logs/example-job-name/123", 397 expJobPath: "kubernetes-jenkins/logs/example-job-name", 398 }, 399 { 400 name: "presubmit job in GCS with trailing /", 401 src: "gcs/kubernetes-jenkins/pr-logs/pull/test-infra/0000/example-job-name/314159/", 402 expJobPath: "kubernetes-jenkins/pr-logs/directory/example-job-name", 403 }, 404 { 405 name: "presubmit job in GCS without trailing /", 406 src: "gcs/kubernetes-jenkins/pr-logs/pull/test-infra/0000/example-job-name/314159", 407 expJobPath: "kubernetes-jenkins/pr-logs/directory/example-job-name", 408 }, 409 { 410 name: "non-presubmit Prow job", 411 src: "prowjob/example-periodic-job/1111", 412 expJobPath: "chum-bucket/logs/example-periodic-job", 413 }, 414 { 415 name: "Prow presubmit job", 416 src: "prowjob/example-presubmit-job/2222", 417 expJobPath: "chum-bucket/pr-logs/directory/example-presubmit-job", 418 }, 419 { 420 name: "nonexistent job", 421 src: "prowjob/example-periodic-job/0000", 422 expError: true, 423 }, 424 { 425 name: "invalid key type", 426 src: "oh/my/glob/drama/bomb", 427 expError: true, 428 }, 429 { 430 name: "invalid GCS path", 431 src: "gcs/kubernetes-jenkins/bad-path", 432 expError: true, 433 }, 434 { 435 name: "job missing decoration", 436 src: "prowjob/undecorated-job/1", 437 expError: true, 438 }, 439 { 440 name: "job missing GCS config", 441 src: "prowjob/missing-gcs-job/1", 442 expError: true, 443 }, 444 } 445 for _, tc := range testCases { 446 fakeGCSClient := fakeGCSServer.Client() 447 sg := New(fakeJa, &config.Agent{}, fakeGCSClient) 448 jobPath, err := sg.JobPath(tc.src) 449 if tc.expError && err == nil { 450 t.Errorf("test %q: JobPath(%q) expected error", tc.name, tc.src) 451 continue 452 } 453 if !tc.expError && err != nil { 454 t.Errorf("test %q: JobPath(%q) returned unexpected error %v", tc.name, tc.src, err) 455 continue 456 } 457 if jobPath != tc.expJobPath { 458 t.Errorf("test %q: JobPath(%q) expected %q, got %q", tc.name, tc.src, tc.expJobPath, jobPath) 459 } 460 } 461 } 462 463 func TestProwToGCS(t *testing.T) { 464 kc := fkc{ 465 kube.ProwJob{ 466 Spec: kube.ProwJobSpec{ 467 Job: "gubernator-job", 468 }, 469 Status: kube.ProwJobStatus{ 470 URL: "https://gubernator.example.com/build/some-bucket/gubernator-job/1111/", 471 BuildID: "1111", 472 }, 473 }, 474 kube.ProwJob{ 475 Spec: kube.ProwJobSpec{ 476 Job: "spyglass-job", 477 }, 478 Status: kube.ProwJobStatus{ 479 URL: "https://prow.example.com/view/gcs/some-bucket/spyglass-job/2222/", 480 BuildID: "2222", 481 }, 482 }, 483 } 484 fakeJa = jobs.NewJobAgent(kc, map[string]jobs.PodLogClient{kube.DefaultClusterAlias: fpkc("clusterA"), "trusted": fpkc("clusterB")}, &config.Agent{}) 485 fakeJa.Start() 486 487 testCases := []struct { 488 name string 489 key string 490 configPrefix string 491 expectedPath string 492 expectError bool 493 }{ 494 { 495 name: "extraction from gubernator-like URL", 496 key: "gubernator-job/1111", 497 configPrefix: "https://gubernator.example.com/build/", 498 expectedPath: "some-bucket/gubernator-job/1111/", 499 expectError: false, 500 }, 501 { 502 name: "extraction from spyglass-like URL", 503 key: "spyglass-job/2222", 504 configPrefix: "https://prow.example.com/view/gcs/", 505 expectedPath: "some-bucket/spyglass-job/2222/", 506 expectError: false, 507 }, 508 { 509 name: "failed extraction from wrong URL", 510 key: "spyglass-job/1111", 511 configPrefix: "https://gubernator.example.com/build/", 512 expectedPath: "", 513 expectError: true, 514 }, 515 { 516 name: "prefix longer than URL", 517 key: "spyglass-job/2222", 518 configPrefix: strings.Repeat("!", 100), 519 expectError: true, 520 }, 521 } 522 523 for _, tc := range testCases { 524 fakeGCSClient := fakeGCSServer.Client() 525 fakeConfigAgent := fca{ 526 c: config.Config{ 527 ProwConfig: config.ProwConfig{ 528 Plank: config.Plank{ 529 JobURLPrefix: tc.configPrefix, 530 }, 531 }, 532 }, 533 } 534 sg := New(fakeJa, fakeConfigAgent, fakeGCSClient) 535 536 p, err := sg.prowToGCS(tc.key) 537 if err != nil && !tc.expectError { 538 t.Errorf("test %q: unexpected error: %v", tc.key, err) 539 continue 540 } 541 if err == nil && tc.expectError { 542 t.Errorf("test %q: expected an error but instead got success and path '%s'", tc.key, p) 543 continue 544 } 545 if p != tc.expectedPath { 546 t.Errorf("test %q: expected '%s' but got '%s'", tc.key, tc.expectedPath, p) 547 } 548 } 549 } 550 551 func TestFetchArtifactsPodLog(t *testing.T) { 552 kc := fkc{ 553 kube.ProwJob{ 554 Spec: kube.ProwJobSpec{ 555 Agent: kube.KubernetesAgent, 556 Job: "job", 557 }, 558 Status: kube.ProwJobStatus{ 559 PodName: "wowowow", 560 BuildID: "123", 561 URL: "https://gubernator.example.com/build/job/123", 562 }, 563 }, 564 } 565 fakeConfigAgent := fca{ 566 c: config.Config{ 567 ProwConfig: config.ProwConfig{ 568 Plank: config.Plank{ 569 JobURLPrefix: "https://gubernator.example.com/build/", 570 }, 571 }, 572 }, 573 } 574 fakeJa = jobs.NewJobAgent(kc, map[string]jobs.PodLogClient{kube.DefaultClusterAlias: fpkc("clusterA")}, &config.Agent{}) 575 fakeJa.Start() 576 577 fakeGCSClient := fakeGCSServer.Client() 578 579 sg := New(fakeJa, fakeConfigAgent, fakeGCSClient) 580 testKeys := []string{ 581 "prowjob/job/123", 582 "gcs/kubernetes-jenkins/logs/job/123/", 583 "gcs/kubernetes-jenkins/logs/job/123", 584 } 585 586 for _, key := range testKeys { 587 result, err := sg.FetchArtifacts(key, "", 500e6, []string{"build-log.txt"}) 588 if err != nil { 589 t.Errorf("Unexpected error grabbing pod log for %s: %v", key, err) 590 continue 591 } 592 if len(result) != 1 { 593 t.Errorf("Expected 1 artifact for %s, got %d", key, len(result)) 594 continue 595 } 596 content, err := result[0].ReadAll() 597 if err != nil { 598 t.Errorf("Unexpected error reading pod log for %s: %v", key, err) 599 continue 600 } 601 if string(content) != "clusterA" { 602 t.Errorf("Bad pod log content for %s: %q (expected 'clusterA')", key, content) 603 } 604 } 605 }