sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/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 }