diff --git a/provider.go b/provider.go index 2c295010..d5aadb84 100644 --- a/provider.go +++ b/provider.go @@ -53,6 +53,7 @@ func Provider() terraform.ResourceProvider { "google_compute_http_health_check": resourceComputeHttpHealthCheck(), "google_compute_https_health_check": resourceComputeHttpsHealthCheck(), "google_compute_instance": resourceComputeInstance(), + "google_compute_instance_group": resourceComputeInstanceGroup(), "google_compute_instance_group_manager": resourceComputeInstanceGroupManager(), "google_compute_instance_template": resourceComputeInstanceTemplate(), "google_compute_network": resourceComputeNetwork(), diff --git a/resource_compute_instance_group.go b/resource_compute_instance_group.go new file mode 100644 index 00000000..f0b905bf --- /dev/null +++ b/resource_compute_instance_group.go @@ -0,0 +1,317 @@ +package google + +import ( + "fmt" + "log" + "strings" + + "google.golang.org/api/compute/v1" + "google.golang.org/api/googleapi" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceComputeInstanceGroup() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeInstanceGroupCreate, + Read: resourceComputeInstanceGroupRead, + Update: resourceComputeInstanceGroupUpdate, + Delete: resourceComputeInstanceGroupDelete, + + 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, + }, + + "named_port": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + }, + }, + }, + + "instances": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "network": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "size": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + }, + + "zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "self_link": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func getInstanceReferences(instanceUrls []string) (refs []*compute.InstanceReference) { + for _, v := range instanceUrls { + refs = append(refs, &compute.InstanceReference{ + Instance: v, + }) + } + return refs +} + +func validInstanceURLs(instanceUrls []string) bool { + for _, v := range instanceUrls { + if !strings.HasPrefix(v, "https://www.googleapis.com/compute/v1/") { + return false + } + } + return true +} + +func resourceComputeInstanceGroupCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // Build the parameter + instanceGroup := &compute.InstanceGroup{ + Name: d.Get("name").(string), + } + + // Set optional fields + if v, ok := d.GetOk("description"); ok { + instanceGroup.Description = v.(string) + } + + if v, ok := d.GetOk("named_port"); ok { + instanceGroup.NamedPorts = getNamedPorts(v.([]interface{})) + } + + log.Printf("[DEBUG] InstanceGroup insert request: %#v", instanceGroup) + op, err := config.clientCompute.InstanceGroups.Insert( + config.Project, d.Get("zone").(string), instanceGroup).Do() + if err != nil { + return fmt.Errorf("Error creating InstanceGroup: %s", err) + } + + // It probably maybe worked, so store the ID now + d.SetId(instanceGroup.Name) + + // Wait for the operation to complete + err = computeOperationWaitZone(config, op, d.Get("zone").(string), "Creating InstanceGroup") + if err != nil { + return err + } + + if v, ok := d.GetOk("instances"); ok { + instanceUrls := convertStringArr(v.([]interface{})) + if !validInstanceURLs(instanceUrls) { + return fmt.Errorf("Error invalid instance URLs: %v", instanceUrls) + } + + addInstanceReq := &compute.InstanceGroupsAddInstancesRequest{ + Instances: getInstanceReferences(instanceUrls), + } + + log.Printf("[DEBUG] InstanceGroup add instances request: %#v", addInstanceReq) + op, err := config.clientCompute.InstanceGroups.AddInstances( + config.Project, d.Get("zone").(string), d.Id(), addInstanceReq).Do() + if err != nil { + return fmt.Errorf("Error adding instances to InstanceGroup: %s", err) + } + + // Wait for the operation to complete + err = computeOperationWaitZone(config, op, d.Get("zone").(string), "Adding instances to InstanceGroup") + if err != nil { + return err + } + } + + return resourceComputeInstanceGroupRead(d, meta) +} + +func resourceComputeInstanceGroupRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // retreive instance group + instanceGroup, err := config.clientCompute.InstanceGroups.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 InstanceGroup: %s", err) + } + + // retreive instance group members + var memberUrls []string + members, err := config.clientCompute.InstanceGroups.ListInstances( + config.Project, d.Get("zone").(string), d.Id(), &compute.InstanceGroupsListInstancesRequest{ + InstanceState: "ALL", + }).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + // The resource doesn't have any instances + d.Set("instances", nil) + } else { + // any other errors return them + return fmt.Errorf("Error reading InstanceGroup Members: %s", err) + } + } else { + for _, member := range members.Items { + memberUrls = append(memberUrls, member.Instance) + } + log.Printf("[DEBUG] InstanceGroup members: %v", memberUrls) + d.Set("instances", memberUrls) + } + + // Set computed fields + d.Set("network", instanceGroup.Network) + d.Set("size", instanceGroup.Size) + d.Set("self_link", instanceGroup.SelfLink) + + return nil +} +func resourceComputeInstanceGroupUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + // refresh the state incase referenced instances have been removed earlier in the run + err := resourceComputeInstanceGroupRead(d, meta) + if err != nil { + return fmt.Errorf("Error reading InstanceGroup: %s", err) + } + + d.Partial(true) + + if d.HasChange("instances") { + // to-do check for no instances + from_, to_ := d.GetChange("instances") + + from := convertStringArr(from_.([]interface{})) + to := convertStringArr(to_.([]interface{})) + + if !validInstanceURLs(from) { + return fmt.Errorf("Error invalid instance URLs: %v", from) + } + if !validInstanceURLs(to) { + return fmt.Errorf("Error invalid instance URLs: %v", from) + } + + add, remove := calcAddRemove(from, to) + + if len(remove) > 0 { + removeReq := &compute.InstanceGroupsRemoveInstancesRequest{ + Instances: getInstanceReferences(remove), + } + + log.Printf("[DEBUG] InstanceGroup remove instances request: %#v", removeReq) + removeOp, err := config.clientCompute.InstanceGroups.RemoveInstances( + config.Project, d.Get("zone").(string), d.Id(), removeReq).Do() + if err != nil { + return fmt.Errorf("Error removing instances from InstanceGroup: %s", err) + } + + // Wait for the operation to complete + err = computeOperationWaitZone(config, removeOp, d.Get("zone").(string), "Updating InstanceGroup") + if err != nil { + return err + } + } + + if len(add) > 0 { + + addReq := &compute.InstanceGroupsAddInstancesRequest{ + Instances: getInstanceReferences(add), + } + + log.Printf("[DEBUG] InstanceGroup adding instances request: %#v", addReq) + addOp, err := config.clientCompute.InstanceGroups.AddInstances( + config.Project, d.Get("zone").(string), d.Id(), addReq).Do() + if err != nil { + return fmt.Errorf("Error adding instances from InstanceGroup: %s", err) + } + + // Wait for the operation to complete + err = computeOperationWaitZone(config, addOp, d.Get("zone").(string), "Updating InstanceGroup") + if err != nil { + return err + } + } + + d.SetPartial("instances") + } + + if d.HasChange("named_port") { + namedPorts := getNamedPorts(d.Get("named_port").([]interface{})) + + namedPortsReq := &compute.InstanceGroupsSetNamedPortsRequest{ + NamedPorts: namedPorts, + } + + log.Printf("[DEBUG] InstanceGroup updating named ports request: %#v", namedPortsReq) + op, err := config.clientCompute.InstanceGroups.SetNamedPorts( + config.Project, d.Get("zone").(string), d.Id(), namedPortsReq).Do() + if err != nil { + return fmt.Errorf("Error updating named ports for InstanceGroup: %s", err) + } + + err = computeOperationWaitZone(config, op, d.Get("zone").(string), "Updating InstanceGroup") + if err != nil { + return err + } + d.SetPartial("named_port") + } + + d.Partial(false) + + return resourceComputeInstanceGroupRead(d, meta) +} + +func resourceComputeInstanceGroupDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + zone := d.Get("zone").(string) + op, err := config.clientCompute.InstanceGroups.Delete(config.Project, zone, d.Id()).Do() + if err != nil { + return fmt.Errorf("Error deleting InstanceGroup: %s", err) + } + + err = computeOperationWaitZone(config, op, zone, "Deleting InstanceGroup") + if err != nil { + return err + } + + d.SetId("") + return nil +} diff --git a/resource_compute_instance_group_test.go b/resource_compute_instance_group_test.go new file mode 100644 index 00000000..320d308c --- /dev/null +++ b/resource_compute_instance_group_test.go @@ -0,0 +1,299 @@ +package google + +import ( + "fmt" + "testing" + + "google.golang.org/api/compute/v1" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccComputeInstanceGroup_basic(t *testing.T) { + var instanceGroup compute.InstanceGroup + var instanceName = fmt.Sprintf("instancegroup-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccComputeInstanceGroup_destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstanceGroup_basic(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccComputeInstanceGroup_exists( + "google_compute_instance_group.basic", &instanceGroup), + testAccComputeInstanceGroup_exists( + "google_compute_instance_group.empty", &instanceGroup), + ), + }, + }, + }) +} + +func TestAccComputeInstanceGroup_update(t *testing.T) { + var instanceGroup compute.InstanceGroup + var instanceName = fmt.Sprintf("instancegroup-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccComputeInstanceGroup_destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstanceGroup_update(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccComputeInstanceGroup_exists( + "google_compute_instance_group.update", &instanceGroup), + testAccComputeInstanceGroup_named_ports( + "google_compute_instance_group.update", + map[string]int64{"http": 8080, "https": 8443}, + &instanceGroup), + ), + }, + resource.TestStep{ + Config: testAccComputeInstanceGroup_update2(instanceName), + Check: resource.ComposeTestCheckFunc( + testAccComputeInstanceGroup_exists( + "google_compute_instance_group.update", &instanceGroup), + testAccComputeInstanceGroup_updated( + "google_compute_instance_group.update", 3, &instanceGroup), + testAccComputeInstanceGroup_named_ports( + "google_compute_instance_group.update", + map[string]int64{"http": 8081, "test": 8444}, + &instanceGroup), + ), + }, + }, + }) +} + +func testAccComputeInstanceGroup_destroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_compute_instance_group" { + continue + } + _, err := config.clientCompute.InstanceGroups.Get( + config.Project, rs.Primary.Attributes["zone"], rs.Primary.ID).Do() + if err == nil { + return fmt.Errorf("InstanceGroup still exists") + } + } + + return nil +} + +func testAccComputeInstanceGroup_exists(n string, instanceGroup *compute.InstanceGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + + found, err := config.clientCompute.InstanceGroups.Get( + config.Project, rs.Primary.Attributes["zone"], rs.Primary.ID).Do() + if err != nil { + return err + } + + if found.Name != rs.Primary.ID { + return fmt.Errorf("InstanceGroup not found") + } + + *instanceGroup = *found + + return nil + } +} + +func testAccComputeInstanceGroup_updated(n string, size int64, instanceGroup *compute.InstanceGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + + instanceGroup, err := config.clientCompute.InstanceGroups.Get( + config.Project, rs.Primary.Attributes["zone"], rs.Primary.ID).Do() + if err != nil { + return err + } + + // Cannot check the target pool as the instance creation is asynchronous. However, can + // check the target_size. + if instanceGroup.Size != size { + return fmt.Errorf("instance count incorrect") + } + + return nil + } +} + +func testAccComputeInstanceGroup_named_ports(n string, np map[string]int64, instanceGroup *compute.InstanceGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + + instanceGroup, err := config.clientCompute.InstanceGroups.Get( + config.Project, rs.Primary.Attributes["zone"], rs.Primary.ID).Do() + if err != nil { + return err + } + + var found bool + for _, namedPort := range instanceGroup.NamedPorts { + found = false + for name, port := range np { + if namedPort.Name == name && namedPort.Port == port { + found = true + } + } + if !found { + return fmt.Errorf("named port incorrect") + } + } + + return nil + } +} + +func testAccComputeInstanceGroup_basic(instance string) string { + return fmt.Sprintf(` + resource "google_compute_instance" "ig_instance" { + name = "%s" + machine_type = "n1-standard-1" + can_ip_forward = false + zone = "us-central1-c" + + disk { + image = "debian-7-wheezy-v20140814" + } + + network_interface { + network = "default" + } + } + + resource "google_compute_instance_group" "basic" { + description = "Terraform test instance group" + name = "%s" + zone = "us-central1-c" + instances = [ "${google_compute_instance.ig_instance.self_link}" ] + named_port { + name = "http" + port = "8080" + } + named_port { + name = "https" + port = "8443" + } + } + + resource "google_compute_instance_group" "empty" { + description = "Terraform test instance group empty" + name = "%s-empty" + zone = "us-central1-c" + named_port { + name = "http" + port = "8080" + } + named_port { + name = "https" + port = "8443" + } + }`, instance, instance, instance) +} + +func testAccComputeInstanceGroup_update(instance string) string { + return fmt.Sprintf(` + resource "google_compute_instance" "ig_instance" { + name = "%s-${count.index}" + machine_type = "n1-standard-1" + can_ip_forward = false + zone = "us-central1-c" + count = 1 + + disk { + image = "debian-7-wheezy-v20140814" + } + + network_interface { + network = "default" + } + } + + resource "google_compute_instance_group" "update" { + description = "Terraform test instance group" + name = "%s" + zone = "us-central1-c" + instances = [ "${google_compute_instance.ig_instance.self_link}" ] + named_port { + name = "http" + port = "8080" + } + named_port { + name = "https" + port = "8443" + } + }`, instance, instance) +} + +// Change IGM's instance template and target size +func testAccComputeInstanceGroup_update2(instance string) string { + return fmt.Sprintf(` + resource "google_compute_instance" "ig_instance" { + name = "%s-${count.index}" + machine_type = "n1-standard-1" + can_ip_forward = false + zone = "us-central1-c" + count = 3 + + disk { + image = "debian-7-wheezy-v20140814" + } + + network_interface { + network = "default" + } + } + + resource "google_compute_instance_group" "update" { + description = "Terraform test instance group" + name = "%s" + zone = "us-central1-c" + instances = [ "${google_compute_instance.ig_instance.*.self_link}" ] + + named_port { + name = "http" + port = "8081" + } + named_port { + name = "test" + port = "8444" + } + }`, instance, instance) +}