diff --git a/.changelog/3373.txt b/.changelog/3373.txt new file mode 100644 index 00000000000..77999fe14e6 --- /dev/null +++ b/.changelog/3373.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +appengine: Added `automatic_scaling` `basic_scaling` and `manual_scaling` to `google_app_engine_standard_app_version` +``` diff --git a/google/resource_app_engine_flexible_app_version.go b/google/resource_app_engine_flexible_app_version.go index d2708d6f130..5bff3a277fb 100644 --- a/google/resource_app_engine_flexible_app_version.go +++ b/google/resource_app_engine_flexible_app_version.go @@ -552,9 +552,12 @@ Defaults to F1 for AutomaticScaling and B1 for ManualScaling.`, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "instances": { - Type: schema.TypeInt, - Required: true, - Description: `Number of instances to assign to the service at the start. This number can later be altered by using the Modules API set_num_instances() function.`, + Type: schema.TypeInt, + Required: true, + Description: `Number of instances to assign to the service at the start. + +**Note:** When managing the number of instances at runtime through the App Engine Admin API or the (now deprecated) Python 2 +Modules API set_num_instances() you must use 'lifecycle.ignore_changes = ["manual_scaling"[0].instances]' to prevent drift detection.`, }, }, }, diff --git a/google/resource_app_engine_standard_app_version.go b/google/resource_app_engine_standard_app_version.go index 8a9e351ba8b..ce731de2ee4 100644 --- a/google/resource_app_engine_standard_app_version.go +++ b/google/resource_app_engine_standard_app_version.go @@ -18,6 +18,7 @@ import ( "fmt" "log" "reflect" + "strconv" "time" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" @@ -42,14 +43,9 @@ func resourceAppEngineStandardAppVersion() *schema.Resource { }, Schema: map[string]*schema.Schema{ - "runtime": { - Type: schema.TypeString, - Required: true, - Description: `Desired runtime. Example python27.`, - }, "deployment": { Type: schema.TypeList, - Optional: true, + Required: true, Description: `Code and application artifacts that make up this version.`, MaxItems: 1, Elem: &schema.Resource{ @@ -103,6 +99,104 @@ All files must be readable using the credentials supplied with this call.`, }, }, }, + "runtime": { + Type: schema.TypeString, + Required: true, + Description: `Desired runtime. Example python27.`, + }, + "automatic_scaling": { + Type: schema.TypeList, + Optional: true, + Description: `Automatic scaling is based on request rate, response latencies, and other application metrics.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max_concurrent_requests": { + Type: schema.TypeInt, + Optional: true, + Description: `Number of concurrent requests an automatic scaling instance can accept before the scheduler spawns a new instance. + +Defaults to a runtime-specific value.`, + }, + "max_idle_instances": { + Type: schema.TypeInt, + Optional: true, + Description: `Maximum number of idle instances that should be maintained for this version.`, + }, + "max_pending_latency": { + Type: schema.TypeString, + Optional: true, + Description: `Maximum amount of time that a request should wait in the pending queue before starting a new instance to handle it. +A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s".`, + }, + "min_idle_instances": { + Type: schema.TypeInt, + Optional: true, + Description: `Minimum number of idle instances that should be maintained for this version. Only applicable for the default version of a service.`, + }, + "min_pending_latency": { + Type: schema.TypeString, + Optional: true, + Description: `Minimum amount of time a request should wait in the pending queue before starting a new instance to handle it. +A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s".`, + }, + "standard_scheduler_settings": { + Type: schema.TypeList, + Optional: true, + Description: `Scheduler settings for standard environment.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max_instances": { + Type: schema.TypeInt, + Optional: true, + Description: `Maximum number of instances to run for this version. Set to zero to disable maxInstances configuration.`, + }, + "min_instances": { + Type: schema.TypeInt, + Optional: true, + Description: `Minimum number of instances to run for this version. Set to zero to disable minInstances configuration.`, + }, + "target_cpu_utilization": { + Type: schema.TypeFloat, + Optional: true, + Description: `Target CPU utilization ratio to maintain when scaling. Should be a value in the range [0.50, 0.95], zero, or a negative value.`, + }, + "target_throughput_utilization": { + Type: schema.TypeFloat, + Optional: true, + Description: `Target throughput utilization ratio to maintain when scaling. Should be a value in the range [0.50, 0.95], zero, or a negative value.`, + }, + }, + }, + }, + }, + }, + ConflictsWith: []string{"basic_scaling", "manual_scaling"}, + }, + "basic_scaling": { + Type: schema.TypeList, + Optional: true, + Description: `Basic scaling creates instances when your application receives requests. Each instance will be shut down when the application becomes idle. Basic scaling is ideal for work that is intermittent or driven by user activity.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max_instances": { + Type: schema.TypeInt, + Required: true, + Description: `Maximum number of instances to create for this version. Must be in the range [1.0, 200.0].`, + }, + "idle_timeout": { + Type: schema.TypeString, + Optional: true, + Description: `Duration of time after the last request that an instance must wait before the instance is shut down. +A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". Defaults to 900s.`, + Default: "900s", + }, + }, + }, + ConflictsWith: []string{"automatic_scaling", "manual_scaling"}, + }, "entrypoint": { Type: schema.TypeList, Optional: true, @@ -231,10 +325,12 @@ All URLs that begin with this prefix are handled by this handler, using the port }, "instance_class": { Type: schema.TypeString, + Computed: true, Optional: true, Description: `Instance class that is used to run this version. Valid values are -AutomaticScaling F1, F2, F4, F4_1G -(Only AutomaticScaling is supported at the moment)`, +AutomaticScaling: F1, F2, F4, F4_1G +BasicScaling or ManualScaling: B1, B2, B4, B4_1G, B8 +Defaults to F1 for AutomaticScaling and B2 for ManualScaling and BasicScaling. If no scaling is specified, AutomaticScaling is chosen.`, }, "libraries": { Type: schema.TypeList, @@ -255,6 +351,25 @@ AutomaticScaling F1, F2, F4, F4_1G }, }, }, + "manual_scaling": { + Type: schema.TypeList, + Optional: true, + Description: `A service with manual scaling runs continuously, allowing you to perform complex initialization and rely on the state of its memory over time.`, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "instances": { + Type: schema.TypeInt, + Required: true, + Description: `Number of instances to assign to the service at the start. + +**Note:** When managing the number of instances at runtime through the App Engine Admin API or the (now deprecated) Python 2 +Modules API set_num_instances() you must use 'lifecycle.ignore_changes = ["manual_scaling"[0].instances]' to prevent drift detection.`, + }, + }, + }, + ConflictsWith: []string{"automatic_scaling", "basic_scaling"}, + }, "runtime_api_version": { Type: schema.TypeString, Optional: true, @@ -367,6 +482,24 @@ func resourceAppEngineStandardAppVersionCreate(d *schema.ResourceData, meta inte } else if v, ok := d.GetOkExists("instance_class"); !isEmptyValue(reflect.ValueOf(instanceClassProp)) && (ok || !reflect.DeepEqual(v, instanceClassProp)) { obj["instanceClass"] = instanceClassProp } + automaticScalingProp, err := expandAppEngineStandardAppVersionAutomaticScaling(d.Get("automatic_scaling"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("automatic_scaling"); !isEmptyValue(reflect.ValueOf(automaticScalingProp)) && (ok || !reflect.DeepEqual(v, automaticScalingProp)) { + obj["automaticScaling"] = automaticScalingProp + } + basicScalingProp, err := expandAppEngineStandardAppVersionBasicScaling(d.Get("basic_scaling"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("basic_scaling"); !isEmptyValue(reflect.ValueOf(basicScalingProp)) && (ok || !reflect.DeepEqual(v, basicScalingProp)) { + obj["basicScaling"] = basicScalingProp + } + manualScalingProp, err := expandAppEngineStandardAppVersionManualScaling(d.Get("manual_scaling"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("manual_scaling"); !isEmptyValue(reflect.ValueOf(manualScalingProp)) && (ok || !reflect.DeepEqual(v, manualScalingProp)) { + obj["manualScaling"] = manualScalingProp + } lockName, err := replaceVars(d, config, "apps/{{project}}") if err != nil { @@ -458,6 +591,18 @@ func resourceAppEngineStandardAppVersionRead(d *schema.ResourceData, meta interf if err := d.Set("libraries", flattenAppEngineStandardAppVersionLibraries(res["libraries"], d, config)); err != nil { return fmt.Errorf("Error reading StandardAppVersion: %s", err) } + if err := d.Set("instance_class", flattenAppEngineStandardAppVersionInstanceClass(res["instanceClass"], d, config)); err != nil { + return fmt.Errorf("Error reading StandardAppVersion: %s", err) + } + if err := d.Set("automatic_scaling", flattenAppEngineStandardAppVersionAutomaticScaling(res["automaticScaling"], d, config)); err != nil { + return fmt.Errorf("Error reading StandardAppVersion: %s", err) + } + if err := d.Set("basic_scaling", flattenAppEngineStandardAppVersionBasicScaling(res["basicScaling"], d, config)); err != nil { + return fmt.Errorf("Error reading StandardAppVersion: %s", err) + } + if err := d.Set("manual_scaling", flattenAppEngineStandardAppVersionManualScaling(res["manualScaling"], d, config)); err != nil { + return fmt.Errorf("Error reading StandardAppVersion: %s", err) + } return nil } @@ -531,6 +676,24 @@ func resourceAppEngineStandardAppVersionUpdate(d *schema.ResourceData, meta inte } else if v, ok := d.GetOkExists("instance_class"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, instanceClassProp)) { obj["instanceClass"] = instanceClassProp } + automaticScalingProp, err := expandAppEngineStandardAppVersionAutomaticScaling(d.Get("automatic_scaling"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("automatic_scaling"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, automaticScalingProp)) { + obj["automaticScaling"] = automaticScalingProp + } + basicScalingProp, err := expandAppEngineStandardAppVersionBasicScaling(d.Get("basic_scaling"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("basic_scaling"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, basicScalingProp)) { + obj["basicScaling"] = basicScalingProp + } + manualScalingProp, err := expandAppEngineStandardAppVersionManualScaling(d.Get("manual_scaling"), d, config) + if err != nil { + return err + } else if v, ok := d.GetOkExists("manual_scaling"); !isEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, manualScalingProp)) { + obj["manualScaling"] = manualScalingProp + } lockName, err := replaceVars(d, config, "apps/{{project}}") if err != nil { @@ -807,6 +970,219 @@ func flattenAppEngineStandardAppVersionLibrariesVersion(v interface{}, d *schema return v } +func flattenAppEngineStandardAppVersionInstanceClass(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenAppEngineStandardAppVersionAutomaticScaling(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["max_concurrent_requests"] = + flattenAppEngineStandardAppVersionAutomaticScalingMaxConcurrentRequests(original["maxConcurrentRequests"], d, config) + transformed["max_idle_instances"] = + flattenAppEngineStandardAppVersionAutomaticScalingMaxIdleInstances(original["maxIdleInstances"], d, config) + transformed["max_pending_latency"] = + flattenAppEngineStandardAppVersionAutomaticScalingMaxPendingLatency(original["maxPendingLatency"], d, config) + transformed["min_idle_instances"] = + flattenAppEngineStandardAppVersionAutomaticScalingMinIdleInstances(original["minIdleInstances"], d, config) + transformed["min_pending_latency"] = + flattenAppEngineStandardAppVersionAutomaticScalingMinPendingLatency(original["minPendingLatency"], d, config) + transformed["standard_scheduler_settings"] = + flattenAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettings(original["standardSchedulerSettings"], d, config) + return []interface{}{transformed} +} +func flattenAppEngineStandardAppVersionAutomaticScalingMaxConcurrentRequests(v interface{}, d *schema.ResourceData, config *Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := strconv.ParseInt(strVal, 10, 64); 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 flattenAppEngineStandardAppVersionAutomaticScalingMaxIdleInstances(v interface{}, d *schema.ResourceData, config *Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := strconv.ParseInt(strVal, 10, 64); 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 flattenAppEngineStandardAppVersionAutomaticScalingMaxPendingLatency(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenAppEngineStandardAppVersionAutomaticScalingMinIdleInstances(v interface{}, d *schema.ResourceData, config *Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := strconv.ParseInt(strVal, 10, 64); 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 flattenAppEngineStandardAppVersionAutomaticScalingMinPendingLatency(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettings(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["target_cpu_utilization"] = + flattenAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsTargetCpuUtilization(original["targetCpuUtilization"], d, config) + transformed["target_throughput_utilization"] = + flattenAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsTargetThroughputUtilization(original["targetThroughputUtilization"], d, config) + transformed["min_instances"] = + flattenAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsMinInstances(original["minInstances"], d, config) + transformed["max_instances"] = + flattenAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsMaxInstances(original["maxInstances"], d, config) + return []interface{}{transformed} +} +func flattenAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsTargetCpuUtilization(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsTargetThroughputUtilization(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsMinInstances(v interface{}, d *schema.ResourceData, config *Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := strconv.ParseInt(strVal, 10, 64); 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 flattenAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsMaxInstances(v interface{}, d *schema.ResourceData, config *Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := strconv.ParseInt(strVal, 10, 64); 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 flattenAppEngineStandardAppVersionBasicScaling(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["idle_timeout"] = + flattenAppEngineStandardAppVersionBasicScalingIdleTimeout(original["idleTimeout"], d, config) + transformed["max_instances"] = + flattenAppEngineStandardAppVersionBasicScalingMaxInstances(original["maxInstances"], d, config) + return []interface{}{transformed} +} +func flattenAppEngineStandardAppVersionBasicScalingIdleTimeout(v interface{}, d *schema.ResourceData, config *Config) interface{} { + return v +} + +func flattenAppEngineStandardAppVersionBasicScalingMaxInstances(v interface{}, d *schema.ResourceData, config *Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := strconv.ParseInt(strVal, 10, 64); 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 flattenAppEngineStandardAppVersionManualScaling(v interface{}, d *schema.ResourceData, config *Config) interface{} { + if v == nil { + return nil + } + original := v.(map[string]interface{}) + if len(original) == 0 { + return nil + } + transformed := make(map[string]interface{}) + transformed["instances"] = + flattenAppEngineStandardAppVersionManualScalingInstances(original["instances"], d, config) + return []interface{}{transformed} +} +func flattenAppEngineStandardAppVersionManualScalingInstances(v interface{}, d *schema.ResourceData, config *Config) interface{} { + // Handles the string fixed64 format + if strVal, ok := v.(string); ok { + if intVal, err := strconv.ParseInt(strVal, 10, 64); 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 expandAppEngineStandardAppVersionVersionId(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { return v, nil } @@ -1197,3 +1573,190 @@ func expandAppEngineStandardAppVersionEntrypointShell(v interface{}, d Terraform func expandAppEngineStandardAppVersionInstanceClass(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { return v, nil } + +func expandAppEngineStandardAppVersionAutomaticScaling(v interface{}, d TerraformResourceData, config *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{}) + + transformedMaxConcurrentRequests, err := expandAppEngineStandardAppVersionAutomaticScalingMaxConcurrentRequests(original["max_concurrent_requests"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMaxConcurrentRequests); val.IsValid() && !isEmptyValue(val) { + transformed["maxConcurrentRequests"] = transformedMaxConcurrentRequests + } + + transformedMaxIdleInstances, err := expandAppEngineStandardAppVersionAutomaticScalingMaxIdleInstances(original["max_idle_instances"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMaxIdleInstances); val.IsValid() && !isEmptyValue(val) { + transformed["maxIdleInstances"] = transformedMaxIdleInstances + } + + transformedMaxPendingLatency, err := expandAppEngineStandardAppVersionAutomaticScalingMaxPendingLatency(original["max_pending_latency"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMaxPendingLatency); val.IsValid() && !isEmptyValue(val) { + transformed["maxPendingLatency"] = transformedMaxPendingLatency + } + + transformedMinIdleInstances, err := expandAppEngineStandardAppVersionAutomaticScalingMinIdleInstances(original["min_idle_instances"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMinIdleInstances); val.IsValid() && !isEmptyValue(val) { + transformed["minIdleInstances"] = transformedMinIdleInstances + } + + transformedMinPendingLatency, err := expandAppEngineStandardAppVersionAutomaticScalingMinPendingLatency(original["min_pending_latency"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMinPendingLatency); val.IsValid() && !isEmptyValue(val) { + transformed["minPendingLatency"] = transformedMinPendingLatency + } + + transformedStandardSchedulerSettings, err := expandAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettings(original["standard_scheduler_settings"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedStandardSchedulerSettings); val.IsValid() && !isEmptyValue(val) { + transformed["standardSchedulerSettings"] = transformedStandardSchedulerSettings + } + + return transformed, nil +} + +func expandAppEngineStandardAppVersionAutomaticScalingMaxConcurrentRequests(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAppEngineStandardAppVersionAutomaticScalingMaxIdleInstances(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAppEngineStandardAppVersionAutomaticScalingMaxPendingLatency(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAppEngineStandardAppVersionAutomaticScalingMinIdleInstances(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAppEngineStandardAppVersionAutomaticScalingMinPendingLatency(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettings(v interface{}, d TerraformResourceData, config *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{}) + + transformedTargetCpuUtilization, err := expandAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsTargetCpuUtilization(original["target_cpu_utilization"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedTargetCpuUtilization); val.IsValid() && !isEmptyValue(val) { + transformed["targetCpuUtilization"] = transformedTargetCpuUtilization + } + + transformedTargetThroughputUtilization, err := expandAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsTargetThroughputUtilization(original["target_throughput_utilization"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedTargetThroughputUtilization); val.IsValid() && !isEmptyValue(val) { + transformed["targetThroughputUtilization"] = transformedTargetThroughputUtilization + } + + transformedMinInstances, err := expandAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsMinInstances(original["min_instances"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMinInstances); val.IsValid() && !isEmptyValue(val) { + transformed["minInstances"] = transformedMinInstances + } + + transformedMaxInstances, err := expandAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsMaxInstances(original["max_instances"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMaxInstances); val.IsValid() && !isEmptyValue(val) { + transformed["maxInstances"] = transformedMaxInstances + } + + return transformed, nil +} + +func expandAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsTargetCpuUtilization(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsTargetThroughputUtilization(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsMinInstances(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAppEngineStandardAppVersionAutomaticScalingStandardSchedulerSettingsMaxInstances(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAppEngineStandardAppVersionBasicScaling(v interface{}, d TerraformResourceData, config *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{}) + + transformedIdleTimeout, err := expandAppEngineStandardAppVersionBasicScalingIdleTimeout(original["idle_timeout"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedIdleTimeout); val.IsValid() && !isEmptyValue(val) { + transformed["idleTimeout"] = transformedIdleTimeout + } + + transformedMaxInstances, err := expandAppEngineStandardAppVersionBasicScalingMaxInstances(original["max_instances"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedMaxInstances); val.IsValid() && !isEmptyValue(val) { + transformed["maxInstances"] = transformedMaxInstances + } + + return transformed, nil +} + +func expandAppEngineStandardAppVersionBasicScalingIdleTimeout(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAppEngineStandardAppVersionBasicScalingMaxInstances(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} + +func expandAppEngineStandardAppVersionManualScaling(v interface{}, d TerraformResourceData, config *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{}) + + transformedInstances, err := expandAppEngineStandardAppVersionManualScalingInstances(original["instances"], d, config) + if err != nil { + return nil, err + } else if val := reflect.ValueOf(transformedInstances); val.IsValid() && !isEmptyValue(val) { + transformed["instances"] = transformedInstances + } + + return transformed, nil +} + +func expandAppEngineStandardAppVersionManualScalingInstances(v interface{}, d TerraformResourceData, config *Config) (interface{}, error) { + return v, nil +} diff --git a/google/resource_app_engine_standard_app_version_generated_test.go b/google/resource_app_engine_standard_app_version_generated_test.go index 6b814f9ff4e..3364a1ab059 100644 --- a/google/resource_app_engine_standard_app_version_generated_test.go +++ b/google/resource_app_engine_standard_app_version_generated_test.go @@ -43,7 +43,7 @@ func TestAccAppEngineStandardAppVersion_appEngineStandardAppVersionExample(t *te ResourceName: "google_app_engine_standard_app_version.myapp_v1", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"threadsafe", "env_variables", "deployment", "entrypoint", "instance_class", "service", "delete_service_on_destroy"}, + ImportStateVerifyIgnore: []string{"threadsafe", "env_variables", "deployment", "entrypoint", "service", "delete_service_on_destroy"}, }, }, }) @@ -70,6 +70,20 @@ resource "google_app_engine_standard_app_version" "myapp_v1" { port = "8080" } + automatic_scaling { + max_concurrent_requests = 10 + min_idle_instances = 1 + max_idle_instances = 3 + min_pending_latency = "1s" + max_pending_latency = "5s" + standard_scheduler_settings { + target_cpu_utilization = 0.5 + target_throughput_utilization = 0.75 + min_instances = 2 + max_instances = 10 + } + } + delete_service_on_destroy = true } @@ -92,6 +106,10 @@ resource "google_app_engine_standard_app_version" "myapp_v2" { port = "8080" } + basic_scaling { + max_instances = 5 + } + noop_on_destroy = true } diff --git a/google/resource_app_engine_standard_app_version_test.go b/google/resource_app_engine_standard_app_version_test.go new file mode 100644 index 00000000000..fadb4523ce8 --- /dev/null +++ b/google/resource_app_engine_standard_app_version_test.go @@ -0,0 +1,200 @@ +package google + +import ( + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "testing" +) + +func TestAccAppEngineStandardAppVersion_update(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "org_id": getTestOrgFromEnv(t), + "billing_account": getTestBillingAccountFromEnv(t), + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAppEngineStandardAppVersionDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccAppEngineStandardAppVersion_python(context), + }, + { + ResourceName: "google_app_engine_standard_app_version.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"env_variables", "deployment", "entrypoint", "service", "noop_on_destroy"}, + }, + { + Config: testAccAppEngineStandardAppVersion_pythonUpdate(context), + }, + { + ResourceName: "google_app_engine_standard_app_version.foo", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"env_variables", "deployment", "entrypoint", "service", "noop_on_destroy"}, + }, + }, + }) +} + +func testAccAppEngineStandardAppVersion_python(context map[string]interface{}) string { + return Nprintf(` +resource "google_project" "my_project" { + name = "tf-test-appeng-std%{random_suffix}" + project_id = "tf-test-appeng-std%{random_suffix}" + org_id = "%{org_id}" + billing_account = "%{billing_account}" +} + +resource "google_app_engine_application" "app" { + project = google_project.my_project.project_id + location_id = "us-central" +} + +resource "google_project_service" "project" { + project = google_project.my_project.project_id + service = "appengine.googleapis.com" + + disable_dependent_services = false +} + +resource "google_app_engine_standard_app_version" "foo" { + project = google_project_service.project.project + version_id = "v1" + service = "default" + runtime = "python38" + + entrypoint { + shell = "gunicorn -b :$PORT main:app" + } + + deployment { + files { + name = "main.py" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.main.name}" + } + + files { + name = "requirements.txt" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.requirements.name}" + } + } + + env_variables = { + port = "8000" + } + + instance_class = "F2" + + automatic_scaling { + max_concurrent_requests = 10 + min_idle_instances = 1 + max_idle_instances = 3 + min_pending_latency = "1s" + max_pending_latency = "5s" + standard_scheduler_settings { + target_cpu_utilization = 0.5 + target_throughput_utilization = 0.75 + min_instances = 2 + max_instances = 10 + } + } + + noop_on_destroy = true +} + +resource "google_storage_bucket" "bucket" { + project = google_project.my_project.project_id + name = "tf-test-%{random_suffix}-standard-ae-bucket" +} + +resource "google_storage_bucket_object" "requirements" { + name = "requirements.txt" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/appengine/hello-world-flask/requirements.txt" +} + +resource "google_storage_bucket_object" "main" { + name = "main.py" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/appengine/hello-world-flask/main.py" +}`, context) +} + +func testAccAppEngineStandardAppVersion_pythonUpdate(context map[string]interface{}) string { + return Nprintf(` +resource "google_project" "my_project" { + name = "tf-test-appeng-std%{random_suffix}" + project_id = "tf-test-appeng-std%{random_suffix}" + org_id = "%{org_id}" + billing_account = "%{billing_account}" +} + +resource "google_app_engine_application" "app" { + project = google_project.my_project.project_id + location_id = "us-central" +} + +resource "google_project_service" "project" { + project = google_project.my_project.project_id + service = "appengine.googleapis.com" + + disable_dependent_services = false +} + +resource "google_app_engine_standard_app_version" "foo" { + project = google_project_service.project.project + version_id = "v1" + service = "default" + runtime = "python38" + + entrypoint { + shell = "gunicorn -b :$PORT main:app" + } + + deployment { + files { + name = "main.py" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.main.name}" + } + + files { + name = "requirements.txt" + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/${google_storage_bucket_object.requirements.name}" + } + } + + env_variables = { + port = "8000" + } + + instance_class = "B2" + + basic_scaling { + max_instances = 5 + } + + noop_on_destroy = true +} + +resource "google_storage_bucket" "bucket" { + project = google_project.my_project.project_id + name = "tf-test-%{random_suffix}-standard-ae-bucket" +} + +resource "google_storage_bucket_object" "requirements" { + name = "requirements.txt" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/appengine/hello-world-flask/requirements.txt" +} + +resource "google_storage_bucket_object" "main" { + name = "main.py" + bucket = google_storage_bucket.bucket.name + source = "./test-fixtures/appengine/hello-world-flask/main.py" +}`, context) +} diff --git a/website/docs/r/app_engine_flexible_app_version.html.markdown b/website/docs/r/app_engine_flexible_app_version.html.markdown index 0471b37d7bc..668430c8d6b 100644 --- a/website/docs/r/app_engine_flexible_app_version.html.markdown +++ b/website/docs/r/app_engine_flexible_app_version.html.markdown @@ -591,7 +591,9 @@ The `manual_scaling` block supports: * `instances` - (Required) - Number of instances to assign to the service at the start. This number can later be altered by using the Modules API set_num_instances() function. + Number of instances to assign to the service at the start. + **Note:** When managing the number of instances at runtime through the App Engine Admin API or the (now deprecated) Python 2 + Modules API set_num_instances() you must use `lifecycle.ignore_changes = ["manual_scaling"[0].instances]` to prevent drift detection. ## Attributes Reference diff --git a/website/docs/r/app_engine_standard_app_version.html.markdown b/website/docs/r/app_engine_standard_app_version.html.markdown index ceede773e07..06042962b99 100644 --- a/website/docs/r/app_engine_standard_app_version.html.markdown +++ b/website/docs/r/app_engine_standard_app_version.html.markdown @@ -23,8 +23,9 @@ description: |- # google\_app\_engine\_standard\_app\_version Standard App Version resource to create a new version of standard GAE Application. +Learn about the differences between the standard environment and the flexible environment +at https://cloud.google.com/appengine/docs/the-appengine-environments. Currently supporting Zip and File Containers. -Currently does not support async operation checking. To get more information about StandardAppVersion, see: @@ -61,6 +62,20 @@ resource "google_app_engine_standard_app_version" "myapp_v1" { port = "8080" } + automatic_scaling { + max_concurrent_requests = 10 + min_idle_instances = 1 + max_idle_instances = 3 + min_pending_latency = "1s" + max_pending_latency = "5s" + standard_scheduler_settings { + target_cpu_utilization = 0.5 + target_throughput_utilization = 0.75 + min_instances = 2 + max_instances = 10 + } + } + delete_service_on_destroy = true } @@ -83,6 +98,10 @@ resource "google_app_engine_standard_app_version" "myapp_v2" { port = "8080" } + basic_scaling { + max_instances = 5 + } + noop_on_destroy = true } @@ -106,6 +125,44 @@ The following arguments are supported: (Required) Desired runtime. Example python27. +* `deployment` - + (Required) + Code and application artifacts that make up this version. Structure is documented below. + + +The `deployment` block supports: + +* `zip` - + (Optional) + Zip File Structure is documented below. + +* `files` - + (Optional) + Manifest of the files stored in Google Cloud Storage that are included as part of this version. + All files must be readable using the credentials supplied with this call. Structure is documented below. + + +The `zip` block supports: + +* `source_url` - + (Required) + Source URL + +* `files_count` - + (Optional) + files count + +The `files` block supports: + +* `name` - (Required) The identifier for this object. Format specified above. + +* `sha1_sum` - + (Optional) + SHA1 checksum of the file + +* `source_url` - + (Required) + Source URL - - - @@ -136,10 +193,6 @@ The following arguments are supported: (Optional) Environment variables available to the application. -* `deployment` - - (Optional) - Code and application artifacts that make up this version. Structure is documented below. - * `entrypoint` - (Optional) The entrypoint for the application. Structure is documented below. @@ -147,8 +200,21 @@ The following arguments are supported: * `instance_class` - (Optional) Instance class that is used to run this version. Valid values are - AutomaticScaling F1, F2, F4, F4_1G - (Only AutomaticScaling is supported at the moment) + AutomaticScaling: F1, F2, F4, F4_1G + BasicScaling or ManualScaling: B1, B2, B4, B4_1G, B8 + Defaults to F1 for AutomaticScaling and B2 for ManualScaling and BasicScaling. If no scaling is specified, AutomaticScaling is chosen. + +* `automatic_scaling` - + (Optional) + Automatic scaling is based on request rate, response latencies, and other application metrics. Structure is documented below. + +* `basic_scaling` - + (Optional) + Basic scaling creates instances when your application receives requests. Each instance will be shut down when the application becomes idle. Basic scaling is ideal for work that is intermittent or driven by user activity. Structure is documented below. + +* `manual_scaling` - + (Optional) + A service with manual scaling runs continuously, allowing you to perform complex initialization and rely on the state of its memory over time. Structure is documented below. * `service` - (Optional) @@ -242,45 +308,78 @@ The `libraries` block supports: (Optional) Version of the library to select, or "latest". -The `deployment` block supports: +The `entrypoint` block supports: -* `zip` - +* `shell` - + (Required) + The format should be a shell command that can be fed to bash -c. + +The `automatic_scaling` block supports: + +* `max_concurrent_requests` - (Optional) - Zip File Structure is documented below. + Number of concurrent requests an automatic scaling instance can accept before the scheduler spawns a new instance. + Defaults to a runtime-specific value. -* `files` - +* `max_idle_instances` - (Optional) - Manifest of the files stored in Google Cloud Storage that are included as part of this version. - All files must be readable using the credentials supplied with this call. Structure is documented below. + Maximum number of idle instances that should be maintained for this version. +* `max_pending_latency` - + (Optional) + Maximum amount of time that a request should wait in the pending queue before starting a new instance to handle it. + A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". -The `zip` block supports: +* `min_idle_instances` - + (Optional) + Minimum number of idle instances that should be maintained for this version. Only applicable for the default version of a service. -* `source_url` - - (Required) - Source URL +* `min_pending_latency` - + (Optional) + Minimum amount of time a request should wait in the pending queue before starting a new instance to handle it. + A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". -* `files_count` - +* `standard_scheduler_settings` - (Optional) - files count + Scheduler settings for standard environment. Structure is documented below. -The `files` block supports: -* `name` - (Required) The identifier for this object. Format specified above. +The `standard_scheduler_settings` block supports: -* `sha1_sum` - +* `target_cpu_utilization` - (Optional) - SHA1 checksum of the file + Target CPU utilization ratio to maintain when scaling. Should be a value in the range [0.50, 0.95], zero, or a negative value. -* `source_url` - +* `target_throughput_utilization` - + (Optional) + Target throughput utilization ratio to maintain when scaling. Should be a value in the range [0.50, 0.95], zero, or a negative value. + +* `min_instances` - + (Optional) + Minimum number of instances to run for this version. Set to zero to disable minInstances configuration. + +* `max_instances` - + (Optional) + Maximum number of instances to run for this version. Set to zero to disable maxInstances configuration. + +The `basic_scaling` block supports: + +* `idle_timeout` - + (Optional) + Duration of time after the last request that an instance must wait before the instance is shut down. + A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". Defaults to 900s. + +* `max_instances` - (Required) - Source URL + Maximum number of instances to create for this version. Must be in the range [1.0, 200.0]. -The `entrypoint` block supports: +The `manual_scaling` block supports: -* `shell` - +* `instances` - (Required) - The format should be a shell command that can be fed to bash -c. + Number of instances to assign to the service at the start. + **Note:** When managing the number of instances at runtime through the App Engine Admin API or the (now deprecated) Python 2 + Modules API set_num_instances() you must use `lifecycle.ignore_changes = ["manual_scaling"[0].instances]` to prevent drift detection. ## Attributes Reference