github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/storage/chunk/client/gcp/gcs_object_client.go (about)

     1  package gcp
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"flag"
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"time"
    11  
    12  	"cloud.google.com/go/storage"
    13  	"github.com/grafana/dskit/flagext"
    14  	"github.com/pkg/errors"
    15  	"github.com/prometheus/client_golang/prometheus"
    16  	"google.golang.org/api/iterator"
    17  	"google.golang.org/api/option"
    18  	google_http "google.golang.org/api/transport/http"
    19  
    20  	"github.com/grafana/loki/pkg/storage/chunk/client"
    21  	"github.com/grafana/loki/pkg/storage/chunk/client/hedging"
    22  	"github.com/grafana/loki/pkg/storage/chunk/client/util"
    23  )
    24  
    25  type ClientFactory func(ctx context.Context, opts ...option.ClientOption) (*storage.Client, error)
    26  
    27  type GCSObjectClient struct {
    28  	cfg GCSConfig
    29  
    30  	defaultBucket *storage.BucketHandle
    31  	getsBuckets   *storage.BucketHandle
    32  }
    33  
    34  // GCSConfig is config for the GCS Chunk Client.
    35  type GCSConfig struct {
    36  	BucketName       string         `yaml:"bucket_name"`
    37  	ServiceAccount   flagext.Secret `yaml:"service_account"`
    38  	ChunkBufferSize  int            `yaml:"chunk_buffer_size"`
    39  	RequestTimeout   time.Duration  `yaml:"request_timeout"`
    40  	EnableOpenCensus bool           `yaml:"enable_opencensus"`
    41  	EnableHTTP2      bool           `yaml:"enable_http2"`
    42  
    43  	Insecure bool `yaml:"-"`
    44  }
    45  
    46  // RegisterFlags registers flags.
    47  func (cfg *GCSConfig) RegisterFlags(f *flag.FlagSet) {
    48  	cfg.RegisterFlagsWithPrefix("", f)
    49  }
    50  
    51  // RegisterFlagsWithPrefix registers flags with prefix.
    52  func (cfg *GCSConfig) RegisterFlagsWithPrefix(prefix string, f *flag.FlagSet) {
    53  	f.StringVar(&cfg.BucketName, prefix+"gcs.bucketname", "", "Name of GCS bucket. Please refer to https://cloud.google.com/docs/authentication/production for more information about how to configure authentication.")
    54  	f.Var(&cfg.ServiceAccount, prefix+"gcs.service-account", "Service account key content in JSON format, refer to https://cloud.google.com/iam/docs/creating-managing-service-account-keys for creation.")
    55  	f.IntVar(&cfg.ChunkBufferSize, prefix+"gcs.chunk-buffer-size", 0, "The size of the buffer that GCS client for each PUT request. 0 to disable buffering.")
    56  	f.DurationVar(&cfg.RequestTimeout, prefix+"gcs.request-timeout", 0, "The duration after which the requests to GCS should be timed out.")
    57  	f.BoolVar(&cfg.EnableOpenCensus, prefix+"gcs.enable-opencensus", true, "Enable OpenCensus (OC) instrumentation for all requests.")
    58  	f.BoolVar(&cfg.EnableHTTP2, prefix+"gcs.enable-http2", true, "Enable HTTP2 connections.")
    59  }
    60  
    61  // NewGCSObjectClient makes a new chunk.Client that writes chunks to GCS.
    62  func NewGCSObjectClient(ctx context.Context, cfg GCSConfig, hedgingCfg hedging.Config) (*GCSObjectClient, error) {
    63  	return newGCSObjectClient(ctx, cfg, hedgingCfg, storage.NewClient)
    64  }
    65  
    66  func newGCSObjectClient(ctx context.Context, cfg GCSConfig, hedgingCfg hedging.Config, clientFactory ClientFactory) (*GCSObjectClient, error) {
    67  	// Disabling http2 and hedging is not allowed for POST/LIST/DELETE requests.
    68  	// This is because there's no benefit for these requests.
    69  	// Those requests are handled by the default bucket handle.
    70  	bucket, err := newBucketHandle(ctx, cfg, hedgingCfg, true, false, clientFactory)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	getsBucket, err := newBucketHandle(ctx, cfg, hedgingCfg, cfg.EnableHTTP2, true, clientFactory)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	return &GCSObjectClient{
    79  		cfg:           cfg,
    80  		defaultBucket: bucket,
    81  		getsBuckets:   getsBucket,
    82  	}, nil
    83  }
    84  
    85  func newBucketHandle(ctx context.Context, cfg GCSConfig, hedgingCfg hedging.Config, enableHTTP2, hedging bool, clientFactory ClientFactory) (*storage.BucketHandle, error) {
    86  	var opts []option.ClientOption
    87  	transport, err := gcsTransport(ctx, storage.ScopeReadWrite, cfg.Insecure, enableHTTP2, cfg.ServiceAccount)
    88  	if err != nil {
    89  		return nil, err
    90  	}
    91  	httpClient := gcsInstrumentation(transport)
    92  
    93  	if hedging {
    94  		httpClient, err = hedgingCfg.ClientWithRegisterer(httpClient, prometheus.WrapRegistererWithPrefix("loki_", prometheus.DefaultRegisterer))
    95  		if err != nil {
    96  			return nil, err
    97  		}
    98  	}
    99  
   100  	opts = append(opts, option.WithHTTPClient(httpClient))
   101  	if !cfg.EnableOpenCensus {
   102  		opts = append(opts, option.WithTelemetryDisabled())
   103  	}
   104  
   105  	client, err := clientFactory(ctx, opts...)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  
   110  	return client.Bucket(cfg.BucketName), nil
   111  }
   112  
   113  func (s *GCSObjectClient) Stop() {
   114  }
   115  
   116  // GetObject returns a reader and the size for the specified object key from the configured GCS bucket.
   117  func (s *GCSObjectClient) GetObject(ctx context.Context, objectKey string) (io.ReadCloser, int64, error) {
   118  	var cancel context.CancelFunc = func() {}
   119  	if s.cfg.RequestTimeout > 0 {
   120  		ctx, cancel = context.WithTimeout(ctx, s.cfg.RequestTimeout)
   121  	}
   122  
   123  	rc, size, err := s.getObject(ctx, objectKey)
   124  	if err != nil {
   125  		// cancel the context if there is an error.
   126  		cancel()
   127  		return nil, 0, err
   128  	}
   129  	// else return a wrapped ReadCloser which cancels the context while closing the reader.
   130  	return util.NewReadCloserWithContextCancelFunc(rc, cancel), size, nil
   131  }
   132  
   133  func (s *GCSObjectClient) getObject(ctx context.Context, objectKey string) (rc io.ReadCloser, size int64, err error) {
   134  	reader, err := s.getsBuckets.Object(objectKey).NewReader(ctx)
   135  	if err != nil {
   136  		return nil, 0, err
   137  	}
   138  
   139  	return reader, reader.Attrs.Size, nil
   140  }
   141  
   142  // PutObject puts the specified bytes into the configured GCS bucket at the provided key
   143  func (s *GCSObjectClient) PutObject(ctx context.Context, objectKey string, object io.ReadSeeker) error {
   144  	writer := s.defaultBucket.Object(objectKey).NewWriter(ctx)
   145  	// Default GCSChunkSize is 8M and for each call, 8M is allocated xD
   146  	// By setting it to 0, we just upload the object in a single a request
   147  	// which should work for our chunk sizes.
   148  	writer.ChunkSize = s.cfg.ChunkBufferSize
   149  
   150  	if _, err := io.Copy(writer, object); err != nil {
   151  		_ = writer.Close()
   152  		return err
   153  	}
   154  	return writer.Close()
   155  }
   156  
   157  // List implements chunk.ObjectClient.
   158  func (s *GCSObjectClient) List(ctx context.Context, prefix, delimiter string) ([]client.StorageObject, []client.StorageCommonPrefix, error) {
   159  	var storageObjects []client.StorageObject
   160  	var commonPrefixes []client.StorageCommonPrefix
   161  	q := &storage.Query{Prefix: prefix, Delimiter: delimiter}
   162  
   163  	// Using delimiter and selected attributes doesn't work well together -- it returns nothing.
   164  	// Reason is that Go's API only sets "fields=items(name,updated)" parameter in the request,
   165  	// but what we really need is "fields=prefixes,items(name,updated)". Unfortunately we cannot set that,
   166  	// so instead we don't use attributes selection when using delimiter.
   167  	if delimiter == "" {
   168  		err := q.SetAttrSelection([]string{"Name", "Updated"})
   169  		if err != nil {
   170  			return nil, nil, err
   171  		}
   172  	}
   173  
   174  	iter := s.defaultBucket.Objects(ctx, q)
   175  	for {
   176  		if ctx.Err() != nil {
   177  			return nil, nil, ctx.Err()
   178  		}
   179  
   180  		attr, err := iter.Next()
   181  		if err != nil {
   182  			if err == iterator.Done {
   183  				break
   184  			}
   185  			return nil, nil, err
   186  		}
   187  
   188  		// When doing query with Delimiter, Prefix is the only field set for entries which represent synthetic "directory entries".
   189  		if attr.Prefix != "" {
   190  			commonPrefixes = append(commonPrefixes, client.StorageCommonPrefix(attr.Prefix))
   191  			continue
   192  		}
   193  
   194  		storageObjects = append(storageObjects, client.StorageObject{
   195  			Key:        attr.Name,
   196  			ModifiedAt: attr.Updated,
   197  		})
   198  	}
   199  
   200  	return storageObjects, commonPrefixes, nil
   201  }
   202  
   203  // DeleteObject deletes the specified object key from the configured GCS bucket.
   204  func (s *GCSObjectClient) DeleteObject(ctx context.Context, objectKey string) error {
   205  	err := s.defaultBucket.Object(objectKey).Delete(ctx)
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	return nil
   211  }
   212  
   213  // IsObjectNotFoundErr returns true if error means that object is not found. Relevant to GetObject and DeleteObject operations.
   214  func (s *GCSObjectClient) IsObjectNotFoundErr(err error) bool {
   215  	return errors.Is(err, storage.ErrObjectNotExist)
   216  }
   217  
   218  func gcsTransport(ctx context.Context, scope string, insecure bool, http2 bool, serviceAccount flagext.Secret) (http.RoundTripper, error) {
   219  	customTransport := &http.Transport{
   220  		Proxy: http.ProxyFromEnvironment,
   221  		DialContext: (&net.Dialer{
   222  			Timeout:   30 * time.Second,
   223  			KeepAlive: 30 * time.Second,
   224  		}).DialContext,
   225  		ForceAttemptHTTP2:     true,
   226  		MaxIdleConns:          200,
   227  		MaxIdleConnsPerHost:   200,
   228  		IdleConnTimeout:       90 * time.Second,
   229  		TLSHandshakeTimeout:   10 * time.Second,
   230  		ExpectContinueTimeout: 1 * time.Second,
   231  	}
   232  	if !http2 {
   233  		// disable HTTP/2 by setting TLSNextProto to non-nil empty map, as per the net/http documentation.
   234  		// see http2 section of https://pkg.go.dev/net/http
   235  		customTransport.TLSNextProto = make(map[string]func(string, *tls.Conn) http.RoundTripper)
   236  		customTransport.ForceAttemptHTTP2 = false
   237  	}
   238  	transportOptions := []option.ClientOption{option.WithScopes(scope)}
   239  	if insecure {
   240  		customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
   241  		// When using `insecure` (testing only), we add a fake API key as well to skip credential chain lookups.
   242  		transportOptions = append(transportOptions, option.WithAPIKey("insecure"))
   243  	}
   244  	if serviceAccount.String() != "" {
   245  		transportOptions = append(transportOptions, option.WithCredentialsJSON([]byte(serviceAccount.String())))
   246  	}
   247  	return google_http.NewTransport(ctx, customTransport, transportOptions...)
   248  }