Navigation Menu

Skip to content

Commit

Permalink
feat(bigquery): enable project autodetection, expose project ids furt…
Browse files Browse the repository at this point in the history
…her (#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.
  • Loading branch information
shollyman committed Jun 24, 2021
1 parent 8ebbf6b commit 267787e
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 0 deletions.
39 changes: 39 additions & 0 deletions bigquery/bigquery.go
Expand Up @@ -16,6 +16,7 @@ package bigquery

import (
"context"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -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 (
Expand Down Expand Up @@ -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),
Expand All @@ -68,20 +82,45 @@ 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,
}
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.
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)
Expand Down
18 changes: 18 additions & 0 deletions bigquery/integration_test.go
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions bigquery/job.go
Expand Up @@ -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
Expand Down

0 comments on commit 267787e

Please sign in to comment.