sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/sidecar/run_test.go (about) 1 /* 2 Copyright 2017 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 sidecar 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "io" 24 "os" 25 "path" 26 "path/filepath" 27 "regexp" 28 "strconv" 29 "strings" 30 "sync" 31 "testing" 32 "time" 33 34 "github.com/sirupsen/logrus" 35 "sigs.k8s.io/prow/pkg/entrypoint" 36 "sigs.k8s.io/prow/pkg/gcsupload" 37 "sigs.k8s.io/prow/pkg/pod-utils/downwardapi" 38 "sigs.k8s.io/prow/pkg/pod-utils/wrapper" 39 40 "k8s.io/apimachinery/pkg/api/equality" 41 "k8s.io/apimachinery/pkg/util/diff" 42 "k8s.io/apimachinery/pkg/util/sets" 43 prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 44 ) 45 46 var re = regexp.MustCompile(`(?m)(Failed to open) .*log\.txt: .*$`) 47 48 func TestWait(t *testing.T) { 49 aborted := strconv.Itoa(entrypoint.AbortedErrorCode) 50 skip := strconv.Itoa(entrypoint.PreviousErrorCode) 51 const ( 52 pass = "0" 53 fail = "1" 54 ) 55 cases := []struct { 56 name string 57 markers []string 58 abort bool 59 pass bool 60 accessDenied bool 61 missing bool 62 failures int 63 }{ 64 { 65 name: "pass, not abort when 1 item passes", 66 markers: []string{pass}, 67 pass: true, 68 }, 69 { 70 name: "pass when all items pass", 71 markers: []string{pass, pass, pass}, 72 pass: true, 73 }, 74 { 75 name: "fail, not abort when 1 item fails", 76 markers: []string{fail}, 77 failures: 1, 78 }, 79 { 80 name: "fail when any item fails", 81 markers: []string{pass, fail, pass}, 82 failures: 1, 83 }, 84 { 85 name: "abort and fail when 1 item aborts", 86 markers: []string{aborted}, 87 abort: true, 88 failures: 1, 89 }, 90 { 91 name: "abort when any item aborts", 92 markers: []string{pass, aborted, fail}, 93 abort: true, 94 failures: 2, 95 }, 96 { 97 name: "fail when marker cannot be read", 98 markers: []string{pass, "not-an-exit-code", pass}, 99 failures: 1, 100 }, 101 { 102 name: "fail when marker does not exist", 103 markers: []string{pass}, 104 missing: true, 105 failures: 1, 106 }, 107 { 108 name: "count all failures", 109 markers: []string{pass, fail, aborted, skip, fail, pass}, 110 abort: true, 111 failures: 3, 112 }, 113 } 114 115 for _, tc := range cases { 116 t.Run(tc.name, func(t *testing.T) { 117 tmpDir := t.TempDir() 118 119 var entries []wrapper.Options 120 121 for i, m := range tc.markers { 122 p := path.Join(tmpDir, fmt.Sprintf("marker-%d.txt", i)) 123 var opt wrapper.Options 124 opt.MarkerFile = p 125 if err := os.WriteFile(p, []byte(m), 0600); err != nil { 126 t.Fatalf("could not create marker %d: %v", i, err) 127 } 128 entries = append(entries, opt) 129 } 130 131 ctx, cancel := context.WithCancel(context.Background()) 132 if tc.missing { 133 entries = append(entries, wrapper.Options{MarkerFile: "missing-marker.txt"}) 134 go cancel() 135 } 136 137 pass, abort, failures := wait(ctx, entries) 138 cancel() 139 if pass != tc.pass { 140 t.Errorf("expected pass %t != actual %t", tc.pass, pass) 141 } 142 if abort != tc.abort { 143 t.Errorf("expected abort %t != actual %t", tc.abort, abort) 144 } 145 if failures != tc.failures { 146 t.Errorf("expected failures %d != actual %d", tc.failures, failures) 147 } 148 }) 149 } 150 } 151 152 func TestWaitParallelContainers(t *testing.T) { 153 aborted := strconv.Itoa(entrypoint.AbortedErrorCode) 154 skip := strconv.Itoa(entrypoint.PreviousErrorCode) 155 const ( 156 pass = "0" 157 fail = "1" 158 missingMarkerTimeout = time.Second 159 ) 160 cases := []struct { 161 name string 162 markers []string 163 abort bool 164 pass bool 165 accessDenied bool 166 missing bool 167 failures int 168 }{ 169 { 170 name: "pass, not abort when 1 item passes", 171 markers: []string{pass}, 172 pass: true, 173 }, 174 { 175 name: "pass when all items pass", 176 markers: []string{pass, pass, pass}, 177 pass: true, 178 }, 179 { 180 name: "fail, not abort when 1 item fails", 181 markers: []string{fail}, 182 failures: 1, 183 }, 184 { 185 name: "fail when any item fails", 186 markers: []string{pass, fail, pass}, 187 failures: 1, 188 }, 189 { 190 name: "abort and fail when 1 item aborts", 191 markers: []string{aborted}, 192 abort: true, 193 failures: 1, 194 }, 195 { 196 name: "abort when any item aborts", 197 markers: []string{pass, aborted, fail}, 198 abort: true, 199 failures: 2, 200 }, 201 { 202 name: "fail when marker does not exist", 203 markers: []string{pass}, 204 missing: true, 205 failures: 1, 206 }, 207 { 208 name: "count all failures", 209 markers: []string{pass, fail, aborted, skip, fail, pass}, 210 abort: true, 211 failures: 3, 212 }, 213 } 214 215 for _, tc := range cases { 216 t.Run(tc.name, func(t *testing.T) { 217 tmpDir := t.TempDir() 218 219 var entries []wrapper.Options 220 221 for i := range tc.markers { 222 p := path.Join(tmpDir, fmt.Sprintf("marker-%d.txt", i)) 223 var opt wrapper.Options 224 opt.MarkerFile = p 225 entries = append(entries, opt) 226 } 227 228 if tc.missing { 229 missingPath := path.Join(tmpDir, "missing-marker.txt") 230 entries = append(entries, wrapper.Options{MarkerFile: missingPath}) 231 } 232 233 ctx, cancel := context.WithCancel(context.Background()) 234 235 type WaitResult struct { 236 pass bool 237 abort bool 238 failures int 239 } 240 241 waitResultsCh := make(chan WaitResult) 242 243 go func() { 244 pass, abort, failures := wait(ctx, entries) 245 waitResultsCh <- WaitResult{pass, abort, failures} 246 }() 247 248 errCh := make(chan error, len(tc.markers)) 249 for i, m := range tc.markers { 250 251 options := entries[i] 252 253 entrypointOptions := entrypoint.Options{ 254 Options: &options, 255 } 256 marker, err := strconv.Atoi(m) 257 if err != nil { 258 errCh <- fmt.Errorf("invalid exit code: %w", err) 259 } 260 go func() { 261 errCh <- entrypointOptions.Mark(marker) 262 }() 263 264 } 265 266 if tc.missing { 267 go func() { 268 time.Sleep(missingMarkerTimeout) 269 cancel() 270 errCh <- nil 271 }() 272 } 273 274 for range tc.markers { 275 if err := <-errCh; err != nil { 276 t.Fatalf("could not create marker: %v", err) 277 } 278 } 279 280 waitRes := <-waitResultsCh 281 282 cancel() 283 if waitRes.pass != tc.pass { 284 t.Errorf("expected pass %t != actual %t", tc.pass, waitRes.pass) 285 } 286 if waitRes.abort != tc.abort { 287 t.Errorf("expected abort %t != actual %t", tc.abort, waitRes.abort) 288 } 289 if waitRes.failures != tc.failures { 290 t.Errorf("expected failures %d != actual %d", tc.failures, waitRes.failures) 291 } 292 }) 293 } 294 } 295 296 func TestCombineMetadata(t *testing.T) { 297 cases := []struct { 298 name string 299 pieces []string 300 expected map[string]interface{} 301 }{ 302 { 303 name: "no problem when metadata file is not there", 304 pieces: []string{"missing"}, 305 }, 306 { 307 name: "simple metadata", 308 pieces: []string{`{"hello": "world"}`}, 309 expected: map[string]interface{}{ 310 "hello": "world", 311 }, 312 }, 313 { 314 name: "merge pieces", 315 pieces: []string{ 316 `{"hello": "hello", "world": "world", "first": 1}`, 317 `{"hello": "hola", "world": "world", "second": 2}`, 318 }, 319 expected: map[string]interface{}{ 320 "hello": "hola", 321 "world": "world", 322 "first": 1.0, 323 "second": 2.0, 324 }, 325 }, 326 { 327 name: "errors go into sidecar-errors", 328 pieces: []string{ 329 `{"hello": "there"}`, 330 "missing", 331 "read-error", 332 "json-error", // this is invalid json 333 `{"world": "thanks"}`, 334 }, 335 expected: map[string]interface{}{ 336 "hello": "there", 337 "world": "thanks", 338 errorKey: map[string]error{ 339 name(2): errors.New("read"), 340 name(3): errors.New("json"), 341 }, 342 }, 343 }, 344 } 345 346 for _, tc := range cases { 347 t.Run(tc.name, func(t *testing.T) { 348 tmpDir := t.TempDir() 349 var entries []wrapper.Options 350 351 for i, m := range tc.pieces { 352 p := path.Join(tmpDir, fmt.Sprintf("metadata-%d.txt", i)) 353 var opt wrapper.Options 354 opt.MetadataFile = p 355 entries = append(entries, opt) 356 if m == "missing" { 357 continue 358 } else if m == "read-error" { 359 if err := os.Mkdir(p, 0700); err != nil { 360 t.Fatalf("could not create %s: %v", p, err) 361 } 362 continue 363 } 364 // not-json is invalid json 365 if err := os.WriteFile(p, []byte(m), 0600); err != nil { 366 t.Fatalf("could not create metadata %d: %v", i, err) 367 } 368 } 369 370 actual := combineMetadata(entries) 371 expectedErrors, _ := tc.expected[errorKey].(map[string]error) 372 actualErrors, _ := actual[errorKey].(map[string]error) 373 delete(tc.expected, errorKey) 374 delete(actual, errorKey) 375 if !equality.Semantic.DeepEqual(tc.expected, actual) { 376 t.Errorf("maps do not match:\n%s", diff.ObjectReflectDiff(tc.expected, actual)) 377 } 378 379 if !equality.Semantic.DeepEqual(sets.KeySet[string](expectedErrors), sets.KeySet[string](actualErrors)) { // ignore the error values 380 t.Errorf("errors do not match:\n%s", diff.ObjectReflectDiff(expectedErrors, actualErrors)) 381 } 382 }) 383 } 384 } 385 386 func name(idx int) string { 387 return nameEntry(idx, wrapper.Options{}) 388 } 389 390 func TestLogReaders(t *testing.T) { 391 cases := []struct { 392 name string 393 containerNames []string 394 processLogs map[string]string 395 expected map[string]string 396 }{ 397 { 398 name: "works with 1 container", 399 containerNames: []string{ 400 "test", 401 }, 402 processLogs: map[string]string{ 403 "process-log.txt": "hello world", 404 }, 405 expected: map[string]string{ 406 "build-log.txt": "hello world", 407 }, 408 }, 409 { 410 name: "works with 1 container with no name", 411 containerNames: []string{ 412 "", 413 }, 414 processLogs: map[string]string{ 415 "process-log.txt": "hello world", 416 }, 417 expected: map[string]string{ 418 "build-log.txt": "hello world", 419 }, 420 }, 421 { 422 name: "multiple logs works", 423 containerNames: []string{ 424 "test1", 425 "test2", 426 }, 427 processLogs: map[string]string{ 428 "test1-log.txt": "hello", 429 "test2-log.txt": "world", 430 }, 431 expected: map[string]string{ 432 "test1-build-log.txt": "hello", 433 "test2-build-log.txt": "world", 434 }, 435 }, 436 { 437 name: "note when a part has a problem", 438 containerNames: []string{ 439 "test1", 440 "test2", 441 "test3", 442 }, 443 processLogs: map[string]string{ 444 "test1-log.txt": "hello", 445 "test2-log.txt": "missing", 446 "test3-log.txt": "world", 447 }, 448 expected: map[string]string{ 449 "test1-build-log.txt": "hello", 450 "test2-build-log.txt": "Failed to open test2-log.txt: whatever\n", 451 "test3-build-log.txt": "world", 452 }, 453 }, 454 } 455 456 for _, tc := range cases { 457 t.Run(tc.name, func(t *testing.T) { 458 tmpDir := t.TempDir() 459 460 for name, log := range tc.processLogs { 461 p := path.Join(tmpDir, name) 462 if log == "missing" { 463 continue 464 } 465 if err := os.WriteFile(p, []byte(log), 0600); err != nil { 466 t.Fatalf("could not create log %s: %v", name, err) 467 } 468 } 469 470 var entries []wrapper.Options 471 472 for _, containerName := range tc.containerNames { 473 log := "process-log.txt" 474 if len(tc.containerNames) > 1 { 475 log = fmt.Sprintf("%s-log.txt", containerName) 476 } 477 p := path.Join(tmpDir, log) 478 var opt wrapper.Options 479 opt.ProcessLog = p 480 opt.ContainerName = containerName 481 entries = append(entries, opt) 482 } 483 484 readers := logReadersFuncs(entries) 485 const repl = "$1 <SNIP>" 486 actual := make(map[string]string) 487 for name, newReader := range readers { 488 r, err := newReader() 489 if err != nil { 490 t.Fatalf("failed to make reader: %v", err) 491 } 492 buf, err := io.ReadAll(r) 493 if err != nil { 494 t.Fatalf("failed to read all: %v", err) 495 } 496 actual[name] = re.ReplaceAllString(string(buf), repl) 497 if err := r.Close(); err != nil { 498 t.Fatalf("failed to close reader: %v", err) 499 } 500 } 501 502 for name, log := range tc.expected { 503 tc.expected[name] = re.ReplaceAllString(log, repl) 504 } 505 506 if !equality.Semantic.DeepEqual(tc.expected, actual) { 507 t.Errorf("maps do not match:\n%s", diff.ObjectReflectDiff(tc.expected, actual)) 508 } 509 }) 510 } 511 512 } 513 514 func TestSideCarLogsUpload(t *testing.T) { 515 logFile, err := LogSetup() 516 if err != nil { 517 t.Fatalf("Unable to set up log file") 518 } 519 defer os.Remove(logFile.Name()) 520 testString := "Testing...Hello world!" 521 logrus.Info(testString) 522 var once sync.Once 523 524 localOutputDir := t.TempDir() 525 526 options := Options{ 527 GcsOptions: &gcsupload.Options{ 528 GCSConfiguration: &prowapi.GCSConfiguration{ 529 PathStrategy: prowapi.PathStrategyExplicit, 530 Bucket: "bucket", 531 LocalOutputDir: localOutputDir, 532 }, 533 }, 534 } 535 536 spec := &downwardapi.JobSpec{ 537 Job: "job", 538 Type: prowapi.PostsubmitJob, 539 Refs: &prowapi.Refs{ 540 Org: "org", 541 Repo: "repo", 542 Pulls: []prowapi.Pull{ 543 { 544 Number: 1, 545 }, 546 }, 547 }, 548 BuildID: "build", 549 } 550 551 entries := options.entries() 552 metadata := combineMetadata(entries) 553 buildLogs := logReadersFuncs(entries) 554 555 options.doUpload(context.Background(), spec, true, false, metadata, buildLogs, logFile, &once) 556 557 files, err := os.ReadDir(localOutputDir) 558 if err != nil { 559 t.Errorf("Unable to access files in directory: %v", err) 560 } 561 if len(files) == 0 { 562 t.Fatal("Log file was not uploaded") 563 } 564 565 var s []byte 566 s, err = os.ReadFile(filepath.Join(localOutputDir, LogFileName)) 567 if err != nil { 568 t.Fatalf("Unable to read log file: %v", err) 569 } 570 571 f := string(s) 572 if !strings.Contains(f, testString) { 573 t.Fatal("Log file not correctly capturing logs") 574 } 575 576 }