Skip to content

Commit 267787e

Browse files
authored
feat(bigquery): enable project autodetection, expose project ids further (#4312)
PR supersedes: #4076 Related: #1294 With this change, project autodetection is enabled via use of a sentinel value, and the retained project identifier is now exposed on the Client and Job resources via the Project() function.
1 parent 8ebbf6b commit 267787e

File tree

3 files changed

+62
-0
lines changed

3 files changed

+62
-0
lines changed

Diff for: bigquery/bigquery.go

+39
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package bigquery
1616

1717
import (
1818
"context"
19+
"errors"
1920
"fmt"
2021
"io"
2122
"net/http"
@@ -29,6 +30,7 @@ import (
2930
bq "google.golang.org/api/bigquery/v2"
3031
"google.golang.org/api/googleapi"
3132
"google.golang.org/api/option"
33+
"google.golang.org/api/transport"
3234
)
3335

3436
const (
@@ -56,8 +58,20 @@ type Client struct {
5658
bqs *bq.Service
5759
}
5860

61+
// DetectProjectID is a sentinel value that instructs NewClient to detect the
62+
// project ID. It is given in place of the projectID argument. NewClient will
63+
// use the project ID from the given credentials or the default credentials
64+
// (https://developers.google.com/accounts/docs/application-default-credentials)
65+
// if no credentials were provided. When providing credentials, not all
66+
// options will allow NewClient to extract the project ID. Specifically a JWT
67+
// does not have the project ID encoded.
68+
const DetectProjectID = "*detect-project-id*"
69+
5970
// NewClient constructs a new Client which can perform BigQuery operations.
6071
// Operations performed via the client are billed to the specified GCP project.
72+
//
73+
// If the project ID is set to DetectProjectID, NewClient will attempt to detect
74+
// the project ID from credentials.
6175
func NewClient(ctx context.Context, projectID string, opts ...option.ClientOption) (*Client, error) {
6276
o := []option.ClientOption{
6377
option.WithScopes(Scope),
@@ -68,20 +82,45 @@ func NewClient(ctx context.Context, projectID string, opts ...option.ClientOptio
6882
if err != nil {
6983
return nil, fmt.Errorf("bigquery: constructing client: %v", err)
7084
}
85+
86+
if projectID == DetectProjectID {
87+
projectID, err = detectProjectID(ctx, opts...)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to detect project: %v", err)
90+
}
91+
}
92+
7193
c := &Client{
7294
projectID: projectID,
7395
bqs: bqs,
7496
}
7597
return c, nil
7698
}
7799

100+
// Project returns the project ID or number for this instance of the client, which may have
101+
// either been explicitly specified or autodetected.
102+
func (c *Client) Project() string {
103+
return c.projectID
104+
}
105+
78106
// Close closes any resources held by the client.
79107
// Close should be called when the client is no longer needed.
80108
// It need not be called at program exit.
81109
func (c *Client) Close() error {
82110
return nil
83111
}
84112

113+
func detectProjectID(ctx context.Context, opts ...option.ClientOption) (string, error) {
114+
creds, err := transport.Creds(ctx, opts...)
115+
if err != nil {
116+
return "", fmt.Errorf("fetching creds: %v", err)
117+
}
118+
if creds.ProjectID == "" {
119+
return "", errors.New("credentials did not provide a valid ProjectID")
120+
}
121+
return creds.ProjectID, nil
122+
}
123+
85124
// Calls the Jobs.Insert RPC and returns a Job.
86125
func (c *Client) insertJob(ctx context.Context, job *bq.Job, media io.Reader) (*Job, error) {
87126
call := c.bqs.Jobs.Insert(c.projectID, job).Context(ctx)

Diff for: bigquery/integration_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,24 @@ func initTestState(client *Client, t time.Time) func() {
228228
}
229229
}
230230

231+
func TestIntegration_DetectProjectID(t *testing.T) {
232+
ctx := context.Background()
233+
testCreds := testutil.Credentials(ctx)
234+
if testCreds == nil {
235+
t.Skip("test credentials not present, skipping")
236+
}
237+
238+
if _, err := NewClient(ctx, DetectProjectID, option.WithCredentials(testCreds)); err != nil {
239+
t.Errorf("test NewClient: %v", err)
240+
}
241+
242+
badTS := testutil.ErroringTokenSource{}
243+
244+
if badClient, err := NewClient(ctx, DetectProjectID, option.WithTokenSource(badTS)); err == nil {
245+
t.Errorf("expected error from bad token source, NewClient succeeded with project: %s", badClient.Project())
246+
}
247+
}
248+
231249
func TestIntegration_TableCreate(t *testing.T) {
232250
// Check that creating a record field with an empty schema is an error.
233251
if client == nil {

Diff for: bigquery/job.go

+5
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ func (c *Client) JobFromIDLocation(ctx context.Context, id, location string) (j
6363
return bqToJob(bqjob, c)
6464
}
6565

66+
// Project returns the job's project.
67+
func (j *Job) Project() string {
68+
return j.projectID
69+
}
70+
6671
// ID returns the job's ID.
6772
func (j *Job) ID() string {
6873
return j.jobID

0 commit comments

Comments
 (0)