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  }