Skip to content

Commit f3e7ca9

Browse files
Deeven-SeruYuan325
andauthored
feat: add quotaProject support for BigQuery and Looker conversational analytics (#2610)
Fixes #2543 ## Summary - Add optional `quotaProject` to BigQuery source config - Apply `option.WithQuotaProject(quotaProject)` when creating BigQuery clients/services - Add optional `quotaProject` to Looker source config - For `looker-conversational-analytics`, set `X-Goog-User-Project` header when configured ## Why When `useClientOAuth: true`, quota/billing can be attributed to the token-issuing project. This allows explicit quota attribution to a deployment-controlled project. ## Validation - go test ./internal/sources/bigquery - go test ./internal/sources/looker - go test ./internal/tools/looker/lookerconversationalanalytics --------- Co-authored-by: Yuan Teoh <45984206+Yuan325@users.noreply.github.com>
1 parent 17108de commit f3e7ca9

6 files changed

Lines changed: 90 additions & 5 deletions

File tree

internal/sources/bigquery/bigquery.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ type Config struct {
8888
WriteMode string `yaml:"writeMode"`
8989
AllowedDatasets StringOrStringSlice `yaml:"allowedDatasets"`
9090
UseClientOAuth string `yaml:"useClientOAuth"`
91+
QuotaProject string `yaml:"quotaProject"`
9192
ImpersonateServiceAccount string `yaml:"impersonateServiceAccount"`
9293
Scopes StringOrStringSlice `yaml:"scopes"`
9394
MaxQueryResultRows int `yaml:"maxQueryResultRows"`
@@ -166,7 +167,7 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
166167

167168
if strings.ToLower(r.UseClientOAuth) == "false" || r.UseClientOAuth == "" {
168169
// Initializes a BigQuery Google SQL source
169-
client, restService, tokenSource, err = initBigQueryConnection(ctx, tracer, r.Name, r.Project, r.Location, r.ImpersonateServiceAccount, r.Scopes)
170+
client, restService, tokenSource, err = initBigQueryConnection(ctx, tracer, r.Name, r.Project, r.Location, r.QuotaProject, r.ImpersonateServiceAccount, r.Scopes)
170171
if err != nil {
171172
return nil, fmt.Errorf("error creating client from ADC: %w", err)
172173
}
@@ -183,7 +184,7 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
183184
s.AuthTokenHeaderName = r.UseClientOAuth
184185
}
185186
// use client OAuth
186-
baseClientCreator, err := newBigQueryClientCreator(ctx, tracer, r.Project, r.Location, r.Name)
187+
baseClientCreator, err := newBigQueryClientCreator(ctx, tracer, r.Project, r.Location, r.QuotaProject, r.Name)
187188
if err != nil {
188189
return nil, fmt.Errorf("error constructing client creator: %w", err)
189190
}
@@ -446,6 +447,10 @@ func (s *Source) BigQueryLocation() string {
446447
return s.Location
447448
}
448449

450+
func (s *Source) BigQueryQuotaProject() string {
451+
return s.QuotaProject
452+
}
453+
449454
func (s *Source) BigQueryTokenSource() oauth2.TokenSource {
450455
return s.TokenSource
451456
}
@@ -690,6 +695,7 @@ func initBigQueryConnection(
690695
name string,
691696
project string,
692697
location string,
698+
quotaProject string,
693699
impersonateServiceAccount string,
694700
scopes []string,
695701
) (*bigqueryapi.Client, *bigqueryrestapi.Service, oauth2.TokenSource, error) {
@@ -741,6 +747,10 @@ func initBigQueryConnection(
741747
}
742748
}
743749

750+
if quotaProject != "" {
751+
opts = append(opts, option.WithQuotaProject(quotaProject))
752+
}
753+
744754
// Initialize the high-level BigQuery client
745755
client, err := bigqueryapi.NewClient(ctx, project, opts...)
746756
if err != nil {
@@ -764,6 +774,7 @@ func initBigQueryConnectionWithOAuthToken(
764774
tracer trace.Tracer,
765775
project string,
766776
location string,
777+
quotaProject string,
767778
name string,
768779
userAgent string,
769780
tokenString string,
@@ -777,16 +788,24 @@ func initBigQueryConnectionWithOAuthToken(
777788
}
778789
ts := oauth2.StaticTokenSource(token)
779790

791+
opts := []option.ClientOption{
792+
option.WithUserAgent(userAgent),
793+
option.WithTokenSource(ts),
794+
}
795+
if quotaProject != "" {
796+
opts = append(opts, option.WithQuotaProject(quotaProject))
797+
}
798+
780799
// Initialize the BigQuery client with tokenSource
781-
client, err := bigqueryapi.NewClient(ctx, project, option.WithUserAgent(userAgent), option.WithTokenSource(ts))
800+
client, err := bigqueryapi.NewClient(ctx, project, opts...)
782801
if err != nil {
783802
return nil, nil, fmt.Errorf("failed to create BigQuery client for project %q: %w", project, err)
784803
}
785804
client.Location = location
786805

787806
if wantRestService {
788807
// Initialize the low-level BigQuery REST service using the same credentials
789-
restService, err := bigqueryrestapi.NewService(ctx, option.WithUserAgent(userAgent), option.WithTokenSource(ts))
808+
restService, err := bigqueryrestapi.NewService(ctx, opts...)
790809
if err != nil {
791810
return nil, nil, fmt.Errorf("failed to create BigQuery v2 service: %w", err)
792811
}
@@ -804,6 +823,7 @@ func newBigQueryClientCreator(
804823
tracer trace.Tracer,
805824
project string,
806825
location string,
826+
quotaProject string,
807827
name string,
808828
) (func(string, bool) (*bigqueryapi.Client, *bigqueryrestapi.Service, error), error) {
809829
userAgent, err := util.UserAgentFromContext(ctx)
@@ -812,7 +832,7 @@ func newBigQueryClientCreator(
812832
}
813833

814834
return func(tokenString string, wantRestService bool) (*bigqueryapi.Client, *bigqueryrestapi.Service, error) {
815-
return initBigQueryConnectionWithOAuthToken(ctx, tracer, project, location, name, userAgent, tokenString, wantRestService)
835+
return initBigQueryConnectionWithOAuthToken(ctx, tracer, project, location, quotaProject, name, userAgent, tokenString, wantRestService)
816836
}, nil
817837
}
818838

internal/sources/bigquery/bigquery_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,28 @@ func TestParseFromYamlBigQuery(t *testing.T) {
154154
},
155155
},
156156
},
157+
{
158+
desc: "quota project with client auth example",
159+
in: `
160+
kind: source
161+
name: my-instance
162+
type: bigquery
163+
project: my-project
164+
location: us
165+
useClientOAuth: true
166+
quotaProject: billing-project
167+
`,
168+
want: map[string]sources.SourceConfig{
169+
"my-instance": bigquery.Config{
170+
Name: "my-instance",
171+
Type: bigquery.SourceType,
172+
Project: "my-project",
173+
Location: "us",
174+
UseClientOAuth: "true",
175+
QuotaProject: "billing-project",
176+
},
177+
},
178+
},
157179
{
158180
desc: "with allowed datasets example",
159181
in: `

internal/sources/looker/looker.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ type Config struct {
7676
ShowHiddenFields bool `yaml:"show_hidden_fields"`
7777
Project string `yaml:"project"`
7878
Location string `yaml:"location"`
79+
QuotaProject string `yaml:"quotaProject"`
7980
SessionLength int64 `yaml:"sessionLength"`
8081
}
8182

@@ -178,6 +179,10 @@ func (s *Source) GoogleCloudLocation() string {
178179
return s.Location
179180
}
180181

182+
func (s *Source) GoogleCloudQuotaProject() string {
183+
return s.QuotaProject
184+
}
185+
181186
func (s *Source) GoogleCloudTokenSource() oauth2.TokenSource {
182187
return s.TokenSource
183188
}

internal/sources/looker/looker_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,36 @@ func TestParseFromYamlLooker(t *testing.T) {
6565
},
6666
},
6767
},
68+
{
69+
desc: "with quota project",
70+
in: `
71+
kind: source
72+
name: my-looker-instance
73+
type: looker
74+
base_url: http://example.looker.com/
75+
client_id: jasdl;k;tjl
76+
client_secret: sdakl;jgflkasdfkfg
77+
quotaProject: billing-project
78+
`,
79+
want: map[string]sources.SourceConfig{
80+
"my-looker-instance": looker.Config{
81+
Name: "my-looker-instance",
82+
Type: looker.SourceType,
83+
BaseURL: "http://example.looker.com/",
84+
ClientId: "jasdl;k;tjl",
85+
ClientSecret: "sdakl;jgflkasdfkfg",
86+
Timeout: "600s",
87+
SslVerification: true,
88+
UseClientOAuth: "false",
89+
ShowHiddenModels: true,
90+
ShowHiddenExplores: true,
91+
ShowHiddenFields: true,
92+
Location: "us",
93+
QuotaProject: "billing-project",
94+
SessionLength: 1200,
95+
},
96+
},
97+
},
6898
}
6999
for _, tc := range tcs {
70100
t.Run(tc.desc, func(t *testing.T) {

internal/tools/bigquery/bigqueryconversationalanalytics/bigqueryconversationalanalytics.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ type compatibleSource interface {
6464
BigQueryTokenSourceWithScope(ctx context.Context, scopes []string) (oauth2.TokenSource, error)
6565
BigQueryProject() string
6666
BigQueryLocation() string
67+
BigQueryQuotaProject() string
6768
GetMaxQueryResultRows() int
6869
UseClientAuthorization() bool
6970
GetAuthTokenHeaderName() string
@@ -216,6 +217,9 @@ func (t Tool) Invoke(ctx context.Context, primitiveMgr tools.SourceProvider, par
216217
"Content-Type": "application/json",
217218
"X-Goog-API-Client": util.GDAClientID,
218219
}
220+
if quotaProject := source.BigQueryQuotaProject(); quotaProject != "" {
221+
headers["X-Goog-User-Project"] = quotaProject
222+
}
219223

220224
payload := CAPayload{
221225
Messages: []Message{{UserMessage: UserMessage{Text: finalQueryText}}},

internal/tools/looker/lookerconversationalanalytics/lookerconversationalanalytics.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type compatibleSource interface {
5959
GoogleCloudTokenSourceWithScope(ctx context.Context, scope string) (oauth2.TokenSource, error)
6060
GoogleCloudProject() string
6161
GoogleCloudLocation() string
62+
GoogleCloudQuotaProject() string
6263
UseClientAuthorization() bool
6364
GetAuthTokenHeaderName() string
6465
LookerApiSettings() *rtl.ApiSettings
@@ -259,6 +260,9 @@ func (t Tool) Invoke(ctx context.Context, primitiveMgr tools.SourceProvider, par
259260
"Content-Type": "application/json",
260261
"X-Goog-API-Client": util.GDAClientID,
261262
}
263+
if quotaProject := source.GoogleCloudQuotaProject(); quotaProject != "" {
264+
headers["X-Goog-User-Project"] = quotaProject
265+
}
262266

263267
payload := CAPayload{
264268
Messages: []Message{{UserMessage: UserMessage{Text: userQuery}}},

0 commit comments

Comments
 (0)