github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/spyglass/gcsartifact.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  	"io/ioutil"
    24  
    25  	"cloud.google.com/go/storage"
    26  	"github.com/sirupsen/logrus"
    27  
    28  	"k8s.io/test-infra/prow/spyglass/lenses"
    29  )
    30  
    31  // GCSArtifact represents some output of a prow job stored in GCS
    32  type GCSArtifact struct {
    33  	// The handle of the object in GCS
    34  	handle artifactHandle
    35  
    36  	// The link to the Artifact in GCS
    37  	link string
    38  
    39  	// The path of the Artifact within the job
    40  	path string
    41  
    42  	// sizeLimit is the max size to read before failing
    43  	sizeLimit int64
    44  
    45  	// ctx provides context for cancellation and timeout. Embedded in struct to preserve
    46  	// conformance with io.ReaderAt
    47  	ctx context.Context
    48  }
    49  
    50  type artifactHandle interface {
    51  	Attrs(ctx context.Context) (*storage.ObjectAttrs, error)
    52  	NewRangeReader(ctx context.Context, offset, length int64) (io.ReadCloser, error)
    53  	NewReader(ctx context.Context) (io.ReadCloser, error)
    54  }
    55  
    56  // NewGCSArtifact returns a new GCSArtifact with a given handle, canonical link, and path within the job
    57  func NewGCSArtifact(ctx context.Context, handle artifactHandle, link string, path string, sizeLimit int64) *GCSArtifact {
    58  	return &GCSArtifact{
    59  		handle:    handle,
    60  		link:      link,
    61  		path:      path,
    62  		sizeLimit: sizeLimit,
    63  		ctx:       ctx,
    64  	}
    65  }
    66  
    67  func fieldsFor(a *GCSArtifact) logrus.Fields {
    68  	return logrus.Fields{
    69  		"artifact": a.path,
    70  	}
    71  }
    72  
    73  // Size returns the size of the artifact in GCS
    74  func (a *GCSArtifact) Size() (int64, error) {
    75  	attrs, err := a.handle.Attrs(a.ctx)
    76  	if err != nil {
    77  		return 0, fmt.Errorf("error getting gcs attributes for artifact: %v", err)
    78  	}
    79  	return attrs.Size, nil
    80  }
    81  
    82  // JobPath gets the GCS path of the artifact within the current job
    83  func (a *GCSArtifact) JobPath() string {
    84  	return a.path
    85  }
    86  
    87  // CanonicalLink gets the GCS web address of the artifact
    88  func (a *GCSArtifact) CanonicalLink() string {
    89  	return a.link
    90  }
    91  
    92  // ReadAt reads len(p) bytes from a file in GCS at offset off
    93  func (a *GCSArtifact) ReadAt(p []byte, off int64) (n int, err error) {
    94  	gzipped, err := a.gzipped()
    95  	if err != nil {
    96  		return 0, fmt.Errorf("error checking artifact for gzip compression: %v", err)
    97  	}
    98  	if gzipped {
    99  		return 0, lenses.ErrGzipOffsetRead
   100  	}
   101  	artifactSize, err := a.Size()
   102  	if err != nil {
   103  		return 0, fmt.Errorf("error getting artifact size: %v", err)
   104  	}
   105  	if off >= artifactSize {
   106  		return 0, fmt.Errorf("offset must be less than artifact size")
   107  	}
   108  	var gotEOF bool
   109  	toRead := int64(len(p))
   110  	if toRead+off > artifactSize {
   111  		return 0, fmt.Errorf("read range exceeds artifact contents")
   112  	} else if toRead+off == artifactSize {
   113  		gotEOF = true
   114  	}
   115  	reader, err := a.handle.NewRangeReader(a.ctx, off, toRead)
   116  	defer reader.Close()
   117  	if err != nil {
   118  		return 0, fmt.Errorf("error getting artifact reader: %v", err)
   119  	}
   120  	n, err = reader.Read(p)
   121  	if err != nil {
   122  		return 0, fmt.Errorf("error reading from artifact: %v", err)
   123  	}
   124  	if gotEOF {
   125  		return n, io.EOF
   126  	}
   127  	return n, nil
   128  }
   129  
   130  // ReadAtMost reads at most n bytes from a file in GCS. If the file is compressed (gzip) in GCS, n bytes
   131  // of gzipped content will be downloaded and decompressed into potentially GREATER than n bytes of content.
   132  func (a *GCSArtifact) ReadAtMost(n int64) ([]byte, error) {
   133  	var reader io.ReadCloser
   134  	var p []byte
   135  	gzipped, err := a.gzipped()
   136  	if err != nil {
   137  		return nil, fmt.Errorf("error checking artifact for gzip compression: %v", err)
   138  	}
   139  	if gzipped {
   140  		reader, err = a.handle.NewReader(a.ctx)
   141  		if err != nil {
   142  			return nil, fmt.Errorf("error getting artifact reader: %v", err)
   143  		}
   144  		defer reader.Close()
   145  		p, err = ioutil.ReadAll(reader) // Must readall for gzipped files
   146  		if err != nil {
   147  			return nil, fmt.Errorf("error reading all from artifact: %v", err)
   148  		}
   149  		artifactSize := int64(len(p))
   150  		readRange := n
   151  		if n > artifactSize {
   152  			readRange = artifactSize
   153  			return p[:readRange], io.EOF
   154  		}
   155  		return p[:readRange], nil
   156  
   157  	}
   158  	artifactSize, err := a.Size()
   159  	if err != nil {
   160  		return nil, fmt.Errorf("error getting artifact size: %v", err)
   161  	}
   162  	readRange := n
   163  	var gotEOF bool
   164  	if n > artifactSize {
   165  		gotEOF = true
   166  		readRange = artifactSize
   167  	}
   168  	reader, err = a.handle.NewRangeReader(a.ctx, 0, readRange)
   169  	if err != nil {
   170  		return nil, fmt.Errorf("error getting artifact reader: %v", err)
   171  	}
   172  	defer reader.Close()
   173  	p, err = ioutil.ReadAll(reader)
   174  	if err != nil {
   175  		return nil, fmt.Errorf("error reading all from artifact: %v", err)
   176  	}
   177  	if gotEOF {
   178  		return p, io.EOF
   179  	}
   180  	return p, nil
   181  }
   182  
   183  // ReadAll will either read the entire file or throw an error if file size is too big
   184  func (a *GCSArtifact) ReadAll() ([]byte, error) {
   185  	size, err := a.Size()
   186  	if err != nil {
   187  		return nil, fmt.Errorf("error getting artifact size: %v", err)
   188  	}
   189  	if size > a.sizeLimit {
   190  		return nil, lenses.ErrFileTooLarge
   191  	}
   192  	reader, err := a.handle.NewReader(a.ctx)
   193  	if err != nil {
   194  		return nil, fmt.Errorf("error getting artifact reader: %v", err)
   195  	}
   196  	defer reader.Close()
   197  	p, err := ioutil.ReadAll(reader)
   198  	if err != nil {
   199  		return nil, fmt.Errorf("error reading all from artifact: %v", err)
   200  	}
   201  	return p, nil
   202  }
   203  
   204  // ReadTail reads the last n bytes from a file in GCS
   205  func (a *GCSArtifact) ReadTail(n int64) ([]byte, error) {
   206  	gzipped, err := a.gzipped()
   207  	if err != nil {
   208  		return nil, fmt.Errorf("error checking artifact for gzip compression: %v", err)
   209  	}
   210  	if gzipped {
   211  		return nil, lenses.ErrGzipOffsetRead
   212  	}
   213  	size, err := a.Size()
   214  	if err != nil {
   215  		return nil, fmt.Errorf("error getting artifact size: %v", err)
   216  	}
   217  	var offset int64
   218  	if n >= size {
   219  		offset = 0
   220  	} else {
   221  		offset = size - n
   222  	}
   223  	reader, err := a.handle.NewRangeReader(a.ctx, offset, -1)
   224  	defer reader.Close()
   225  	if err != nil && err != io.EOF {
   226  		return nil, fmt.Errorf("error getting artifact reader: %v", err)
   227  	}
   228  	read, err := ioutil.ReadAll(reader)
   229  	if err != nil {
   230  		return nil, fmt.Errorf("error reading all from artiact: %v", err)
   231  	}
   232  	return read, nil
   233  }
   234  
   235  // gzipped returns whether the file is gzip-encoded in GCS
   236  func (a *GCSArtifact) gzipped() (bool, error) {
   237  	attrs, err := a.handle.Attrs(a.ctx)
   238  	if err != nil {
   239  		return false, fmt.Errorf("error getting gcs attributes for artifact: %v", err)
   240  	}
   241  	return attrs.ContentEncoding == "gzip", nil
   242  }