github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/spyglass/storageartifact.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  	"context"
    21  	"fmt"
    22  	"io"
    23  	"sync"
    24  
    25  	pkgio "sigs.k8s.io/prow/pkg/io"
    26  	"sigs.k8s.io/prow/pkg/spyglass/lenses"
    27  )
    28  
    29  // StorageArtifact represents some output of a prow job stored in GCS
    30  type StorageArtifact struct {
    31  	// The handle of the object in GCS
    32  	handle artifactHandle
    33  
    34  	// The link to the Artifact in GCS
    35  	link string
    36  
    37  	// The path of the Artifact within the job
    38  	path string
    39  
    40  	// sizeLimit is the max size to read before failing
    41  	sizeLimit int64
    42  
    43  	// ctx provides context for cancellation and timeout. Embedded in struct to preserve
    44  	// conformance with io.ReaderAt
    45  	ctx context.Context
    46  
    47  	attrs *pkgio.Attributes
    48  
    49  	lock sync.RWMutex
    50  }
    51  
    52  type artifactHandle interface {
    53  	Attrs(ctx context.Context) (pkgio.Attributes, error)
    54  	NewRangeReader(ctx context.Context, offset, length int64) (io.ReadCloser, error)
    55  	NewReader(ctx context.Context) (io.ReadCloser, error)
    56  	UpdateAttrs(context.Context, pkgio.ObjectAttrsToUpdate) (*pkgio.Attributes, error)
    57  }
    58  
    59  // NewStorageArtifact returns a new StorageArtifact with a given handle, canonical link, and path within the job
    60  func NewStorageArtifact(ctx context.Context, handle artifactHandle, link string, path string, sizeLimit int64) *StorageArtifact {
    61  	return &StorageArtifact{
    62  		handle:    handle,
    63  		link:      link,
    64  		path:      path,
    65  		sizeLimit: sizeLimit,
    66  		ctx:       ctx,
    67  	}
    68  }
    69  
    70  func (a *StorageArtifact) fetchAttrs() (*pkgio.Attributes, error) {
    71  	a.lock.RLock()
    72  	attrs := a.attrs
    73  	a.lock.RUnlock()
    74  	if attrs != nil {
    75  		return attrs, nil
    76  	}
    77  	if a.attrs != nil {
    78  		return a.attrs, nil
    79  	}
    80  	{
    81  		attrs, err := a.handle.Attrs(a.ctx)
    82  		if err != nil {
    83  			return nil, err
    84  		}
    85  		a.lock.Lock()
    86  		defer a.lock.Unlock()
    87  		a.attrs = &attrs
    88  	}
    89  	return a.attrs, nil
    90  }
    91  
    92  // Size returns the size of the artifact in GCS
    93  func (a *StorageArtifact) Size() (int64, error) {
    94  	attrs, err := a.fetchAttrs()
    95  	if err != nil {
    96  		return 0, fmt.Errorf("error getting gcs attributes for artifact: %w", err)
    97  	}
    98  	return attrs.Size, nil
    99  }
   100  
   101  func (a *StorageArtifact) Metadata() (map[string]string, error) {
   102  	attrs, err := a.fetchAttrs()
   103  	if err != nil {
   104  		return nil, fmt.Errorf("fetch attributes: %w", err)
   105  	}
   106  	return attrs.Metadata, nil
   107  }
   108  
   109  func (a *StorageArtifact) UpdateMetadata(meta map[string]string) error {
   110  	attrs, err := a.handle.UpdateAttrs(a.ctx, pkgio.ObjectAttrsToUpdate{
   111  		Metadata: meta,
   112  	})
   113  	if err != nil {
   114  		return err
   115  	}
   116  	a.lock.Lock()
   117  	a.attrs = attrs
   118  	a.lock.Unlock()
   119  	return nil
   120  }
   121  
   122  // JobPath gets the GCS path of the artifact within the current job
   123  func (a *StorageArtifact) JobPath() string {
   124  	return a.path
   125  }
   126  
   127  // CanonicalLink gets the GCS web address of the artifact
   128  func (a *StorageArtifact) CanonicalLink() string {
   129  	return a.link
   130  }
   131  
   132  // ReadAt reads len(p) bytes from a file in GCS at offset off
   133  func (a *StorageArtifact) ReadAt(p []byte, off int64) (n int, err error) {
   134  	if int64(len(p)) > a.sizeLimit {
   135  		return 0, lenses.ErrRequestSizeTooLarge
   136  	}
   137  	gzipped, err := a.gzipped()
   138  	if err != nil {
   139  		return 0, fmt.Errorf("error checking artifact for gzip compression: %w", err)
   140  	}
   141  	if gzipped {
   142  		return 0, lenses.ErrGzipOffsetRead
   143  	}
   144  	artifactSize, err := a.Size()
   145  	if err != nil {
   146  		return 0, fmt.Errorf("error getting artifact size: %w", err)
   147  	}
   148  	if off >= artifactSize {
   149  		return 0, fmt.Errorf("offset must be less than artifact size")
   150  	}
   151  	var gotEOF bool
   152  	toRead := int64(len(p))
   153  	if toRead+off > artifactSize {
   154  		return 0, fmt.Errorf("read range exceeds artifact contents")
   155  	} else if toRead+off == artifactSize {
   156  		gotEOF = true
   157  	}
   158  	reader, err := a.handle.NewRangeReader(a.ctx, off, toRead)
   159  	if err != nil {
   160  		return 0, fmt.Errorf("error getting artifact reader: %w", err)
   161  	}
   162  	defer reader.Close()
   163  	// We need to keep reading until we fill the buffer or hit EOF.
   164  	offset := 0
   165  	for offset < len(p) {
   166  		n, err = reader.Read(p[offset:])
   167  		offset += n
   168  		if err != nil {
   169  			if err == io.EOF && gotEOF {
   170  				break
   171  			}
   172  			return 0, fmt.Errorf("error reading from artifact: %w", err)
   173  		}
   174  	}
   175  	if gotEOF {
   176  		return offset, io.EOF
   177  	}
   178  	return offset, nil
   179  }
   180  
   181  // ReadAtMost reads at most n bytes from a file in GCS. If the file is compressed (gzip) in GCS, n bytes
   182  // of gzipped content will be downloaded and decompressed into potentially GREATER than n bytes of content.
   183  func (a *StorageArtifact) ReadAtMost(n int64) ([]byte, error) {
   184  	if n > a.sizeLimit {
   185  		return nil, lenses.ErrRequestSizeTooLarge
   186  	}
   187  	var reader io.ReadCloser
   188  	var p []byte
   189  	gzipped, err := a.gzipped()
   190  	if err != nil {
   191  		return nil, fmt.Errorf("error checking artifact for gzip compression: %w", err)
   192  	}
   193  	if gzipped {
   194  		reader, err = a.handle.NewReader(a.ctx)
   195  		if err != nil {
   196  			return nil, fmt.Errorf("error getting artifact reader: %w", err)
   197  		}
   198  		defer reader.Close()
   199  		p, err = io.ReadAll(reader) // Must readall for gzipped files
   200  		if err != nil {
   201  			return nil, fmt.Errorf("error reading all from artifact: %w", err)
   202  		}
   203  		artifactSize := int64(len(p))
   204  		readRange := n
   205  		if n > artifactSize {
   206  			readRange = artifactSize
   207  			return p[:readRange], io.EOF
   208  		}
   209  		return p[:readRange], nil
   210  
   211  	}
   212  	artifactSize, err := a.Size()
   213  	if err != nil {
   214  		return nil, fmt.Errorf("error getting artifact size: %w", err)
   215  	}
   216  	readRange := n
   217  	var gotEOF bool
   218  	if n > artifactSize {
   219  		gotEOF = true
   220  		readRange = artifactSize
   221  	}
   222  	reader, err = a.handle.NewRangeReader(a.ctx, 0, readRange)
   223  	if err != nil {
   224  		return nil, fmt.Errorf("error getting artifact reader: %w", err)
   225  	}
   226  	defer reader.Close()
   227  	p, err = io.ReadAll(reader)
   228  	if err != nil {
   229  		return nil, fmt.Errorf("error reading all from artifact: %w", err)
   230  	}
   231  	if gotEOF {
   232  		return p, io.EOF
   233  	}
   234  	return p, nil
   235  }
   236  
   237  // ReadAll will either read the entire file or throw an error if file size is too big
   238  func (a *StorageArtifact) ReadAll() ([]byte, error) {
   239  	size, err := a.Size()
   240  	if err != nil {
   241  		return nil, fmt.Errorf("error getting artifact size: %w", err)
   242  	}
   243  	if size > a.sizeLimit {
   244  		return nil, lenses.ErrFileTooLarge
   245  	}
   246  	reader, err := a.handle.NewReader(a.ctx)
   247  	if err != nil {
   248  		return nil, fmt.Errorf("error getting artifact reader: %w", err)
   249  	}
   250  	defer reader.Close()
   251  	p, err := io.ReadAll(reader)
   252  	if err != nil {
   253  		return nil, fmt.Errorf("error reading all from artifact: %w", err)
   254  	}
   255  	return p, nil
   256  }
   257  
   258  // ReadTail reads the last n bytes from a file in GCS
   259  func (a *StorageArtifact) ReadTail(n int64) ([]byte, error) {
   260  	if n > a.sizeLimit {
   261  		return nil, lenses.ErrRequestSizeTooLarge
   262  	}
   263  	gzipped, err := a.gzipped()
   264  	if err != nil {
   265  		return nil, fmt.Errorf("error checking artifact for gzip compression: %w", err)
   266  	}
   267  	if gzipped {
   268  		return nil, lenses.ErrGzipOffsetRead
   269  	}
   270  	size, err := a.Size()
   271  	if err != nil {
   272  		return nil, fmt.Errorf("error getting artifact size: %w", err)
   273  	}
   274  	var offset int64
   275  	if n >= size {
   276  		offset = 0
   277  	} else {
   278  		offset = size - n
   279  	}
   280  	reader, err := a.handle.NewRangeReader(a.ctx, offset, -1)
   281  	if err != nil && err != io.EOF {
   282  		return nil, fmt.Errorf("error getting artifact reader: %w", err)
   283  	}
   284  	defer reader.Close()
   285  	read, err := io.ReadAll(reader)
   286  	if err != nil {
   287  		return nil, fmt.Errorf("error reading all from artiact: %w", err)
   288  	}
   289  	return read, nil
   290  }
   291  
   292  // gzipped returns whether the file is gzip-encoded in GCS
   293  func (a *StorageArtifact) gzipped() (bool, error) {
   294  	attrs, err := a.handle.Attrs(a.ctx)
   295  	if err != nil {
   296  		return false, fmt.Errorf("error getting gcs attributes for artifact: %w", err)
   297  	}
   298  	return attrs.ContentEncoding == "gzip", nil
   299  }