sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/pod-utils/gcs/upload_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 gcs 18 19 import ( 20 "bytes" 21 "context" 22 "errors" 23 "fmt" 24 stdio "io" 25 "k8s.io/apimachinery/pkg/util/sets" 26 "os" 27 "path" 28 "reflect" 29 "sync" 30 "testing" 31 32 "github.com/fsouza/fake-gcs-server/fakestorage" 33 34 "k8s.io/apimachinery/pkg/util/diff" 35 "sigs.k8s.io/prow/pkg/io" 36 "sigs.k8s.io/prow/pkg/io/providers" 37 ) 38 39 type ( 40 fakeReader struct { 41 closeWillFail bool 42 meta *readerFuncMetadata 43 } 44 readerFuncMetadata struct { 45 NewReaderAttemptsNum int 46 ReaderWasClosed bool 47 } 48 readerFuncOptions struct { 49 newAlwaysFail bool 50 newFailsOnNthAttempt int 51 closeFailsOnNthAttempt int 52 closeAlwaysFails bool 53 } 54 ) 55 56 func (r *fakeReader) Read(p []byte) (n int, err error) { 57 return 0, stdio.EOF 58 } 59 60 func (r *fakeReader) Close() error { 61 if r.closeWillFail { 62 return errors.New("fake reader: close fails") 63 } 64 r.meta.ReaderWasClosed = true 65 return nil 66 } 67 68 func newReaderFunc(opt readerFuncOptions) (ReaderFunc, *readerFuncMetadata) { 69 meta := readerFuncMetadata{} 70 return ReaderFunc(func() (io.ReadCloser, error) { 71 defer func() { 72 meta.NewReaderAttemptsNum += 1 73 }() 74 if opt.newAlwaysFail { 75 return nil, errors.New("reader func: always failing") 76 } 77 if opt.newFailsOnNthAttempt > -1 && meta.NewReaderAttemptsNum == opt.newFailsOnNthAttempt { 78 return nil, fmt.Errorf("reader func: fails on attempt no.: %d", meta.NewReaderAttemptsNum) 79 } 80 closeWillFail := opt.closeAlwaysFails 81 if opt.closeFailsOnNthAttempt > -1 && meta.NewReaderAttemptsNum == opt.closeFailsOnNthAttempt { 82 closeWillFail = true 83 } 84 return &fakeReader{closeWillFail: closeWillFail, meta: &meta}, nil 85 }), &meta 86 } 87 88 func TestUploadNewReaderFunc(t *testing.T) { 89 var testCases = []struct { 90 name string 91 compressFileTypes []string 92 isErrExpected bool 93 readerFuncOpts readerFuncOptions 94 wantReaderFuncMeta readerFuncMetadata 95 }{ 96 { 97 name: "Succeed on first retry", 98 isErrExpected: false, 99 readerFuncOpts: readerFuncOptions{ 100 newFailsOnNthAttempt: -1, 101 closeFailsOnNthAttempt: -1, 102 }, 103 wantReaderFuncMeta: readerFuncMetadata{ 104 NewReaderAttemptsNum: 1, 105 ReaderWasClosed: true, 106 }, 107 }, 108 { 109 name: "Reader cannot be created", 110 isErrExpected: true, 111 readerFuncOpts: readerFuncOptions{ 112 newAlwaysFail: true, 113 newFailsOnNthAttempt: -1, 114 closeFailsOnNthAttempt: -1, 115 }, 116 wantReaderFuncMeta: readerFuncMetadata{ 117 NewReaderAttemptsNum: 4, 118 ReaderWasClosed: false, 119 }, 120 }, 121 { 122 name: "Fail on first attempt", 123 readerFuncOpts: readerFuncOptions{ 124 newFailsOnNthAttempt: 0, 125 closeFailsOnNthAttempt: -1, 126 }, 127 wantReaderFuncMeta: readerFuncMetadata{ 128 NewReaderAttemptsNum: 2, 129 ReaderWasClosed: true, 130 }, 131 }, 132 { 133 name: "Compress files", 134 compressFileTypes: []string{"*"}, 135 readerFuncOpts: readerFuncOptions{ 136 newFailsOnNthAttempt: -1, 137 closeFailsOnNthAttempt: -1, 138 }, 139 wantReaderFuncMeta: readerFuncMetadata{ 140 NewReaderAttemptsNum: 1, 141 ReaderWasClosed: true, 142 }, 143 }, 144 } 145 tempDir := t.TempDir() 146 for _, testCase := range testCases { 147 t.Run(testCase.name, func(t *testing.T) { 148 f, err := os.CreateTemp(tempDir, "*test-upload-new-read-fn") 149 if err != nil { 150 t.Fatalf("create tmp file: %v", err) 151 } 152 uploadTargets := make(map[string]UploadFunc) 153 readerFunc, readerFuncMeta := newReaderFunc(testCase.readerFuncOpts) 154 uploadTargets[path.Base(f.Name())] = DataUpload(readerFunc) 155 bucket := fmt.Sprintf("%s://%s", providers.File, path.Dir(f.Name())) 156 err = Upload(context.TODO(), bucket, "", "", testCase.compressFileTypes, uploadTargets) 157 if testCase.isErrExpected && err == nil { 158 t.Errorf("error expected but got nil") 159 } 160 if !reflect.DeepEqual(testCase.wantReaderFuncMeta, *readerFuncMeta) { 161 t.Errorf("unexpected ReaderFuncMetadata: %s", diff.ObjectReflectDiff(testCase.wantReaderFuncMeta, *readerFuncMeta)) 162 } 163 }) 164 } 165 166 } 167 168 func TestUploadWithRetries(t *testing.T) { 169 170 // doesPass = true, isFlaky = false => Pass in first attempt 171 // doesPass = true, isFlaky = true => Pass in second attempt 172 // doesPass = false, isFlaky = don't care => Fail to upload in all attempts 173 type destUploadBehavior struct { 174 dest string 175 isFlaky bool 176 doesPass bool 177 } 178 179 var testCases = []struct { 180 name string 181 destUploadBehaviors []destUploadBehavior 182 }{ 183 { 184 name: "all passed", 185 destUploadBehaviors: []destUploadBehavior{ 186 { 187 dest: "all-pass-dest1", 188 doesPass: true, 189 isFlaky: false, 190 }, 191 { 192 dest: "all-pass-dest2", 193 doesPass: true, 194 isFlaky: false, 195 }, 196 }, 197 }, 198 { 199 name: "all passed with retries", 200 destUploadBehaviors: []destUploadBehavior{ 201 { 202 dest: "all-pass-retries-dest1", 203 doesPass: true, 204 isFlaky: true, 205 }, 206 { 207 dest: "all-pass-retries-dest2", 208 doesPass: true, 209 isFlaky: false, 210 }, 211 }, 212 }, 213 { 214 name: "all failed", 215 destUploadBehaviors: []destUploadBehavior{ 216 { 217 dest: "all-failed-dest1", 218 doesPass: false, 219 isFlaky: false, 220 }, 221 { 222 dest: "all-failed-dest2", 223 doesPass: false, 224 isFlaky: false, 225 }, 226 }, 227 }, 228 { 229 name: "some failed", 230 destUploadBehaviors: []destUploadBehavior{ 231 { 232 dest: "some-failed-dest1", 233 doesPass: true, 234 isFlaky: false, 235 }, 236 { 237 dest: "some-failed-dest2", 238 doesPass: false, 239 isFlaky: false, 240 }, 241 }, 242 }, 243 } 244 245 for _, testCase := range testCases { 246 t.Run(testCase.name, func(t *testing.T) { 247 248 uploadFuncs := map[string]UploadFunc{} 249 250 currentTestStates := map[string]destUploadBehavior{} 251 currentTestStatesLock := sync.Mutex{} 252 253 for _, destBehavior := range testCase.destUploadBehaviors { 254 255 currentTestStates[destBehavior.dest] = destBehavior 256 257 uploadFuncs[destBehavior.dest] = func(destBehavior destUploadBehavior) UploadFunc { 258 259 return func(writer dataWriter) error { 260 currentTestStatesLock.Lock() 261 defer currentTestStatesLock.Unlock() 262 263 currentDestUploadBehavior := currentTestStates[destBehavior.dest] 264 265 if !currentDestUploadBehavior.doesPass { 266 return fmt.Errorf("%v: %v failed", testCase.name, destBehavior.dest) 267 } 268 269 if currentDestUploadBehavior.isFlaky { 270 currentDestUploadBehavior.isFlaky = false 271 currentTestStates[destBehavior.dest] = currentDestUploadBehavior 272 return fmt.Errorf("%v: %v flaky", testCase.name, destBehavior.dest) 273 } 274 275 delete(currentTestStates, destBehavior.dest) 276 return nil 277 } 278 }(destBehavior) 279 280 } 281 282 ctx := context.Background() 283 err := Upload(ctx, "", "", "", []string{}, uploadFuncs) 284 285 isErrExpected := false 286 for _, currentTestState := range currentTestStates { 287 288 if currentTestState.doesPass { 289 t.Errorf("%v: %v did not get uploaded", testCase.name, currentTestState.dest) 290 break 291 } 292 293 if !isErrExpected && !currentTestState.doesPass { 294 isErrExpected = true 295 } 296 } 297 298 if (err != nil) != isErrExpected { 299 t.Errorf("%v: Got unexpected error response: %v", testCase.name, err) 300 } 301 }) 302 303 } 304 } 305 306 func TestShouldCompressFileType(t *testing.T) { 307 testCases := []struct { 308 name string 309 dest string 310 compressFileTypes sets.Set[string] 311 expected bool 312 }{ 313 { 314 name: "compress all", 315 dest: "some-file.txt", 316 compressFileTypes: sets.New[string]("*"), 317 expected: true, 318 }, 319 { 320 name: "compress txt only", 321 dest: "some-file.txt", 322 compressFileTypes: sets.New[string]("txt"), 323 expected: true, 324 }, 325 { 326 name: "compress other file types", 327 dest: "some-file.txt", 328 compressFileTypes: sets.New[string]("log", "json"), 329 expected: false, 330 }, 331 } 332 for _, tc := range testCases { 333 t.Run(tc.name, func(t *testing.T) { 334 result := shouldCompressFileType(tc.dest, tc.compressFileTypes) 335 if tc.expected != result { 336 t.Errorf("result (%v) did not match expected (%v)", result, tc.expected) 337 } 338 }) 339 } 340 } 341 342 func Test_openerObjectWriter_Write(t *testing.T) { 343 344 fakeBucket := "test-bucket" 345 fakeGCSServer := fakestorage.NewServer([]fakestorage.Object{}) 346 fakeGCSServer.CreateBucketWithOpts(fakestorage.CreateBucketOpts{Name: fakeBucket}) 347 defer fakeGCSServer.Stop() 348 fakeGCSClient := fakeGCSServer.Client() 349 350 tests := []struct { 351 name string 352 ObjectDest string 353 ObjectContent []byte 354 compressFileType bool 355 wantN int 356 wantErr bool 357 }{ 358 { 359 name: "write regular file", 360 ObjectDest: "build/log.text", 361 ObjectContent: []byte("Oh wow\nlogs\nthis is\ncrazy"), 362 wantN: 25, 363 wantErr: false, 364 }, 365 { 366 name: "write empty file", 367 ObjectDest: "build/marker", 368 ObjectContent: []byte(""), 369 wantN: 0, 370 wantErr: false, 371 }, 372 { 373 name: "compress file", 374 ObjectDest: "build/log.text", 375 ObjectContent: []byte("Oh wow\nlogs\nthis is\ncrazy\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Integer leo risus, cursus eget libero in, auctor placerat lorem. Nulla elementum arcu sem, vel tempor risus cursus nec. Nulla aliquam quam in ex aliquet elementum. Praesent molestie vulputate magna, eu ultrices mi tincidunt eget. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nunc blandit viverra magna eget volutpat. Suspendisse in metus et leo interdum gravida eget vel mi. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla facilities. Maecenas sem urna, aliquam vel enim ullamcorper, sagittis lacinia elit. Donec malesuada tempor varius. Proin feugiat elit metus, eu vulputate magna mattis et. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Phasellus hendrerit turpis id convallis ornare.\n\nCras sed quam mattis, venenatis diam ac, posuere odio. Fusce eu pharetra ipsum. Maecenas dignissim nulla ut mauris bibendum consequat. Quisque euismod lacus nec dapibus tempus. Nunc eget felis sed arcu commodo scelerisque dapibus nec nullam.\n\n"), 376 compressFileType: true, 377 wantN: 1132, 378 wantErr: false, 379 }, 380 { 381 name: "compress filetype, but file is too small to compress", 382 ObjectDest: "build/log.text", 383 ObjectContent: []byte("Oh wow\nlogs\nthis is\ncrazy"), 384 compressFileType: true, 385 wantN: 25, 386 wantErr: false, 387 }, 388 { 389 name: "compress filetype, but file is hidden gzip", 390 ObjectDest: "build/log.text", 391 ObjectContent: []byte("\u001F\u008B\bOh wow\nlogs\nthis is\ncrazy\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Integer leo risus, cursus eget libero in, auctor placerat lorem. Nulla elementum arcu sem, vel tempor risus cursus nec. Nulla aliquam quam in ex aliquet elementum. Praesent molestie vulputate magna, eu ultrices mi tincidunt eget. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nunc blandit viverra magna eget volutpat. Suspendisse in metus et leo interdum gravida eget vel mi. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nulla facilities. Maecenas sem urna, aliquam vel enim ullamcorper, sagittis lacinia elit. Donec malesuada tempor varius. Proin feugiat elit metus, eu vulputate magna mattis et. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Phasellus hendrerit turpis id convallis ornare.\n\nCras sed quam mattis, venenatis diam ac, posuere odio. Fusce eu pharetra ipsum. Maecenas dignissim nulla ut mauris bibendum consequat. Quisque euismod lacus nec dapibus tempus. Nunc eget felis sed arcu commodo scelerisque dapibus nec nullam.\n\n"), 392 compressFileType: true, 393 wantN: 1136, 394 wantErr: false, 395 }, 396 } 397 for _, tt := range tests { 398 t.Run(tt.name, func(t *testing.T) { 399 w := &openerObjectWriter{ 400 Opener: io.NewGCSOpener(fakeGCSClient), 401 Context: context.Background(), 402 Bucket: fmt.Sprintf("gs://%s", fakeBucket), 403 Dest: tt.ObjectDest, 404 compressFileType: tt.compressFileType, 405 } 406 gotN, err := w.Write(tt.ObjectContent) 407 if (err != nil) != tt.wantErr { 408 t.Errorf("Write() error = %v, wantErr %v", err, tt.wantErr) 409 return 410 } 411 if gotN != tt.wantN { 412 t.Errorf("Write() gotN = %v, want %v", gotN, tt.wantN) 413 } 414 415 if err := w.Close(); (err != nil) != tt.wantErr { 416 t.Errorf("Close() error = %v, wantErr %v", err, tt.wantErr) 417 return 418 } 419 420 // read object back from bucket and compare with written object 421 reader, err := fakeGCSClient.Bucket(fakeBucket).Object(tt.ObjectDest).NewReader(context.Background()) 422 if err != nil { 423 t.Errorf("Got unexpected error reading object %s: %v", tt.ObjectDest, err) 424 } 425 gotObjectContent, err := stdio.ReadAll(reader) 426 if err != nil { 427 t.Errorf("Got unexpected error reading object %s: %v", tt.ObjectDest, err) 428 } 429 if !bytes.Equal(tt.ObjectContent, gotObjectContent) { 430 t.Errorf("Write() gotObjectContent = %v, want %v", gotObjectContent, tt.ObjectContent) 431 } 432 }) 433 } 434 } 435 436 func Test_openerObjectWriter_fullUploadPath(t *testing.T) { 437 tests := []struct { 438 name string 439 bucket string 440 dest string 441 want string 442 }{ 443 { 444 name: "simple path", 445 bucket: "bucket-A", 446 dest: "path/to/some/file.json", 447 want: "gs://bucket-A/path/to/some/file.json", 448 }, 449 } 450 for _, tt := range tests { 451 t.Run(tt.name, func(t *testing.T) { 452 w := &openerObjectWriter{ 453 Bucket: fmt.Sprintf("gs://%s", tt.bucket), 454 Dest: tt.dest, 455 } 456 got := w.fullUploadPath() 457 458 if got != tt.want { 459 t.Errorf("fullUploadPath(): got %v, want %v", got, tt.want) 460 return 461 } 462 }) 463 } 464 }