From eb9a93862bb0f1c43d0cf235fcbd0370652b73ef Mon Sep 17 00:00:00 2001 From: Paul Hinze Date: Thu, 12 Nov 2015 16:13:07 -0600 Subject: [PATCH] provider/google: read credentials as contents instead of path Building on the work in #3846, shifting the Google provider's configuration option from `account_file` to `credentials`. --- builtin/providers/google/config.go | 46 +++------------- builtin/providers/google/config_test.go | 10 ++-- builtin/providers/google/provider.go | 54 +++++++++++++------ builtin/providers/google/provider_test.go | 4 +- .../docs/providers/google/index.html.markdown | 29 +++++++--- 5 files changed, 75 insertions(+), 68 deletions(-) diff --git a/builtin/providers/google/config.go b/builtin/providers/google/config.go index 3edb68ef0..218fda06f 100644 --- a/builtin/providers/google/config.go +++ b/builtin/providers/google/config.go @@ -3,13 +3,12 @@ package google import ( "encoding/json" "fmt" - "io/ioutil" "log" "net/http" - "os" "runtime" "strings" + "github.com/hashicorp/terraform/helper/pathorcontents" "github.com/hashicorp/terraform/terraform" "golang.org/x/oauth2" "golang.org/x/oauth2/google" @@ -24,7 +23,7 @@ import ( // Config is the configuration structure used to instantiate the Google // provider. type Config struct { - AccountFile string + Credentials string Project string Region string @@ -44,46 +43,17 @@ func (c *Config) loadAndValidate() error { "https://www.googleapis.com/auth/devstorage.full_control", } - if c.AccountFile == "" { - c.AccountFile = os.Getenv("GOOGLE_ACCOUNT_FILE") - } - if c.Project == "" { - c.Project = os.Getenv("GOOGLE_PROJECT") - } - if c.Region == "" { - c.Region = os.Getenv("GOOGLE_REGION") - } - var client *http.Client - if c.AccountFile != "" { - contents := c.AccountFile + 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 { - // If account_file was not JSON, assume it is a file path instead - if _, err := os.Stat(c.AccountFile); os.IsNotExist(err) { - return fmt.Errorf( - "account_file path does not exist: %s", - c.AccountFile) - } - - b, err := ioutil.ReadFile(c.AccountFile) - if err != nil { - return fmt.Errorf( - "Error reading account_file from path '%s': %s", - c.AccountFile, - err) - } - - contents = string(b) - - if err := parseJSON(&account, contents); err != nil { - return fmt.Errorf( - "Error parsing account file '%s': %s", - contents, - err) - } + return fmt.Errorf("Error parsing credentials '%s': %s", contents, err) } // Get the token for use in our requests diff --git a/builtin/providers/google/config_test.go b/builtin/providers/google/config_test.go index cc1b6213f..648f93a68 100644 --- a/builtin/providers/google/config_test.go +++ b/builtin/providers/google/config_test.go @@ -5,11 +5,11 @@ import ( "testing" ) -const testFakeAccountFilePath = "./test-fixtures/fake_account.json" +const testFakeCredentialsPath = "./test-fixtures/fake_account.json" func TestConfigLoadAndValidate_accountFilePath(t *testing.T) { config := Config{ - AccountFile: testFakeAccountFilePath, + Credentials: testFakeCredentialsPath, Project: "my-gce-project", Region: "us-central1", } @@ -21,12 +21,12 @@ func TestConfigLoadAndValidate_accountFilePath(t *testing.T) { } func TestConfigLoadAndValidate_accountFileJSON(t *testing.T) { - contents, err := ioutil.ReadFile(testFakeAccountFilePath) + contents, err := ioutil.ReadFile(testFakeCredentialsPath) if err != nil { t.Fatalf("error: %v", err) } config := Config{ - AccountFile: string(contents), + Credentials: string(contents), Project: "my-gce-project", Region: "us-central1", } @@ -39,7 +39,7 @@ func TestConfigLoadAndValidate_accountFileJSON(t *testing.T) { func TestConfigLoadAndValidate_accountFileJSONInvalid(t *testing.T) { config := Config{ - AccountFile: "{this is not json}", + Credentials: "{this is not json}", Project: "my-gce-project", Region: "us-central1", } diff --git a/builtin/providers/google/provider.go b/builtin/providers/google/provider.go index 3cfc363d0..b2d083bc2 100644 --- a/builtin/providers/google/provider.go +++ b/builtin/providers/google/provider.go @@ -3,8 +3,8 @@ package google import ( "encoding/json" "fmt" - "os" + "github.com/hashicorp/terraform/helper/pathorcontents" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) @@ -18,6 +18,14 @@ func Provider() terraform.ResourceProvider { Optional: true, DefaultFunc: schema.EnvDefaultFunc("GOOGLE_ACCOUNT_FILE", nil), ValidateFunc: validateAccountFile, + Deprecated: "Use the credentials field instead", + }, + + "credentials": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("GOOGLE_CREDENTIALS", nil), + ValidateFunc: validateCredentials, }, "project": &schema.Schema{ @@ -73,8 +81,12 @@ func Provider() terraform.ResourceProvider { } func providerConfigure(d *schema.ResourceData) (interface{}, error) { + credentials := d.Get("credentials").(string) + if credentials == "" { + credentials = d.Get("account_file").(string) + } config := Config{ - AccountFile: d.Get("account_file").(string), + Credentials: credentials, Project: d.Get("project").(string), Region: d.Get("region").(string), } @@ -97,22 +109,34 @@ func validateAccountFile(v interface{}, k string) (warnings []string, errors []e return } - var account accountFile - if err := json.Unmarshal([]byte(value), &account); err != nil { - warnings = append(warnings, ` -account_file is not valid JSON, so we are assuming it is a file path. This -support will be removed in the future. Please update your configuration to use -${file("filename.json")} instead.`) - } else { - return + contents, wasPath, err := pathorcontents.Read(value) + if err != nil { + errors = append(errors, fmt.Errorf("Error loading Account File: %s", err)) + } + if wasPath { + warnings = append(warnings, `account_file was provided as a path instead of +as file contents. This support will be removed in the future. Please update +your configuration to use ${file("filename.json")} instead.`) } - if _, err := os.Stat(value); err != nil { + var account accountFile + if err := json.Unmarshal([]byte(contents), &account); err != nil { errors = append(errors, - fmt.Errorf( - "account_file path could not be read from '%s': %s", - value, - err)) + fmt.Errorf("account_file not valid JSON '%s': %s", contents, err)) + } + + return +} + +func validateCredentials(v interface{}, k string) (warnings []string, errors []error) { + if v == nil || v.(string) == "" { + return + } + creds := v.(string) + var account accountFile + if err := json.Unmarshal([]byte(creds), &account); err != nil { + errors = append(errors, + fmt.Errorf("credentials are not valid JSON '%s': %s", creds, err)) } return diff --git a/builtin/providers/google/provider_test.go b/builtin/providers/google/provider_test.go index 2275e188f..827a7f575 100644 --- a/builtin/providers/google/provider_test.go +++ b/builtin/providers/google/provider_test.go @@ -29,8 +29,8 @@ func TestProvider_impl(t *testing.T) { } func testAccPreCheck(t *testing.T) { - if v := os.Getenv("GOOGLE_ACCOUNT_FILE"); v == "" { - t.Fatal("GOOGLE_ACCOUNT_FILE must be set for acceptance tests") + if v := os.Getenv("GOOGLE_CREDENTIALS"); v == "" { + t.Fatal("GOOGLE_CREDENTIALS must be set for acceptance tests") } if v := os.Getenv("GOOGLE_PROJECT"); v == "" { diff --git a/website/source/docs/providers/google/index.html.markdown b/website/source/docs/providers/google/index.html.markdown index 3bbef84c6..14a208d6a 100644 --- a/website/source/docs/providers/google/index.html.markdown +++ b/website/source/docs/providers/google/index.html.markdown @@ -19,14 +19,14 @@ Use the navigation to the left to read about the available resources. ``` # Configure the Google Cloud provider provider "google" { - account_file = "${file("account.json")}" - project = "my-gce-project" - region = "us-central1" + credentials = "${file("account.json")}" + project = "my-gce-project" + region = "us-central1" } # Create a new instance resource "google_compute_instance" "default" { - ... + ... } ``` @@ -34,12 +34,12 @@ resource "google_compute_instance" "default" { The following keys can be used to configure the provider. -* `account_file` - (Required) Contents of the JSON file used to describe your +* `credentials` - (Optional) Contents of the JSON file used to describe your account credentials, downloaded from Google Cloud Console. More details on - retrieving this file are below. The `account file` can be "" if you are running - terraform from a GCE instance with a properly-configured [Compute Engine + retrieving this file are below. Credentials may be blank if you are running + Terraform from a GCE instance with a properly-configured [Compute Engine Service Account](https://cloud.google.com/compute/docs/authentication). This - can also be specified with the `GOOGLE_ACCOUNT_FILE` shell environment + can also be specified with the `GOOGLE_CREDENTIALS` shell environment variable. * `project` - (Required) The ID of the project to apply any resources to. This @@ -48,6 +48,19 @@ The following keys can be used to configure the provider. * `region` - (Required) The region to operate under. This can also be specified with the `GOOGLE_REGION` shell environment variable. +The following keys are supported for backwards compatibility, and may be +removed in a future version: + +* `account_file` - __Deprecated: please use `credentials` instead.__ + Path to or contents of the JSON file used to describe your + account credentials, downloaded from Google Cloud Console. More details on + retrieving this file are below. The `account file` can be "" if you are running + terraform from a GCE instance with a properly-configured [Compute Engine + Service Account](https://cloud.google.com/compute/docs/authentication). This + can also be specified with the `GOOGLE_ACCOUNT_FILE` shell environment + variable. + + ## Authentication JSON File Authenticating with Google Cloud services requires a JSON