From 80e2023e6b24d00f4dbb3ec4d77196a75012a9fe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 25 Aug 2014 11:48:20 -0700 Subject: [PATCH] providers/google: first pass --- config.go | 115 +++++++++++++++++++++ config_test.go | 41 ++++++++ provider.go | 41 ++++++++ provider_test.go | 39 ++++++++ resource_compute_instance.go | 17 ++++ resource_compute_instance_test.go | 161 ++++++++++++++++++++++++++++++ test-fixtures/fake_account.json | 7 ++ test-fixtures/fake_client.json | 11 ++ 8 files changed, 432 insertions(+) create mode 100644 config.go create mode 100644 config_test.go create mode 100644 provider.go create mode 100644 provider_test.go create mode 100644 resource_compute_instance.go create mode 100644 resource_compute_instance_test.go create mode 100644 test-fixtures/fake_account.json create mode 100644 test-fixtures/fake_client.json diff --git a/config.go b/config.go new file mode 100644 index 00000000..d83f0f1c --- /dev/null +++ b/config.go @@ -0,0 +1,115 @@ +package google + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + + "code.google.com/p/goauth2/oauth" + "code.google.com/p/goauth2/oauth/jwt" + "code.google.com/p/google-api-go-client/compute/v1" +) + +const clientScopes string = "https://www.googleapis.com/auth/compute" + +// Config is the configuration structure used to instantiate the Google +// provider. +type Config struct { + AccountFile string + ClientSecretsFile string + + clientCompute *compute.Service +} + +func (c *Config) loadAndValidate() error { + var account accountFile + var secrets clientSecretsFile + + // TODO: validation that it isn't blank + if c.AccountFile == "" { + c.AccountFile = os.Getenv("GOOGLE_ACCOUNT_FILE") + } + if c.ClientSecretsFile == "" { + c.ClientSecretsFile = os.Getenv("GOOGLE_CLIENT_FILE") + } + + if err := loadJSON(&account, c.AccountFile); err != nil { + return fmt.Errorf( + "Error loading account file '%s': %s", + c.AccountFile, + err) + } + + if err := loadJSON(&secrets, c.ClientSecretsFile); err != nil { + return fmt.Errorf( + "Error loading client secrets file '%s': %s", + c.ClientSecretsFile, + 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)) + log.Printf("[INFO] -- Token URL: %s", secrets.Web.TokenURI) + jwtTok := jwt.NewToken( + account.ClientEmail, + clientScopes, + []byte(account.PrivateKey)) + jwtTok.ClaimSet.Aud = secrets.Web.TokenURI + token, err := jwtTok.Assert(new(http.Client)) + if err != nil { + return fmt.Errorf("Error retrieving auth token: %s", err) + } + + // Instantiate the transport to communicate to Google + transport := &oauth.Transport{ + Config: &oauth.Config{ + ClientId: account.ClientId, + Scope: clientScopes, + TokenURL: secrets.Web.TokenURI, + AuthURL: secrets.Web.AuthURI, + }, + Token: token, + } + + log.Printf("[INFO] Instantiating GCE client...") + c.clientCompute, err = compute.New(transport.Client()) + if err != nil { + return err + } + + 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"` +} + +// clientSecretsFile represents the structure of the client secrets JSON file. +type clientSecretsFile struct { + Web struct { + AuthURI string `json:"auth_uri"` + ClientEmail string `json:"client_email"` + ClientId string `json:"client_id"` + TokenURI string `json:"token_uri"` + } +} + +func loadJSON(result interface{}, path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + dec := json.NewDecoder(f) + return dec.Decode(result) +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 00000000..2558c834 --- /dev/null +++ b/config_test.go @@ -0,0 +1,41 @@ +package google + +import ( + "reflect" + "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) + } + + expected := accountFile{ + PrivateKeyId: "foo", + PrivateKey: "bar", + ClientEmail: "foo@bar.com", + ClientId: "id@foo.com", + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} + +func TestConfigLoadJSON_client(t *testing.T) { + var actual clientSecretsFile + if err := loadJSON(&actual, "./test-fixtures/fake_client.json"); err != nil { + t.Fatalf("err: %s", err) + } + + var expected clientSecretsFile + expected.Web.AuthURI = "https://accounts.google.com/o/oauth2/auth" + expected.Web.ClientEmail = "foo@developer.gserviceaccount.com" + expected.Web.ClientId = "foo.apps.googleusercontent.com" + expected.Web.TokenURI = "https://accounts.google.com/o/oauth2/token" + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v", actual) + } +} diff --git a/provider.go b/provider.go new file mode 100644 index 00000000..71ef37d8 --- /dev/null +++ b/provider.go @@ -0,0 +1,41 @@ +package google + +import ( + "github.com/hashicorp/terraform/helper/schema" +) + +// Provider returns a terraform.ResourceProvider. +func Provider() *schema.Provider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "account_file": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "client_secrets_file": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "google_compute_instance": resourceComputeInstance(), + }, + + ConfigureFunc: providerConfigure, + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := Config{ + AccountFile: d.Get("account_file").(string), + ClientSecretsFile: d.Get("client_secrets_file").(string), + } + + if err := config.loadAndValidate(); err != nil { + return nil, err + } + + return nil, nil +} diff --git a/provider_test.go b/provider_test.go new file mode 100644 index 00000000..9139f5fc --- /dev/null +++ b/provider_test.go @@ -0,0 +1,39 @@ +package google + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider() + testAccProviders = map[string]terraform.ResourceProvider{ + "google": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +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_CLIENT_FILE"); v == "" { + t.Fatal("GOOGLE_CLIENT_FILE must be set for acceptance tests") + } +} diff --git a/resource_compute_instance.go b/resource_compute_instance.go new file mode 100644 index 00000000..c2abd0e1 --- /dev/null +++ b/resource_compute_instance.go @@ -0,0 +1,17 @@ +package google + +import( + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceComputeInstance() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeInstanceCreate, + + Schema: map[string]*schema.Schema{}, + } +} + +func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) error { + return nil +} diff --git a/resource_compute_instance_test.go b/resource_compute_instance_test.go new file mode 100644 index 00000000..067512ec --- /dev/null +++ b/resource_compute_instance_test.go @@ -0,0 +1,161 @@ +package google + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccComputeInstance_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + //CheckDestroy: testAccCheckHerokuAppDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstance_basic, + /* + Check: resource.ComposeTestCheckFunc( + testAccCheckHerokuAppExists("heroku_app.foobar", &app), + testAccCheckHerokuAppAttributes(&app), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "name", "terraform-test-app"), + resource.TestCheckResourceAttr( + "heroku_app.foobar", "config_vars.0.FOO", "bar"), + ), + */ + }, + }, + }) +} + +/* +func testAccCheckHerokuAppDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*heroku.Client) + + for _, rs := range s.Resources { + if rs.Type != "heroku_app" { + continue + } + + _, err := client.AppInfo(rs.ID) + + if err == nil { + return fmt.Errorf("App still exists") + } + } + + return nil +} + +func testAccCheckHerokuAppAttributes(app *heroku.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*heroku.Client) + + if app.Region.Name != "us" { + return fmt.Errorf("Bad region: %s", app.Region.Name) + } + + if app.Stack.Name != "cedar" { + return fmt.Errorf("Bad stack: %s", app.Stack.Name) + } + + if app.Name != "terraform-test-app" { + return fmt.Errorf("Bad name: %s", app.Name) + } + + vars, err := client.ConfigVarInfo(app.Name) + if err != nil { + return err + } + + if vars["FOO"] != "bar" { + return fmt.Errorf("Bad config vars: %v", vars) + } + + return nil + } +} + +func testAccCheckHerokuAppAttributesUpdated(app *heroku.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*heroku.Client) + + if app.Name != "terraform-test-renamed" { + return fmt.Errorf("Bad name: %s", app.Name) + } + + vars, err := client.ConfigVarInfo(app.Name) + if err != nil { + return err + } + + // Make sure we kept the old one + if vars["FOO"] != "bing" { + return fmt.Errorf("Bad config vars: %v", vars) + } + + if vars["BAZ"] != "bar" { + return fmt.Errorf("Bad config vars: %v", vars) + } + + return nil + + } +} + +func testAccCheckHerokuAppAttributesNoVars(app *heroku.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*heroku.Client) + + if app.Name != "terraform-test-app" { + return fmt.Errorf("Bad name: %s", app.Name) + } + + vars, err := client.ConfigVarInfo(app.Name) + if err != nil { + return err + } + + if len(vars) != 0 { + return fmt.Errorf("vars exist: %v", vars) + } + + return nil + } +} + +func testAccCheckHerokuAppExists(n string, app *heroku.App) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.Resources[n] + + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.ID == "" { + return fmt.Errorf("No App Name is set") + } + + client := testAccProvider.Meta().(*heroku.Client) + + foundApp, err := client.AppInfo(rs.ID) + + if err != nil { + return err + } + + if foundApp.Name != rs.ID { + return fmt.Errorf("App not found") + } + + *app = *foundApp + + return nil + } +} +*/ + +const testAccComputeInstance_basic = ` +resource "google_compute_instance" "foobar" { +}` diff --git a/test-fixtures/fake_account.json b/test-fixtures/fake_account.json new file mode 100644 index 00000000..f3362d6d --- /dev/null +++ b/test-fixtures/fake_account.json @@ -0,0 +1,7 @@ +{ + "private_key_id": "foo", + "private_key": "bar", + "client_email": "foo@bar.com", + "client_id": "id@foo.com", + "type": "service_account" +} diff --git a/test-fixtures/fake_client.json b/test-fixtures/fake_client.json new file mode 100644 index 00000000..d88fe4cd --- /dev/null +++ b/test-fixtures/fake_client.json @@ -0,0 +1,11 @@ +{ + "web": { + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "client_secret": "foo", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "client_email": "foo@developer.gserviceaccount.com", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/foo@developer.gserviceaccount.com", + "client_id": "foo.apps.googleusercontent.com", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs" + } +}