sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/spyglass/storageartifact_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 "bytes" 21 "compress/gzip" 22 "context" 23 "fmt" 24 "io" 25 "testing" 26 27 prowv1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 28 pkgio "sigs.k8s.io/prow/pkg/io" 29 "sigs.k8s.io/prow/pkg/spyglass/api" 30 "sigs.k8s.io/prow/pkg/spyglass/lenses" 31 ) 32 33 type ByteReadCloser struct { 34 io.Reader 35 incompleteRead bool 36 } 37 38 func (rc *ByteReadCloser) Close() error { 39 return nil 40 } 41 42 func (rc *ByteReadCloser) Read(p []byte) (int, error) { 43 if rc.incompleteRead { 44 p = p[:len(p)/2] 45 rc.incompleteRead = false 46 } 47 read, err := rc.Reader.Read(p) 48 if err != nil { 49 return 0, err 50 } 51 if bytes.Equal(p[:read], []byte("deeper unreadable contents")) { 52 return 0, fmt.Errorf("it's just turtes all the way down") 53 } 54 return read, nil 55 } 56 57 type fakeArtifactHandle struct { 58 oAttrs pkgio.Attributes 59 contents []byte 60 incompleteRead bool 61 } 62 63 func (h *fakeArtifactHandle) Attrs(ctx context.Context) (pkgio.Attributes, error) { 64 if bytes.Equal(h.contents, []byte("no attrs")) { 65 return pkgio.Attributes{}, fmt.Errorf("error getting attrs") 66 } 67 return h.oAttrs, nil 68 } 69 70 func (h *fakeArtifactHandle) UpdateAttrs(ctx context.Context, cur pkgio.ObjectAttrsToUpdate) (*pkgio.Attributes, error) { 71 if cur.ContentEncoding != nil { 72 h.oAttrs.ContentEncoding = *cur.ContentEncoding 73 } 74 for name, value := range cur.Metadata { 75 if value != "" { 76 cur.Metadata[name] = value 77 } else if cur.Metadata[name] != "" { 78 delete(cur.Metadata, name) 79 } 80 } 81 return &h.oAttrs, nil 82 } 83 84 func (h *fakeArtifactHandle) NewRangeReader(ctx context.Context, offset, length int64) (io.ReadCloser, error) { 85 if bytes.Equal(h.contents, []byte("unreadable contents")) { 86 return nil, fmt.Errorf("cannot read unreadable contents") 87 } 88 lenContents := int64(len(h.contents)) 89 var err error 90 var toRead int64 91 if length < 0 { 92 toRead = lenContents - offset 93 err = io.EOF 94 } else { 95 toRead = length 96 if offset+length > lenContents { 97 toRead = lenContents - offset 98 err = io.EOF 99 } 100 } 101 return &ByteReadCloser{bytes.NewReader(h.contents[offset : offset+toRead]), h.incompleteRead}, err 102 } 103 104 func (h *fakeArtifactHandle) NewReader(ctx context.Context) (io.ReadCloser, error) { 105 var buf bytes.Buffer 106 zw := gzip.NewWriter(&buf) 107 _, err := zw.Write([]byte("unreadable contents")) 108 if err != nil { 109 return nil, fmt.Errorf("Failed to gzip log text, err: %w", err) 110 } 111 if err := zw.Close(); err != nil { 112 return nil, fmt.Errorf("Failed to close gzip writer, err: %w", err) 113 } 114 if bytes.Equal(h.contents, buf.Bytes()) { 115 return nil, fmt.Errorf("cannot read unreadable contents, even if they're gzipped") 116 } 117 if bytes.Equal(h.contents, []byte("unreadable contents")) { 118 return nil, fmt.Errorf("cannot read unreadable contents") 119 } 120 return &ByteReadCloser{bytes.NewReader(h.contents), false}, nil 121 } 122 123 // Tests reading the tail n bytes of data from an artifact 124 func TestReadTail(t *testing.T) { 125 var buf bytes.Buffer 126 zw := gzip.NewWriter(&buf) 127 _, err := zw.Write([]byte("Oh wow\nlogs\nthis is\ncrazy")) 128 if err != nil { 129 t.Fatalf("Failed to gzip log text, err: %v", err) 130 } 131 if err := zw.Close(); err != nil { 132 t.Fatalf("Failed to close gzip writer, err: %v", err) 133 } 134 gzippedLog := buf.Bytes() 135 testCases := []struct { 136 name string 137 n int64 138 contents []byte 139 encoding string 140 expected []byte 141 expectErr bool 142 }{ 143 { 144 name: "ReadTail example build log", 145 n: 4, 146 contents: []byte("Oh wow\nlogs\nthis is\ncrazy"), 147 expected: []byte("razy"), 148 expectErr: false, 149 }, 150 { 151 name: "ReadTail build log, gzipped", 152 n: 23, 153 contents: gzippedLog, 154 encoding: "gzip", 155 expectErr: true, 156 }, 157 { 158 name: "ReadTail build log, claimed gzipped but not actually gzipped", 159 n: 2333, 160 contents: []byte("Oh wow\nlogs\nthis is\ncrazy"), 161 encoding: "gzip", 162 expectErr: true, 163 }, 164 { 165 name: "ReadTail N>size of build log", 166 n: 2222, 167 contents: []byte("Oh wow\nlogs\nthis is\ncrazy"), 168 expected: []byte("Oh wow\nlogs\nthis is\ncrazy"), 169 expectErr: false, 170 }, 171 } 172 for _, tc := range testCases { 173 artifact := NewStorageArtifact(context.Background(), &fakeArtifactHandle{ 174 contents: tc.contents, 175 oAttrs: pkgio.Attributes{ 176 Size: int64(len(tc.contents)), 177 ContentEncoding: tc.encoding, 178 }, 179 }, "", "build-log.txt", 500e6) 180 actualBytes, err := artifact.ReadTail(tc.n) 181 if err != nil && !tc.expectErr { 182 t.Fatalf("Test %s failed with err: %v", tc.name, err) 183 } 184 if err == nil && tc.expectErr { 185 t.Errorf("Test %s did not produce error when expected", tc.name) 186 } 187 if !bytes.Equal(actualBytes, tc.expected) { 188 t.Errorf("Test %s failed.\nExpected: %s\nActual: %s", tc.name, tc.expected, actualBytes) 189 } 190 } 191 } 192 193 // Tests reading at most n bytes of data from files in GCS 194 func TestReadAtMost(t *testing.T) { 195 var buf bytes.Buffer 196 zw := gzip.NewWriter(&buf) 197 _, err := zw.Write([]byte("Oh wow\nlogs\nthis is\ncrazy")) 198 if err != nil { 199 t.Fatalf("Failed to gzip log text, err: %v", err) 200 } 201 if err := zw.Close(); err != nil { 202 t.Fatalf("Failed to close gzip writer, err: %v", err) 203 } 204 testCases := []struct { 205 name string 206 n int64 207 contents []byte 208 encoding string 209 expected []byte 210 expectErr bool 211 expectEOF bool 212 }{ 213 { 214 name: "ReadAtMost example build log", 215 n: 4, 216 contents: []byte("Oh wow\nlogs\nthis is\ncrazy"), 217 expected: []byte("Oh w"), 218 expectErr: false, 219 }, 220 { 221 name: "ReadAtMost build log, transparently gzipped", 222 n: 8, 223 contents: []byte("Oh wow\nlogs\nthis is\ncrazy"), 224 expected: []byte("Oh wow\nl"), 225 encoding: "gzip", 226 expectErr: false, 227 }, 228 { 229 name: "ReadAtMost unreadable contents", 230 n: 2, 231 contents: []byte("unreadable contents"), 232 expectErr: true, 233 }, 234 { 235 name: "ReadAtMost unreadable contents", 236 n: 45, 237 contents: []byte("deeper unreadable contents"), 238 expectErr: true, 239 }, 240 { 241 name: "ReadAtMost N>size of build log", 242 n: 2222, 243 contents: []byte("Oh wow\nlogs\nthis is\ncrazy"), 244 expected: []byte("Oh wow\nlogs\nthis is\ncrazy"), 245 expectErr: true, 246 expectEOF: true, 247 }, 248 } 249 for _, tc := range testCases { 250 artifact := NewStorageArtifact(context.Background(), &fakeArtifactHandle{ 251 contents: tc.contents, 252 oAttrs: pkgio.Attributes{ 253 Size: int64(len(tc.contents)), 254 ContentEncoding: tc.encoding, 255 }, 256 }, "", "build-log.txt", 500e6) 257 actualBytes, err := artifact.ReadAtMost(tc.n) 258 if err != nil && !tc.expectErr { 259 if tc.expectEOF && err != io.EOF { 260 t.Fatalf("Test %s failed with err: %v, expected EOF", tc.name, err) 261 } 262 t.Fatalf("Test %s failed with err: %v", tc.name, err) 263 } 264 if err != nil && tc.expectEOF && err != io.EOF { 265 t.Fatalf("Test %s failed with err: %v, expected EOF", tc.name, err) 266 } 267 if err == nil && tc.expectErr { 268 t.Errorf("Test %s did not produce error when expected", tc.name) 269 } 270 if !bytes.Equal(actualBytes, tc.expected) { 271 t.Errorf("Test %s failed.\nExpected: %s\nActual: %s", tc.name, tc.expected, actualBytes) 272 } 273 } 274 } 275 276 // Tests reading at offset from files in GCS 277 func TestReadAt(t *testing.T) { 278 var buf bytes.Buffer 279 zw := gzip.NewWriter(&buf) 280 _, err := zw.Write([]byte("Oh wow\nlogs\nthis is\ncrazy")) 281 if err != nil { 282 t.Fatalf("Failed to gzip log text, err: %v", err) 283 } 284 if err := zw.Close(); err != nil { 285 t.Fatalf("Failed to close gzip writer, err: %v", err) 286 } 287 gzippedLog := buf.Bytes() 288 testCases := []struct { 289 name string 290 n int64 291 offset int64 292 contents []byte 293 encoding string 294 expected []byte 295 expectErr bool 296 incompleteRead bool 297 }{ 298 { 299 name: "ReadAt example build log", 300 n: 4, 301 offset: 6, 302 contents: []byte("Oh wow\nlogs\nthis is\ncrazy"), 303 expected: []byte("\nlog"), 304 expectErr: false, 305 }, 306 { 307 name: "ReadAt offset past file size", 308 n: 4, 309 offset: 400, 310 contents: []byte("Oh wow\nlogs\nthis is\ncrazy"), 311 expectErr: true, 312 }, 313 { 314 name: "ReadAt needs multiple internal Reads", 315 n: 4, 316 offset: 6, 317 contents: []byte("Oh wow\nlogs\nthis is\ncrazy"), 318 expected: []byte("\nlog"), 319 incompleteRead: true, 320 }, 321 { 322 name: "ReadAt build log, gzipped", 323 n: 23, 324 contents: gzippedLog, 325 encoding: "gzip", 326 expectErr: true, 327 }, 328 { 329 name: "ReadAt, claimed gzipped but not actually gzipped", 330 n: 2333, 331 contents: []byte("Oh wow\nlogs\nthis is\ncrazy"), 332 encoding: "gzip", 333 expectErr: true, 334 }, 335 { 336 name: "ReadAt offset negative", 337 offset: -3, 338 n: 32, 339 contents: []byte("Oh wow\nlogs\nthis is\ncrazy"), 340 expectErr: true, 341 }, 342 } 343 for _, tc := range testCases { 344 artifact := NewStorageArtifact(context.Background(), &fakeArtifactHandle{ 345 contents: tc.contents, 346 oAttrs: pkgio.Attributes{ 347 Size: int64(len(tc.contents)), 348 ContentEncoding: tc.encoding, 349 }, 350 incompleteRead: tc.incompleteRead, 351 }, "", "build-log.txt", 500e6) 352 p := make([]byte, tc.n) 353 bytesRead, err := artifact.ReadAt(p, tc.offset) 354 if err != nil && !tc.expectErr { 355 t.Fatalf("Test %s failed with err: %v", tc.name, err) 356 } 357 if err == nil && tc.expectErr { 358 t.Errorf("Test %s did not produce error when expected", tc.name) 359 } 360 readBytes := p[:bytesRead] 361 if !bytes.Equal(readBytes, tc.expected) { 362 t.Errorf("Test %s failed.\nExpected: %s\nActual: %s", tc.name, tc.expected, readBytes) 363 } 364 } 365 366 } 367 368 // Tests reading all data from files in GCS 369 func TestReadAll(t *testing.T) { 370 testCases := []struct { 371 name string 372 sizeLimit int64 373 contents []byte 374 expectErr bool 375 expected []byte 376 }{ 377 { 378 name: "ReadAll example build log", 379 contents: []byte("Oh wow\nlogs\nthis is\ncrazy"), 380 sizeLimit: 500e6, 381 expected: []byte("Oh wow\nlogs\nthis is\ncrazy"), 382 }, 383 { 384 name: "ReadAll example too large build log", 385 sizeLimit: 20, 386 contents: []byte("Oh wow\nlogs\nthis is\ncrazy"), 387 expectErr: true, 388 expected: nil, 389 }, 390 { 391 name: "ReadAll unable to get reader", 392 sizeLimit: 500e6, 393 contents: []byte("unreadable contents"), 394 expectErr: true, 395 expected: nil, 396 }, 397 { 398 name: "ReadAll unable to read contents", 399 sizeLimit: 500e6, 400 contents: []byte("deeper unreadable contents"), 401 expectErr: true, 402 expected: nil, 403 }, 404 } 405 for _, tc := range testCases { 406 artifact := NewStorageArtifact(context.Background(), &fakeArtifactHandle{ 407 contents: tc.contents, 408 oAttrs: pkgio.Attributes{ 409 Size: int64(len(tc.contents)), 410 }, 411 }, "", "build-log.txt", tc.sizeLimit) 412 413 actualBytes, err := artifact.ReadAll() 414 if err != nil && !tc.expectErr { 415 t.Fatalf("Test %s failed with err: %v", tc.name, err) 416 } 417 if err == nil && tc.expectErr { 418 t.Errorf("Test %s did not produce error when expected", tc.name) 419 } 420 if !bytes.Equal(actualBytes, tc.expected) { 421 t.Errorf("Test %s failed.\nExpected: %s\nActual: %s", tc.name, tc.expected, actualBytes) 422 } 423 } 424 } 425 426 func TestSize_GCS(t *testing.T) { 427 fakeGCSClient := fakeGCSServer.Client() 428 fakeOpener := pkgio.NewGCSOpener(fakeGCSClient) 429 startedContent := []byte("hi jason, im started") 430 testCases := []struct { 431 name string 432 handle artifactHandle 433 expected int64 434 expectErr string 435 }{ 436 { 437 name: "Test size simple", 438 handle: &fakeArtifactHandle{ 439 contents: startedContent, 440 oAttrs: pkgio.Attributes{ 441 Size: int64(len(startedContent)), 442 }, 443 }, 444 expected: int64(len(startedContent)), 445 }, 446 { 447 name: "Test size from attrs error", 448 handle: &fakeArtifactHandle{ 449 contents: []byte("no attrs"), 450 oAttrs: pkgio.Attributes{ 451 Size: 8, 452 }, 453 }, 454 expectErr: "error getting gcs attributes for artifact: error getting attrs", 455 }, 456 { 457 name: "Size of nonexistentArtifact", 458 handle: &storageArtifactHandle{ 459 Opener: fakeOpener, 460 Name: "gs://test-bucket/logs/example-ci-run/404/started.json", 461 }, 462 expectErr: "error getting gcs attributes for artifact: storage: object doesn't exist", 463 }, 464 } 465 for _, tc := range testCases { 466 artifact := NewStorageArtifact(context.Background(), tc.handle, "", prowv1.StartedStatusFile, 500e6) 467 actual, err := artifact.Size() 468 var actualErr string 469 if err != nil { 470 actualErr = err.Error() 471 } 472 if actualErr != tc.expectErr { 473 t.Fatalf("%s failed getting size for artifact %s, error = %v, expectErr %v", tc.name, artifact.JobPath(), actualErr, tc.expectErr) 474 } 475 if tc.expected != actual { 476 t.Errorf("Test %s failed.\nExpected:\n%d\nActual:\n%d", tc.name, tc.expected, actual) 477 } 478 } 479 } 480 481 func TestStorageArtifact_RespectsSizeLimit(t *testing.T) { 482 contents := "Supercalifragilisticexpialidocious" 483 numRequestedBytes := int64(10) 484 485 testCases := []struct { 486 name string 487 expected error 488 skipGzip bool 489 action func(api.Artifact) error 490 }{ 491 { 492 name: "ReadAll", 493 expected: lenses.ErrFileTooLarge, 494 action: func(a api.Artifact) error { 495 _, err := a.ReadAll() 496 return err 497 }, 498 }, 499 { 500 name: "ReadAt", 501 expected: lenses.ErrRequestSizeTooLarge, 502 skipGzip: true, // `offset read on gzipped files unsupported` 503 action: func(a api.Artifact) error { 504 buf := make([]byte, numRequestedBytes) 505 _, err := a.ReadAt(buf, 3) 506 return err 507 }, 508 }, 509 { 510 name: "ReadAtMost", 511 expected: lenses.ErrRequestSizeTooLarge, 512 action: func(a api.Artifact) error { 513 _, err := a.ReadAtMost(numRequestedBytes) 514 return err 515 }, 516 }, 517 { 518 name: "ReadTail", 519 expected: lenses.ErrRequestSizeTooLarge, 520 skipGzip: true, // `offset read on gzipped files unsupported` 521 action: func(a api.Artifact) error { 522 _, err := a.ReadTail(numRequestedBytes) 523 return err 524 }, 525 }, 526 } 527 for _, tc := range testCases { 528 for _, encoding := range []string{"", "gzip"} { 529 if encoding == "gzip" && tc.skipGzip { 530 continue 531 } 532 t.Run(tc.name+encoding+"_NoErrors", func(nested *testing.T) { 533 sizeLimit := int64(2 * len(contents)) 534 artifact := NewStorageArtifact(context.Background(), &fakeArtifactHandle{ 535 contents: []byte(contents), 536 oAttrs: pkgio.Attributes{ 537 Size: int64(len(contents)), 538 ContentEncoding: encoding, 539 }, 540 }, "some-link-path", "build-log.txt", sizeLimit) 541 actual := tc.action(artifact) 542 if actual != nil { 543 nested.Fatalf("unexpected error: %s", actual) 544 } 545 }) 546 t.Run(tc.name+encoding+"_WithErrors", func(nested *testing.T) { 547 sizeLimit := int64(5) 548 artifact := NewStorageArtifact(context.Background(), &fakeArtifactHandle{ 549 contents: []byte(contents), 550 oAttrs: pkgio.Attributes{ 551 Size: int64(len(contents)), 552 ContentEncoding: encoding, 553 }, 554 }, "some-link-path", "build-log.txt", sizeLimit) 555 actual := tc.action(artifact) 556 if actual == nil { 557 nested.Fatalf("expected error (%s), but got: nil", tc.expected) 558 } else if tc.expected.Error() != actual.Error() { 559 nested.Fatalf("expected error (%s), but got: %s", tc.expected, actual) 560 } 561 }) 562 } 563 } 564 }