Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GCS: Adds HTTP Config similar to S3 #86

Merged
merged 4 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ We use *breaking :warning:* to mark changes that are not backward compatible (re
- [#92](https://github.com/thanos-io/objstore/pull/92) GCS: Allow using a gRPC client.
- [#94](https://github.com/thanos-io/objstore/pull/94) Allow timingReadCloser to be seeker
- [#96](https://github.com/thanos-io/objstore/pull/96) Allow nopCloserWithObjectSize to be seeker
- [#86](https://github.com/thanos-io/objstore/pull/86) GCS: Add HTTP Config to GCS

### Changed
- [#38](https://github.com/thanos-io/objstore/pull/38) *: Upgrade minio-go version to `v7.0.45`.
Expand Down
10 changes: 10 additions & 0 deletions exthttp/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ import (
"github.com/prometheus/common/model"
)

var DefaultHTTPConfig = HTTPConfig{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might lead to an unexpected breaking change with every other provider except S3. Do we know what the defaults for GCS currently are?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC nowadays how objstorage works then it will eventually call defaultBaseTransport on the google-api-go-client in transport/http/dial.go which actually uses http.DefaultTransport src/net/http/transport.go, they only overwrite MaxIdleConnsPerHost to 100 (same value we use).

So looking at the differences if we were to move forward with my change we would be setting:

  • MaxConnsPerHost Since this is not set on the DefaultTransport struct I believe it will just use default int value of 0 which would match our value
  • ResponseHeaderTimeout Again IIUC here the default here would be 0 which would differ from our "default" of 2 minutes.
  • DialContext.DualStack This apparently has been deprecated so we can even remove it from our "default" since it's enabled by default

And we would not be setting ForceAttemptHTTP2 which http.DefaultTransport sets. This is where we could break some things.

With all this being said I don't have much experience with all this so I'm happy to play it safe and change the PR to make GCS use http.DefaultTransport

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed explanation 👍 Let's try this out and see if it has any significant impact.

IdleConnTimeout: model.Duration(90 * time.Second),
ResponseHeaderTimeout: model.Duration(2 * time.Minute),
TLSHandshakeTimeout: model.Duration(10 * time.Second),
ExpectContinueTimeout: model.Duration(1 * time.Second),
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
MaxConnsPerHost: 0,
}

// HTTPConfig stores the http.Transport configuration for the cos and s3 minio client.
type HTTPConfig struct {
IdleConnTimeout model.Duration `yaml:"idle_conn_timeout"`
Expand Down
54 changes: 49 additions & 5 deletions providers/gcs/gcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import (
"context"
"fmt"
"io"
"net/http"
"runtime"
"strings"
"testing"
"time"

"cloud.google.com/go/storage"
"github.com/go-kit/log"
Expand All @@ -19,17 +21,23 @@ import (
"golang.org/x/oauth2/google"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
htransport "google.golang.org/api/transport/http"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gopkg.in/yaml.v2"

"github.com/thanos-io/objstore"
"github.com/thanos-io/objstore/exthttp"
)

// DirDelim is the delimiter used to model a directory structure in an object store bucket.
const DirDelim = "/"

var DefaultConfig = Config{
HTTPConfig: exthttp.DefaultHTTPConfig,
}

// Config stores the configuration for gcs bucket.
type Config struct {
Bucket string `yaml:"bucket"`
Expand All @@ -39,7 +47,8 @@ type Config struct {
// when direct path is not enabled.
// See https://pkg.go.dev/cloud.google.com/go/storage#hdr-Experimental_gRPC_API for more details
// on how to enable direct path.
GRPCConnPoolSize int `yaml:"grpc_conn_pool_size"`
GRPCConnPoolSize int `yaml:"grpc_conn_pool_size"`
HTTPConfig exthttp.HTTPConfig `yaml:"http_config"`
}

// Bucket implements the store.Bucket and shipper.Bucket interfaces against GCS.
Expand All @@ -51,14 +60,23 @@ type Bucket struct {
closer io.Closer
}

// parseConfig unmarshals a buffer into a Config with default values.
func parseConfig(conf []byte) (Config, error) {
config := DefaultConfig
if err := yaml.UnmarshalStrict(conf, &config); err != nil {
return Config{}, err
}

return config, nil
}

// NewBucket returns a new Bucket against the given bucket handle.
func NewBucket(ctx context.Context, logger log.Logger, conf []byte, component string) (*Bucket, error) {
var gc Config
if err := yaml.Unmarshal(conf, &gc); err != nil {
config, err := parseConfig(conf)
if err != nil {
return nil, err
}

return NewBucketWithConfig(ctx, logger, gc, component)
return NewBucketWithConfig(ctx, logger, config, component)
}

// NewBucketWithConfig returns a new Bucket with gcs Config struct.
Expand All @@ -76,12 +94,38 @@ func NewBucketWithConfig(ctx context.Context, logger log.Logger, gc Config, comp
return nil, errors.Wrap(err, "failed to create credentials from JSON")
}
opts = append(opts, option.WithCredentials(credentials))
} else {
opts = append(opts, option.WithoutAuthentication())
Comment on lines +97 to +98

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is described as "Fix tests"; it stops the GCS client from using config from environment variables.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, From what I understand, this would break GCP workload identity from working

Context: https://cloud-native.slack.com/archives/CK5RSSC10/p1633961188039700

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Literally came to this repo to report this bug; this just hit me in a pre-prod environment and came to report it here :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoaoBraveCoding would you have time to look into this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the test fails as we call htransport.NewTransport later which errors out. Trying to fix here: #106

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had plans to fix it this morning but @saswatamcode beat me to it (thank you!) Apologies for having broken GCS authentication with env vars!

}

opts = append(opts,
option.WithUserAgent(fmt.Sprintf("thanos-%s/%s (%s)", component, version.Version, runtime.Version())),
)

// Check if a roundtripper has been set in the config
// otherwise build the default transport.
var rt http.RoundTripper
if gc.HTTPConfig.Transport != nil {
rt = gc.HTTPConfig.Transport
} else {
var err error
rt, err = exthttp.DefaultTransport(gc.HTTPConfig)
if err != nil {
return nil, err
}
}

gRT, err := htransport.NewTransport(context.Background(), rt, opts...)
if err != nil {
return nil, err
}

httpCli := &http.Client{
Transport: gRT,
Timeout: time.Duration(gc.HTTPConfig.IdleConnTimeout),
}
opts = append(opts, option.WithHTTPClient(httpCli))

return newBucket(ctx, logger, gc, opts)
}

Expand Down
57 changes: 57 additions & 0 deletions providers/gcs/gcs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (
"net/http/httptest"
"os"
"testing"
"time"

"github.com/efficientgo/core/testutil"
"github.com/go-kit/log"
"github.com/prometheus/common/model"
)

func TestBucket_Get_ShouldReturnErrorIfServerTruncateResponse(t *testing.T) {
Expand Down Expand Up @@ -44,3 +46,58 @@ func TestBucket_Get_ShouldReturnErrorIfServerTruncateResponse(t *testing.T) {
testutil.NotOk(t, err)
testutil.Equals(t, "storage: partial request not satisfied", err.Error())
}

func TestParseConfig_HTTPConfig(t *testing.T) {
for _, tc := range []struct {
name string
input string
assertions func(cfg Config)
}{
{
name: "DefaultHTTPConfig",
input: `bucket: abcd`,
assertions: func(cfg Config) {
testutil.Equals(t, cfg.HTTPConfig.IdleConnTimeout, model.Duration(90*time.Second))
testutil.Equals(t, cfg.HTTPConfig.ResponseHeaderTimeout, model.Duration(2*time.Minute))
testutil.Equals(t, cfg.HTTPConfig.InsecureSkipVerify, false)
},
},
{
name: "CustomHTTPConfig",
input: `bucket: abcd
http_config:
insecure_skip_verify: true
idle_conn_timeout: 50s
response_header_timeout: 1m`,
assertions: func(cfg Config) {
testutil.Equals(t, cfg.HTTPConfig.IdleConnTimeout, model.Duration(50*time.Second))
testutil.Equals(t, cfg.HTTPConfig.ResponseHeaderTimeout, model.Duration(1*time.Minute))
testutil.Equals(t, cfg.HTTPConfig.InsecureSkipVerify, true)
},
},
{
name: "CustomHTTPConfigWithTLS",
input: `bucket: abcd
http_config:
tls_config:
ca_file: /certs/ca.crt
cert_file: /certs/cert.crt
key_file: /certs/key.key
server_name: server
insecure_skip_verify: false`,
assertions: func(cfg Config) {
testutil.Equals(t, "/certs/ca.crt", cfg.HTTPConfig.TLSConfig.CAFile)
testutil.Equals(t, "/certs/cert.crt", cfg.HTTPConfig.TLSConfig.CertFile)
testutil.Equals(t, "/certs/key.key", cfg.HTTPConfig.TLSConfig.KeyFile)
testutil.Equals(t, "server", cfg.HTTPConfig.TLSConfig.ServerName)
testutil.Equals(t, false, cfg.HTTPConfig.TLSConfig.InsecureSkipVerify)
},
},
} {
t.Run(tc.name, func(t *testing.T) {
cfg, err := parseConfig([]byte(tc.input))
testutil.Ok(t, err)
tc.assertions(cfg)
})
}
}
14 changes: 2 additions & 12 deletions providers/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"strconv"
"strings"
"testing"
"time"

"github.com/efficientgo/core/logerrcapture"
"github.com/go-kit/log"
Expand All @@ -23,7 +22,6 @@ import (
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/minio/minio-go/v7/pkg/encrypt"
"github.com/pkg/errors"
"github.com/prometheus/common/model"
"github.com/prometheus/common/version"
"gopkg.in/yaml.v2"

Expand Down Expand Up @@ -101,16 +99,8 @@ const (
)

var DefaultConfig = Config{
PutUserMetadata: map[string]string{},
HTTPConfig: exthttp.HTTPConfig{
IdleConnTimeout: model.Duration(90 * time.Second),
ResponseHeaderTimeout: model.Duration(2 * time.Minute),
TLSHandshakeTimeout: model.Duration(10 * time.Second),
ExpectContinueTimeout: model.Duration(1 * time.Second),
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
MaxConnsPerHost: 0,
},
PutUserMetadata: map[string]string{},
HTTPConfig: exthttp.DefaultHTTPConfig,
PartSize: 1024 * 1024 * 64, // 64MB.
BucketLookupType: AutoLookup,
SendContentMd5: true, // Default to using MD5.
Expand Down
Loading