From d574c6a910920c98f02c33f983494a83a0e02d88 Mon Sep 17 00:00:00 2001 From: Scott Stevenson <26748905+usererror@users.noreply.github.com> Date: Mon, 9 Oct 2017 13:59:38 -0400 Subject: [PATCH] Add resource_compute_region_autoscaler (#544) * Add resource_compute_region_autoscaler * Add import acceptance tests, reuse zonal autoscaler code * Enforce single autoscaling policy requirement at terraform plan time --- .../import_compute_region_autoscaler_test.go | 36 +++ google/provider.go | 1 + google/resource_compute_autoscaler.go | 159 +++++------ google/resource_compute_region_autoscaler.go | 189 +++++++++++++ ...resource_compute_region_autoscaler_test.go | 262 ++++++++++++++++++ 5 files changed, 565 insertions(+), 82 deletions(-) create mode 100644 google/import_compute_region_autoscaler_test.go create mode 100644 google/resource_compute_region_autoscaler.go create mode 100644 google/resource_compute_region_autoscaler_test.go diff --git a/google/import_compute_region_autoscaler_test.go b/google/import_compute_region_autoscaler_test.go new file mode 100644 index 00000000..bd3c896b --- /dev/null +++ b/google/import_compute_region_autoscaler_test.go @@ -0,0 +1,36 @@ +package google + +import ( + "testing" + + "fmt" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccComputeRegionAutoscaler_importBasic(t *testing.T) { + resourceName := "google_compute_region_autoscaler.foobar" + + var it_name = fmt.Sprintf("region-autoscaler-test-%s", acctest.RandString(10)) + var tp_name = fmt.Sprintf("region-autoscaler-test-%s", acctest.RandString(10)) + var igm_name = fmt.Sprintf("region-autoscaler-test-%s", acctest.RandString(10)) + var autoscaler_name = fmt.Sprintf("region-autoscaler-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeRegionAutoscalerDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeRegionAutoscaler_basic(it_name, tp_name, igm_name, autoscaler_name), + }, + + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/google/provider.go b/google/provider.go index bec82472..066b2617 100644 --- a/google/provider.go +++ b/google/provider.go @@ -87,6 +87,7 @@ func Provider() terraform.ResourceProvider { "google_compute_network_peering": resourceComputeNetworkPeering(), "google_compute_project_metadata": resourceComputeProjectMetadata(), "google_compute_project_metadata_item": resourceComputeProjectMetadataItem(), + "google_compute_region_autoscaler": resourceComputeRegionAutoscaler(), "google_compute_region_backend_service": resourceComputeRegionBackendService(), "google_compute_region_instance_group_manager": resourceComputeRegionInstanceGroupManager(), "google_compute_route": resourceComputeRoute(), diff --git a/google/resource_compute_autoscaler.go b/google/resource_compute_autoscaler.go index d4cf49d6..71844572 100644 --- a/google/resource_compute_autoscaler.go +++ b/google/resource_compute_autoscaler.go @@ -3,12 +3,85 @@ package google import ( "fmt" "log" - "strings" + + compute "google.golang.org/api/compute/v1" "github.com/hashicorp/terraform/helper/schema" - "google.golang.org/api/compute/v1" ) +var autoscalingPolicy *schema.Schema = &schema.Schema{ + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "min_replicas": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + + "max_replicas": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + + "cooldown_period": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 60, + }, + + "cpu_utilization": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "target": &schema.Schema{ + Type: schema.TypeFloat, + Required: true, + }, + }, + }, + }, + + "metric": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "target": &schema.Schema{ + Type: schema.TypeFloat, + Required: true, + }, + + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + + "load_balancing_utilization": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "target": &schema.Schema{ + Type: schema.TypeFloat, + Required: true, + }, + }, + }, + }, + }, + }, +} + func resourceComputeAutoscaler() *schema.Resource { return &schema.Resource{ Create: resourceComputeAutoscalerCreate, @@ -37,77 +110,7 @@ func resourceComputeAutoscaler() *schema.Resource { ForceNew: true, }, - "autoscaling_policy": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "min_replicas": &schema.Schema{ - Type: schema.TypeInt, - Required: true, - }, - - "max_replicas": &schema.Schema{ - Type: schema.TypeInt, - Required: true, - }, - - "cooldown_period": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - Default: 60, - }, - - "cpu_utilization": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "target": &schema.Schema{ - Type: schema.TypeFloat, - Required: true, - }, - }, - }, - }, - - "metric": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - "target": &schema.Schema{ - Type: schema.TypeFloat, - Required: true, - }, - - "type": &schema.Schema{ - Type: schema.TypeString, - Required: true, - }, - }, - }, - }, - - "load_balancing_utilization": &schema.Schema{ - Type: schema.TypeList, - Optional: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "target": &schema.Schema{ - Type: schema.TypeFloat, - Required: true, - }, - }, - }, - }, - }, - }, - }, + "autoscaling_policy": autoscalingPolicy, "description": &schema.Schema{ Type: schema.TypeString, @@ -140,11 +143,6 @@ func buildAutoscaler(d *schema.ResourceData) (*compute.Autoscaler, error) { scaler.Description = v.(string) } - aspCount := d.Get("autoscaling_policy.#").(int) - if aspCount != 1 { - return nil, fmt.Errorf("The autoscaler must have exactly one autoscaling_policy, found %d.", aspCount) - } - prefix := "autoscaling_policy.0." scaler.AutoscalingPolicy = &compute.AutoscalingPolicy{ @@ -154,7 +152,6 @@ func buildAutoscaler(d *schema.ResourceData) (*compute.Autoscaler, error) { } // Check that only one autoscaling policy is defined - policyCounter := 0 if _, ok := d.GetOk(prefix + "cpu_utilization"); ok { if d.Get(prefix+"cpu_utilization.0.target").(float64) != 0 { @@ -183,7 +180,6 @@ func buildAutoscaler(d *schema.ResourceData) (*compute.Autoscaler, error) { }, } } - } if _, ok := d.GetOk("autoscaling_policy.0.load_balancing_utilization"); ok { if d.Get(prefix+"load_balancing_utilization.0.target").(float64) != 0 { @@ -320,11 +316,10 @@ func resourceComputeAutoscalerRead(d *schema.ResourceData, meta interface{}) err return nil } - zoneUrl := strings.Split(scaler.Zone, "/") d.Set("self_link", scaler.SelfLink) d.Set("name", scaler.Name) d.Set("target", scaler.Target) - d.Set("zone", zoneUrl[len(zoneUrl)-1]) + d.Set("zone", GetResourceNameFromSelfLink(scaler.Zone)) d.Set("description", scaler.Description) if scaler.AutoscalingPolicy != nil { d.Set("autoscaling_policy", flattenAutoscalingPolicy(scaler.AutoscalingPolicy)) diff --git a/google/resource_compute_region_autoscaler.go b/google/resource_compute_region_autoscaler.go new file mode 100644 index 00000000..407f365a --- /dev/null +++ b/google/resource_compute_region_autoscaler.go @@ -0,0 +1,189 @@ +package google + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceComputeRegionAutoscaler() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeRegionAutoscalerCreate, + Read: resourceComputeRegionAutoscalerRead, + Update: resourceComputeRegionAutoscalerUpdate, + Delete: resourceComputeRegionAutoscalerDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + ForceNew: true, + Required: true, + }, + + "target": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "autoscaling_policy": autoscalingPolicy, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "project": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "self_link": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceComputeRegionAutoscalerCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + // Get the region + log.Printf("[DEBUG] Loading region: %s", d.Get("region").(string)) + region, err := config.clientCompute.Regions.Get( + project, d.Get("region").(string)).Do() + if err != nil { + return fmt.Errorf( + "Error loading region '%s': %s", d.Get("region").(string), err) + } + + scaler, err := buildAutoscaler(d) + if err != nil { + return err + } + + op, err := config.clientCompute.RegionAutoscalers.Insert( + project, region.Name, scaler).Do() + if err != nil { + return fmt.Errorf("Error creating Autoscaler: %s", err) + } + + // It probably maybe worked, so store the ID now + d.SetId(scaler.Name) + + err = computeOperationWait(config, op, project, "Creating Autoscaler") + if err != nil { + return err + } + + return resourceComputeRegionAutoscalerRead(d, meta) +} + +func resourceComputeRegionAutoscalerRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + region, err := getRegion(d, config) + if err != nil { + return err + } + + scaler, err := config.clientCompute.RegionAutoscalers.Get( + project, region, d.Id()).Do() + if err != nil { + return handleNotFoundError(err, d, fmt.Sprintf("Autoscaler %q", d.Id())) + } + + if scaler == nil { + log.Printf("[WARN] Removing Autoscaler %q because it's gone", d.Get("name").(string)) + d.SetId("") + return nil + } + + d.Set("self_link", scaler.SelfLink) + d.Set("name", scaler.Name) + d.Set("target", scaler.Target) + d.Set("region", GetResourceNameFromSelfLink(scaler.Region)) + d.Set("description", scaler.Description) + if scaler.AutoscalingPolicy != nil { + d.Set("autoscaling_policy", flattenAutoscalingPolicy(scaler.AutoscalingPolicy)) + } + + return nil +} + +func resourceComputeRegionAutoscalerUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + region := d.Get("region").(string) + + scaler, err := buildAutoscaler(d) + if err != nil { + return err + } + + op, err := config.clientCompute.RegionAutoscalers.Update( + project, region, scaler).Do() + if err != nil { + return fmt.Errorf("Error updating Autoscaler: %s", err) + } + + // It probably maybe worked, so store the ID now + d.SetId(scaler.Name) + + err = computeOperationWait(config, op, project, "Updating Autoscaler") + if err != nil { + return err + } + + return resourceComputeRegionAutoscalerRead(d, meta) +} + +func resourceComputeRegionAutoscalerDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + region := d.Get("region").(string) + op, err := config.clientCompute.RegionAutoscalers.Delete( + project, region, d.Id()).Do() + if err != nil { + return fmt.Errorf("Error deleting autoscaler: %s", err) + } + + err = computeOperationWait(config, op, project, "Deleting Autoscaler") + if err != nil { + return err + } + + d.SetId("") + return nil +} diff --git a/google/resource_compute_region_autoscaler_test.go b/google/resource_compute_region_autoscaler_test.go new file mode 100644 index 00000000..755d96a4 --- /dev/null +++ b/google/resource_compute_region_autoscaler_test.go @@ -0,0 +1,262 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/compute/v1" +) + +func TestAccComputeRegionAutoscaler_basic(t *testing.T) { + var ascaler compute.Autoscaler + + var it_name = fmt.Sprintf("region-autoscaler-test-%s", acctest.RandString(10)) + var tp_name = fmt.Sprintf("region-autoscaler-test-%s", acctest.RandString(10)) + var igm_name = fmt.Sprintf("region-autoscaler-test-%s", acctest.RandString(10)) + var autoscaler_name = fmt.Sprintf("region-autoscaler-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeRegionAutoscalerDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeRegionAutoscaler_basic(it_name, tp_name, igm_name, autoscaler_name), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeRegionAutoscalerExists( + "google_compute_region_autoscaler.foobar", &ascaler), + ), + }, + }, + }) +} + +func TestAccComputeRegionAutoscaler_update(t *testing.T) { + var ascaler compute.Autoscaler + + var it_name = fmt.Sprintf("region-autoscaler-test-%s", acctest.RandString(10)) + var tp_name = fmt.Sprintf("region-autoscaler-test-%s", acctest.RandString(10)) + var igm_name = fmt.Sprintf("region-autoscaler-test-%s", acctest.RandString(10)) + var autoscaler_name = fmt.Sprintf("region-autoscaler-test-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeRegionAutoscalerDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeRegionAutoscaler_basic(it_name, tp_name, igm_name, autoscaler_name), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeRegionAutoscalerExists( + "google_compute_region_autoscaler.foobar", &ascaler), + ), + }, + resource.TestStep{ + Config: testAccComputeRegionAutoscaler_update(it_name, tp_name, igm_name, autoscaler_name), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeRegionAutoscalerExists( + "google_compute_region_autoscaler.foobar", &ascaler), + testAccCheckComputeRegionAutoscalerUpdated( + "google_compute_region_autoscaler.foobar", 10), + ), + }, + }, + }) +} + +func testAccCheckComputeRegionAutoscalerDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_compute_region_autoscaler" { + continue + } + + _, err := config.clientCompute.RegionAutoscalers.Get( + config.Project, rs.Primary.Attributes["region"], rs.Primary.ID).Do() + if err == nil { + return fmt.Errorf("Autoscaler still exists") + } + } + + return nil +} + +func testAccCheckComputeRegionAutoscalerExists(n string, ascaler *compute.Autoscaler) 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.RegionAutoscalers.Get( + config.Project, rs.Primary.Attributes["region"], rs.Primary.ID).Do() + if err != nil { + return err + } + + if found.Name != rs.Primary.ID { + return fmt.Errorf("Autoscaler not found") + } + + *ascaler = *found + + return nil + } +} + +func testAccCheckComputeRegionAutoscalerUpdated(n string, max int64) 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) + + ascaler, err := config.clientCompute.RegionAutoscalers.Get( + config.Project, rs.Primary.Attributes["region"], rs.Primary.ID).Do() + if err != nil { + return err + } + + if ascaler.AutoscalingPolicy.MaxNumReplicas != max { + return fmt.Errorf("maximum replicas incorrect") + } + + return nil + } +} + +func testAccComputeRegionAutoscaler_basic(it_name, tp_name, igm_name, autoscaler_name string) string { + return fmt.Sprintf(` +resource "google_compute_instance_template" "foobar" { + name = "%s" + machine_type = "n1-standard-1" + can_ip_forward = false + tags = ["foo", "bar"] + + disk { + source_image = "debian-cloud/debian-8-jessie-v20160803" + auto_delete = true + boot = true + } + + network_interface { + network = "default" + } + + metadata { + foo = "bar" + } + + service_account { + scopes = ["userinfo-email", "compute-ro", "storage-ro"] + } +} + +resource "google_compute_target_pool" "foobar" { + description = "Resource created for Terraform acceptance testing" + name = "%s" + session_affinity = "CLIENT_IP_PROTO" +} + +resource "google_compute_region_instance_group_manager" "foobar" { + description = "Terraform test instance group manager" + name = "%s" + instance_template = "${google_compute_instance_template.foobar.self_link}" + target_pools = ["${google_compute_target_pool.foobar.self_link}"] + base_instance_name = "foobar" + region = "us-central1" +} + +resource "google_compute_region_autoscaler" "foobar" { + description = "Resource created for Terraform acceptance testing" + name = "%s" + region = "us-central1" + target = "${google_compute_region_instance_group_manager.foobar.self_link}" + autoscaling_policy = { + max_replicas = 5 + min_replicas = 1 + cooldown_period = 60 + cpu_utilization = { + target = 0.5 + } + } + +} +`, it_name, tp_name, igm_name, autoscaler_name) +} + +func testAccComputeRegionAutoscaler_update(it_name, tp_name, igm_name, autoscaler_name string) string { + return fmt.Sprintf(` +resource "google_compute_instance_template" "foobar" { + name = "%s" + machine_type = "n1-standard-1" + can_ip_forward = false + tags = ["foo", "bar"] + + disk { + source_image = "debian-cloud/debian-8-jessie-v20160803" + auto_delete = true + boot = true + } + + network_interface { + network = "default" + } + + metadata { + foo = "bar" + } + + service_account { + scopes = ["userinfo-email", "compute-ro", "storage-ro"] + } +} + +resource "google_compute_target_pool" "foobar" { + description = "Resource created for Terraform acceptance testing" + name = "%s" + session_affinity = "CLIENT_IP_PROTO" +} + +resource "google_compute_region_instance_group_manager" "foobar" { + description = "Terraform test instance group manager" + name = "%s" + instance_template = "${google_compute_instance_template.foobar.self_link}" + target_pools = ["${google_compute_target_pool.foobar.self_link}"] + base_instance_name = "foobar" + region = "us-central1" +} + +resource "google_compute_region_autoscaler" "foobar" { + description = "Resource created for Terraform acceptance testing" + name = "%s" + region = "us-central1" + target = "${google_compute_region_instance_group_manager.foobar.self_link}" + autoscaling_policy = { + max_replicas = 10 + min_replicas = 1 + cooldown_period = 60 + cpu_utilization = { + target = 0.5 + } + } + +} +`, it_name, tp_name, igm_name, autoscaler_name) +}