diff --git a/builtin/providers/google/config.go b/builtin/providers/google/config.go new file mode 100644 index 000000000..91f8992a0 --- /dev/null +++ b/builtin/providers/google/config.go @@ -0,0 +1,123 @@ +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 + Project string + Region 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 c.Project == "" { + c.Project = os.Getenv("GOOGLE_PROJECT") + } + if c.Region == "" { + c.Region = os.Getenv("GOOGLE_REGION") + } + + 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/builtin/providers/google/config_test.go b/builtin/providers/google/config_test.go new file mode 100644 index 000000000..25d424cda --- /dev/null +++ b/builtin/providers/google/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/builtin/providers/google/image.go b/builtin/providers/google/image.go new file mode 100644 index 000000000..7b19b415a --- /dev/null +++ b/builtin/providers/google/image.go @@ -0,0 +1,43 @@ +package google + +import ( + "strings" + + "code.google.com/p/google-api-go-client/compute/v1" +) + +// readImage finds the image with the given name. +func readImage(c *Config, name string) (*compute.Image, error) { + // First, always try ourselves first. + image, err := c.clientCompute.Images.Get(c.Project, name).Do() + if err == nil && image != nil && image.SelfLink != "" { + return image, nil + } + + // This is a map of names to the project name where a public image is + // hosted. GCE doesn't have an API to simply look up an image without + // a project so we do this jank thing. + imageMap := map[string]string{ + "centos": "centos-cloud", + "coreos": "coreos-cloud", + "debian": "debian-cloud", + "opensuse": "opensuse-cloud", + "rhel": "rhel-cloud", + "sles": "suse-cloud", + } + + // If we match a lookup for an alternate project, then try that next. + // If not, we return the error. + var project string + for k, v := range imageMap { + if strings.Contains(name, k) { + project = v + break + } + } + if project == "" { + return nil, err + } + + return c.clientCompute.Images.Get(project, name).Do() +} diff --git a/builtin/providers/google/operation.go b/builtin/providers/google/operation.go new file mode 100644 index 000000000..32bf79a5e --- /dev/null +++ b/builtin/providers/google/operation.go @@ -0,0 +1,79 @@ +package google + +import ( + "bytes" + "fmt" + + "code.google.com/p/google-api-go-client/compute/v1" + "github.com/hashicorp/terraform/helper/resource" +) + +// OperationWaitType is an enum specifying what type of operation +// we're waiting on. +type OperationWaitType byte + +const ( + OperationWaitInvalid OperationWaitType = iota + OperationWaitGlobal + OperationWaitRegion + OperationWaitZone +) + +type OperationWaiter struct { + Service *compute.Service + Op *compute.Operation + Project string + Region string + Zone string + Type OperationWaitType +} + +func (w *OperationWaiter) RefreshFunc() resource.StateRefreshFunc { + return func() (interface{}, string, error) { + var op *compute.Operation + var err error + + switch w.Type { + case OperationWaitGlobal: + op, err = w.Service.GlobalOperations.Get( + w.Project, w.Op.Name).Do() + case OperationWaitRegion: + op, err = w.Service.RegionOperations.Get( + w.Project, w.Region, w.Op.Name).Do() + case OperationWaitZone: + op, err = w.Service.ZoneOperations.Get( + w.Project, w.Zone, w.Op.Name).Do() + default: + return nil, "bad-type", fmt.Errorf( + "Invalid wait type: %#v", w.Type) + } + + if err != nil { + return nil, "", err + } + + return op, op.Status, nil + } +} + +func (w *OperationWaiter) Conf() *resource.StateChangeConf { + return &resource.StateChangeConf{ + Pending: []string{"PENDING", "RUNNING"}, + Target: "DONE", + Refresh: w.RefreshFunc(), + } +} + +// OperationError wraps compute.OperationError and implements the +// error interface so it can be returned. +type OperationError compute.OperationError + +func (e OperationError) Error() string { + var buf bytes.Buffer + + for _, err := range e.Errors { + buf.WriteString(err.Message + "\n") + } + + return buf.String() +} diff --git a/builtin/providers/google/provider.go b/builtin/providers/google/provider.go new file mode 100644 index 000000000..5fbba686a --- /dev/null +++ b/builtin/providers/google/provider.go @@ -0,0 +1,58 @@ +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, + }, + + "project": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "google_compute_address": resourceComputeAddress(), + "google_compute_disk": resourceComputeDisk(), + "google_compute_firewall": resourceComputeFirewall(), + "google_compute_instance": resourceComputeInstance(), + "google_compute_network": resourceComputeNetwork(), + "google_compute_route": resourceComputeRoute(), + }, + + ConfigureFunc: providerConfigure, + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := Config{ + AccountFile: d.Get("account_file").(string), + ClientSecretsFile: d.Get("client_secrets_file").(string), + Project: d.Get("project").(string), + Region: d.Get("region").(string), + } + + if err := config.loadAndValidate(); err != nil { + return nil, err + } + + return &config, nil +} diff --git a/builtin/providers/google/provider_test.go b/builtin/providers/google/provider_test.go new file mode 100644 index 000000000..9139f5fce --- /dev/null +++ b/builtin/providers/google/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/builtin/providers/google/resource_compute_address.go b/builtin/providers/google/resource_compute_address.go new file mode 100644 index 000000000..a8f1ecf0c --- /dev/null +++ b/builtin/providers/google/resource_compute_address.go @@ -0,0 +1,130 @@ +package google + +import ( + "fmt" + "log" + "time" + + "code.google.com/p/google-api-go-client/compute/v1" + "code.google.com/p/google-api-go-client/googleapi" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceComputeAddress() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeAddressCreate, + Read: resourceComputeAddressRead, + Delete: resourceComputeAddressDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceComputeAddressCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Build the address parameter + addr := &compute.Address{Name: d.Get("name").(string)} + log.Printf("[DEBUG] Address insert request: %#v", addr) + op, err := config.clientCompute.Addresses.Insert( + config.Project, config.Region, addr).Do() + if err != nil { + return fmt.Errorf("Error creating address: %s", err) + } + + // It probably maybe worked, so store the ID now + d.SetId(addr.Name) + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Region: config.Region, + Type: OperationWaitRegion, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for address to create: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // The resource didn't actually create + d.SetId("") + + // Return the error + return OperationError(*op.Error) + } + + return resourceComputeAddressRead(d, meta) +} + +func resourceComputeAddressRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + addr, err := config.clientCompute.Addresses.Get( + config.Project, config.Region, d.Id()).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + // The resource doesn't exist anymore + d.SetId("") + + return nil + } + + return fmt.Errorf("Error reading address: %s", err) + } + + d.Set("address", addr.Address) + + return nil +} + +func resourceComputeAddressDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Delete the address + op, err := config.clientCompute.Addresses.Delete( + config.Project, config.Region, d.Id()).Do() + if err != nil { + return fmt.Errorf("Error deleting address: %s", err) + } + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Region: config.Region, + Type: OperationWaitRegion, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for address to delete: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // Return the error + return OperationError(*op.Error) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/google/resource_compute_address_test.go b/builtin/providers/google/resource_compute_address_test.go new file mode 100644 index 000000000..e0c576aee --- /dev/null +++ b/builtin/providers/google/resource_compute_address_test.go @@ -0,0 +1,81 @@ +package google + +import ( + "fmt" + "testing" + + "code.google.com/p/google-api-go-client/compute/v1" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccComputeAddress_basic(t *testing.T) { + var addr compute.Address + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeAddressDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeAddress_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeAddressExists( + "google_compute_address.foobar", &addr), + ), + }, + }, + }) +} + +func testAccCheckComputeAddressDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.Resources { + if rs.Type != "google_compute_address" { + continue + } + + _, err := config.clientCompute.Addresses.Get( + config.Project, config.Region, rs.ID).Do() + if err == nil { + return fmt.Errorf("Address still exists") + } + } + + return nil +} + +func testAccCheckComputeAddressExists(n string, addr *compute.Address) 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 ID is set") + } + + config := testAccProvider.Meta().(*Config) + + found, err := config.clientCompute.Addresses.Get( + config.Project, config.Region, rs.ID).Do() + if err != nil { + return err + } + + if found.Name != rs.ID { + return fmt.Errorf("Addr not found") + } + + *addr = *found + + return nil + } +} + +const testAccComputeAddress_basic = ` +resource "google_compute_address" "foobar" { + name = "terraform-test" +}` diff --git a/builtin/providers/google/resource_compute_disk.go b/builtin/providers/google/resource_compute_disk.go new file mode 100644 index 000000000..e69cf9a16 --- /dev/null +++ b/builtin/providers/google/resource_compute_disk.go @@ -0,0 +1,157 @@ +package google + +import ( + "fmt" + "log" + "time" + + "code.google.com/p/google-api-go-client/compute/v1" + "code.google.com/p/google-api-go-client/googleapi" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceComputeDisk() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeDiskCreate, + Read: resourceComputeDiskRead, + Delete: resourceComputeDiskDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "image": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "size": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceComputeDiskCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Build the disk parameter + disk := &compute.Disk{ + Name: d.Get("name").(string), + SizeGb: int64(d.Get("size").(int)), + } + + // If we were given a source image, load that. + if v, ok := d.GetOk("image"); ok { + log.Printf("[DEBUG] Loading image: %s", v.(string)) + image, err := readImage(config, v.(string)) + if err != nil { + return fmt.Errorf( + "Error loading image '%s': %s", + v.(string), err) + } + + disk.SourceImage = image.SelfLink + } + + op, err := config.clientCompute.Disks.Insert( + config.Project, d.Get("zone").(string), disk).Do() + if err != nil { + return fmt.Errorf("Error creating disk: %s", err) + } + + // It probably maybe worked, so store the ID now + d.SetId(disk.Name) + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Zone: d.Get("zone").(string), + Type: OperationWaitZone, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for disk to create: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // The resource didn't actually create + d.SetId("") + + // Return the error + return OperationError(*op.Error) + } + + return resourceComputeDiskRead(d, meta) +} + +func resourceComputeDiskRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + _, err := config.clientCompute.Disks.Get( + config.Project, d.Get("zone").(string), d.Id()).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + // The resource doesn't exist anymore + d.SetId("") + + return nil + } + + return fmt.Errorf("Error reading disk: %s", err) + } + + return nil +} + +func resourceComputeDiskDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Delete the disk + op, err := config.clientCompute.Disks.Delete( + config.Project, d.Get("zone").(string), d.Id()).Do() + if err != nil { + return fmt.Errorf("Error deleting disk: %s", err) + } + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Zone: d.Get("zone").(string), + Type: OperationWaitZone, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for disk to delete: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // Return the error + return OperationError(*op.Error) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/google/resource_compute_disk_test.go b/builtin/providers/google/resource_compute_disk_test.go new file mode 100644 index 000000000..188741fa6 --- /dev/null +++ b/builtin/providers/google/resource_compute_disk_test.go @@ -0,0 +1,84 @@ +package google + +import ( + "fmt" + "testing" + + "code.google.com/p/google-api-go-client/compute/v1" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccComputeDisk_basic(t *testing.T) { + var disk compute.Disk + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeDiskDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeDisk_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeDiskExists( + "google_compute_disk.foobar", &disk), + ), + }, + }, + }) +} + +func testAccCheckComputeDiskDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.Resources { + if rs.Type != "google_compute_disk" { + continue + } + + _, err := config.clientCompute.Disks.Get( + config.Project, rs.Attributes["zone"], rs.ID).Do() + if err == nil { + return fmt.Errorf("Disk still exists") + } + } + + return nil +} + +func testAccCheckComputeDiskExists(n string, disk *compute.Disk) 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 ID is set") + } + + config := testAccProvider.Meta().(*Config) + + found, err := config.clientCompute.Disks.Get( + config.Project, rs.Attributes["zone"], rs.ID).Do() + if err != nil { + return err + } + + if found.Name != rs.ID { + return fmt.Errorf("Disk not found") + } + + *disk = *found + + return nil + } +} + +const testAccComputeDisk_basic = ` +resource "google_compute_disk" "foobar" { + name = "terraform-test" + image = "debian-7-wheezy-v20140814" + size = 50 + zone = "us-central1-a" +}` diff --git a/builtin/providers/google/resource_compute_firewall.go b/builtin/providers/google/resource_compute_firewall.go new file mode 100644 index 000000000..2dfccb71f --- /dev/null +++ b/builtin/providers/google/resource_compute_firewall.go @@ -0,0 +1,292 @@ +package google + +import ( + "bytes" + "fmt" + "sort" + "time" + + "code.google.com/p/google-api-go-client/compute/v1" + "code.google.com/p/google-api-go-client/googleapi" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceComputeFirewall() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeFirewallCreate, + Read: resourceComputeFirewallRead, + Update: resourceComputeFirewallUpdate, + Delete: resourceComputeFirewallDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "network": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "allow": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "ports": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + }, + }, + Set: resourceComputeFirewallAllowHash, + }, + + "source_ranges": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + + "source_tags": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + }, + } +} + +func resourceComputeFirewallAllowHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["protocol"].(string))) + + // We need to make sure to sort the strings below so that we always + // generate the same hash code no matter what is in the set. + if v, ok := m["ports"]; ok { + vs := v.(*schema.Set).List() + s := make([]string, len(vs)) + for i, raw := range vs { + s[i] = raw.(string) + } + sort.Strings(s) + + for _, v := range s { + buf.WriteString(fmt.Sprintf("%s-", v)) + } + } + + return hashcode.String(buf.String()) +} + +func resourceComputeFirewallCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + firewall, err := resourceFirewall(d, meta) + if err != nil { + return err + } + + op, err := config.clientCompute.Firewalls.Insert( + config.Project, firewall).Do() + if err != nil { + return fmt.Errorf("Error creating firewall: %s", err) + } + + // It probably maybe worked, so store the ID now + d.SetId(firewall.Name) + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Type: OperationWaitGlobal, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for firewall to create: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // The resource didn't actually create + d.SetId("") + + // Return the error + return OperationError(*op.Error) + } + + return resourceComputeFirewallRead(d, meta) +} + +func resourceComputeFirewallRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + _, err := config.clientCompute.Firewalls.Get( + config.Project, d.Id()).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + // The resource doesn't exist anymore + d.SetId("") + + return nil + } + + return fmt.Errorf("Error reading firewall: %s", err) + } + + return nil +} + +func resourceComputeFirewallUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + firewall, err := resourceFirewall(d, meta) + if err != nil { + return err + } + + op, err := config.clientCompute.Firewalls.Update( + config.Project, d.Id(), firewall).Do() + if err != nil { + return fmt.Errorf("Error updating firewall: %s", err) + } + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Type: OperationWaitGlobal, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for firewall to update: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // Return the error + return OperationError(*op.Error) + } + + return resourceComputeFirewallRead(d, meta) +} + +func resourceComputeFirewallDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Delete the firewall + op, err := config.clientCompute.Firewalls.Delete( + config.Project, d.Id()).Do() + if err != nil { + return fmt.Errorf("Error deleting firewall: %s", err) + } + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Type: OperationWaitGlobal, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for firewall to delete: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // Return the error + return OperationError(*op.Error) + } + + d.SetId("") + return nil +} + +func resourceFirewall( + d *schema.ResourceData, + meta interface{}) (*compute.Firewall, error) { + config := meta.(*Config) + + // Look up the network to attach the firewall to + network, err := config.clientCompute.Networks.Get( + config.Project, d.Get("network").(string)).Do() + if err != nil { + return nil, fmt.Errorf("Error reading network: %s", err) + } + + // Build up the list of allowed entries + var allowed []*compute.FirewallAllowed + if v := d.Get("allow").(*schema.Set); v.Len() > 0 { + allowed = make([]*compute.FirewallAllowed, 0, v.Len()) + for _, v := range v.List() { + m := v.(map[string]interface{}) + + var ports []string + if v := m["ports"].(*schema.Set); v.Len() > 0 { + ports = make([]string, v.Len()) + for i, v := range v.List() { + ports[i] = v.(string) + } + } + + allowed = append(allowed, &compute.FirewallAllowed{ + IPProtocol: m["protocol"].(string), + Ports: ports, + }) + } + } + + // Build up the list of sources + var sourceRanges, sourceTags []string + if v := d.Get("source_ranges").(*schema.Set); v.Len() > 0 { + sourceRanges = make([]string, v.Len()) + for i, v := range v.List() { + sourceRanges[i] = v.(string) + } + } + if v := d.Get("source_tags").(*schema.Set); v.Len() > 0 { + sourceTags = make([]string, v.Len()) + for i, v := range v.List() { + sourceTags[i] = v.(string) + } + } + + // Build the firewall parameter + return &compute.Firewall{ + Name: d.Get("name").(string), + Network: network.SelfLink, + Allowed: allowed, + SourceRanges: sourceRanges, + SourceTags: sourceTags, + }, nil +} diff --git a/builtin/providers/google/resource_compute_firewall_test.go b/builtin/providers/google/resource_compute_firewall_test.go new file mode 100644 index 000000000..a801bd86b --- /dev/null +++ b/builtin/providers/google/resource_compute_firewall_test.go @@ -0,0 +1,152 @@ +package google + +import ( + "fmt" + "testing" + + "code.google.com/p/google-api-go-client/compute/v1" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccComputeFirewall_basic(t *testing.T) { + var firewall compute.Firewall + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeFirewallDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeFirewall_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeFirewallExists( + "google_compute_firewall.foobar", &firewall), + ), + }, + }, + }) +} + +func TestAccComputeFirewall_update(t *testing.T) { + var firewall compute.Firewall + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeFirewallDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeFirewall_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeFirewallExists( + "google_compute_firewall.foobar", &firewall), + ), + }, + resource.TestStep{ + Config: testAccComputeFirewall_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeFirewallExists( + "google_compute_firewall.foobar", &firewall), + testAccCheckComputeFirewallPorts( + &firewall, "80-255"), + ), + }, + }, + }) +} + +func testAccCheckComputeFirewallDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.Resources { + if rs.Type != "google_compute_firewall" { + continue + } + + _, err := config.clientCompute.Firewalls.Get( + config.Project, rs.ID).Do() + if err == nil { + return fmt.Errorf("Firewall still exists") + } + } + + return nil +} + +func testAccCheckComputeFirewallExists(n string, firewall *compute.Firewall) 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 ID is set") + } + + config := testAccProvider.Meta().(*Config) + + found, err := config.clientCompute.Firewalls.Get( + config.Project, rs.ID).Do() + if err != nil { + return err + } + + if found.Name != rs.ID { + return fmt.Errorf("Firewall not found") + } + + *firewall = *found + + return nil + } +} + +func testAccCheckComputeFirewallPorts( + firewall *compute.Firewall, ports string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if len(firewall.Allowed) == 0 { + return fmt.Errorf("no allowed rules") + } + + if firewall.Allowed[0].Ports[0] != ports { + return fmt.Errorf("bad: %#v", firewall.Allowed[0].Ports) + } + + return nil + } +} + +const testAccComputeFirewall_basic = ` +resource "google_compute_network" "foobar" { + name = "terraform-test" + ipv4_range = "10.0.0.0/16" +} + +resource "google_compute_firewall" "foobar" { + name = "terraform-test" + network = "${google_compute_network.foobar.name}" + source_tags = ["foo"] + + allow { + protocol = "icmp" + } +}` + +const testAccComputeFirewall_update = ` +resource "google_compute_network" "foobar" { + name = "terraform-test" + ipv4_range = "10.0.0.0/16" +} + +resource "google_compute_firewall" "foobar" { + name = "terraform-test" + network = "${google_compute_network.foobar.name}" + source_tags = ["foo"] + + allow { + protocol = "tcp" + ports = ["80-255"] + } +}` diff --git a/builtin/providers/google/resource_compute_instance.go b/builtin/providers/google/resource_compute_instance.go new file mode 100644 index 000000000..9079c7a54 --- /dev/null +++ b/builtin/providers/google/resource_compute_instance.go @@ -0,0 +1,462 @@ +package google + +import ( + "fmt" + "log" + "time" + + "code.google.com/p/google-api-go-client/compute/v1" + "code.google.com/p/google-api-go-client/googleapi" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceComputeInstance() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeInstanceCreate, + Read: resourceComputeInstanceRead, + Update: resourceComputeInstanceUpdate, + Delete: resourceComputeInstanceDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "machine_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "disk": &schema.Schema{ + Type: schema.TypeList, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // TODO(mitchellh): one of image or disk is required + + "disk": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "image": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "network": &schema.Schema{ + Type: schema.TypeList, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "internal_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "metadata": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeMap, + }, + }, + + "tags": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + + "metadata_fingerprint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "tags_fingerprint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Get the zone + log.Printf("[DEBUG] Loading zone: %s", d.Get("zone").(string)) + zone, err := config.clientCompute.Zones.Get( + config.Project, d.Get("zone").(string)).Do() + if err != nil { + return fmt.Errorf( + "Error loading zone '%s': %s", d.Get("zone").(string), err) + } + + // Get the machine type + log.Printf("[DEBUG] Loading machine type: %s", d.Get("machine_type").(string)) + machineType, err := config.clientCompute.MachineTypes.Get( + config.Project, zone.Name, d.Get("machine_type").(string)).Do() + if err != nil { + return fmt.Errorf( + "Error loading machine type: %s", + err) + } + + // Build up the list of disks + disksCount := d.Get("disk.#").(int) + disks := make([]*compute.AttachedDisk, 0, disksCount) + for i := 0; i < disksCount; i++ { + prefix := fmt.Sprintf("disk.%d", i) + + var sourceLink string + + // Load up the disk for this disk if specified + if v, ok := d.GetOk(prefix + ".disk"); ok { + diskName := v.(string) + disk, err := config.clientCompute.Disks.Get( + config.Project, zone.Name, diskName).Do() + if err != nil { + return fmt.Errorf( + "Error loading disk '%s': %s", + diskName, err) + } + + sourceLink = disk.SelfLink + } + + // Load up the image for this disk if specified + if v, ok := d.GetOk(prefix + ".image"); ok { + imageName := v.(string) + image, err := readImage(config, imageName) + if err != nil { + return fmt.Errorf( + "Error loading image '%s': %s", + imageName, err) + } + + sourceLink = image.SelfLink + } + + // Build the disk + var disk compute.AttachedDisk + disk.Type = "PERSISTENT" + disk.Mode = "READ_WRITE" + disk.Boot = i == 0 + disk.AutoDelete = true + disk.InitializeParams = &compute.AttachedDiskInitializeParams{ + SourceImage: sourceLink, + } + + disks = append(disks, &disk) + } + + // Build up the list of networks + networksCount := d.Get("network.#").(int) + networks := make([]*compute.NetworkInterface, 0, networksCount) + for i := 0; i < networksCount; i++ { + prefix := fmt.Sprintf("network.%d", i) + // Load up the name of this network + networkName := d.Get(prefix + ".source").(string) + network, err := config.clientCompute.Networks.Get( + config.Project, networkName).Do() + if err != nil { + return fmt.Errorf( + "Error loading network '%s': %s", + networkName, err) + } + + // Build the disk + var iface compute.NetworkInterface + iface.AccessConfigs = []*compute.AccessConfig{ + &compute.AccessConfig{ + Type: "ONE_TO_ONE_NAT", + NatIP: d.Get(prefix + ".address").(string), + }, + } + iface.Network = network.SelfLink + + networks = append(networks, &iface) + } + + // Create the instance information + instance := compute.Instance{ + Description: d.Get("description").(string), + Disks: disks, + MachineType: machineType.SelfLink, + Metadata: resourceInstanceMetadata(d), + Name: d.Get("name").(string), + NetworkInterfaces: networks, + Tags: resourceInstanceTags(d), + /* + ServiceAccounts: []*compute.ServiceAccount{ + &compute.ServiceAccount{ + Email: "default", + Scopes: []string{ + "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/compute", + "https://www.googleapis.com/auth/devstorage.full_control", + }, + }, + }, + */ + } + + log.Printf("[INFO] Requesting instance creation") + op, err := config.clientCompute.Instances.Insert( + config.Project, zone.Name, &instance).Do() + if err != nil { + return fmt.Errorf("Error creating instance: %s", err) + } + + // Store the ID now + d.SetId(instance.Name) + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Zone: zone.Name, + Type: OperationWaitZone, + } + state := w.Conf() + state.Delay = 10 * time.Second + state.Timeout = 10 * time.Minute + state.MinTimeout = 2 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for instance to create: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // The resource didn't actually create + d.SetId("") + + // Return the error + return OperationError(*op.Error) + } + + return resourceComputeInstanceRead(d, meta) +} + +func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + instance, err := config.clientCompute.Instances.Get( + config.Project, d.Get("zone").(string), d.Id()).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + // The resource doesn't exist anymore + d.SetId("") + + return nil + } + + return fmt.Errorf("Error reading instance: %s", err) + } + + // Set the networks + for i, iface := range instance.NetworkInterfaces { + prefix := fmt.Sprintf("network.%d", i) + d.Set(prefix+".name", iface.Name) + d.Set(prefix+".internal_address", iface.NetworkIP) + } + + // Set the metadata fingerprint if there is one. + if instance.Metadata != nil { + d.Set("metadata_fingerprint", instance.Metadata.Fingerprint) + } + + // Set the tags fingerprint if there is one. + if instance.Tags != nil { + d.Set("tags_fingerprint", instance.Tags.Fingerprint) + } + + return nil +} + +func resourceComputeInstanceUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // If the Metadata has changed, then update that. + if d.HasChange("metadata") { + metadata := resourceInstanceMetadata(d) + op, err := config.clientCompute.Instances.SetMetadata( + config.Project, d.Get("zone").(string), d.Id(), metadata).Do() + if err != nil { + return fmt.Errorf("Error updating metadata: %s", err) + } + + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Zone: d.Get("zone").(string), + Type: OperationWaitZone, + } + state := w.Conf() + state.Delay = 1 * time.Second + state.Timeout = 5 * time.Minute + state.MinTimeout = 2 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for metadata to update: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // Return the error + return OperationError(*op.Error) + } + } + + if d.HasChange("tags") { + tags := resourceInstanceTags(d) + op, err := config.clientCompute.Instances.SetTags( + config.Project, d.Get("zone").(string), d.Id(), tags).Do() + if err != nil { + return fmt.Errorf("Error updating tags: %s", err) + } + + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Zone: d.Get("zone").(string), + Type: OperationWaitZone, + } + state := w.Conf() + state.Delay = 1 * time.Second + state.Timeout = 5 * time.Minute + state.MinTimeout = 2 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for tags to update: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // Return the error + return OperationError(*op.Error) + } + } + + return resourceComputeInstanceRead(d, meta) +} + +func resourceComputeInstanceDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + op, err := config.clientCompute.Instances.Delete( + config.Project, d.Get("zone").(string), d.Id()).Do() + if err != nil { + return fmt.Errorf("Error deleting instance: %s", err) + } + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Zone: d.Get("zone").(string), + Type: OperationWaitZone, + } + state := w.Conf() + state.Delay = 5 * time.Second + state.Timeout = 5 * time.Minute + state.MinTimeout = 2 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for instance to delete: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // Return the error + return OperationError(*op.Error) + } + + d.SetId("") + return nil +} + +func resourceInstanceMetadata(d *schema.ResourceData) *compute.Metadata { + var metadata *compute.Metadata + if v := d.Get("metadata").([]interface{}); len(v) > 0 { + m := new(compute.Metadata) + m.Items = make([]*compute.MetadataItems, 0, len(v)) + for _, v := range v { + for k, v := range v.(map[string]interface{}) { + m.Items = append(m.Items, &compute.MetadataItems{ + Key: k, + Value: v.(string), + }) + } + } + + // Set the fingerprint. If the metadata has never been set before + // then this will just be blank. + m.Fingerprint = d.Get("metadata_fingerprint").(string) + + metadata = m + } + + return metadata +} + +func resourceInstanceTags(d *schema.ResourceData) *compute.Tags { + // Calculate the tags + var tags *compute.Tags + if v := d.Get("tags"); v != nil { + vs := v.(*schema.Set).List() + tags = new(compute.Tags) + tags.Items = make([]string, len(vs)) + for i, v := range v.(*schema.Set).List() { + tags.Items[i] = v.(string) + } + + tags.Fingerprint = d.Get("tags_fingerprint").(string) + } + + return tags +} diff --git a/builtin/providers/google/resource_compute_instance_test.go b/builtin/providers/google/resource_compute_instance_test.go new file mode 100644 index 000000000..24a752148 --- /dev/null +++ b/builtin/providers/google/resource_compute_instance_test.go @@ -0,0 +1,246 @@ +package google + +import ( + "fmt" + "testing" + + "code.google.com/p/google-api-go-client/compute/v1" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccComputeInstance_basic(t *testing.T) { + var instance compute.Instance + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstance_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceTag(&instance, "foo"), + testAccCheckComputeInstanceMetadata(&instance, "foo", "bar"), + ), + }, + }, + }) +} + +func TestAccComputeInstance_IP(t *testing.T) { + var instance compute.Instance + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstance_ip, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceNetwork(&instance), + ), + }, + }, + }) +} + +func TestAccComputeInstance_update(t *testing.T) { + var instance compute.Instance + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstance_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + ), + }, + resource.TestStep{ + Config: testAccComputeInstance_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceExists( + "google_compute_instance.foobar", &instance), + testAccCheckComputeInstanceMetadata( + &instance, "bar", "baz"), + testAccCheckComputeInstanceTag(&instance, "baz"), + ), + }, + }, + }) +} + +func testAccCheckComputeInstanceDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.Resources { + if rs.Type != "google_compute_instance" { + continue + } + + _, err := config.clientCompute.Instances.Get( + config.Project, rs.Attributes["zone"], rs.ID).Do() + if err == nil { + return fmt.Errorf("Instance still exists") + } + } + + return nil +} + +func testAccCheckComputeInstanceExists(n string, instance *compute.Instance) 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 ID is set") + } + + config := testAccProvider.Meta().(*Config) + + found, err := config.clientCompute.Instances.Get( + config.Project, rs.Attributes["zone"], rs.ID).Do() + if err != nil { + return err + } + + if found.Name != rs.ID { + return fmt.Errorf("Instance not found") + } + + *instance = *found + + return nil + } +} + +func testAccCheckComputeInstanceMetadata( + instance *compute.Instance, + k string, v string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if instance.Metadata == nil { + return fmt.Errorf("no metadata") + } + + for _, item := range instance.Metadata.Items { + if k != item.Key { + continue + } + + if v == item.Value { + return nil + } + + return fmt.Errorf("bad value for %s: %s", k, item.Value) + } + + return fmt.Errorf("metadata not found: %s", k) + } +} + +func testAccCheckComputeInstanceNetwork(instance *compute.Instance) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, i := range instance.NetworkInterfaces { + for _, c := range i.AccessConfigs { + if c.NatIP == "" { + return fmt.Errorf("no NAT IP") + } + } + } + + return nil + } +} + +func testAccCheckComputeInstanceTag(instance *compute.Instance, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if instance.Tags == nil { + return fmt.Errorf("no tags") + } + + for _, k := range instance.Tags.Items { + if k == n { + return nil + } + } + + return fmt.Errorf("tag not found: %s", n) + } +} + +const testAccComputeInstance_basic = ` +resource "google_compute_instance" "foobar" { + name = "terraform-test" + machine_type = "n1-standard-1" + zone = "us-central1-a" + tags = ["foo", "bar"] + + disk { + image = "debian-7-wheezy-v20140814" + } + + network { + source = "default" + } + + metadata { + foo = "bar" + } +}` + +const testAccComputeInstance_update = ` +resource "google_compute_instance" "foobar" { + name = "terraform-test" + machine_type = "n1-standard-1" + zone = "us-central1-a" + tags = ["baz"] + + disk { + image = "debian-7-wheezy-v20140814" + } + + network { + source = "default" + } + + metadata { + bar = "baz" + } +}` + +const testAccComputeInstance_ip = ` +resource "google_compute_address" "foo" { + name = "foo" +} + +resource "google_compute_instance" "foobar" { + name = "terraform-test" + machine_type = "n1-standard-1" + zone = "us-central1-a" + tags = ["foo", "bar"] + + disk { + image = "debian-7-wheezy-v20140814" + } + + network { + source = "default" + address = "${google_compute_address.foo.address}" + } + + metadata { + foo = "bar" + } +}` diff --git a/builtin/providers/google/resource_compute_network.go b/builtin/providers/google/resource_compute_network.go new file mode 100644 index 000000000..b79ac2ade --- /dev/null +++ b/builtin/providers/google/resource_compute_network.go @@ -0,0 +1,137 @@ +package google + +import ( + "fmt" + "log" + "time" + + "code.google.com/p/google-api-go-client/compute/v1" + "code.google.com/p/google-api-go-client/googleapi" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceComputeNetwork() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeNetworkCreate, + Read: resourceComputeNetworkRead, + Delete: resourceComputeNetworkDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "ipv4_range": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "gateway_ipv4": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceComputeNetworkCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Build the network parameter + network := &compute.Network{ + Name: d.Get("name").(string), + IPv4Range: d.Get("ipv4_range").(string), + } + log.Printf("[DEBUG] Network insert request: %#v", network) + op, err := config.clientCompute.Networks.Insert( + config.Project, network).Do() + if err != nil { + return fmt.Errorf("Error creating network: %s", err) + } + + // It probably maybe worked, so store the ID now + d.SetId(network.Name) + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Type: OperationWaitGlobal, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for network to create: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // The resource didn't actually create + d.SetId("") + + // Return the error + return OperationError(*op.Error) + } + + return resourceComputeNetworkRead(d, meta) +} + +func resourceComputeNetworkRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + network, err := config.clientCompute.Networks.Get( + config.Project, d.Id()).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + // The resource doesn't exist anymore + d.SetId("") + + return nil + } + + return fmt.Errorf("Error reading network: %s", err) + } + + d.Set("gateway_ipv4", network.GatewayIPv4) + + return nil +} + +func resourceComputeNetworkDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Delete the network + op, err := config.clientCompute.Networks.Delete( + config.Project, d.Id()).Do() + if err != nil { + return fmt.Errorf("Error deleting network: %s", err) + } + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Type: OperationWaitGlobal, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for network to delete: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // Return the error + return OperationError(*op.Error) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/google/resource_compute_network_test.go b/builtin/providers/google/resource_compute_network_test.go new file mode 100644 index 000000000..60c278117 --- /dev/null +++ b/builtin/providers/google/resource_compute_network_test.go @@ -0,0 +1,82 @@ +package google + +import ( + "fmt" + "testing" + + "code.google.com/p/google-api-go-client/compute/v1" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccComputeNetwork_basic(t *testing.T) { + var network compute.Network + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeNetworkDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeNetwork_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeNetworkExists( + "google_compute_network.foobar", &network), + ), + }, + }, + }) +} + +func testAccCheckComputeNetworkDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.Resources { + if rs.Type != "google_compute_network" { + continue + } + + _, err := config.clientCompute.Networks.Get( + config.Project, rs.ID).Do() + if err == nil { + return fmt.Errorf("Network still exists") + } + } + + return nil +} + +func testAccCheckComputeNetworkExists(n string, network *compute.Network) 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 ID is set") + } + + config := testAccProvider.Meta().(*Config) + + found, err := config.clientCompute.Networks.Get( + config.Project, rs.ID).Do() + if err != nil { + return err + } + + if found.Name != rs.ID { + return fmt.Errorf("Network not found") + } + + *network = *found + + return nil + } +} + +const testAccComputeNetwork_basic = ` +resource "google_compute_network" "foobar" { + name = "terraform-test" + ipv4_range = "10.0.0.0/16" +}` diff --git a/builtin/providers/google/resource_compute_route.go b/builtin/providers/google/resource_compute_route.go new file mode 100644 index 000000000..0c15dbaa4 --- /dev/null +++ b/builtin/providers/google/resource_compute_route.go @@ -0,0 +1,234 @@ +package google + +import ( + "fmt" + "log" + "time" + + "code.google.com/p/google-api-go-client/compute/v1" + "code.google.com/p/google-api-go-client/googleapi" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceComputeRoute() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeRouteCreate, + Read: resourceComputeRouteRead, + Delete: resourceComputeRouteDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "dest_range": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "network": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "next_hop_ip": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "next_hop_instance": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "next_hop_instance_zone": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "next_hop_gateway": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "next_hop_network": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "priority": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + + "tags": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + }, + } +} + +func resourceComputeRouteCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Look up the network to attach the route to + network, err := config.clientCompute.Networks.Get( + config.Project, d.Get("network").(string)).Do() + if err != nil { + return fmt.Errorf("Error reading network: %s", err) + } + + // Next hop data + var nextHopInstance, nextHopIp, nextHopNetwork, nextHopGateway string + if v, ok := d.GetOk("next_hop_ip"); ok { + nextHopIp = v.(string) + } + if v, ok := d.GetOk("next_hop_gateway"); ok { + nextHopGateway = v.(string) + } + if v, ok := d.GetOk("next_hop_instance"); ok { + nextInstance, err := config.clientCompute.Instances.Get( + config.Project, + d.Get("next_hop_instance_zone").(string), + v.(string)).Do() + if err != nil { + return fmt.Errorf("Error reading instance: %s", err) + } + + nextHopInstance = nextInstance.SelfLink + } + if v, ok := d.GetOk("next_hop_network"); ok { + nextNetwork, err := config.clientCompute.Networks.Get( + config.Project, v.(string)).Do() + if err != nil { + return fmt.Errorf("Error reading network: %s", err) + } + + nextHopNetwork = nextNetwork.SelfLink + } + + // Tags + var tags []string + if v := d.Get("tags").(*schema.Set); v.Len() > 0 { + tags = make([]string, v.Len()) + for i, v := range v.List() { + tags[i] = v.(string) + } + } + + // Build the route parameter + route := &compute.Route{ + Name: d.Get("name").(string), + DestRange: d.Get("dest_range").(string), + Network: network.SelfLink, + NextHopInstance: nextHopInstance, + NextHopIp: nextHopIp, + NextHopNetwork: nextHopNetwork, + NextHopGateway: nextHopGateway, + Priority: int64(d.Get("priority").(int)), + Tags: tags, + } + log.Printf("[DEBUG] Route insert request: %#v", route) + op, err := config.clientCompute.Routes.Insert( + config.Project, route).Do() + if err != nil { + return fmt.Errorf("Error creating route: %s", err) + } + + // It probably maybe worked, so store the ID now + d.SetId(route.Name) + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Type: OperationWaitGlobal, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for route to create: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // The resource didn't actually create + d.SetId("") + + // Return the error + return OperationError(*op.Error) + } + + return resourceComputeRouteRead(d, meta) +} + +func resourceComputeRouteRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + _, err := config.clientCompute.Routes.Get( + config.Project, d.Id()).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + // The resource doesn't exist anymore + d.SetId("") + + return nil + } + + return fmt.Errorf("Error reading route: %#v", err) + } + + return nil +} + +func resourceComputeRouteDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Delete the route + op, err := config.clientCompute.Routes.Delete( + config.Project, d.Id()).Do() + if err != nil { + return fmt.Errorf("Error deleting route: %s", err) + } + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Type: OperationWaitGlobal, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for route to delete: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // Return the error + return OperationError(*op.Error) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/google/resource_compute_route_test.go b/builtin/providers/google/resource_compute_route_test.go new file mode 100644 index 000000000..eb0721d96 --- /dev/null +++ b/builtin/providers/google/resource_compute_route_test.go @@ -0,0 +1,90 @@ +package google + +import ( + "fmt" + "testing" + + "code.google.com/p/google-api-go-client/compute/v1" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccComputeRoute_basic(t *testing.T) { + var route compute.Route + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeRouteDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeRoute_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeRouteExists( + "google_compute_route.foobar", &route), + ), + }, + }, + }) +} + +func testAccCheckComputeRouteDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.Resources { + if rs.Type != "google_compute_route" { + continue + } + + _, err := config.clientCompute.Routes.Get( + config.Project, rs.ID).Do() + if err == nil { + return fmt.Errorf("Route still exists") + } + } + + return nil +} + +func testAccCheckComputeRouteExists(n string, route *compute.Route) 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 ID is set") + } + + config := testAccProvider.Meta().(*Config) + + found, err := config.clientCompute.Routes.Get( + config.Project, rs.ID).Do() + if err != nil { + return err + } + + if found.Name != rs.ID { + return fmt.Errorf("Route not found") + } + + *route = *found + + return nil + } +} + +const testAccComputeRoute_basic = ` +resource "google_compute_network" "foobar" { + name = "terraform-test" + ipv4_range = "10.0.0.0/16" +} + +resource "google_compute_route" "foobar" { + name = "terraform-test" + dest_range = "15.0.0.0/24" + network = "${google_compute_network.foobar.name}" + next_hop_ip = "10.0.1.5" + priority = 100 +}` diff --git a/builtin/providers/google/test-fixtures/fake_account.json b/builtin/providers/google/test-fixtures/fake_account.json new file mode 100644 index 000000000..f3362d6d2 --- /dev/null +++ b/builtin/providers/google/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/builtin/providers/google/test-fixtures/fake_client.json b/builtin/providers/google/test-fixtures/fake_client.json new file mode 100644 index 000000000..d88fe4cd7 --- /dev/null +++ b/builtin/providers/google/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" + } +} diff --git a/website/source/docs/providers/google/index.html.markdown b/website/source/docs/providers/google/index.html.markdown new file mode 100644 index 000000000..5294ac3b5 --- /dev/null +++ b/website/source/docs/providers/google/index.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "google" +page_title: "Provider: Google Cloud" +sidebar_current: "docs-google-index" +--- + +# Google Cloud Provider + +The Google Cloud provider is used to interact with +[Google Cloud services](https://cloud.google.com/). The provider needs +to be configured with the proper credentials before it can be used. + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +# Configure the Google Cloud provider +provider "google" { + account_file = "account.json" + client_secrets_file = "client_secrets.json" + project = "my-gce-project" + region = "us-central1" +} + +# Create a new instance +resource "google_compute_instance" "default" { + ... +} +``` + +## Configuration Reference + +The following keys can be used to configure the provider. + +* `account_file` - (Required) Path to the JSON file used to describe + your account credentials, downloaded from Google Cloud Console. More + details on retrieving this file are below. + +* `client_secrets_file` - (Required) Path to the JSON file containing + the secrets for your account, downloaded from Google Cloud Console. + More details on retrieving this file are below. + +* `project` - (Required) The name of the project to apply any resources to. + +* `region` - (Required) The region to operate under. + +## Authentication JSON Files + +Authenticating with Google Cloud services requires two separate JSON +files: one which we call the _account file_ and the _client secrets file_. + +Both of these files are downloaded directly from the +[Google Developers Console](https://console.developers.google.com). To make +the process more straightforwarded, it is documented here. + +1. Log into the [Google Developers Console](https://console.developers.google.com) + and select a project. + +2. Under the "APIs & Auth" section, click "Credentials." + +3. Click the "Download JSON" button under the "Compute Engine and App Engine" + account in the OAuth section. The file should start with "client\_secrets". + This is your _client secrets file_. + +4. Create a new OAuth client ID and select "Service Account" as the type + of account. Once created, a JSON file should be downloaded. This is your + _account file_. diff --git a/website/source/docs/providers/google/r/compute_address.html.markdown b/website/source/docs/providers/google/r/compute_address.html.markdown new file mode 100644 index 000000000..08c98b0d3 --- /dev/null +++ b/website/source/docs/providers/google/r/compute_address.html.markdown @@ -0,0 +1,31 @@ +--- +layout: "google" +page_title: "Google: google_compute_address" +sidebar_current: "docs-google-resource-address" +--- + +# google\_compute\_address + +Creates a static IP address resource for Google Compute Engine. + +## Example Usage + +``` +resource "google_compute_address" "default" { + name = "test-address" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) A unique name for the resource, required by GCE. + Changing this forces a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `name` - The name of the resource. +* `address` - The IP address that was allocated. diff --git a/website/source/docs/providers/google/r/compute_disk.html.markdown b/website/source/docs/providers/google/r/compute_disk.html.markdown new file mode 100644 index 000000000..96e43efec --- /dev/null +++ b/website/source/docs/providers/google/r/compute_disk.html.markdown @@ -0,0 +1,42 @@ +--- +layout: "google" +page_title: "Google: google_compute_disk" +sidebar_current: "docs-google-resource-disk" +--- + +# google\_compute\_disk + +Creates a new persistent disk within GCE, based on another disk. + +## Example Usage + +``` +resource "google_compute_disk" "default" { + name = "test-disk" + zone = "us-central1-a" + image = "debian7-wheezy" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) A unique name for the resource, required by GCE. + Changing this forces a new resource to be created. + +* `zone` - (Required) The zone where this disk will be available. + +* `image` - (Optional) The machine image to base this disk off of. + +* `size` - (Optional) The size of the image in gigabytes. If not specified, + it will inherit the size of its base image. + +## Attributes Reference + +The following attributes are exported: + +* `name` - The name of the resource. +* `zone` - The zone where the resource is located. +* `image` - The name of the image the disk is based off of. +* `size` - The size of the disk in gigabytes. diff --git a/website/source/docs/providers/google/r/compute_firewall.html.markdown b/website/source/docs/providers/google/r/compute_firewall.html.markdown new file mode 100644 index 000000000..1a0584754 --- /dev/null +++ b/website/source/docs/providers/google/r/compute_firewall.html.markdown @@ -0,0 +1,62 @@ +--- +layout: "google" +page_title: "Google: google_compute_firewall" +sidebar_current: "docs-google-resource-firewall" +--- + +# google\_compute\_firewall + +Manages a firewall resource within GCE. + +## Example Usage + +``` +resource "google_compute_firewall" "default" { + name = "test" + network = "${google_compute_network.other.name}" + + allow { + protocol = "icmp" + } + + allow { + protocol = "tcp" + ports = ["80", "8080", "1000-2000"] + } + + source_tags = ["web"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) A unique name for the resource, required by GCE. + Changing this forces a new resource to be created. + +* `network` - (Required) The name of the network to attach this firewall to. + +* `allow` - (Required) Can be specified multiple times for each allow + rule. Each allow block supports fields documented below. + +* `source_ranges` - (Optional) A list of source CIDR ranges that this + firewall applies to. + +* `source_tags` - (Optional) A list of tags that this firewall applies to. + +The `allow` block supports: + +* `protocol` - (Required) The name of the protocol to allow. + +* `ports` - (Optional) List of ports and/or port ranges to allow. This can + only be specified if the protocol is TCP or UDP. + +## Attributes Reference + +The following attributes are exported: + +* `name` - The name of the resource. +* `network` - The network that this resource is attached to. +* `source_ranges` - The CIDR block ranges this firewall applies to. +* `source_tags` - The tags that this firewall applies to. diff --git a/website/source/docs/providers/google/r/compute_instance.html.markdown b/website/source/docs/providers/google/r/compute_instance.html.markdown new file mode 100644 index 000000000..5c9ddced5 --- /dev/null +++ b/website/source/docs/providers/google/r/compute_instance.html.markdown @@ -0,0 +1,80 @@ +--- +layout: "google" +page_title: "Google: google_compute_instance" +sidebar_current: "docs-google-resource-instance" +--- + +# google\_compute\_instance + +Manages a VM instance resource within GCE. + +## Example Usage + +``` +resource "google_compute_instance" "default" { + name = "test" + machine_type = "n1-standard-1" + zone = "us-central1-a" + tags = ["foo", "bar"] + + disk { + image = "debian-7-wheezy-v20140814" + } + + network { + source = "default" + } + + metadata { + foo = "bar" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) A unique name for the resource, required by GCE. + Changing this forces a new resource to be created. + +* `description` - (Optional) A brief description of this resource. + +* `machine_type` - (Required) The machine type to create. + +* `zone` - (Required) The zone that the machine should be created in. + +* `disk` - (Required) Disks to attach to the instance. This can be specified + multiple times for multiple disks. Structure is documented below. + +* `metadata` - (Optional) Metadata key/value pairs to make available from + within the instance. + +* `network` - (Required) Networks to attach to the instance. This can be + specified multiple times for multiple networks. Structure is documented + below. + +* `tags` - (Optional) Tags to attach to the instance. + +The `disk` block supports: + +* `disk` - (Required if image not set) The name of the disk (such as + those managed by `google_compute_disk`) to attach. + +* `image` - (Required if disk not set) The name of the image to base + this disk off of. + +The `network` block supports: + +* `source` - (Required) The name of the network to attach this interface to. + +* `address` - (Optional) The IP address of a reserved IP address to assign + to this interface. + +## Attributes Reference + +The following attributes are exported: + +* `name` - The name of the resource. +* `machine_type` - The type of machine. +* `zone` - The zone the machine lives in. diff --git a/website/source/docs/providers/google/r/compute_network.html.markdown b/website/source/docs/providers/google/r/compute_network.html.markdown new file mode 100644 index 000000000..52ae1c7bc --- /dev/null +++ b/website/source/docs/providers/google/r/compute_network.html.markdown @@ -0,0 +1,36 @@ +--- +layout: "google" +page_title: "Google: google_compute_network" +sidebar_current: "docs-google-resource-network" +--- + +# google\_compute\_network + +Manages a network within GCE. + +## Example Usage + +``` +resource "google_compute_network" "default" { + name = "test" + ipv4_range = "10.0.0.0/16" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) A unique name for the resource, required by GCE. + Changing this forces a new resource to be created. + +* `ipv4_range` - (Required) The IPv4 address range that machines in this + network are assigned to, represented as a CIDR block. + +## Attributes Reference + +The following attributes are exported: + +* `name` - The name of the resource. +* `ipv4_range` - The CIDR block of this network. +* `gateway_ipv4` - The IPv4 address of the gateway. diff --git a/website/source/docs/providers/google/r/compute_route.html.markdown b/website/source/docs/providers/google/r/compute_route.html.markdown new file mode 100644 index 000000000..b3ebe3313 --- /dev/null +++ b/website/source/docs/providers/google/r/compute_route.html.markdown @@ -0,0 +1,72 @@ +--- +layout: "google" +page_title: "Google: google_compute_route" +sidebar_current: "docs-google-resource-route" +--- + +# google\_compute\_route + +Manages a network route within GCE. + +## Example Usage + +``` +resource "google_compute_network" "foobar" { + name = "test" + ipv4_range = "10.0.0.0/16" +} + +resource "google_compute_route" "foobar" { + name = "test" + dest_range = "15.0.0.0/24" + network = "${google_compute_network.foobar.name}" + next_hop_ip = "10.0.1.5" + priority = 100 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) A unique name for the resource, required by GCE. + Changing this forces a new resource to be created. + +* `dest_range` - (Required) The destination IPv4 address range that this + route applies to. + +* `network` - (Required) The name of the network to attach this route to. + +* `next_hop_ip` - (Optional) The IP address of the next hop if this route + is matched. + +* `next_hop_instance` - (Optional) The name of the VM instance to route to + if this route is matched. + +* `next_hop_instance_zone` - (Optional) The zone of the instance specified + in `next_hop_instance`. + +* `next_hop_gateway` - (Optional) The name of the internet gateway to route + to if this route is matched. + +* `next_hop_network` - (Optional) The name of the network to route to if this + route is matched. + +* `priority` - (Required) The priority of this route, used to break ties. + +* `tags` - (Optional) The tags that this route applies to. + +## Attributes Reference + +The following attributes are exported: + +* `name` - The name of the resource. +* `dest_range` - The detination CIDR block of this route. +* `network` - The name of the network of this route. +* `next_hop_ip` - The IP address of the next hop, if available. +* `next_hop_instance` - The name of the instance of the next hop, if available. +* `next_hop_instance_zone` - The zone of the next hop instance, if available. +* `next_hop_gateway` - The name of the next hop gateway, if available. +* `next_hop_network` - The name of the next hop network, if available. +* `priority` - The priority of this route. +* `tags` - The tags this route applies to. diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index efa809b5c..dd14e213d 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -96,6 +96,10 @@ DNSimple + > + Google Cloud + + > Heroku diff --git a/website/source/layouts/google.erb b/website/source/layouts/google.erb new file mode 100644 index 000000000..1c6dfe902 --- /dev/null +++ b/website/source/layouts/google.erb @@ -0,0 +1,46 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + +<%= yield %> + <% end %> diff --git a/website/source/stylesheets/_docs.less b/website/source/stylesheets/_docs.less index 0f9d614cf..02797d407 100755 --- a/website/source/stylesheets/_docs.less +++ b/website/source/stylesheets/_docs.less @@ -9,6 +9,7 @@ body.page-sub{ body.layout-consul, body.layout-dnsimple, body.layout-cloudflare, +body.layout-google, body.layout-heroku, body.layout-mailgun, body.layout-digitalocean, diff --git a/website/source/stylesheets/main.css b/website/source/stylesheets/main.css index 2f2593354..5d0bb7fa2 100644 --- a/website/source/stylesheets/main.css +++ b/website/source/stylesheets/main.css @@ -1617,6 +1617,7 @@ body.page-sub { body.layout-consul, body.layout-dnsimple, body.layout-cloudflare, +body.layout-google, body.layout-heroku, body.layout-mailgun, body.layout-digitalocean, @@ -1630,6 +1631,7 @@ body.layout-intro { body.layout-consul > .container .col-md-8[role=main], body.layout-dnsimple > .container .col-md-8[role=main], body.layout-cloudflare > .container .col-md-8[role=main], +body.layout-google > .container .col-md-8[role=main], body.layout-heroku > .container .col-md-8[role=main], body.layout-mailgun > .container .col-md-8[role=main], body.layout-digitalocean > .container .col-md-8[role=main], @@ -1644,6 +1646,7 @@ body.layout-intro > .container .col-md-8[role=main] { body.layout-consul > .container .col-md-8[role=main] > div, body.layout-dnsimple > .container .col-md-8[role=main] > div, body.layout-cloudflare > .container .col-md-8[role=main] > div, +body.layout-google > .container .col-md-8[role=main] > div, body.layout-heroku > .container .col-md-8[role=main] > div, body.layout-mailgun > .container .col-md-8[role=main] > div, body.layout-digitalocean > .container .col-md-8[role=main] > div,