From 5bf258809f20d564e8b8ba19da14138f468b30bf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 25 Aug 2014 14:57:17 -0700 Subject: [PATCH] providers/google: compute_instance --- builtin/providers/google/image.go | 43 ++++ .../google/resource_compute_instance.go | 232 +++++++++++++++++- .../google/resource_compute_instance_test.go | 91 +++++++ 3 files changed, 364 insertions(+), 2 deletions(-) create mode 100644 builtin/providers/google/image.go create mode 100644 builtin/providers/google/resource_compute_instance_test.go 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/resource_compute_instance.go b/builtin/providers/google/resource_compute_instance.go index c2abd0e17..7a14ba443 100644 --- a/builtin/providers/google/resource_compute_instance.go +++ b/builtin/providers/google/resource_compute_instance.go @@ -1,17 +1,245 @@ package google -import( +import ( + "fmt" + "log" + "time" + + "code.google.com/p/google-api-go-client/compute/v1" "github.com/hashicorp/terraform/helper/schema" ) func resourceComputeInstance() *schema.Resource { return &schema.Resource{ Create: resourceComputeInstanceCreate, + Read: resourceComputeInstanceRead, + Delete: resourceComputeInstanceDelete, - Schema: map[string]*schema.Schema{}, + 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{ + "source": &schema.Schema{ + Type: schema.TypeString, + Required: 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, + }, + }, + }, + }, + }, } } 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++ { + // Load up the image for this disk + imageName := d.Get(fmt.Sprintf("disk.%d.source", i)).(string) + image, err := readImage(config, imageName) + if err != nil { + return fmt.Errorf( + "Error loading image '%s': %s", + imageName, err) + } + + // 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: image.SelfLink, + } + + 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++ { + // Load up the name of this network + networkName := d.Get(fmt.Sprintf("network.%d.source", i)).(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", + }, + } + 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: &compute.Metadata{ + Items: metadata, + }, + */ + Name: d.Get("name").(string), + NetworkInterfaces: networks, + /* + 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", + }, + }, + }, + Tags: &compute.Tags{ + Items: c.Tags, + }, + */ + } + + 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 + if _, err := state.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for instance to create: %s", err) + } + + return resourceComputeInstanceRead(d, meta) +} + +func resourceComputeInstanceRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + _, err := config.clientCompute.Instances.Get( + config.Project, d.Get("zone").(string), d.Id()).Do() + if err != nil { + return fmt.Errorf("Error reading instance: %s", err) + } + + return nil +} + +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 + if _, err := state.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for instance to create: %s", err) + } + + d.SetId("") return nil } 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..1f0d2bd36 --- /dev/null +++ b/builtin/providers/google/resource_compute_instance_test.go @@ -0,0 +1,91 @@ +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), + ), + }, + }, + }) +} + +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 + } +} + +const testAccComputeInstance_basic = ` +resource "google_compute_instance" "foobar" { + name = "terraform-test" + machine_type = "n1-standard-1" + zone = "us-central1-a" + + disk { + source = "debian-7-wheezy-v20140814" + } + + network { + source = "default" + } +}`