option: support system parameters
- Support system parameters in both gRPC and Http
- For HTTP, we added an http.transport wrapper named as parameterTransport
to inject the parameters into HTTP headers.
- For gRPC, we added an implementation of credentials.PerRPCCredentials which
embeds grpc.TokenSource to inject the parameters into the request metadata.
- X-Goog-IAM-Authorization-Token is not supported since it is not
implemented yet at the server-side.
- For more information please read: https://cloud.google.com/apis/docs/system-parameters
Change-Id: I8a6dc00bb7dcd43cb6ac1899d1f55847264dc019
Reviewed-on: https://code-review.googlesource.com/c/37870
Reviewed-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Jean de Klerk <deklerk@google.com>
Reviewed-by: Chris Broadfoot <cbro@google.com>
diff --git a/internal/settings.go b/internal/settings.go
index 92ccf7d..062301c 100644
--- a/internal/settings.go
+++ b/internal/settings.go
@@ -40,6 +40,11 @@
GRPCDialOpts []grpc.DialOption
GRPCConn *grpc.ClientConn
NoAuth bool
+
+ // Google API system parameters. For more information please read:
+ // https://cloud.google.com/apis/docs/system-parameters
+ QuotaProject string
+ RequestReason string
}
// Validate reports an error if ds is invalid.
@@ -80,6 +85,12 @@
if ds.HTTPClient != nil && ds.GRPCDialOpts != nil {
return errors.New("WithHTTPClient is incompatible with gRPC dial options")
}
+ if ds.HTTPClient != nil && ds.QuotaProject != "" {
+ return errors.New("WithHTTPClient is incompatible with QuotaProject")
+ }
+ if ds.HTTPClient != nil && ds.RequestReason != "" {
+ return errors.New("WithHTTPClient is incompatible with RequestReason")
+ }
return nil
}
diff --git a/internal/settings_test.go b/internal/settings_test.go
index 1522d4c..6f445dd 100644
--- a/internal/settings_test.go
+++ b/internal/settings_test.go
@@ -62,6 +62,8 @@
{HTTPClient: &http.Client{}, GRPCConn: &grpc.ClientConn{}},
{HTTPClient: &http.Client{}, GRPCDialOpts: []grpc.DialOption{grpc.WithInsecure()}},
{Audiences: []string{"foo"}, Scopes: []string{"foo"}},
+ {HTTPClient: &http.Client{}, QuotaProject: "foo"},
+ {HTTPClient: &http.Client{}, RequestReason: "foo"},
} {
err := ds.Validate()
if err == nil {
diff --git a/option/option.go b/option/option.go
index 1e3ace0..0a1c2db 100644
--- a/option/option.go
+++ b/option/option.go
@@ -202,3 +202,34 @@
type withoutAuthentication struct{}
func (w withoutAuthentication) Apply(o *internal.DialSettings) { o.NoAuth = true }
+
+// WithQuotaProject returns a ClientOption that specifies the project used
+// for quota and billing purposes.
+//
+// For more information please read:
+// https://cloud.google.com/apis/docs/system-parameters
+func WithQuotaProject(quotaProject string) ClientOption {
+ return withQuotaProject(quotaProject)
+}
+
+type withQuotaProject string
+
+func (w withQuotaProject) Apply(o *internal.DialSettings) {
+ o.QuotaProject = string(w)
+}
+
+// WithRequestReason returns a ClientOption that specifies a reason for
+// making the request, which is intended to be recorded in audit logging.
+// An example reason would be a support-case ticket number.
+//
+// For more information please read:
+// https://cloud.google.com/apis/docs/system-parameters
+func WithRequestReason(requestReason string) ClientOption {
+ return withRequestReason(requestReason)
+}
+
+type withRequestReason string
+
+func (w withRequestReason) Apply(o *internal.DialSettings) {
+ o.RequestReason = string(w)
+}
diff --git a/option/option_test.go b/option/option_test.go
index 7b49d91..74f013e 100644
--- a/option/option_test.go
+++ b/option/option_test.go
@@ -52,6 +52,8 @@
WithCredentials(&google.DefaultCredentials{ProjectID: "p"}),
WithAPIKey("api-key"),
WithAudiences("https://example.com/"),
+ WithQuotaProject("user-project"),
+ WithRequestReason("Request Reason"),
}
var got internal.DialSettings
for _, opt := range opts {
@@ -67,6 +69,8 @@
CredentialsJSON: []byte(`{some: "json"}`),
APIKey: "api-key",
Audiences: []string{"https://example.com/"},
+ QuotaProject: "user-project",
+ RequestReason: "Request Reason",
}
if !cmp.Equal(got, want, cmpopts.IgnoreUnexported(grpc.ClientConn{})) {
t.Errorf("\ngot %#v\nwant %#v", got, want)
diff --git a/transport/grpc/dial.go b/transport/grpc/dial.go
index 62360f6..4f6b94d 100644
--- a/transport/grpc/dial.go
+++ b/transport/grpc/dial.go
@@ -74,7 +74,11 @@
return nil, err
}
grpcOpts = []grpc.DialOption{
- grpc.WithPerRPCCredentials(oauth.TokenSource{creds.TokenSource}),
+ grpc.WithPerRPCCredentials(grpcTokenSource{
+ TokenSource: oauth.TokenSource{creds.TokenSource},
+ quotaProject: o.QuotaProject,
+ requestReason: o.RequestReason,
+ }),
grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
}
}
@@ -96,3 +100,30 @@
func addOCStatsHandler(opts []grpc.DialOption) []grpc.DialOption {
return append(opts, grpc.WithStatsHandler(&ocgrpc.ClientHandler{}))
}
+
+// grpcTokenSource supplies PerRPCCredentials from an oauth.TokenSource.
+type grpcTokenSource struct {
+ oauth.TokenSource
+
+ // Additional metadata attached as headers.
+ quotaProject string
+ requestReason string
+}
+
+// GetRequestMetadata gets the request metadata as a map from a grpcTokenSource.
+func (ts grpcTokenSource) GetRequestMetadata(ctx context.Context, uri ...string) (
+ map[string]string, error) {
+ metadata, err := ts.TokenSource.GetRequestMetadata(ctx, uri...)
+ if err != nil {
+ return nil, err
+ }
+
+ // Attach system parameters into the metadata
+ if ts.quotaProject != "" {
+ metadata["X-goog-user-project"] = ts.quotaProject
+ }
+ if ts.requestReason != "" {
+ metadata["X-goog-request-reason"] = ts.requestReason
+ }
+ return metadata, nil
+}
diff --git a/transport/http/dial.go b/transport/http/dial.go
index a25da67..c0d8bf2 100644
--- a/transport/http/dial.go
+++ b/transport/http/dial.go
@@ -64,9 +64,11 @@
func newTransport(ctx context.Context, base http.RoundTripper, settings *internal.DialSettings) (http.RoundTripper, error) {
trans := base
- trans = userAgentTransport{
- base: trans,
- userAgent: settings.UserAgent,
+ trans = parameterTransport{
+ base: trans,
+ userAgent: settings.UserAgent,
+ quotaProject: settings.QuotaProject,
+ requestReason: settings.RequestReason,
}
trans = addOCTransport(trans)
switch {
@@ -104,12 +106,15 @@
return &o, nil
}
-type userAgentTransport struct {
- userAgent string
- base http.RoundTripper
+type parameterTransport struct {
+ userAgent string
+ quotaProject string
+ requestReason string
+
+ base http.RoundTripper
}
-func (t userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+func (t parameterTransport) RoundTrip(req *http.Request) (*http.Response, error) {
rt := t.base
if rt == nil {
return nil, errors.New("transport: no Transport specified")
@@ -123,7 +128,16 @@
newReq.Header[k] = vv
}
// TODO(cbro): append to existing User-Agent header?
- newReq.Header["User-Agent"] = []string{t.userAgent}
+ newReq.Header.Set("User-Agent", t.userAgent)
+
+ // Attach system parameters into the header
+ if t.quotaProject != "" {
+ newReq.Header.Set("X-Goog-User-Project", t.quotaProject)
+ }
+ if t.requestReason != "" {
+ newReq.Header.Set("X-Goog-Request-Reason", t.requestReason)
+ }
+
return rt.RoundTrip(&newReq)
}