providers/google: Change account_file to JSON

If JSON fails to parse, treat it as a file path
This commit is contained in:
Justin Campbell 2015-07-27 15:35:52 -04:00
parent 2a04708d66
commit 773852e2d5
4 changed files with 79 additions and 73 deletions

View File

@ -25,10 +25,9 @@ import (
// Config is the configuration structure used to instantiate the Google // Config is the configuration structure used to instantiate the Google
// provider. // provider.
type Config struct { type Config struct {
AccountFile string AccountFile string
AccountFileContents string Project string
Project string Region string
Region string
clientCompute *compute.Service clientCompute *compute.Service
clientContainer *container.Service clientContainer *container.Service
@ -39,13 +38,9 @@ type Config struct {
func (c *Config) loadAndValidate() error { func (c *Config) loadAndValidate() error {
var account accountFile var account accountFile
// TODO: validation that it isn't blank
if c.AccountFile == "" { if c.AccountFile == "" {
c.AccountFile = os.Getenv("GOOGLE_ACCOUNT_FILE") c.AccountFile = os.Getenv("GOOGLE_ACCOUNT_FILE")
} }
if c.AccountFileContents == "" {
c.AccountFileContents = os.Getenv("GOOGLE_ACCOUNT_FILE_CONTENTS")
}
if c.Project == "" { if c.Project == "" {
c.Project = os.Getenv("GOOGLE_PROJECT") c.Project = os.Getenv("GOOGLE_PROJECT")
} }
@ -56,25 +51,32 @@ func (c *Config) loadAndValidate() error {
var client *http.Client var client *http.Client
if c.AccountFile != "" { if c.AccountFile != "" {
if c.AccountFileContents != "" { contents := c.AccountFile
return fmt.Errorf(
"Cannot provide both account_file and account_file_contents", // 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)
} }
b, err := ioutil.ReadFile(c.AccountFile) if err := parseJSON(&account, contents); err != nil {
if err != nil {
return err
}
c.AccountFileContents = string(b)
}
if c.AccountFileContents != "" {
if err := parseJSON(&account, c.AccountFileContents); err != nil {
return fmt.Errorf( return fmt.Errorf(
"Error parsing account file contents '%s': %s", "Error parsing account file '%s': %s",
c.AccountFileContents, contents,
err) err)
} }

View File

@ -7,7 +7,7 @@ import (
const testFakeAccountFilePath = "./test-fixtures/fake_account.json" const testFakeAccountFilePath = "./test-fixtures/fake_account.json"
func TestConfigLoadAndValidate_accountFile(t *testing.T) { func TestConfigLoadAndValidate_accountFilePath(t *testing.T) {
config := Config{ config := Config{
AccountFile: testFakeAccountFilePath, AccountFile: testFakeAccountFilePath,
Project: "my-gce-project", Project: "my-gce-project",
@ -20,15 +20,15 @@ func TestConfigLoadAndValidate_accountFile(t *testing.T) {
} }
} }
func TestConfigLoadAndValidate_accountFileContents(t *testing.T) { func TestConfigLoadAndValidate_accountFileJSON(t *testing.T) {
contents, err := ioutil.ReadFile(testFakeAccountFilePath) contents, err := ioutil.ReadFile(testFakeAccountFilePath)
if err != nil { if err != nil {
t.Fatalf("error: %v", err) t.Fatalf("error: %v", err)
} }
config := Config{ config := Config{
AccountFileContents: string(contents), AccountFile: string(contents),
Project: "my-gce-project", Project: "my-gce-project",
Region: "us-central1", Region: "us-central1",
} }
err = config.loadAndValidate() err = config.loadAndValidate()
@ -37,24 +37,11 @@ func TestConfigLoadAndValidate_accountFileContents(t *testing.T) {
} }
} }
func TestConfigLoadAndValidate_none(t *testing.T) { func TestConfigLoadAndValidate_accountFileJSONInvalid(t *testing.T) {
config := Config{ config := Config{
Project: "my-gce-project", AccountFile: "{this is not json}",
Region: "us-central1", Project: "my-gce-project",
} Region: "us-central1",
err := config.loadAndValidate()
if err != nil {
t.Fatalf("error: %v", err)
}
}
func TestConfigLoadAndValidate_both(t *testing.T) {
config := Config{
AccountFile: testFakeAccountFilePath,
AccountFileContents: "{}",
Project: "my-gce-project",
Region: "us-central1",
} }
if config.loadAndValidate() == nil { if config.loadAndValidate() == nil {

View File

@ -1,6 +1,10 @@
package google package google
import ( import (
"encoding/json"
"fmt"
"os"
"github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -10,15 +14,10 @@ func Provider() terraform.ResourceProvider {
return &schema.Provider{ return &schema.Provider{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"account_file": &schema.Schema{ "account_file": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Required: true,
DefaultFunc: schema.EnvDefaultFunc("GOOGLE_ACCOUNT_FILE", ""), DefaultFunc: schema.EnvDefaultFunc("GOOGLE_ACCOUNT_FILE", nil),
}, ValidateFunc: validateAccountFile,
"account_file_contents": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("GOOGLE_ACCOUNT_FILE_CONTENTS", ""),
}, },
"project": &schema.Schema{ "project": &schema.Schema{
@ -59,10 +58,9 @@ func Provider() terraform.ResourceProvider {
func providerConfigure(d *schema.ResourceData) (interface{}, error) { func providerConfigure(d *schema.ResourceData) (interface{}, error) {
config := Config{ config := Config{
AccountFile: d.Get("account_file").(string), AccountFile: d.Get("account_file").(string),
AccountFileContents: d.Get("account_file_contents").(string), Project: d.Get("project").(string),
Project: d.Get("project").(string), Region: d.Get("region").(string),
Region: d.Get("region").(string),
} }
if err := config.loadAndValidate(); err != nil { if err := config.loadAndValidate(); err != nil {
@ -71,3 +69,28 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) {
return &config, nil 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.`)
return
}
if _, err := os.Stat(value); os.IsNotExist(err) {
errors = append(errors, err)
fmt.Errorf("account_file path does not exist: %s", value)
}
return
}

View File

@ -19,7 +19,7 @@ Use the navigation to the left to read about the available resources.
``` ```
# Configure the Google Cloud provider # Configure the Google Cloud provider
provider "google" { provider "google" {
account_file = "account.json" account_file = "${file("account.json")}"
project = "my-gce-project" project = "my-gce-project"
region = "us-central1" region = "us-central1"
} }
@ -34,19 +34,13 @@ resource "google_compute_instance" "default" {
The following keys can be used to configure the provider. The following keys can be used to configure the provider.
* `account_file` - (Required, unless `account_file_contents` is present) Path * `account_file` - (Required) Contents of the JSON file used to describe your
to the JSON file used to describe your account credentials, downloaded from account credentials, downloaded from Google Cloud Console. More details on
Google Cloud Console. More details on retrieving this file are below. The retrieving this file are below. The `account file` can be "" if you are running
_account file_ can be "" if you are running terraform from a GCE instance with terraform from a GCE instance with a properly-configured [Compute Engine
a properly-configured [Compute Engine Service Service Account](https://cloud.google.com/compute/docs/authentication). This
Account](https://cloud.google.com/compute/docs/authentication). This can also can also be specified with the `GOOGLE_ACCOUNT_FILE` shell environment
be specified with the `GOOGLE_ACCOUNT_FILE` shell environment variable. variable.
* `account_file_contents` - (Required, unless `account_file` is present) The
contents of `account_file`. This can be used to pass the account credentials
with a Terraform var or environment variable if the account file is not
accessible. This can also be specified with the `GOOGLE_ACCOUNT_FILE_CONTENTS`
shell environment variable.
* `project` - (Required) The ID of the project to apply any resources to. This * `project` - (Required) The ID of the project to apply any resources to. This
can also be specified with the `GOOGLE_PROJECT` shell environment variable. can also be specified with the `GOOGLE_PROJECT` shell environment variable.