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  }