diff --git a/.changelog/24448.txt b/.changelog/24448.txt new file mode 100644 index 000000000000..35a6ac78c34a --- /dev/null +++ b/.changelog/24448.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_spot_fleet_request: Add `overrides.instance_requirements` argument +``` \ No newline at end of file diff --git a/internal/service/ec2/ec2_spot_fleet_request.go b/internal/service/ec2/ec2_spot_fleet_request.go index c24f3ecf4f15..08fe624012a2 100644 --- a/internal/service/ec2/ec2_spot_fleet_request.go +++ b/internal/service/ec2/ec2_spot_fleet_request.go @@ -389,6 +389,288 @@ func ResourceSpotFleetRequest() *schema.Resource { Optional: true, ForceNew: true, }, + "instance_requirements": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "accelerator_count": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(0), + }, + "min": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + }, + }, + }, + "accelerator_manufacturers": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(ec2.AcceleratorManufacturer_Values(), false), + }, + }, + "accelerator_names": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(ec2.AcceleratorName_Values(), false), + }, + }, + "accelerator_total_memory_mib": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + "min": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + }, + }, + }, + "accelerator_types": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(ec2.AcceleratorType_Values(), false), + }, + }, + "bare_metal": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(ec2.BareMetal_Values(), false), + }, + "baseline_ebs_bandwidth_mbps": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + "min": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + }, + }, + }, + "burstable_performance": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(ec2.BurstablePerformance_Values(), false), + }, + "cpu_manufacturers": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(ec2.CpuManufacturer_Values(), false), + }, + }, + "excluded_instance_types": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + MaxItems: 400, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "instance_generations": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(ec2.InstanceGeneration_Values(), false), + }, + }, + "local_storage": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(ec2.LocalStorage_Values(), false), + }, + "local_storage_types": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(ec2.LocalStorageType_Values(), false), + }, + }, + "memory_gib_per_vcpu": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max": { + Type: schema.TypeFloat, + Optional: true, + ForceNew: true, + ValidateFunc: verify.FloatGreaterThan(0.0), + }, + "min": { + Type: schema.TypeFloat, + Optional: true, + ForceNew: true, + ValidateFunc: verify.FloatGreaterThan(0.0), + }, + }, + }, + }, + "memory_mib": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + "min": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + }, + }, + }, + "network_interface_count": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + "min": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + }, + }, + }, + "on_demand_max_price_percentage_over_lowest_price": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + "require_hibernate_support": { + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + "spot_max_price_percentage_over_lowest_price": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + "total_local_storage_gb": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max": { + Type: schema.TypeFloat, + Optional: true, + ForceNew: true, + ValidateFunc: verify.FloatGreaterThan(0.0), + }, + "min": { + Type: schema.TypeFloat, + Optional: true, + ForceNew: true, + ValidateFunc: verify.FloatGreaterThan(0.0), + }, + }, + }, + }, + "vcpu_count": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + "min": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(1), + }, + }, + }, + }, + }, + }, + }, "instance_type": { Type: schema.TypeString, Optional: true, @@ -420,7 +702,6 @@ func ResourceSpotFleetRequest() *schema.Resource { }, }, }, - Set: hashLaunchTemplateOverrides, }, }, }, @@ -546,7 +827,6 @@ func resourceSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) er tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) _, launchSpecificationOk := d.GetOk("launch_specification") - _, launchTemplateConfigsOk := d.GetOk("launch_template_config") // http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-SpotFleetRequestConfigData spotFleetConfig := &ec2.SpotFleetRequestConfigData{ @@ -568,9 +848,8 @@ func resourceSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) er spotFleetConfig.LaunchSpecifications = launchSpecs } - if launchTemplateConfigsOk { - launchTemplates := buildLaunchTemplateConfigs(d) - spotFleetConfig.LaunchTemplateConfigs = launchTemplates + if v, ok := d.GetOk("launch_template_config"); ok && v.(*schema.Set).Len() > 0 { + spotFleetConfig.LaunchTemplateConfigs = expandLaunchTemplateConfigs(v.(*schema.Set).List()) } if v, ok := d.GetOk("excess_capacity_termination_policy"); ok { @@ -775,10 +1054,8 @@ func resourceSpotFleetRequestRead(d *schema.ResourceData, meta interface{}) erro return fmt.Errorf("setting tags_all: %w", err) } - if len(config.LaunchTemplateConfigs) > 0 { - if err := d.Set("launch_template_config", flattenFleetLaunchTemplateConfig(config.LaunchTemplateConfigs)); err != nil { - return fmt.Errorf("setting launch_template_config: %w", err) - } + if err := d.Set("launch_template_config", flattenLaunchTemplateConfigs(config.LaunchTemplateConfigs)); err != nil { + return fmt.Errorf("setting launch_template_config: %w", err) } d.Set("on_demand_target_capacity", config.OnDemandTargetCapacity) @@ -1172,140 +1449,402 @@ func buildSpotFleetLaunchSpecifications(d *schema.ResourceData, meta interface{} return specs, nil } -func buildLaunchTemplateConfigs(d *schema.ResourceData) []*ec2.LaunchTemplateConfig { - launchTemplateConfigs := d.Get("launch_template_config").(*schema.Set) - configs := make([]*ec2.LaunchTemplateConfig, 0) +func expandLaunchTemplateConfig(tfMap map[string]interface{}) *ec2.LaunchTemplateConfig { + if tfMap == nil { + return nil + } - for _, launchTemplateConfig := range launchTemplateConfigs.List() { + apiObject := &ec2.LaunchTemplateConfig{} - ltc := &ec2.LaunchTemplateConfig{} + if v, ok := tfMap["launch_template_specification"].([]interface{}); ok && len(v) > 0 { + apiObject.LaunchTemplateSpecification = expandFleetLaunchTemplateSpecification(v[0].(map[string]interface{})) + } - ltcMap := launchTemplateConfig.(map[string]interface{}) + if v, ok := tfMap["overrides"].(*schema.Set); ok && v.Len() > 0 { + apiObject.Overrides = expandLaunchTemplateOverrideses(v.List()) + } - //launch template spec - if v, ok := ltcMap["launch_template_specification"]; ok { - vL := v.([]interface{}) - lts := vL[0].(map[string]interface{}) + return apiObject +} - flts := &ec2.FleetLaunchTemplateSpecification{} +func expandLaunchTemplateConfigs(tfList []interface{}) []*ec2.LaunchTemplateConfig { + if len(tfList) == 0 { + return nil + } - if v, ok := lts["id"].(string); ok && v != "" { - flts.LaunchTemplateId = aws.String(v) - } + var apiObjects []*ec2.LaunchTemplateConfig - if v, ok := lts["name"].(string); ok && v != "" { - flts.LaunchTemplateName = aws.String(v) - } + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) - if v, ok := lts["version"].(string); ok && v != "" { - flts.Version = aws.String(v) - } + if !ok { + continue + } - ltc.LaunchTemplateSpecification = flts + apiObject := expandLaunchTemplateConfig(tfMap) + if apiObject == nil { + continue } - if v, ok := ltcMap["overrides"]; ok && v.(*schema.Set).Len() > 0 { - vL := v.(*schema.Set).List() - overrides := make([]*ec2.LaunchTemplateOverrides, 0) + apiObjects = append(apiObjects, apiObject) + } - for _, v := range vL { - ors := v.(map[string]interface{}) - lto := &ec2.LaunchTemplateOverrides{} + return apiObjects +} - if v, ok := ors["availability_zone"].(string); ok && v != "" { - lto.AvailabilityZone = aws.String(v) - } +func expandFleetLaunchTemplateSpecification(tfMap map[string]interface{}) *ec2.FleetLaunchTemplateSpecification { + if tfMap == nil { + return nil + } - if v, ok := ors["instance_type"].(string); ok && v != "" { - lto.InstanceType = aws.String(v) - } + apiObject := &ec2.FleetLaunchTemplateSpecification{} - if v, ok := ors["spot_price"].(string); ok && v != "" { - lto.SpotPrice = aws.String(v) - } + if v, ok := tfMap["id"].(string); ok && v != "" { + apiObject.LaunchTemplateId = aws.String(v) + } - if v, ok := ors["subnet_id"].(string); ok && v != "" { - lto.SubnetId = aws.String(v) - } + if v, ok := tfMap["name"].(string); ok && v != "" { + apiObject.LaunchTemplateName = aws.String(v) + } - if v, ok := ors["weighted_capacity"].(float64); ok && v > 0 { - lto.WeightedCapacity = aws.Float64(v) - } + if v, ok := tfMap["version"].(string); ok && v != "" { + apiObject.Version = aws.String(v) + } - if v, ok := ors["priority"].(float64); ok { - lto.Priority = aws.Float64(v) - } + return apiObject +} - overrides = append(overrides, lto) - } +func expandLaunchTemplateOverrides(tfMap map[string]interface{}) *ec2.LaunchTemplateOverrides { + if tfMap == nil { + return nil + } - ltc.Overrides = overrides + apiObject := &ec2.LaunchTemplateOverrides{} + + if v, ok := tfMap["availability_zone"].(string); ok && v != "" { + apiObject.AvailabilityZone = aws.String(v) + } + + if v, ok := tfMap["instance_requirements"].([]interface{}); ok && len(v) > 0 { + apiObject.InstanceRequirements = expandInstanceRequirements(v[0].(map[string]interface{})) + } + + if v, ok := tfMap["instance_type"].(string); ok && v != "" { + apiObject.InstanceType = aws.String(v) + } + + if v, ok := tfMap["priority"].(float64); ok && v != 0.0 { + apiObject.Priority = aws.Float64(v) + } + + if v, ok := tfMap["spot_price"].(string); ok && v != "" { + apiObject.SpotPrice = aws.String(v) + } + + if v, ok := tfMap["subnet_id"].(string); ok && v != "" { + apiObject.SubnetId = aws.String(v) + } + + if v, ok := tfMap["weighted_capacity"].(float64); ok && v != 0.0 { + apiObject.WeightedCapacity = aws.Float64(v) + } + + return apiObject +} + +func expandLaunchTemplateOverrideses(tfList []interface{}) []*ec2.LaunchTemplateOverrides { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*ec2.LaunchTemplateOverrides + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue } - configs = append(configs, ltc) + apiObject := expandLaunchTemplateOverrides(tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) } - return configs + return apiObjects } -func expandSpotMaintenanceStrategies(l []interface{}) *ec2.SpotMaintenanceStrategies { - if len(l) == 0 || l[0] == nil { +func expandInstanceRequirements(tfMap map[string]interface{}) *ec2.InstanceRequirements { + if tfMap == nil { return nil } - m := l[0].(map[string]interface{}) + apiObject := &ec2.InstanceRequirements{} - fleetSpotMaintenanceStrategies := &ec2.SpotMaintenanceStrategies{ - CapacityRebalance: expandSpotCapacityRebalance(m["capacity_rebalance"].([]interface{})), + if v, ok := tfMap["accelerator_count"].([]interface{}); ok && len(v) > 0 { + apiObject.AcceleratorCount = expandAcceleratorCount(v[0].(map[string]interface{})) } - return fleetSpotMaintenanceStrategies + if v, ok := tfMap["accelerator_manufacturers"].(*schema.Set); ok && v.Len() > 0 { + apiObject.AcceleratorManufacturers = flex.ExpandStringSet(v) + } + + if v, ok := tfMap["accelerator_names"].(*schema.Set); ok && v.Len() > 0 { + apiObject.AcceleratorNames = flex.ExpandStringSet(v) + } + + if v, ok := tfMap["accelerator_total_memory_mib"].([]interface{}); ok && len(v) > 0 { + apiObject.AcceleratorTotalMemoryMiB = expandAcceleratorTotalMemoryMiB(v[0].(map[string]interface{})) + } + + if v, ok := tfMap["accelerator_types"].(*schema.Set); ok && v.Len() > 0 { + apiObject.AcceleratorTypes = flex.ExpandStringSet(v) + } + + if v, ok := tfMap["bare_metal"].(string); ok && v != "" { + apiObject.BareMetal = aws.String(v) + } + + if v, ok := tfMap["baseline_ebs_bandwidth_mbps"].([]interface{}); ok && len(v) > 0 { + apiObject.BaselineEbsBandwidthMbps = expandBaselineEbsBandwidthMbps(v[0].(map[string]interface{})) + } + + if v, ok := tfMap["burstable_performance"].(string); ok && v != "" { + apiObject.BurstablePerformance = aws.String(v) + } + + if v, ok := tfMap["cpu_manufacturers"].(*schema.Set); ok && v.Len() > 0 { + apiObject.CpuManufacturers = flex.ExpandStringSet(v) + } + + if v, ok := tfMap["excluded_instance_types"].(*schema.Set); ok && v.Len() > 0 { + apiObject.ExcludedInstanceTypes = flex.ExpandStringSet(v) + } + + if v, ok := tfMap["instance_generations"].(*schema.Set); ok && v.Len() > 0 { + apiObject.InstanceGenerations = flex.ExpandStringSet(v) + } + + if v, ok := tfMap["local_storage"].(string); ok && v != "" { + apiObject.LocalStorage = aws.String(v) + } + + if v, ok := tfMap["local_storage_types"].(*schema.Set); ok && v.Len() > 0 { + apiObject.LocalStorageTypes = flex.ExpandStringSet(v) + } + + if v, ok := tfMap["memory_gib_per_vcpu"].([]interface{}); ok && len(v) > 0 { + apiObject.MemoryGiBPerVCpu = expandMemoryGiBPerVCpu(v[0].(map[string]interface{})) + } + + if v, ok := tfMap["memory_mib"].([]interface{}); ok && len(v) > 0 { + apiObject.MemoryMiB = expandMemoryMiB(v[0].(map[string]interface{})) + } + + if v, ok := tfMap["network_interface_count"].([]interface{}); ok && len(v) > 0 { + apiObject.NetworkInterfaceCount = expandNetworkInterfaceCount(v[0].(map[string]interface{})) + } + + if v, ok := tfMap["on_demand_max_price_percentage_over_lowest_price"].(int); ok && v != 0 { + apiObject.OnDemandMaxPricePercentageOverLowestPrice = aws.Int64(int64(v)) + } + + if v, ok := tfMap["require_hibernate_support"].(bool); ok && v { + apiObject.RequireHibernateSupport = aws.Bool(v) + } + + if v, ok := tfMap["spot_max_price_percentage_over_lowest_price"].(int); ok && v != 0 { + apiObject.SpotMaxPricePercentageOverLowestPrice = aws.Int64(int64(v)) + } + + if v, ok := tfMap["total_local_storage_gb"].([]interface{}); ok && len(v) > 0 { + apiObject.TotalLocalStorageGB = expandTotalLocalStorageGB(v[0].(map[string]interface{})) + } + + if v, ok := tfMap["vcpu_count"].([]interface{}); ok && len(v) > 0 { + apiObject.VCpuCount = expandVCpuCountRange(v[0].(map[string]interface{})) + } + + return apiObject } -func expandSpotCapacityRebalance(l []interface{}) *ec2.SpotCapacityRebalance { - if len(l) == 0 || l[0] == nil { +func expandAcceleratorCount(tfMap map[string]interface{}) *ec2.AcceleratorCount { + if tfMap == nil { return nil } - m := l[0].(map[string]interface{}) + apiObject := &ec2.AcceleratorCount{} - capacityRebalance := &ec2.SpotCapacityRebalance{} + if v, ok := tfMap["max"].(int); ok { + apiObject.Max = aws.Int64(int64(v)) + } - if v, ok := m["replacement_strategy"]; ok && v.(string) != "" { - capacityRebalance.ReplacementStrategy = aws.String(v.(string)) + if v, ok := tfMap["min"].(int); ok { + apiObject.Min = aws.Int64(int64(v)) } - return capacityRebalance + return apiObject } -func flattenSpotFleetRequestLaunchTemplateOverrides(override *ec2.LaunchTemplateOverrides) map[string]interface{} { - m := make(map[string]interface{}) +func expandAcceleratorTotalMemoryMiB(tfMap map[string]interface{}) *ec2.AcceleratorTotalMemoryMiB { + if tfMap == nil { + return nil + } + + apiObject := &ec2.AcceleratorTotalMemoryMiB{} - if override.AvailabilityZone != nil { - m["availability_zone"] = aws.StringValue(override.AvailabilityZone) + if v, ok := tfMap["max"].(int); ok { + apiObject.Max = aws.Int64(int64(v)) } - if override.InstanceType != nil { - m["instance_type"] = aws.StringValue(override.InstanceType) + + if v, ok := tfMap["min"].(int); ok { + apiObject.Min = aws.Int64(int64(v)) } - if override.SpotPrice != nil { - m["spot_price"] = aws.StringValue(override.SpotPrice) + return apiObject +} + +func expandBaselineEbsBandwidthMbps(tfMap map[string]interface{}) *ec2.BaselineEbsBandwidthMbps { + if tfMap == nil { + return nil } - if override.SubnetId != nil { - m["subnet_id"] = aws.StringValue(override.SubnetId) + apiObject := &ec2.BaselineEbsBandwidthMbps{} + + if v, ok := tfMap["max"].(int); ok { + apiObject.Max = aws.Int64(int64(v)) } - if override.WeightedCapacity != nil { - m["weighted_capacity"] = aws.Float64Value(override.WeightedCapacity) + if v, ok := tfMap["min"].(int); ok { + apiObject.Min = aws.Int64(int64(v)) } - if override.Priority != nil { - m["priority"] = aws.Float64Value(override.Priority) + return apiObject +} + +func expandMemoryGiBPerVCpu(tfMap map[string]interface{}) *ec2.MemoryGiBPerVCpu { + if tfMap == nil { + return nil } - return m + apiObject := &ec2.MemoryGiBPerVCpu{} + + if v, ok := tfMap["max"].(float64); ok { + apiObject.Max = aws.Float64(v) + } + + if v, ok := tfMap["min"].(float64); ok { + apiObject.Min = aws.Float64(v) + } + + return apiObject +} + +func expandMemoryMiB(tfMap map[string]interface{}) *ec2.MemoryMiB { + if tfMap == nil { + return nil + } + + apiObject := &ec2.MemoryMiB{} + + if v, ok := tfMap["max"].(int); ok { + apiObject.Max = aws.Int64(int64(v)) + } + + if v, ok := tfMap["min"].(int); ok { + apiObject.Min = aws.Int64(int64(v)) + } + + return apiObject +} + +func expandNetworkInterfaceCount(tfMap map[string]interface{}) *ec2.NetworkInterfaceCount { + if tfMap == nil { + return nil + } + + apiObject := &ec2.NetworkInterfaceCount{} + + if v, ok := tfMap["max"].(int); ok { + apiObject.Max = aws.Int64(int64(v)) + } + + if v, ok := tfMap["min"].(int); ok { + apiObject.Min = aws.Int64(int64(v)) + } + + return apiObject +} + +func expandTotalLocalStorageGB(tfMap map[string]interface{}) *ec2.TotalLocalStorageGB { + if tfMap == nil { + return nil + } + + apiObject := &ec2.TotalLocalStorageGB{} + + if v, ok := tfMap["max"].(float64); ok { + apiObject.Max = aws.Float64(v) + } + + if v, ok := tfMap["min"].(float64); ok { + apiObject.Min = aws.Float64(v) + } + + return apiObject +} + +func expandVCpuCountRange(tfMap map[string]interface{}) *ec2.VCpuCountRange { + if tfMap == nil { + return nil + } + + apiObject := &ec2.VCpuCountRange{} + + if v, ok := tfMap["max"].(int); ok { + apiObject.Max = aws.Int64(int64(v)) + } + + if v, ok := tfMap["min"].(int); ok { + apiObject.Min = aws.Int64(int64(v)) + } + + return apiObject +} + +func expandSpotMaintenanceStrategies(l []interface{}) *ec2.SpotMaintenanceStrategies { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + fleetSpotMaintenanceStrategies := &ec2.SpotMaintenanceStrategies{ + CapacityRebalance: expandSpotCapacityRebalance(m["capacity_rebalance"].([]interface{})), + } + + return fleetSpotMaintenanceStrategies +} + +func expandSpotCapacityRebalance(l []interface{}) *ec2.SpotCapacityRebalance { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + capacityRebalance := &ec2.SpotCapacityRebalance{} + + if v, ok := m["replacement_strategy"]; ok && v.(string) != "" { + capacityRebalance.ReplacementStrategy = aws.String(v.(string)) + } + + return capacityRebalance } func launchSpecsToSet(conn *ec2.EC2, launchSpecs []*ec2.SpotFleetLaunchSpecification) (*schema.Set, error) { @@ -1549,94 +2088,130 @@ func hashLaunchSpecification(v interface{}) int { return create.StringHashcode(buf.String()) } -func hashLaunchTemplateOverrides(v interface{}) int { +func hashEbsBlockDevice(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) - if m["availability_zone"] != nil { - buf.WriteString(fmt.Sprintf("%s-", m["availability_zone"].(string))) - } - if m["subnet_id"] != nil { - buf.WriteString(fmt.Sprintf("%s-", m["subnet_id"].(string))) + if name, ok := m["device_name"]; ok { + buf.WriteString(fmt.Sprintf("%s-", name.(string))) } - if m["spot_price"] != nil { - buf.WriteString(fmt.Sprintf("%s-", m["spot_price"].(string))) + if id, ok := m["snapshot_id"]; ok { + buf.WriteString(fmt.Sprintf("%s-", id.(string))) } - if m["instance_type"] != nil { - buf.WriteString(fmt.Sprintf("%s-", m["instance_type"].(string))) + return create.StringHashcode(buf.String()) +} + +func flattenLaunchTemplateConfig(apiObject *ec2.LaunchTemplateConfig) map[string]interface{} { + if apiObject == nil { + return nil } - if m["weighted_capacity"] != nil { - buf.WriteString(fmt.Sprintf("%f-", m["weighted_capacity"].(float64))) + + tfMap := map[string]interface{}{} + + if v := apiObject.LaunchTemplateSpecification; v != nil { + tfMap["launch_template_specification"] = []interface{}{flattenFleetLaunchTemplateSpecification(v)} } - if m["priority"] != nil { - buf.WriteString(fmt.Sprintf("%f-", m["priority"].(float64))) + + if v := apiObject.Overrides; v != nil { + tfMap["overrides"] = flattenLaunchTemplateOverrideses(v) } - return create.StringHashcode(buf.String()) + return tfMap } -func hashEbsBlockDevice(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - if name, ok := m["device_name"]; ok { - buf.WriteString(fmt.Sprintf("%s-", name.(string))) +func flattenLaunchTemplateConfigs(apiObjects []*ec2.LaunchTemplateConfig) []interface{} { + if len(apiObjects) == 0 { + return nil } - if id, ok := m["snapshot_id"]; ok { - buf.WriteString(fmt.Sprintf("%s-", id.(string))) + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenLaunchTemplateConfig(apiObject)) } - return create.StringHashcode(buf.String()) + + return tfList } -func flattenFleetLaunchTemplateConfig(ltcs []*ec2.LaunchTemplateConfig) []map[string]interface{} { - result := make([]map[string]interface{}, 0) +func flattenFleetLaunchTemplateSpecification(apiObject *ec2.FleetLaunchTemplateSpecification) map[string]interface{} { + if apiObject == nil { + return nil + } - for _, ltc := range ltcs { - ltcRes := map[string]interface{}{} + tfMap := map[string]interface{}{} - if ltc.LaunchTemplateSpecification != nil { - ltcRes["launch_template_specification"] = flattenFleetLaunchTemplateSpecification(ltc.LaunchTemplateSpecification) - } + if v := apiObject.LaunchTemplateId; v != nil { + tfMap["id"] = aws.StringValue(v) + } - if ltc.Overrides != nil { - ltcRes["overrides"] = flattenLaunchTemplateOverrides(ltc.Overrides) - } + if v := apiObject.LaunchTemplateName; v != nil { + tfMap["name"] = aws.StringValue(v) + } - result = append(result, ltcRes) + if v := apiObject.Version; v != nil { + tfMap["version"] = aws.StringValue(v) } - return result + return tfMap } -func flattenFleetLaunchTemplateSpecification(flt *ec2.FleetLaunchTemplateSpecification) []map[string]interface{} { - attrs := map[string]interface{}{} - result := make([]map[string]interface{}, 0) +func flattenLaunchTemplateOverrides(apiObject *ec2.LaunchTemplateOverrides) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} - // unlike autoscaling.LaunchTemplateConfiguration, FleetLaunchTemplateSpecs only return what was set - if flt.LaunchTemplateId != nil { - attrs["id"] = aws.StringValue(flt.LaunchTemplateId) + if v := apiObject.AvailabilityZone; v != nil { + tfMap["availability_zone"] = aws.StringValue(v) } - if flt.LaunchTemplateName != nil { - attrs["name"] = aws.StringValue(flt.LaunchTemplateName) + if v := apiObject.InstanceRequirements; v != nil { + tfMap["instance_requirements"] = []interface{}{flattenInstanceRequirements(v)} } - // version is returned only if it was previously set - if flt.Version != nil { - attrs["version"] = aws.StringValue(flt.Version) - } else { - attrs["version"] = nil + if v := apiObject.InstanceType; v != nil { + tfMap["instance_type"] = aws.StringValue(v) + } + + if v := apiObject.Priority; v != nil { + tfMap["priority"] = aws.Float64Value(v) + } + + if v := apiObject.SpotPrice; v != nil { + tfMap["spot_price"] = aws.StringValue(v) + } + + if v := apiObject.SubnetId; v != nil { + tfMap["subnet_id"] = aws.StringValue(v) } - result = append(result, attrs) + if v := apiObject.WeightedCapacity; v != nil { + tfMap["weighted_capacity"] = aws.Float64Value(v) + } - return result + return tfMap } -func flattenLaunchTemplateOverrides(overrides []*ec2.LaunchTemplateOverrides) *schema.Set { - overrideSet := &schema.Set{F: hashLaunchTemplateOverrides} - for _, override := range overrides { - overrideSet.Add(flattenSpotFleetRequestLaunchTemplateOverrides(override)) +func flattenLaunchTemplateOverrideses(apiObjects []*ec2.LaunchTemplateOverrides) []interface{} { + if len(apiObjects) == 0 { + return nil } - return overrideSet + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenLaunchTemplateOverrides(apiObject)) + } + + return tfList } func flattenSpotMaintenanceStrategies(spotMaintenanceStrategies *ec2.SpotMaintenanceStrategies) []interface{} { diff --git a/internal/service/ec2/ec2_spot_fleet_request_test.go b/internal/service/ec2/ec2_spot_fleet_request_test.go index 5ea739860721..0df16d02b645 100644 --- a/internal/service/ec2/ec2_spot_fleet_request_test.go +++ b/internal/service/ec2/ec2_spot_fleet_request_test.go @@ -241,7 +241,7 @@ func TestAccEC2SpotFleetRequest_LaunchTemplate_multiple(t *testing.T) { }) } -func TestAccEC2SpotFleetRequest_launchTemplateWithOverrides(t *testing.T) { +func TestAccEC2SpotFleetRequest_launchTemplateWithInstanceTypeOverrides(t *testing.T) { var sfr ec2.SpotFleetRequestConfig rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) validUntil := time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339) @@ -259,12 +259,76 @@ func TestAccEC2SpotFleetRequest_launchTemplateWithOverrides(t *testing.T) { CheckDestroy: testAccCheckSpotFleetRequestDestroy, Steps: []resource.TestStep{ { - Config: testAccSpotFleetRequestLaunchTemplateWithOverridesConfig(rName, publicKey, validUntil), + Config: testAccSpotFleetRequestLaunchTemplateWithInstanceTypeOverridesConfig(rName, publicKey, validUntil), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckSpotFleetRequestExists(resourceName, &sfr), resource.TestCheckResourceAttr(resourceName, "spot_request_state", "active"), resource.TestCheckResourceAttr(resourceName, "launch_specification.#", "0"), resource.TestCheckResourceAttr(resourceName, "launch_template_config.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "launch_template_config.*", map[string]string{ + "overrides.#": "2", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "launch_template_config.*.overrides.*", map[string]string{ + "instance_requirements.#": "0", + "instance_type": "t1.micro", + "weighted_capacity": "2", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "launch_template_config.*.overrides.*", map[string]string{ + "instance_requirements.#": "0", + "instance_type": "m3.medium", + "priority": "1", + "spot_price": "0.26", + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"wait_for_fulfillment"}, + }, + }, + }) +} + +func TestAccEC2SpotFleetRequest_launchTemplateWithInstanceRequirementsOverrides(t *testing.T) { + var sfr ec2.SpotFleetRequestConfig + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + validUntil := time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339) + resourceName := "aws_spot_fleet_request.test" + + publicKey, _, err := sdkacctest.RandSSHKeyPair(acctest.DefaultEmailAddress) + if err != nil { + t.Fatalf("error generating random SSH key: %s", err) + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheckSpotFleetRequest(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckSpotFleetRequestDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSpotFleetRequestLaunchTemplateWithInstanceRequirementsOverridesConfig(rName, publicKey, validUntil), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckSpotFleetRequestExists(resourceName, &sfr), + resource.TestCheckResourceAttr(resourceName, "spot_request_state", "active"), + resource.TestCheckResourceAttr(resourceName, "launch_specification.#", "0"), + resource.TestCheckResourceAttr(resourceName, "launch_template_config.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "launch_template_config.*", map[string]string{ + "overrides.#": "1", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "launch_template_config.*.overrides.*", map[string]string{ + "instance_requirements.#": "1", + "instance_requirements.0.instance_generations.#": "1", + "instance_requirements.0.memory_mib.#": "1", + "instance_requirements.0.memory_mib.0.max": "50000", + "instance_requirements.0.memory_mib.0.min": "500", + "instance_requirements.0.vcpu_count.#": "1", + "instance_requirements.0.vcpu_count.0.max": "8", + "instance_requirements.0.vcpu_count.0.min": "1", + "instance_type": "", + }), ), }, { @@ -2050,7 +2114,7 @@ resource "aws_spot_fleet_request" "test" { `, rName, validUntil)) } -func testAccSpotFleetRequestLaunchTemplateWithOverridesConfig(rName, publicKey, validUntil string) string { +func testAccSpotFleetRequestLaunchTemplateWithInstanceTypeOverridesConfig(rName, publicKey, validUntil string) string { return acctest.ConfigCompose(testAccSpotFleetRequestBaseConfig(rName, publicKey), fmt.Sprintf(` resource "aws_launch_template" "test" { name = %[1]q @@ -2099,6 +2163,62 @@ resource "aws_spot_fleet_request" "test" { `, rName, validUntil)) } +func testAccSpotFleetRequestLaunchTemplateWithInstanceRequirementsOverridesConfig(rName, publicKey, validUntil string) string { + return acctest.ConfigCompose(testAccSpotFleetRequestBaseConfig(rName, publicKey), fmt.Sprintf(` +resource "aws_launch_template" "test" { + name = %[1]q + image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = data.aws_ec2_instance_type_offering.available.instance_type + key_name = aws_key_pair.test.key_name + + tag_specifications { + resource_type = "instance" + + tags = { + Name = %[1]q + } + } +} + +resource "aws_spot_fleet_request" "test" { + iam_fleet_role = aws_iam_role.test.arn + spot_price = "0.05" + target_capacity = 2 + valid_until = %[2]q + terminate_instances_with_expiration = true + instance_interruption_behaviour = "stop" + wait_for_fulfillment = true + + launch_template_config { + launch_template_specification { + name = aws_launch_template.test.name + version = aws_launch_template.test.latest_version + } + + overrides { + availability_zone = data.aws_availability_zones.available.names[2] + + instance_requirements { + vcpu_count { + min = 1 + max = 8 + } + + memory_mib { + min = 500 + max = 50000 + } + + instance_generations = ["current"] + } + } + } + + depends_on = [aws_iam_policy_attachment.test] +} +`, rName, validUntil)) +} + func testAccSpotFleetRequestExcessCapacityTerminationConfig(rName, publicKey, validUntil string) string { return acctest.ConfigCompose(testAccSpotFleetRequestBaseConfig(rName, publicKey), fmt.Sprintf(` resource "aws_spot_fleet_request" "test" { diff --git a/website/docs/r/spot_fleet_request.html.markdown b/website/docs/r/spot_fleet_request.html.markdown index 0b54e51303cd..d42c1af645ff 100644 --- a/website/docs/r/spot_fleet_request.html.markdown +++ b/website/docs/r/spot_fleet_request.html.markdown @@ -276,12 +276,112 @@ The `launch_template_config` block supports the following: ### Overrides * `availability_zone` - (Optional) The availability zone in which to place the request. +* `instance_requirements` - (Optional) The instance requirements. See below. * `instance_type` - (Optional) The type of instance to request. * `priority` - (Optional) The priority for the launch template override. The lower the number, the higher the priority. If no number is set, the launch template override has the lowest priority. * `spot_price` - (Optional) The maximum spot bid for this override request. * `subnet_id` - (Optional) The subnet in which to launch the requested instance. * `weighted_capacity` - (Optional) The capacity added to the fleet by a fulfilled request. +### Instance Requirements + +This configuration block supports the following: + +* `accelerator_count` - (Optional) Block describing the minimum and maximum number of accelerators (GPUs, FPGAs, or AWS Inferentia chips). Default is no minimum or maximum. + * `min` - (Optional) Minimum. + * `max` - (Optional) Maximum. Set to `0` to exclude instance types with accelerators. +* `accelerator_manufacturers` - (Optional) List of accelerator manufacturer names. Default is any manufacturer. + + ``` + Valid names: + * amazon-web-services + * amd + * nvidia + * xilinx + ``` + +* `accelerator_names` - (Optional) List of accelerator names. Default is any acclerator. + + ``` + Valid names: + * a100 - NVIDIA A100 GPUs + * v100 - NVIDIA V100 GPUs + * k80 - NVIDIA K80 GPUs + * t4 - NVIDIA T4 GPUs + * m60 - NVIDIA M60 GPUs + * radeon-pro-v520 - AMD Radeon Pro V520 GPUs + * vu9p - Xilinx VU9P FPGAs + ``` + +* `accelerator_total_memory_mib` - (Optional) Block describing the minimum and maximum total memory of the accelerators. Default is no minimum or maximum. + * `min` - (Optional) Minimum. + * `max` - (Optional) Maximum. +* `accelerator_types` - (Optional) List of accelerator types. Default is any accelerator type. + + ``` + Valid types: + * fpga + * gpu + * inference + ``` + +* `bare_metal` - (Optional) Indicate whether bare metal instace types should be `included`, `excluded`, or `required`. Default is `excluded`. +* `baseline_ebs_bandwidth_mbps` - (Optional) Block describing the minimum and maximum baseline EBS bandwidth, in Mbps. Default is no minimum or maximum. + * `min` - (Optional) Minimum. + * `max` - (Optional) Maximum. +* `burstable_performance` - (Optional) Indicate whether burstable performance instance types should be `included`, `excluded`, or `required`. Default is `excluded`. +* `cpu_manufacturers` (Optional) List of CPU manufacturer names. Default is any manufacturer. + + ~> **NOTE**: Don't confuse the CPU hardware manufacturer with the CPU hardware architecture. Instances will be launched with a compatible CPU architecture based on the Amazon Machine Image (AMI) that you specify in your launch template. + + ``` + Valid names: + * amazon-web-services + * amd + * intel + ``` + +* `excluded_instance_types` - (Optional) List of instance types to exclude. You can use strings with one or more wild cards, represented by an asterisk (\*). The following are examples: `c5*`, `m5a.*`, `r*`, `*3*`. For example, if you specify `c5*`, you are excluding the entire C5 instance family, which includes all C5a and C5n instance types. If you specify `m5a.*`, you are excluding all the M5a instance types, but not the M5n instance types. Maximum of 400 entries in the list; each entry is limited to 30 characters. Default is no excluded instance types. +* `instance_generations` - (Optional) List of instance generation names. Default is any generation. + + ``` + Valid names: + * current - Recommended for best performance. + * previous - For existing applications optimized for older instance types. + ``` + +* `local_storage` - (Optional) Indicate whether instance types with local storage volumes are `included`, `excluded`, or `required`. Default is `included`. +* `local_storage_types` - (Optional) List of local storage type names. Default any storage type. + + ``` + Value names: + * hdd - hard disk drive + * ssd - solid state drive + ``` + +* `memory_gib_per_vcpu` - (Optional) Block describing the minimum and maximum amount of memory (GiB) per vCPU. Default is no minimum or maximum. + * `min` - (Optional) Minimum. May be a decimal number, e.g. `0.5`. + * `max` - (Optional) Maximum. May be a decimal number, e.g. `0.5`. +* `memory_mib` - (Optional) Block describing the minimum and maximum amount of memory (MiB). Default is no maximum. + * `min` - (Optional) Minimum. + * `max` - (Optional) Maximum. +* `network_interface_count` - (Optional) Block describing the minimum and maximum number of network interfaces. Default is no minimum or maximum. + * `min` - (Optional) Minimum. + * `max` - (Optional) Maximum. +* `on_demand_max_price_percentage_over_lowest_price` - (Optional) The price protection threshold for On-Demand Instances. This is the maximum you’ll pay for an On-Demand Instance, expressed as a percentage higher than the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 Auto Scaling selects instance types with your attributes, we will exclude instance types whose price is higher than your threshold. The parameter accepts an integer, which Amazon EC2 Auto Scaling interprets as a percentage. To turn off price protection, specify a high value, such as 999999. Default is 20. + + If you set DesiredCapacityType to vcpu or memory-mib, the price protection threshold is applied based on the per vCPU or per memory price instead of the per instance price. +* `require_hibernate_support` - (Optional) Indicate whether instance types must support On-Demand Instance Hibernation, either `true` or `false`. Default is `false`. +* `spot_max_price_percentage_over_lowest_price` - (Optional) The price protection threshold for Spot Instances. This is the maximum you’ll pay for a Spot Instance, expressed as a percentage higher than the cheapest M, C, or R instance type with your specified attributes. When Amazon EC2 Auto Scaling selects instance types with your attributes, we will exclude instance types whose price is higher than your threshold. The parameter accepts an integer, which Amazon EC2 Auto Scaling interprets as a percentage. To turn off price protection, specify a high value, such as 999999. Default is 100. + + If you set DesiredCapacityType to vcpu or memory-mib, the price protection threshold is applied based on the per vCPU or per memory price instead of the per instance price. +* `total_local_storage_gb` - (Optional) Block describing the minimum and maximum total local storage (GB). Default is no minimum or maximum. + * `min` - (Optional) Minimum. May be a decimal number, e.g. `0.5`. + * `max` - (Optional) Maximum. May be a decimal number, e.g. `0.5`. +* `vcpu_count` - (Optional) Block describing the minimum and maximum number of vCPUs. Default is no maximum. + * `min` - (Optional) Minimum. + * `max` - (Optional) Maximum. + ### Timeouts The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) for certain actions: