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 }