Repository: googleapis/google-cloud-go
Pull request #4312 (2021-06-24 16:59:50)

feat(bigquery): enable project autodetection, expose project ids further
PR supersedes: https://github.com/googleapis/google-cloud-go/pull/4076

Related: https://github.com/googleapis/google-cloud-go/issues/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.


Patch:
diff --git a/bigquery/bigquery.go b/bigquery/bigquery.go
index 0a32f02c3d..28ea5446f1 100644
--- a/bigquery/bigquery.go
+++ b/bigquery/bigquery.go
@@ -16,6 +16,7 @@ package bigquery
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
@@ -29,6 +30,7 @@ import (
 	bq "google.golang.org/api/bigquery/v2"
 	"google.golang.org/api/googleapi"
 	"google.golang.org/api/option"
+	"google.golang.org/api/transport"
 )
 
 const (
@@ -56,8 +58,20 @@ type Client struct {
 	bqs       *bq.Service
 }
 
+// DetectProjectID is a sentinel value that instructs NewClient to detect the
+// project ID. It is given in place of the projectID argument. NewClient will
+// use the project ID from the given credentials or the default credentials
+// (https://developers.google.com/accounts/docs/application-default-credentials)
+// if no credentials were provided. When providing credentials, not all
+// options will allow NewClient to extract the project ID. Specifically a JWT
+// does not have the project ID encoded.
+const DetectProjectID = "*detect-project-id*"
+
 // NewClient constructs a new Client which can perform BigQuery operations.
 // Operations performed via the client are billed to the specified GCP project.
+//
+// If the project ID is set to DetectProjectID, NewClient will attempt to detect
+// the project ID from credentials.
 func NewClient(ctx context.Context, projectID string, opts ...option.ClientOption) (*Client, error) {
 	o := []option.ClientOption{
 		option.WithScopes(Scope),
@@ -68,6 +82,14 @@ func NewClient(ctx context.Context, projectID string, opts ...option.ClientOptio
 	if err != nil {
 		return nil, fmt.Errorf("bigquery: constructing client: %v", err)
 	}
+
+	if projectID == DetectProjectID {
+		projectID, err = detectProjectID(ctx, opts...)
+		if err != nil {
+			return nil, fmt.Errorf("failed to detect project: %v", err)
+		}
+	}
+
 	c := &Client{
 		projectID: projectID,
 		bqs:       bqs,
@@ -75,6 +97,12 @@ func NewClient(ctx context.Context, projectID string, opts ...option.ClientOptio
 	return c, nil
 }
 
+// Project returns the project ID or number for this instance of the client, which may have
+// either been explicitly specified or autodetected.
+func (c *Client) Project() string {
+	return c.projectID
+}
+
 // Close closes any resources held by the client.
 // Close should be called when the client is no longer needed.
 // It need not be called at program exit.
@@ -82,6 +110,17 @@ func (c *Client) Close() error {
 	return nil
 }
 
+func detectProjectID(ctx context.Context, opts ...option.ClientOption) (string, error) {
+	creds, err := transport.Creds(ctx, opts...)
+	if err != nil {
+		return "", fmt.Errorf("fetching creds: %v", err)
+	}
+	if creds.ProjectID == "" {
+		return "", errors.New("credentials did not provide a valid ProjectID")
+	}
+	return creds.ProjectID, nil
+}
+
 // Calls the Jobs.Insert RPC and returns a Job.
 func (c *Client) insertJob(ctx context.Context, job *bq.Job, media io.Reader) (*Job, error) {
 	call := c.bqs.Jobs.Insert(c.projectID, job).Context(ctx)
diff --git a/bigquery/job.go b/bigquery/job.go
index 2d259f910b..725775dfd6 100644
--- a/bigquery/job.go
+++ b/bigquery/job.go
@@ -63,6 +63,11 @@ func (c *Client) JobFromIDLocation(ctx context.Context, id, location string) (j
 	return bqToJob(bqjob, c)
 }
 
+// Project returns the job's project.
+func (j *Job) Project() string {
+	return j.projectID
+}
+
 // ID returns the job's ID.
 func (j *Job) ID() string {
 	return j.jobID


Test patch:
diff --git a/bigquery/integration_test.go b/bigquery/integration_test.go
index 9cca940d87..8b7b2a1e5a 100644
--- a/bigquery/integration_test.go
+++ b/bigquery/integration_test.go
@@ -228,6 +228,24 @@ func initTestState(client *Client, t time.Time) func() {
 	}
 }
 
+func TestIntegration_DetectProjectID(t *testing.T) {
+	ctx := context.Background()
+	testCreds := testutil.Credentials(ctx)
+	if testCreds == nil {
+		t.Skip("test credentials not present, skipping")
+	}
+
+	if _, err := NewClient(ctx, DetectProjectID, option.WithCredentials(testCreds)); err != nil {
+		t.Errorf("test NewClient: %v", err)
+	}
+
+	badTS := testutil.ErroringTokenSource{}
+
+	if badClient, err := NewClient(ctx, DetectProjectID, option.WithTokenSource(badTS)); err == nil {
+		t.Errorf("expected error from bad token source, NewClient succeeded with project: %s", badClient.Project())
+	}
+}
+
 func TestIntegration_TableCreate(t *testing.T) {
 	// Check that creating a record field with an empty schema is an error.
 	if client == nil {