diff --git a/builtin/providers/google/config.go b/builtin/providers/google/config.go index 6af5fbd61b..838966528f 100644 --- a/builtin/providers/google/config.go +++ b/builtin/providers/google/config.go @@ -3,10 +3,12 @@ package google import ( "encoding/json" "fmt" + "io/ioutil" "log" "net/http" "os" "runtime" + "strings" // TODO(dcunnin): Use version code from version.go @@ -36,7 +38,6 @@ type Config struct { func (c *Config) loadAndValidate() error { var account accountFile - // TODO: validation that it isn't blank if c.AccountFile == "" { c.AccountFile = os.Getenv("GOOGLE_ACCOUNT_FILE") } @@ -50,11 +51,33 @@ func (c *Config) loadAndValidate() error { var client *http.Client if c.AccountFile != "" { - if err := loadJSON(&account, c.AccountFile); err != nil { - return fmt.Errorf( - "Error loading account file '%s': %s", - c.AccountFile, - err) + contents := c.AccountFile + + // 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) + } } clientScopes := []string{ @@ -146,13 +169,9 @@ type accountFile struct { ClientId string `json:"client_id"` } -func loadJSON(result interface{}, path string) error { - f, err := os.Open(path) - if err != nil { - return err - } - defer f.Close() +func parseJSON(result interface{}, contents string) error { + r := strings.NewReader(contents) + dec := json.NewDecoder(r) - dec := json.NewDecoder(f) return dec.Decode(result) } diff --git a/builtin/providers/google/config_test.go b/builtin/providers/google/config_test.go index b4ee585209..cc1b6213fa 100644 --- a/builtin/providers/google/config_test.go +++ b/builtin/providers/google/config_test.go @@ -1,24 +1,50 @@ package google import ( - "reflect" + "io/ioutil" "testing" ) -func TestConfigLoadJSON_account(t *testing.T) { - var actual accountFile - if err := loadJSON(&actual, "./test-fixtures/fake_account.json"); err != nil { - t.Fatalf("err: %s", err) +const testFakeAccountFilePath = "./test-fixtures/fake_account.json" + +func TestConfigLoadAndValidate_accountFilePath(t *testing.T) { + config := Config{ + AccountFile: testFakeAccountFilePath, + Project: "my-gce-project", + Region: "us-central1", } - expected := accountFile{ - PrivateKeyId: "foo", - PrivateKey: "bar", - ClientEmail: "foo@bar.com", - ClientId: "id@foo.com", - } - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("bad: %#v", actual) + err := config.loadAndValidate() + if err != nil { + t.Fatalf("error: %v", err) + } +} + +func TestConfigLoadAndValidate_accountFileJSON(t *testing.T) { + contents, err := ioutil.ReadFile(testFakeAccountFilePath) + if err != nil { + t.Fatalf("error: %v", err) + } + config := Config{ + AccountFile: string(contents), + Project: "my-gce-project", + Region: "us-central1", + } + + err = config.loadAndValidate() + if err != nil { + t.Fatalf("error: %v", err) + } +} + +func TestConfigLoadAndValidate_accountFileJSONInvalid(t *testing.T) { + config := Config{ + AccountFile: "{this is not json}", + Project: "my-gce-project", + Region: "us-central1", + } + + if config.loadAndValidate() == nil { + t.Fatalf("expected error, but got nil") } } diff --git a/builtin/providers/google/provider.go b/builtin/providers/google/provider.go index 30cef8c1bd..6dade8deaa 100644 --- a/builtin/providers/google/provider.go +++ b/builtin/providers/google/provider.go @@ -1,6 +1,10 @@ package google import ( + "encoding/json" + "fmt" + "os" + "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" ) @@ -10,9 +14,10 @@ func Provider() terraform.ResourceProvider { return &schema.Provider{ Schema: map[string]*schema.Schema{ "account_file": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("GOOGLE_ACCOUNT_FILE", nil), + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc("GOOGLE_ACCOUNT_FILE", nil), + ValidateFunc: validateAccountFile, }, "project": &schema.Schema{ @@ -64,3 +69,31 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { return &config, nil } + +func validateAccountFile(v interface{}, k string) (warnings []string, errors []error) { + value := v.(string) + + if value == "" { + 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 + } + + if _, err := os.Stat(value); err != nil { + errors = append(errors, + fmt.Errorf( + "account_file path could not be read from '%s': %s", + value, + err)) + } + + return +} diff --git a/website/source/docs/providers/google/index.html.markdown b/website/source/docs/providers/google/index.html.markdown index 8adb9ed908..3bbef84c60 100644 --- a/website/source/docs/providers/google/index.html.markdown +++ b/website/source/docs/providers/google/index.html.markdown @@ -19,7 +19,7 @@ Use the navigation to the left to read about the available resources. ``` # Configure the Google Cloud provider provider "google" { - account_file = "account.json" + account_file = "${file("account.json")}" project = "my-gce-project" region = "us-central1" } @@ -34,12 +34,12 @@ resource "google_compute_instance" "default" { The following keys can be used to configure the provider. -* `account_file` - (Required) Path to the JSON file used to describe your +* `account_file` - (Required) 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 + 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. * `project` - (Required) The ID of the project to apply any resources to. This