github.com/GoogleCloudPlatform/testgrid@v0.0.174/util/gcs/gcs.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 gcs provides utilities for interacting with GCS. 18 // 19 // This includes basic CRUD operations. It is primarily focused on 20 // reading prow build results uploaded to GCS. 21 package gcs 22 23 import ( 24 "bytes" 25 "compress/zlib" 26 "context" 27 "encoding/json" 28 "errors" 29 "fmt" 30 "hash/crc32" 31 "io/ioutil" 32 "log" 33 "net/http" 34 "net/url" 35 "strings" 36 37 statepb "github.com/GoogleCloudPlatform/testgrid/pb/state" 38 39 "cloud.google.com/go/storage" 40 "github.com/golang/protobuf/proto" 41 "google.golang.org/api/googleapi" 42 "google.golang.org/api/option" 43 ) 44 45 // IsPreconditionFailed returns true when the error is an http.StatusPreconditionFailed googleapi.Error. 46 func IsPreconditionFailed(err error) bool { 47 if err == nil { 48 return false 49 } 50 var e *googleapi.Error 51 if !errors.As(err, &e) { 52 return false 53 } 54 return e.Code == http.StatusPreconditionFailed 55 } 56 57 // ClientWithCreds returns a storage client, optionally authenticated with the specified .json creds 58 func ClientWithCreds(ctx context.Context, creds ...string) (*storage.Client, error) { 59 var options []option.ClientOption 60 switch l := len(creds); l { 61 case 0: // Do nothing 62 case 1: 63 options = append(options, option.WithCredentialsFile(creds[0])) 64 default: 65 return nil, fmt.Errorf("%d creds files unsupported (at most 1)", l) 66 } 67 return storage.NewClient(ctx, options...) 68 } 69 70 // Path parses gs://bucket/obj urls 71 type Path struct { 72 url url.URL 73 } 74 75 // NewPath returns a new Path if it parses. 76 func NewPath(path string) (*Path, error) { 77 var p Path 78 err := p.Set(path) 79 if err != nil { 80 return nil, err 81 } 82 return &p, nil 83 } 84 85 // String returns the gs://bucket/obj url 86 func (g Path) String() string { 87 return g.url.String() 88 } 89 90 // URL returns the url 91 func (g Path) URL() url.URL { 92 return g.url 93 } 94 95 // Set updates value from a gs://bucket/obj string, validating errors. 96 func (g *Path) Set(v string) error { 97 u, err := url.Parse(v) 98 if err != nil { 99 return fmt.Errorf("invalid gs:// url %s: %v", v, err) 100 } 101 return g.SetURL(u) 102 } 103 104 // SetURL updates value to the passed in gs://bucket/obj url 105 func (g *Path) SetURL(u *url.URL) error { 106 switch { 107 case u == nil: 108 return errors.New("nil url") 109 case u.Scheme != "gs" && u.Scheme != "" && u.Scheme != "file": 110 return fmt.Errorf("must use a gs://, file://, or local filesystem url: %s", u) 111 case strings.Contains(u.Host, ":"): 112 return fmt.Errorf("gs://bucket may not contain a port: %s", u) 113 case u.Opaque != "": 114 return fmt.Errorf("url must start with gs://: %s", u) 115 case u.User != nil: 116 return fmt.Errorf("gs://bucket may not contain an user@ prefix: %s", u) 117 case u.RawQuery != "": 118 return fmt.Errorf("gs:// url may not contain a ?query suffix: %s", u) 119 case u.Fragment != "": 120 return fmt.Errorf("gs:// url may not contain a #fragment suffix: %s", u) 121 } 122 g.url = *u 123 return nil 124 } 125 126 // MarshalJSON encodes Path as a string 127 func (g Path) MarshalJSON() ([]byte, error) { 128 return json.Marshal(g.String()) 129 } 130 131 // UnmarshalJSON decodes a string into Path 132 func (g *Path) UnmarshalJSON(buf []byte) error { 133 var str string 134 err := json.Unmarshal(buf, &str) 135 if err != nil { 136 return err 137 } 138 if g == nil { 139 g = &Path{} 140 } 141 return g.Set(str) 142 } 143 144 // ResolveReference returns the path relative to the current path 145 func (g Path) ResolveReference(ref *url.URL) (*Path, error) { 146 var newP Path 147 if err := newP.SetURL(g.url.ResolveReference(ref)); err != nil { 148 return nil, err 149 } 150 return &newP, nil 151 } 152 153 // Bucket returns bucket in gs://bucket/obj 154 func (g Path) Bucket() string { 155 return g.url.Host 156 } 157 158 // Object returns path/to/something in gs://bucket/path/to/something 159 func (g Path) Object() string { 160 if g.url.Path == "" { 161 return g.url.Path 162 } 163 return g.url.Path[1:] 164 } 165 166 func calcCRC(buf []byte) uint32 { 167 return crc32.Checksum(buf, crc32.MakeTable(crc32.Castagnoli)) 168 } 169 170 const ( 171 // DefaultACL will use the default acl for this upload. 172 DefaultACL = false 173 // PublicRead will use a ACL for this upload. 174 PublicRead = true 175 // NoCache may cache, but only after verifying. 176 // See https://cloud.google.com/storage/docs/metadata#cache-control 177 NoCache = "no-cache" 178 ) 179 180 // Upload writes bytes to the specified Path by converting the client and path into an ObjectHandle. 181 func Upload(ctx context.Context, client *storage.Client, path Path, buf []byte, worldReadable bool, cacheControl string) (*storage.ObjectAttrs, error) { 182 return realGCSClient{client: client}.Upload(ctx, path, buf, worldReadable, cacheControl) 183 } 184 185 // UploadHandle writes bytes to the specified ObjectHandle 186 func UploadHandle(ctx context.Context, handle *storage.ObjectHandle, buf []byte, worldReadable bool, cacheControl string) (*storage.ObjectAttrs, error) { 187 crc := calcCRC(buf) 188 w := handle.NewWriter(ctx) 189 defer w.Close() 190 if worldReadable { 191 w.ACL = []storage.ACLRule{{Entity: storage.AllUsers, Role: storage.RoleReader}} 192 } 193 if cacheControl != "" { 194 w.ObjectAttrs.CacheControl = cacheControl 195 } 196 w.SendCRC32C = true 197 // Send our CRC32 to ensure google received the same data we sent. 198 // See checksum example at: 199 // https://godoc.org/cloud.google.com/go/storage#Writer.Write 200 w.ObjectAttrs.CRC32C = crc 201 w.ProgressFunc = func(bytes int64) { 202 log.Printf("Uploading gs://%s/%s: %d/%d...", handle.BucketName(), handle.ObjectName(), bytes, len(buf)) 203 } 204 if n, err := w.Write(buf); err != nil { 205 return nil, fmt.Errorf("write: %w", err) 206 } else if n != len(buf) { 207 return nil, fmt.Errorf("partial write: %d < %d", n, len(buf)) 208 } 209 if err := w.Close(); err != nil { 210 return nil, fmt.Errorf("close: %w", err) 211 } 212 return w.Attrs(), nil 213 } 214 215 // DownloadGrid downloads and decompresses a grid from the specified path. 216 func DownloadGrid(ctx context.Context, opener Opener, path Path) (*statepb.Grid, *storage.ReaderObjectAttrs, error) { 217 var g statepb.Grid 218 r, attrs, err := opener.Open(ctx, path) 219 if err != nil && errors.Is(err, storage.ErrObjectNotExist) { 220 return &g, nil, nil 221 } 222 if err != nil { 223 return nil, nil, fmt.Errorf("open: %w", err) 224 } 225 defer r.Close() 226 zr, err := zlib.NewReader(r) 227 if err != nil { 228 return nil, nil, fmt.Errorf("open zlib: %w", err) 229 } 230 pbuf, err := ioutil.ReadAll(zr) 231 if err != nil { 232 return nil, nil, fmt.Errorf("decompress: %w", err) 233 } 234 err = proto.Unmarshal(pbuf, &g) 235 return &g, attrs, err 236 } 237 238 // MarshalGrid serializes a state proto into zlib-compressed bytes. 239 func MarshalGrid(grid *statepb.Grid) ([]byte, error) { 240 buf, err := proto.Marshal(grid) 241 if err != nil { 242 return nil, fmt.Errorf("marshal: %w", err) 243 } 244 var zbuf bytes.Buffer 245 zw := zlib.NewWriter(&zbuf) 246 if _, err = zw.Write(buf); err != nil { 247 return nil, fmt.Errorf("compress: %w", err) 248 } 249 if err = zw.Close(); err != nil { 250 return nil, fmt.Errorf("close: %w", err) 251 } 252 return zbuf.Bytes(), nil 253 }