Merge pull request #2839 from hashicorp/gce-account-file-contents

providers/google: Change account_file to expect a JSON string
This commit is contained in:
Justin Campbell 2015-07-29 16:02:30 -04:00
commit 79e5c9d19b
4 changed files with 114 additions and 36 deletions

View File

@ -3,10 +3,12 @@ package google
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"net/http" "net/http"
"os" "os"
"runtime" "runtime"
"strings"
// TODO(dcunnin): Use version code from version.go // TODO(dcunnin): Use version code from version.go
@ -36,7 +38,6 @@ 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")
} }
@ -50,11 +51,33 @@ func (c *Config) loadAndValidate() error {
var client *http.Client var client *http.Client
if c.AccountFile != "" { if c.AccountFile != "" {
if err := loadJSON(&account, c.AccountFile); err != nil { contents := c.AccountFile
return fmt.Errorf(
"Error loading account file '%s': %s", // Assume account_file is a JSON string
c.AccountFile, if err := parseJSON(&account, contents); err != nil {
err) // 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{ clientScopes := []string{
@ -146,13 +169,9 @@ type accountFile struct {
ClientId string `json:"client_id"` ClientId string `json:"client_id"`
} }
func loadJSON(result interface{}, path string) error { func parseJSON(result interface{}, contents string) error {
f, err := os.Open(path) r := strings.NewReader(contents)
if err != nil { dec := json.NewDecoder(r)
return err
}
defer f.Close()
dec := json.NewDecoder(f)
return dec.Decode(result) return dec.Decode(result)
} }

View File

@ -1,24 +1,50 @@
package google package google
import ( import (
"reflect" "io/ioutil"
"testing" "testing"
) )
func TestConfigLoadJSON_account(t *testing.T) { const testFakeAccountFilePath = "./test-fixtures/fake_account.json"
var actual accountFile
if err := loadJSON(&actual, "./test-fixtures/fake_account.json"); err != nil { func TestConfigLoadAndValidate_accountFilePath(t *testing.T) {
t.Fatalf("err: %s", err) config := Config{
AccountFile: testFakeAccountFilePath,
Project: "my-gce-project",
Region: "us-central1",
} }
expected := accountFile{ err := config.loadAndValidate()
PrivateKeyId: "foo", if err != nil {
PrivateKey: "bar", t.Fatalf("error: %v", err)
ClientEmail: "foo@bar.com", }
ClientId: "id@foo.com", }
}
func TestConfigLoadAndValidate_accountFileJSON(t *testing.T) {
if !reflect.DeepEqual(actual, expected) { contents, err := ioutil.ReadFile(testFakeAccountFilePath)
t.Fatalf("bad: %#v", actual) 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")
} }
} }

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,9 +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", nil), DefaultFunc: schema.EnvDefaultFunc("GOOGLE_ACCOUNT_FILE", nil),
ValidateFunc: validateAccountFile,
}, },
"project": &schema.Schema{ "project": &schema.Schema{
@ -64,3 +69,31 @@ 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.`)
} 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
}

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,12 +34,12 @@ 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) 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 account credentials, downloaded from Google Cloud Console. More details on
retrieving this file are below. The _account file_ can be "" if you retrieving this file are below. The `account file` can be "" if you are running
are running terraform from a GCE instance with a properly-configured [Compute terraform from a GCE instance with a properly-configured [Compute Engine
Engine Service Account](https://cloud.google.com/compute/docs/authentication). Service Account](https://cloud.google.com/compute/docs/authentication). This
This can also be specified with the `GOOGLE_ACCOUNT_FILE` shell environment can also be specified with the `GOOGLE_ACCOUNT_FILE` shell environment
variable. 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