diff --git a/.changelog/9403.txt b/.changelog/9403.txt new file mode 100644 index 00000000000..480a0bc80ef --- /dev/null +++ b/.changelog/9403.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +spanner: added `autoscaling_config` field to `google_spanner_instance` +``` diff --git a/google/services/spanner/resource_spanner_instance.go b/google/services/spanner/resource_spanner_instance.go index 594c7c88be2..8b0f71f8fbb 100644 --- a/google/services/spanner/resource_spanner_instance.go +++ b/google/services/spanner/resource_spanner_instance.go @@ -145,6 +145,69 @@ in length. If not provided, a random string starting with 'tf-' will be selected.`, }, + "autoscaling_config": { + Type: schema.TypeList, + Optional: true, + Description: `The autoscaling configuration. Autoscaling is enabled if this field is set. +When autoscaling is enabled, num_nodes and processing_units are treated as, +OUTPUT_ONLY fields and reflect the current compute capacity allocated to +the instance.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "autoscaling_limits": { + Type: schema.TypeList, + Optional: true, + Description: `Defines scale in controls to reduce the risk of response latency +and outages due to abrupt scale-in events`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max_processing_units": { + Type: schema.TypeInt, + Optional: true, + Description: `Specifies maximum number of processing units allocated to the instance. +If set, this number should be multiples of 1000 and be greater than or equal to +min_processing_units.`, + }, + "min_processing_units": { + Type: schema.TypeInt, + Optional: true, + Description: `Specifies minimum number of processing units allocated to the instance. +If set, this number should be multiples of 1000.`, + }, + }, + }, + }, + "autoscaling_targets": { + Type: schema.TypeList, + Optional: true, + Description: `Defines scale in controls to reduce the risk of response latency +and outages due to abrupt scale-in events`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "high_priority_cpu_utilization_percent": { + Type: schema.TypeInt, + Optional: true, + Description: `Specifies the target high priority cpu utilization percentage that the autoscaler +should be trying to achieve for the instance. +This number is on a scale from 0 (no utilization) to 100 (full utilization)..`, + }, + "storage_utilization_percent": { + Type: schema.TypeInt, + Optional: true, + Description: `Specifies the target storage utilization percentage that the autoscaler +should be trying to achieve for the instance. +This number is on a scale from 0 (no utilization) to 100 (full utilization).`, + }, + }, + }, + }, + }, + }, + ExactlyOneOf: []string{"num_nodes", "processing_units", "autoscaling_config"}, + }, "labels": { Type: schema.TypeMap, Optional: true, @@ -162,7 +225,7 @@ Please refer to the field 'effective_labels' for all of the labels present on th Optional: true, Description: `The number of nodes allocated to this instance. Exactly one of either node_count or processing_units must be present in terraform.`, - ExactlyOneOf: []string{"num_nodes", "processing_units"}, + ExactlyOneOf: []string{"num_nodes", "processing_units", "autoscaling_config"}, }, "processing_units": { Type: schema.TypeInt, @@ -170,7 +233,7 @@ must be present in terraform.`, Optional: true, Description: `The number of processing units allocated to this instance. Exactly one of processing_units or node_count must be present in terraform.`, - ExactlyOneOf: []string{"num_nodes", "processing_units"}, + ExactlyOneOf: []string{"num_nodes", "processing_units", "autoscaling_config"}, }, "effective_labels": { Type: schema.TypeMap, @@ -246,6 +309,12 @@ func resourceSpannerInstanceCreate(d *schema.ResourceData, meta interface{}) err } else if v, ok := d.GetOkExists("processing_units"); !tpgresource.IsEmptyValue(reflect.ValueOf(processingUnitsProp)) && (ok || !reflect.DeepEqual(v, processingUnitsProp)) { obj["processingUnits"] = processingUnitsProp } + autoscalingConfigProp, err := expandSpannerInstanceAutoscalingConfig(d.Get("autoscaling_config"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("autoscaling_config"); !tpgresource.IsEmptyValue(reflect.ValueOf(autoscalingConfigProp)) && (ok || !reflect.DeepEqual(v, autoscalingConfigProp)) { + obj["autoscalingConfig"] = autoscalingConfigProp + } labelsProp, err := expandSpannerInstanceEffectiveLabels(d.Get("effective_labels"), d, config) if err != nil { return err @@ -418,6 +487,9 @@ func resourceSpannerInstanceRead(d *schema.ResourceData, meta interface{}) error if err := d.Set("state", flattenSpannerInstanceState(res["state"], d, config)); err != nil { return fmt.Errorf("Error reading Instance: %s", err) } + if err := d.Set("autoscaling_config", flattenSpannerInstanceAutoscalingConfig(res["autoscalingConfig"], d, config)); err != nil { + return fmt.Errorf("Error reading Instance: %s", err) + } if err := d.Set("terraform_labels", flattenSpannerInstanceTerraformLabels(res["labels"], d, config)); err != nil { return fmt.Errorf("Error reading Instance: %s", err) } @@ -462,6 +534,12 @@ func resourceSpannerInstanceUpdate(d *schema.ResourceData, meta interface{}) err } else if v, ok := d.GetOkExists("processing_units"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, processingUnitsProp)) { obj["processingUnits"] = processingUnitsProp } + autoscalingConfigProp, err := expandSpannerInstanceAutoscalingConfig(d.Get("autoscaling_config"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("autoscaling_config"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, autoscalingConfigProp)) { + obj["autoscalingConfig"] = autoscalingConfigProp + } labelsProp, err := expandSpannerInstanceEffectiveLabels(d.Get("effective_labels"), d, config) if err != nil { return err @@ -686,6 +764,119 @@ func flattenSpannerInstanceState(v interface{}, d *schema.ResourceData, config * return v } +func flattenSpannerInstanceAutoscalingConfig(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["autoscaling_limits"] = + flattenSpannerInstanceAutoscalingConfigAutoscalingLimits(original["autoscalingLimits"], d, config) + transformed["autoscaling_targets"] = + flattenSpannerInstanceAutoscalingConfigAutoscalingTargets(original["autoscalingTargets"], d, config) + return []interface{}{transformed} +} +func flattenSpannerInstanceAutoscalingConfigAutoscalingLimits(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["min_processing_units"] = + flattenSpannerInstanceAutoscalingConfigAutoscalingLimitsMinProcessingUnits(original["minProcessingUnits"], d, config) + transformed["max_processing_units"] = + flattenSpannerInstanceAutoscalingConfigAutoscalingLimitsMaxProcessingUnits(original["maxProcessingUnits"], d, config) + return []interface{}{transformed} +} +func flattenSpannerInstanceAutoscalingConfigAutoscalingLimitsMinProcessingUnits(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := tpgresource.StringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + +func flattenSpannerInstanceAutoscalingConfigAutoscalingLimitsMaxProcessingUnits(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := tpgresource.StringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + +func flattenSpannerInstanceAutoscalingConfigAutoscalingTargets(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["high_priority_cpu_utilization_percent"] = + flattenSpannerInstanceAutoscalingConfigAutoscalingTargetsHighPriorityCpuUtilizationPercent(original["highPriorityCpuUtilizationPercent"], d, config) + transformed["storage_utilization_percent"] = + flattenSpannerInstanceAutoscalingConfigAutoscalingTargetsStorageUtilizationPercent(original["storageUtilizationPercent"], d, config) + return []interface{}{transformed} +} +func flattenSpannerInstanceAutoscalingConfigAutoscalingTargetsHighPriorityCpuUtilizationPercent(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := tpgresource.StringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + +func flattenSpannerInstanceAutoscalingConfigAutoscalingTargetsStorageUtilizationPercent(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := tpgresource.StringToFixed64(strVal); err == nil { + return intVal + } + } + + // number values are represented as float64 + if floatVal, ok := v.(float64); ok { + intVal := int(floatVal) + return intVal + } + + return v // let terraform core handle it otherwise +} + func flattenSpannerInstanceTerraformLabels(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} { if v == nil { return v @@ -735,6 +926,100 @@ func expandSpannerInstanceProcessingUnits(v interface{}, d tpgresource.Terraform return v, nil } +func expandSpannerInstanceAutoscalingConfig(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedAutoscalingLimits, err := expandSpannerInstanceAutoscalingConfigAutoscalingLimits(original["autoscaling_limits"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedAutoscalingLimits); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["autoscalingLimits"] = transformedAutoscalingLimits + } + + transformedAutoscalingTargets, err := expandSpannerInstanceAutoscalingConfigAutoscalingTargets(original["autoscaling_targets"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedAutoscalingTargets); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["autoscalingTargets"] = transformedAutoscalingTargets + } + + return transformed, nil +} + +func expandSpannerInstanceAutoscalingConfigAutoscalingLimits(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedMinProcessingUnits, err := expandSpannerInstanceAutoscalingConfigAutoscalingLimitsMinProcessingUnits(original["min_processing_units"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMinProcessingUnits); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["minProcessingUnits"] = transformedMinProcessingUnits + } + + transformedMaxProcessingUnits, err := expandSpannerInstanceAutoscalingConfigAutoscalingLimitsMaxProcessingUnits(original["max_processing_units"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMaxProcessingUnits); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["maxProcessingUnits"] = transformedMaxProcessingUnits + } + + return transformed, nil +} + +func expandSpannerInstanceAutoscalingConfigAutoscalingLimitsMinProcessingUnits(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandSpannerInstanceAutoscalingConfigAutoscalingLimitsMaxProcessingUnits(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandSpannerInstanceAutoscalingConfigAutoscalingTargets(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + l := v.([]interface{}) + if len(l) == 0 || l[0] == nil { + return nil, nil + } + raw := l[0] + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + + transformedHighPriorityCpuUtilizationPercent, err := expandSpannerInstanceAutoscalingConfigAutoscalingTargetsHighPriorityCpuUtilizationPercent(original["high_priority_cpu_utilization_percent"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedHighPriorityCpuUtilizationPercent); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["highPriorityCpuUtilizationPercent"] = transformedHighPriorityCpuUtilizationPercent + } + + transformedStorageUtilizationPercent, err := expandSpannerInstanceAutoscalingConfigAutoscalingTargetsStorageUtilizationPercent(original["storage_utilization_percent"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedStorageUtilizationPercent); val.IsValid() && !tpgresource.IsEmptyValue(val) { + transformed["storageUtilizationPercent"] = transformedStorageUtilizationPercent + } + + return transformed, nil +} + +func expandSpannerInstanceAutoscalingConfigAutoscalingTargetsHighPriorityCpuUtilizationPercent(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + +func expandSpannerInstanceAutoscalingConfigAutoscalingTargetsStorageUtilizationPercent(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + return v, nil +} + func expandSpannerInstanceEffectiveLabels(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (map[string]string, error) { if v == nil { return map[string]string{}, nil @@ -747,8 +1032,8 @@ func expandSpannerInstanceEffectiveLabels(v interface{}, d tpgresource.Terraform } func resourceSpannerInstanceEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) { - // Temp Logic to accommodate processing_units and num_nodes - if obj["processingUnits"] == nil && obj["nodeCount"] == nil { + // Temp Logic to accommodate autoscaling_config, processing_units and num_nodes + if obj["processingUnits"] == nil && obj["nodeCount"] == nil && obj["autoscalingConfig"] == nil { obj["nodeCount"] = 1 } newObj := make(map[string]interface{}) @@ -786,6 +1071,18 @@ func resourceSpannerInstanceUpdateEncoder(d *schema.ResourceData, meta interface if d.HasChange("processing_units") { updateMask = append(updateMask, "processingUnits") } + if d.HasChange("autoscaling_config.0.autoscaling_limits.0.max_processing_units") { + updateMask = append(updateMask, "autoscalingConfig.autoscalingLimits.maxProcessingUnits") + } + if d.HasChange("autoscaling_config.0.autoscaling_limits.0.min_processing_units") { + updateMask = append(updateMask, "autoscalingConfig.autoscalingLimits.minProcessingUnits") + } + if d.HasChange("autoscaling_config.0.autoscaling_targets.0.high_priority_cpu_utilization_percent") { + updateMask = append(updateMask, "autoscalingConfig.autoscalingTargets.highPriorityCpuUtilizationPercent") + } + if d.HasChange("autoscaling_config.0.autoscaling_targets.0.storage_utilization_percent") { + updateMask = append(updateMask, "autoscalingConfig.autoscalingTargets.storageUtilizationPercent") + } newObj["fieldMask"] = strings.Join(updateMask, ",") return newObj, nil } diff --git a/google/services/spanner/resource_spanner_instance_test.go b/google/services/spanner/resource_spanner_instance_test.go index 436f1b2abfd..f4ede839bb8 100644 --- a/google/services/spanner/resource_spanner_instance_test.go +++ b/google/services/spanner/resource_spanner_instance_test.go @@ -49,7 +49,7 @@ func TestAccSpannerInstance_noNodeCountSpecified(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccSpannerInstance_noNodeCountSpecified(idName), - ExpectError: regexp.MustCompile("one of `num_nodes,processing_units` must be specified"), + ExpectError: regexp.MustCompile(".*one of `autoscaling_config,num_nodes,processing_units`\nmust be specified.*"), }, }, }) @@ -144,6 +144,67 @@ func TestAccSpannerInstance_virtualUpdate(t *testing.T) { }) } +func TestAccSpannerInstance_basicWithAutoscalingUsingProcessingUnitConfig(t *testing.T) { + t.Parallel() + + displayName := fmt.Sprintf("spanner-test-%s-dname", acctest.RandString(t, 10)) + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckSpannerInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccSpannerInstance_basicWithAutoscalerConfigUsingProcessingUnitsAsConfigs(displayName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("google_spanner_instance.basic", "state"), + ), + }, + { + ResourceName: "google_spanner_instance.basic", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccSpannerInstance_basicWithAutoscalingUsingProcessingUnitConfigUpdate(t *testing.T) { + t.Parallel() + + displayName := fmt.Sprintf("spanner-test-%s-dname", acctest.RandString(t, 10)) + acctest.VcrTest(t, resource.TestCase{ + PreCheck: func() { acctest.AccTestPreCheck(t) }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckSpannerInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccSpannerInstance_basicWithAutoscalerConfigUsingProcessingUnitsAsConfigsUpdate(displayName, 1000, 2000, 65, 95), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("google_spanner_instance.basic", "state"), + ), + }, + { + ResourceName: "google_spanner_instance.basic", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"labels", "terraform_labels"}, + }, + { + Config: testAccSpannerInstance_basicWithAutoscalerConfigUsingProcessingUnitsAsConfigsUpdate(displayName, 2000, 3000, 75, 90), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("google_spanner_instance.basic", "state"), + ), + }, + { + ResourceName: "google_spanner_instance.basic", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"labels", "terraform_labels"}, + }, + }, + }) +} + func testAccSpannerInstance_basic(name string) string { return fmt.Sprintf(` resource "google_spanner_instance" "basic" { @@ -205,3 +266,43 @@ resource "google_spanner_instance" "basic" { } `, name, name, virtual) } + +func testAccSpannerInstance_basicWithAutoscalerConfigUsingProcessingUnitsAsConfigs(name string) string { + return fmt.Sprintf(` +resource "google_spanner_instance" "basic" { + name = "%s" + config = "regional-us-central1" + display_name = "%s" + autoscaling_config { + autoscaling_limits { + max_processing_units = 2000 + min_processing_units = 1000 + } + autoscaling_targets { + high_priority_cpu_utilization_percent = 65 + storage_utilization_percent = 95 + } + } +} +`, name, name) +} + +func testAccSpannerInstance_basicWithAutoscalerConfigUsingProcessingUnitsAsConfigsUpdate(name string, minProcessingUnits, maxProcessingUnits, cupUtilizationPercent, storageUtilizationPercent int) string { + return fmt.Sprintf(` +resource "google_spanner_instance" "basic" { + name = "%s" + config = "regional-us-central1" + display_name = "%s" + autoscaling_config { + autoscaling_limits { + max_processing_units = %v + min_processing_units = %v + } + autoscaling_targets { + high_priority_cpu_utilization_percent = %v + storage_utilization_percent = %v + } + } +} +`, name, name, maxProcessingUnits, minProcessingUnits, cupUtilizationPercent, storageUtilizationPercent) +} diff --git a/website/docs/r/spanner_instance.html.markdown b/website/docs/r/spanner_instance.html.markdown index af68ecd8bec..deb02d352a0 100644 --- a/website/docs/r/spanner_instance.html.markdown +++ b/website/docs/r/spanner_instance.html.markdown @@ -134,6 +134,14 @@ The following arguments are supported: **Note**: This field is non-authoritative, and will only manage the labels present in your configuration. Please refer to the field `effective_labels` for all of the labels present on the resource. +* `autoscaling_config` - + (Optional) + The autoscaling configuration. Autoscaling is enabled if this field is set. + When autoscaling is enabled, num_nodes and processing_units are treated as, + OUTPUT_ONLY fields and reflect the current compute capacity allocated to + the instance. + Structure is [documented below](#nested_autoscaling_config). + * `project` - (Optional) The ID of the project in which the resource belongs. If it is not provided, the provider project is used. @@ -141,6 +149,48 @@ The following arguments are supported: This must be set to true if you created a backup manually in the console. +The `autoscaling_config` block supports: + +* `autoscaling_limits` - + (Optional) + Defines scale in controls to reduce the risk of response latency + and outages due to abrupt scale-in events + Structure is [documented below](#nested_autoscaling_limits). + +* `autoscaling_targets` - + (Optional) + Defines scale in controls to reduce the risk of response latency + and outages due to abrupt scale-in events + Structure is [documented below](#nested_autoscaling_targets). + + +The `autoscaling_limits` block supports: + +* `min_processing_units` - + (Optional) + Specifies minimum number of processing units allocated to the instance. + If set, this number should be multiples of 1000. + +* `max_processing_units` - + (Optional) + Specifies maximum number of processing units allocated to the instance. + If set, this number should be multiples of 1000 and be greater than or equal to + min_processing_units. + +The `autoscaling_targets` block supports: + +* `high_priority_cpu_utilization_percent` - + (Optional) + Specifies the target high priority cpu utilization percentage that the autoscaler + should be trying to achieve for the instance. + This number is on a scale from 0 (no utilization) to 100 (full utilization).. + +* `storage_utilization_percent` - + (Optional) + Specifies the target storage utilization percentage that the autoscaler + should be trying to achieve for the instance. + This number is on a scale from 0 (no utilization) to 100 (full utilization). + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are exported: