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  }