diff --git a/google/config.go b/google/config.go index 5b090ff5..44dbdeb2 100644 --- a/google/config.go +++ b/google/config.go @@ -2,11 +2,9 @@ package google import ( "context" - "encoding/json" "fmt" "log" "net/http" - "strings" "github.com/hashicorp/terraform/helper/logging" "github.com/hashicorp/terraform/helper/pathorcontents" @@ -14,8 +12,7 @@ import ( "github.com/terraform-providers/terraform-provider-google/version" "golang.org/x/oauth2" - "golang.org/x/oauth2/google" - "golang.org/x/oauth2/jwt" + googleoauth "golang.org/x/oauth2/google" appengine "google.golang.org/api/appengine/v1" "google.golang.org/api/bigquery/v2" "google.golang.org/api/cloudbilling/v1" @@ -53,6 +50,7 @@ import ( // provider. type Config struct { Credentials string + AccessToken string Project string Region string Zone string @@ -98,7 +96,6 @@ type Config struct { } func (c *Config) loadAndValidate() error { - var account accountFile clientScopes := []string{ "https://www.googleapis.com/auth/compute", "https://www.googleapis.com/auth/cloud-platform", @@ -106,55 +103,13 @@ func (c *Config) loadAndValidate() error { "https://www.googleapis.com/auth/devstorage.full_control", } - var client *http.Client - var tokenSource oauth2.TokenSource - - if c.Credentials != "" { - contents, _, err := pathorcontents.Read(c.Credentials) - if err != nil { - return fmt.Errorf("Error loading credentials: %s", err) - } - - // Assume account_file is a JSON string - if err := parseJSON(&account, contents); err != nil { - return fmt.Errorf("Error parsing credentials '%s': %s", contents, err) - } - - // Get the token for use in our requests - log.Printf("[INFO] Requesting Google token...") - log.Printf("[INFO] -- Email: %s", account.ClientEmail) - log.Printf("[INFO] -- Scopes: %s", clientScopes) - log.Printf("[INFO] -- Private Key Length: %d", len(account.PrivateKey)) - - conf := jwt.Config{ - Email: account.ClientEmail, - PrivateKey: []byte(account.PrivateKey), - Scopes: clientScopes, - TokenURL: "https://accounts.google.com/o/oauth2/token", - } - - // Initiate an http.Client. The following GET request will be - // authorized and authenticated on the behalf of - // your service account. - client = conf.Client(context.Background()) - - tokenSource = conf.TokenSource(context.Background()) - } else { - log.Printf("[INFO] Authenticating using DefaultClient") - err := error(nil) - client, err = google.DefaultClient(context.Background(), clientScopes...) - if err != nil { - return err - } - - tokenSource, err = google.DefaultTokenSource(context.Background(), clientScopes...) - if err != nil { - return err - } + tokenSource, err := c.getTokenSource(clientScopes) + if err != nil { + return err } - c.tokenSource = tokenSource + client := oauth2.NewClient(context.Background(), tokenSource) client.Transport = logging.NewTransport("Google", client.Transport) terraformVersion := httpclient.UserAgentString() @@ -165,8 +120,6 @@ func (c *Config) loadAndValidate() error { c.client = client c.userAgent = userAgent - var err error - log.Printf("[INFO] Instantiating GCE client...") c.clientCompute, err = compute.New(client) if err != nil { @@ -391,17 +344,31 @@ func (c *Config) loadAndValidate() error { return nil } -// accountFile represents the structure of the account file JSON file. -type accountFile struct { - PrivateKeyId string `json:"private_key_id"` - PrivateKey string `json:"private_key"` - ClientEmail string `json:"client_email"` - ClientId string `json:"client_id"` -} +func (c *Config) getTokenSource(clientScopes []string) (oauth2.TokenSource, error) { + if c.AccessToken != "" { + log.Printf("[INFO] Using configured Google access token (length %d)", len(c.AccessToken)) + log.Printf("[INFO] -- Scopes: %s", clientScopes) + token := &oauth2.Token{AccessToken: c.AccessToken} + return oauth2.StaticTokenSource(token), nil + } -func parseJSON(result interface{}, contents string) error { - r := strings.NewReader(contents) - dec := json.NewDecoder(r) + if c.Credentials != "" { + contents, _, err := pathorcontents.Read(c.Credentials) + if err != nil { + return nil, fmt.Errorf("Error loading credentials: %s", err) + } - return dec.Decode(result) + creds, err := googleoauth.CredentialsFromJSON(context.Background(), []byte(contents), clientScopes...) + if err != nil { + return nil, fmt.Errorf("Unable to parse credentials from '%s': %s", contents, err) + } + + log.Printf("[INFO] Requesting Google token using Credential File %q...", c.Credentials) + log.Printf("[INFO] -- Scopes: %s", clientScopes) + return creds.TokenSource, nil + } + + log.Printf("[INFO] Authenticating using DefaultClient") + log.Printf("[INFO] -- Scopes: %s", clientScopes) + return googleoauth.DefaultTokenSource(context.Background(), clientScopes...) } diff --git a/google/config_test.go b/google/config_test.go index 648f93a6..13b83a24 100644 --- a/google/config_test.go +++ b/google/config_test.go @@ -48,3 +48,18 @@ func TestConfigLoadAndValidate_accountFileJSONInvalid(t *testing.T) { t.Fatalf("expected error, but got nil") } } + +func TestConfigLoadValidate_accessToken(t *testing.T) { + accessToken := getTestAccessTokenFromEnv(t) + + config := Config{ + AccessToken: accessToken, + Project: "my-gce-project", + Region: "us-central1", + } + + err := config.loadAndValidate() + if err != nil { + t.Fatalf("error: %v", err) + } +} diff --git a/google/provider.go b/google/provider.go index 5c80b6c5..1d7e6ae1 100644 --- a/google/provider.go +++ b/google/provider.go @@ -1,13 +1,15 @@ package google import ( - "encoding/json" + "context" "fmt" "os" "github.com/hashicorp/terraform/helper/mutexkv" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" + + googleoauth "golang.org/x/oauth2/google" ) // Global MutexKV @@ -28,6 +30,12 @@ func Provider() terraform.ResourceProvider { ValidateFunc: validateCredentials, }, + "access_token": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"credentials"}, + }, + "project": { Type: schema.TypeString, Optional: true, @@ -244,12 +252,17 @@ func ResourceMapWithErrors() (map[string]*schema.Resource, error) { } func providerConfigure(d *schema.ResourceData) (interface{}, error) { - credentials := d.Get("credentials").(string) config := Config{ - Credentials: credentials, - Project: d.Get("project").(string), - Region: d.Get("region").(string), - Zone: d.Get("zone").(string), + Project: d.Get("project").(string), + Region: d.Get("region").(string), + Zone: d.Get("zone").(string), + } + + // Add credential source + if v, ok := d.GetOk("access_token"); ok { + config.AccessToken = v.(string) + } else if v, ok := d.GetOk("credentials"); ok { + config.Credentials = v.(string) } if err := config.loadAndValidate(); err != nil { @@ -268,10 +281,9 @@ func validateCredentials(v interface{}, k string) (warnings []string, errors []e if _, err := os.Stat(creds); err == nil { return } - var account accountFile - if err := json.Unmarshal([]byte(creds), &account); err != nil { + if _, err := googleoauth.CredentialsFromJSON(context.Background(), []byte(creds)); err != nil { errors = append(errors, - fmt.Errorf("credentials are not valid JSON '%s': %s", creds, err)) + fmt.Errorf("JSON credentials in %q are not valid: %s", creds, err)) } return diff --git a/google/provider_test.go b/google/provider_test.go index dacfcac5..e3ce5fcb 100644 --- a/google/provider_test.go +++ b/google/provider_test.go @@ -57,6 +57,10 @@ var billingAccountEnvVars = []string{ "GOOGLE_BILLING_ACCOUNT", } +var accessTokenEnvVars = []string{ + "GOOGLE_OAUTH2_ACCESS_TOKEN", +} + func init() { testAccProvider = Provider().(*schema.Provider) testAccRandomProvider = random.Provider().(*schema.Provider) @@ -202,6 +206,11 @@ func getTestServiceAccountFromEnv(t *testing.T) string { return multiEnvSearch(serviceAccountEnvVars) } +func getTestAccessTokenFromEnv(t *testing.T) string { + skipIfEnvNotSet(t, accessTokenEnvVars...) + return multiEnvSearch(accessTokenEnvVars) +} + func multiEnvSearch(ks []string) string { for _, k := range ks { if v := os.Getenv(k); v != "" {