From fde210719dfc932909be9fab3795801bb01acf8b Mon Sep 17 00:00:00 2001 From: kl4w Date: Sun, 10 Dec 2017 22:16:50 -0500 Subject: [PATCH 1/7] create launch template resource --- aws/provider.go | 1 + aws/resource_aws_launch_template.go | 970 +++++++++++++++++++++++ aws/resource_aws_launch_template_test.go | 199 +++++ aws/validators.go | 12 + aws/validators_test.go | 42 + 5 files changed, 1224 insertions(+) create mode 100644 aws/resource_aws_launch_template.go create mode 100644 aws/resource_aws_launch_template_test.go diff --git a/aws/provider.go b/aws/provider.go index 42de2ffeab1..5cfec0832d3 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -422,6 +422,7 @@ func Provider() terraform.ResourceProvider { "aws_lambda_alias": resourceAwsLambdaAlias(), "aws_lambda_permission": resourceAwsLambdaPermission(), "aws_launch_configuration": resourceAwsLaunchConfiguration(), + "aws_launch_template": resourceAwsLaunchTemplate(), "aws_lightsail_domain": resourceAwsLightsailDomain(), "aws_lightsail_instance": resourceAwsLightsailInstance(), "aws_lightsail_key_pair": resourceAwsLightsailKeyPair(), diff --git a/aws/resource_aws_launch_template.go b/aws/resource_aws_launch_template.go new file mode 100644 index 00000000000..b80303444ac --- /dev/null +++ b/aws/resource_aws_launch_template.go @@ -0,0 +1,970 @@ +package aws + +import ( + "fmt" + "log" + "strconv" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +const awsSpotInstanceTimeLayout = "2006-01-02T15:04:05Z" + +func resourceAwsLaunchTemplate() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsLaunchTemplateCreate, + Read: resourceAwsLaunchTemplateRead, + Update: resourceAwsLaunchTemplateUpdate, + Delete: resourceAwsLaunchTemplateDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name_prefix"}, + ValidateFunc: validateLaunchTemplateName, + }, + + "name_prefix": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validateLaunchTemplateName, + }, + + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if len(value) > 255 { + errors = append(errors, fmt.Errorf( + "%q cannot be longer than 255 characters", k)) + } + return + }, + }, + + "client_token": { + Type: schema.TypeString, + Computed: true, + }, + + "default_version": { + Type: schema.TypeInt, + Computed: true, + }, + + "latest_version": { + Type: schema.TypeInt, + Computed: true, + }, + + "block_device_mappings": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "device_name": { + Type: schema.TypeString, + Optional: true, + }, + "no_device": { + Type: schema.TypeString, + Optional: true, + }, + "virtual_name": { + Type: schema.TypeString, + Optional: true, + }, + "ebs": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delete_on_termination": { + Type: schema.TypeBool, + Optional: true, + }, + "encrypted": { + Type: schema.TypeBool, + Optional: true, + }, + "iops": { + Type: schema.TypeInt, + Optional: true, + }, + "kms_key_id": { + Type: schema.TypeString, + Optional: true, + }, + "snapshot_id": { + Type: schema.TypeString, + Optional: true, + }, + "volume_size": { + Type: schema.TypeInt, + Optional: true, + }, + "volume_type": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + + "credit_specification": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cpu_credits": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "disable_api_termination": { + Type: schema.TypeBool, + Optional: true, + }, + + "ebs_optimized": { + Type: schema.TypeBool, + Optional: true, + }, + + "elastic_gpu_specifications": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "iam_instance_profile": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Optional: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "image_id": { + Type: schema.TypeString, + Optional: true, + }, + + "instance_initiated_shutdown_behavior": { + Type: schema.TypeString, + Optional: true, + }, + + "instance_market_options": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "market_type": { + Type: schema.TypeString, + Optional: true, + }, + "spot_options": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "block_duration_minutes": { + Type: schema.TypeInt, + Optional: true, + }, + "instance_interruption_behavior": { + Type: schema.TypeString, + Optional: true, + }, + "max_price": { + Type: schema.TypeString, + Optional: true, + }, + "spot_instance_type": { + Type: schema.TypeString, + Optional: true, + }, + "valid_until": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + + "instance_type": { + Type: schema.TypeString, + Optional: true, + }, + + "kernel_id": { + Type: schema.TypeString, + Optional: true, + }, + + "key_name": { + Type: schema.TypeString, + Optional: true, + }, + + "monitoring": { + Type: schema.TypeBool, + Optional: true, + }, + + "network_interfaces": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "associate_public_ip_address": { + Type: schema.TypeBool, + Optional: true, + }, + "delete_on_termination": { + Type: schema.TypeBool, + Optional: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "device_index": { + Type: schema.TypeInt, + Optional: true, + }, + "security_groups": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "ipv6_address_count": { + Type: schema.TypeInt, + Computed: true, + }, + "ipv6_addresses": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "network_interface_id": { + Type: schema.TypeString, + Computed: true, + }, + "private_ip_address": { + Type: schema.TypeString, + Optional: true, + }, + "ipv4_addresses": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "ipv4_address_count": { + Type: schema.TypeInt, + Computed: true, + }, + "subnet_id": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "placement": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "affinity": { + Type: schema.TypeString, + Optional: true, + }, + "availability_zone": { + Type: schema.TypeString, + Optional: true, + }, + "group_name": { + Type: schema.TypeString, + Optional: true, + }, + "host_id": { + Type: schema.TypeString, + Optional: true, + }, + "spread_domain": { + Type: schema.TypeString, + Optional: true, + }, + "tenancy": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "ram_disk_id": { + Type: schema.TypeString, + Optional: true, + }, + + "security_group_names": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "vpc_security_group_ids": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "tag_specifications": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "resource_type": { + Type: schema.TypeString, + Optional: true, + }, + "tags": tagsSchema(), + }, + }, + }, + + "user_data": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceAwsLaunchTemplateCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + var ltName string + if v, ok := d.GetOk("name"); ok { + ltName = v.(string) + } else if v, ok := d.GetOk("name_prefix"); ok { + ltName = resource.PrefixedUniqueId(v.(string)) + } else { + ltName = resource.UniqueId() + } + + launchTemplateData, err := buildLaunchTemplateData(d, meta) + if err != nil { + return err + } + + launchTemplateDataOpts := &ec2.RequestLaunchTemplateData{ + BlockDeviceMappings: launchTemplateData.BlockDeviceMappings, + CreditSpecification: launchTemplateData.CreditSpecification, + DisableApiTermination: launchTemplateData.DisableApiTermination, + EbsOptimized: launchTemplateData.EbsOptimized, + ElasticGpuSpecifications: launchTemplateData.ElasticGpuSpecifications, + IamInstanceProfile: launchTemplateData.IamInstanceProfile, + ImageId: launchTemplateData.ImageId, + InstanceInitiatedShutdownBehavior: launchTemplateData.InstanceInitiatedShutdownBehavior, + InstanceMarketOptions: launchTemplateData.InstanceMarketOptions, + InstanceType: launchTemplateData.InstanceType, + KernelId: launchTemplateData.KernelId, + KeyName: launchTemplateData.KeyName, + Monitoring: launchTemplateData.Monitoring, + NetworkInterfaces: launchTemplateData.NetworkInterfaces, + Placement: launchTemplateData.Placement, + RamDiskId: launchTemplateData.RamDiskId, + SecurityGroups: launchTemplateData.SecurityGroups, + SecurityGroupIds: launchTemplateData.SecurityGroupIds, + TagSpecifications: launchTemplateData.TagSpecifications, + UserData: launchTemplateData.UserData, + } + + launchTemplateOpts := &ec2.CreateLaunchTemplateInput{ + ClientToken: aws.String(resource.UniqueId()), + LaunchTemplateName: aws.String(ltName), + LaunchTemplateData: launchTemplateDataOpts, + } + + resp, err := conn.CreateLaunchTemplate(launchTemplateOpts) + if err != nil { + return err + } + + launchTemplate := resp.LaunchTemplate + d.SetId(*launchTemplate.LaunchTemplateId) + + log.Printf("[DEBUG] Launch Template created: %q (version %d)", + *launchTemplate.LaunchTemplateId, *launchTemplate.LatestVersionNumber) + + return resourceAwsLaunchTemplateUpdate(d, meta) +} + +func resourceAwsLaunchTemplateRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + log.Printf("[DEBUG] Reading launch template %s", d.Id()) + + dlt, err := conn.DescribeLaunchTemplates(&ec2.DescribeLaunchTemplatesInput{ + LaunchTemplateIds: []*string{aws.String(d.Id())}, + }) + if err != nil { + return fmt.Errorf("Error getting launch template: %s", err) + } + if len(dlt.LaunchTemplates) == 0 { + d.SetId("") + return nil + } + if *dlt.LaunchTemplates[0].LaunchTemplateId != d.Id() { + return fmt.Errorf("Unable to find launch template: %#v", dlt.LaunchTemplates) + } + + log.Printf("[DEBUG] Found launch template %s", d.Id()) + + lt := dlt.LaunchTemplates[0] + d.Set("name", lt.LaunchTemplateName) + d.Set("latest_version", lt.LatestVersionNumber) + d.Set("default_version", lt.DefaultVersionNumber) + d.Set("tags", tagsToMap(lt.Tags)) + + version := strconv.Itoa(int(*lt.LatestVersionNumber)) + dltv, err := conn.DescribeLaunchTemplateVersions(&ec2.DescribeLaunchTemplateVersionsInput{ + LaunchTemplateId: aws.String(d.Id()), + Versions: []*string{aws.String(version)}, + }) + if err != nil { + return err + } + + log.Printf("[DEBUG] Received launch template version %q (version %d)", d.Id(), *lt.LatestVersionNumber) + + ltData := dltv.LaunchTemplateVersions[0].LaunchTemplateData + + d.Set("disable_api_termination", ltData.DisableApiTermination) + d.Set("ebs_optimized", ltData.EbsOptimized) + d.Set("image_id", ltData.ImageId) + d.Set("instance_initiated_shutdown_behavior", ltData.InstanceInitiatedShutdownBehavior) + d.Set("instance_type", ltData.InstanceType) + d.Set("kernel_id", ltData.KernelId) + d.Set("key_name", ltData.KeyName) + d.Set("monitoring", ltData.Monitoring) + d.Set("ram_dist_id", ltData.RamDiskId) + d.Set("user_data", ltData.UserData) + + if err := d.Set("block_device_mappings", getBlockDeviceMappings(ltData.BlockDeviceMappings)); err != nil { + return err + } + + if err := d.Set("credit_specification", getCreditSpecification(ltData.CreditSpecification)); err != nil { + return err + } + + if err := d.Set("elastic_gpu_specifications", getElasticGpuSpecifications(ltData.ElasticGpuSpecifications)); err != nil { + return err + } + + if err := d.Set("iam_instance_profile", getIamInstanceProfile(ltData.IamInstanceProfile)); err != nil { + return err + } + + if err := d.Set("instance_market_options", getInstanceMarketOptions(ltData.InstanceMarketOptions)); err != nil { + return err + } + + if err := d.Set("network_interfaces", getNetworkInterfaces(ltData.NetworkInterfaces)); err != nil { + return err + } + + if err := d.Set("placement", getPlacement(ltData.Placement)); err != nil { + return err + } + + if err := d.Set("tag_specifications", getTagSpecifications(ltData.TagSpecifications)); err != nil { + return err + } + + return nil +} + +func resourceAwsLaunchTemplateUpdate(d *schema.ResourceData, meta interface{}) error { + return resourceAwsLaunchTemplateRead(d, meta) +} + +func resourceAwsLaunchTemplateDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + log.Printf("[DEBUG] Launch Template destroy: %v", d.Id()) + _, err := conn.DeleteLaunchTemplate(&ec2.DeleteLaunchTemplateInput{ + LaunchTemplateId: aws.String(d.Id()), + }) + if err != nil { + return err + } + + log.Printf("[DEBUG] Launch Template deleted: %v", d.Id()) + return nil +} + +func getBlockDeviceMappings(m []*ec2.LaunchTemplateBlockDeviceMapping) []interface{} { + s := []interface{}{} + for _, v := range m { + mapping := map[string]interface{}{ + "device_name": *v.DeviceName, + "virtual_name": *v.VirtualName, + } + if v.NoDevice != nil { + mapping["no_device"] = *v.NoDevice + } + if v.Ebs != nil { + ebs := map[string]interface{}{ + "delete_on_termination": *v.Ebs.DeleteOnTermination, + "encrypted": *v.Ebs.Encrypted, + "volume_size": *v.Ebs.VolumeSize, + "volume_type": *v.Ebs.VolumeType, + } + if v.Ebs.Iops != nil { + ebs["iops"] = *v.Ebs.Iops + } + if v.Ebs.KmsKeyId != nil { + ebs["kms_key_id"] = *v.Ebs.KmsKeyId + } + if v.Ebs.SnapshotId != nil { + ebs["snapshot_id"] = *v.Ebs.SnapshotId + } + + mapping["ebs"] = ebs + } + s = append(s, mapping) + } + return s +} + +func getCreditSpecification(cs *ec2.CreditSpecification) []interface{} { + s := []interface{}{} + if cs != nil { + s = append(s, map[string]interface{}{ + "cpu_credits": *cs.CpuCredits, + }) + } + return s +} + +func getElasticGpuSpecifications(e []*ec2.ElasticGpuSpecificationResponse) []interface{} { + s := []interface{}{} + for _, v := range e { + s = append(s, map[string]interface{}{ + "type": *v.Type, + }) + } + return s +} + +func getIamInstanceProfile(i *ec2.LaunchTemplateIamInstanceProfileSpecification) []interface{} { + s := []interface{}{} + if i != nil { + s = append(s, map[string]interface{}{ + "arn": *i.Arn, + "name": *i.Name, + }) + } + return s +} + +func getInstanceMarketOptions(m *ec2.LaunchTemplateInstanceMarketOptions) []interface{} { + s := []interface{}{} + if m != nil { + spot := []interface{}{} + so := m.SpotOptions + if so != nil { + spot = append(spot, map[string]interface{}{ + "block_duration_minutes": *so.BlockDurationMinutes, + "instance_interruption_behavior": *so.InstanceInterruptionBehavior, + "max_price": *so.MaxPrice, + "spot_instance_type": *so.SpotInstanceType, + "valid_until": *so.ValidUntil, + }) + } + s = append(s, map[string]interface{}{ + "market_type": *m.MarketType, + "spot_options": spot, + }) + } + return s +} + +func getNetworkInterfaces(n []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecification) []interface{} { + s := []interface{}{} + for _, v := range n { + var ipv6Addresses []string + var ipv4Addresses []string + + networkInterface := map[string]interface{}{ + "associate_public_ip_address": *v.AssociatePublicIpAddress, + "delete_on_termination": *v.DeleteOnTermination, + "description": *v.Description, + "device_index": int(*v.DeviceIndex), + "ipv6_address_count": int(*v.Ipv6AddressCount), + "network_interface_id": *v.NetworkInterfaceId, + "private_ip_address": *v.PrivateIpAddress, + "ipv4_address_count": int(*v.SecondaryPrivateIpAddressCount), + "subnet_id": *v.SubnetId, + } + + for _, address := range v.Ipv6Addresses { + ipv6Addresses = append(ipv6Addresses, *address.Ipv6Address) + } + networkInterface["ipv6_addresses"] = ipv6Addresses + + for _, address := range v.PrivateIpAddresses { + ipv4Addresses = append(ipv4Addresses, *address.PrivateIpAddress) + } + networkInterface["ipv4_addresses"] = ipv4Addresses + + s = append(s, networkInterface) + } + return s +} + +func getPlacement(p *ec2.LaunchTemplatePlacement) []interface{} { + s := []interface{}{} + if p != nil { + s = append(s, map[string]interface{}{ + "affinity": *p.Affinity, + "availability_zone": *p.AvailabilityZone, + "group_name": *p.GroupName, + "host_id": *p.HostId, + "spread_domain": *p.SpreadDomain, + "tenancy": *p.Tenancy, + }) + } + return s +} + +func getTagSpecifications(t []*ec2.LaunchTemplateTagSpecification) []interface{} { + s := []interface{}{} + for _, v := range t { + s = append(s, map[string]interface{}{ + "resource_type": v.ResourceType, + "tags": tagsToMap(v.Tags), + }) + } + return s +} + +type launchTemplateOpts struct { + BlockDeviceMappings []*ec2.LaunchTemplateBlockDeviceMappingRequest + CreditSpecification *ec2.CreditSpecificationRequest + DisableApiTermination *bool + EbsOptimized *bool + ElasticGpuSpecifications []*ec2.ElasticGpuSpecification + IamInstanceProfile *ec2.LaunchTemplateIamInstanceProfileSpecificationRequest + ImageId *string + InstanceInitiatedShutdownBehavior *string + InstanceMarketOptions *ec2.LaunchTemplateInstanceMarketOptionsRequest + InstanceType *string + KernelId *string + KeyName *string + Monitoring *ec2.LaunchTemplatesMonitoringRequest + NetworkInterfaces []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest + Placement *ec2.LaunchTemplatePlacementRequest + RamDiskId *string + SecurityGroupIds []*string + SecurityGroups []*string + TagSpecifications []*ec2.LaunchTemplateTagSpecificationRequest + UserData *string +} + +func buildLaunchTemplateData(d *schema.ResourceData, meta interface{}) (*launchTemplateOpts, error) { + opts := &launchTemplateOpts{ + DisableApiTermination: aws.Bool(d.Get("disable_api_termination").(bool)), + EbsOptimized: aws.Bool(d.Get("ebs_optimized").(bool)), + ImageId: aws.String(d.Get("image_id").(string)), + InstanceInitiatedShutdownBehavior: aws.String(d.Get("instance_initiated_shutdown_behavior").(string)), + InstanceType: aws.String(d.Get("instance_type").(string)), + KernelId: aws.String(d.Get("kernel_id").(string)), + KeyName: aws.String(d.Get("key_name").(string)), + RamDiskId: aws.String(d.Get("ram_disk_id").(string)), + UserData: aws.String(d.Get("user_data").(string)), + } + + if v, ok := d.GetOk("block_device_mappings"); ok { + var blockDeviceMappings []*ec2.LaunchTemplateBlockDeviceMappingRequest + bdms := v.(*schema.Set).List() + + for _, bdm := range bdms { + blockDeviceMap := bdm.(map[string]interface{}) + blockDeviceMappings = append(blockDeviceMappings, readBlockDeviceMappingFromConfig(blockDeviceMap)) + } + opts.BlockDeviceMappings = blockDeviceMappings + } + + if v, ok := d.GetOk("credit_specification"); ok { + cs := v.(*schema.Set).List() + + if len(cs) > 0 { + csData := cs[0].(map[string]interface{}) + csr := &ec2.CreditSpecificationRequest{ + CpuCredits: aws.String(csData["cpu_credits"].(string)), + } + opts.CreditSpecification = csr + } + } + + if v, ok := d.GetOk("elastic_gpu_specifications"); ok { + var elasticGpuSpecifications []*ec2.ElasticGpuSpecification + egsList := v.(*schema.Set).List() + + for _, egs := range egsList { + elasticGpuSpecification := egs.(map[string]interface{}) + elasticGpuSpecifications = append(elasticGpuSpecifications, &ec2.ElasticGpuSpecification{ + Type: aws.String(elasticGpuSpecification["type"].(string)), + }) + } + opts.ElasticGpuSpecifications = elasticGpuSpecifications + } + + if v, ok := d.GetOk("iam_instance_profile"); ok { + iip := v.(*schema.Set).List() + + if len(iip) > 0 { + iipData := iip[0].(map[string]interface{}) + iamInstanceProfile := &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{ + Arn: aws.String(iipData["arn"].(string)), + Name: aws.String(iipData["name"].(string)), + } + opts.IamInstanceProfile = iamInstanceProfile + } + } + + if v, ok := d.GetOk("instance_market_options"); ok { + imo := v.(*schema.Set).List() + + if len(imo) > 0 { + imoData := imo[0].(map[string]interface{}) + spotOptions := &ec2.LaunchTemplateSpotMarketOptionsRequest{} + + if v := imoData["spot_options"]; v != nil { + so := v.(map[string]interface{}) + spotOptions.BlockDurationMinutes = aws.Int64(int64(so["block_duration_minutes"].(int))) + spotOptions.InstanceInterruptionBehavior = aws.String(so["instance_interruption_behavior"].(string)) + spotOptions.MaxPrice = aws.String(so["max_price"].(string)) + spotOptions.SpotInstanceType = aws.String(so["spot_instance_type"].(string)) + + t, err := time.Parse(awsSpotInstanceTimeLayout, so["valid_until"].(string)) + if err != nil { + return nil, fmt.Errorf("Error Parsing Launch Template Spot Options valid until: %s", err.Error()) + } + spotOptions.ValidUntil = aws.Time(t) + } + + instanceMarketOptions := &ec2.LaunchTemplateInstanceMarketOptionsRequest{ + MarketType: aws.String(imoData["market_type"].(string)), + SpotOptions: spotOptions, + } + + opts.InstanceMarketOptions = instanceMarketOptions + } + } + + if v, ok := d.GetOk("monitoring"); ok { + monitoring := &ec2.LaunchTemplatesMonitoringRequest{ + Enabled: aws.Bool(v.(bool)), + } + opts.Monitoring = monitoring + } + + if v, ok := d.GetOk("network_interfaces"); ok { + var networkInterfaces []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest + niList := v.(*schema.Set).List() + + for _, ni := range niList { + var ipv4Addresses []*ec2.PrivateIpAddressSpecification + var ipv6Addresses []*ec2.InstanceIpv6AddressRequest + ni := ni.(map[string]interface{}) + + privateIpAddress := ni["private_ip_address"].(string) + networkInterface := &ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{ + AssociatePublicIpAddress: aws.Bool(ni["associate_public_ip_address"].(bool)), + DeleteOnTermination: aws.Bool(ni["delete_on_termination"].(bool)), + Description: aws.String(ni["description"].(string)), + DeviceIndex: aws.Int64(int64(ni["device_index"].(int))), + NetworkInterfaceId: aws.String(ni["network_interface_id"].(string)), + PrivateIpAddress: aws.String(privateIpAddress), + SubnetId: aws.String(ni["subnet_id"].(string)), + } + + ipv6AddressList := ni["ipv6_addresses"].(*schema.Set).List() + for _, address := range ipv6AddressList { + ipv6Addresses = append(ipv6Addresses, &ec2.InstanceIpv6AddressRequest{ + Ipv6Address: aws.String(address.(string)), + }) + } + networkInterface.Ipv6AddressCount = aws.Int64(int64(len(ipv6AddressList))) + networkInterface.Ipv6Addresses = ipv6Addresses + + ipv4AddressList := ni["ipv4_addresses"].(*schema.Set).List() + for _, address := range ipv4AddressList { + privateIp := &ec2.PrivateIpAddressSpecification{ + Primary: aws.Bool(address.(string) == privateIpAddress), + PrivateIpAddress: aws.String(address.(string)), + } + ipv4Addresses = append(ipv4Addresses, privateIp) + } + networkInterface.SecondaryPrivateIpAddressCount = aws.Int64(int64(len(ipv4AddressList))) + networkInterface.PrivateIpAddresses = ipv4Addresses + + networkInterfaces = append(networkInterfaces, networkInterface) + } + opts.NetworkInterfaces = networkInterfaces + } + + if v, ok := d.GetOk("placement"); ok { + p := v.(*schema.Set).List() + + if len(p) > 0 { + pData := p[0].(map[string]interface{}) + placement := &ec2.LaunchTemplatePlacementRequest{ + Affinity: aws.String(pData["affinity"].(string)), + AvailabilityZone: aws.String(pData["availability_zone"].(string)), + GroupName: aws.String(pData["group_name"].(string)), + HostId: aws.String(pData["host_id"].(string)), + SpreadDomain: aws.String(pData["spread_domain"].(string)), + Tenancy: aws.String(pData["tenancy"].(string)), + } + opts.Placement = placement + } + } + + if v, ok := d.GetOk("tag_specifications"); ok { + var tagSpecifications []*ec2.LaunchTemplateTagSpecificationRequest + t := v.(*schema.Set).List() + + for _, ts := range t { + tsData := ts.(map[string]interface{}) + tags := tagsFromMap(tsData) + tagSpecification := &ec2.LaunchTemplateTagSpecificationRequest{ + ResourceType: aws.String(tsData["resource_type"].(string)), + Tags: tags, + } + tagSpecifications = append(tagSpecifications, tagSpecification) + } + opts.TagSpecifications = tagSpecifications + } + + return opts, nil +} + +func readBlockDeviceMappingFromConfig(bdm map[string]interface{}) *ec2.LaunchTemplateBlockDeviceMappingRequest { + blockDeviceMapping := &ec2.LaunchTemplateBlockDeviceMappingRequest{} + + if v := bdm["device_name"]; v != nil { + blockDeviceMapping.DeviceName = aws.String(v.(string)) + } + + if v := bdm["no_device"]; v != nil { + blockDeviceMapping.NoDevice = aws.String(v.(string)) + } + + if v := bdm["virtual_name"]; v != nil { + blockDeviceMapping.VirtualName = aws.String(v.(string)) + } + + if v := bdm["ebs"]; v.(*schema.Set).Len() > 0 { + ebs := v.(*schema.Set).List() + if len(ebs) > 0 { + ebsData := ebs[0] + //log.Printf("ebsData: %+v\n", ebsData) + blockDeviceMapping.Ebs = readEbsBlockDeviceFromConfig(ebsData.(map[string]interface{})) + } + } + + //log.Printf("block device mapping: %+v\n", *blockDeviceMapping) + return blockDeviceMapping +} + +func readEbsBlockDeviceFromConfig(ebs map[string]interface{}) *ec2.LaunchTemplateEbsBlockDeviceRequest { + ebsDevice := &ec2.LaunchTemplateEbsBlockDeviceRequest{} + + if v := ebs["delete_on_termination"]; v != nil { + ebsDevice.DeleteOnTermination = aws.Bool(v.(bool)) + } + + if v := ebs["encrypted"]; v != nil { + ebsDevice.Encrypted = aws.Bool(v.(bool)) + } + + if v := ebs["iops"]; v != nil { + ebsDevice.Iops = aws.Int64(int64(v.(int))) + } + + if v := ebs["kms_key_id"]; v != nil { + ebsDevice.KmsKeyId = aws.String(v.(string)) + } + + if v := ebs["snapshot_id"]; v != nil { + ebsDevice.SnapshotId = aws.String(v.(string)) + } + + if v := ebs["volume_size"]; v != nil { + ebsDevice.VolumeSize = aws.Int64(int64(v.(int))) + } + + if v := ebs["volume_type"]; v != nil { + ebsDevice.VolumeType = aws.String(v.(string)) + } + + return ebsDevice +} diff --git a/aws/resource_aws_launch_template_test.go b/aws/resource_aws_launch_template_test.go new file mode 100644 index 00000000000..68ad8fb7f3d --- /dev/null +++ b/aws/resource_aws_launch_template_test.go @@ -0,0 +1,199 @@ +package aws + +import ( + "fmt" + "log" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSLaunchTemplate_basic(t *testing.T) { + var template ec2.LaunchTemplate + resName := "aws_launch_template.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLaunchTemplateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLaunchTemplateConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLaunchTemplateExists(resName, &template), + resource.TestCheckResourceAttr(resName, "default_version", "1"), + resource.TestCheckResourceAttr(resName, "latest_version", "1"), + ), + }, + }, + }) +} + +func TestAccAWSLaunchTemplate_data(t *testing.T) { + var template ec2.LaunchTemplate + resName := "aws_launch_template.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLaunchTemplateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLaunchTemplateConfig_data, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLaunchTemplateExists(resName, &template), + resource.TestCheckResourceAttr(resName, "block_device_mappings.#", "1"), + resource.TestCheckResourceAttr(resName, "credit_specification.#", "1"), + resource.TestCheckResourceAttrSet(resName, "disable_api_termination"), + resource.TestCheckResourceAttr(resName, "elastic_gpu_specifications.#", "1"), + resource.TestCheckResourceAttr(resName, "iam_instance_profile.#", "1"), + resource.TestCheckResourceAttrSet(resName, "image_id"), + resource.TestCheckResourceAttrSet(resName, "instance_initiated_shutdown_behavior"), + resource.TestCheckResourceAttr(resName, "instance_market_options.#", "1"), + resource.TestCheckResourceAttrSet(resName, "instance_type"), + resource.TestCheckResourceAttrSet(resName, "kernel_id"), + resource.TestCheckResourceAttrSet(resName, "key_name"), + resource.TestCheckResourceAttrSet(resName, "monitoring"), + resource.TestCheckResourceAttr(resName, "network_interfaces.#", "1"), + resource.TestCheckResourceAttr(resName, "placement.#", "1"), + resource.TestCheckResourceAttrSet(resName, "ram_disk_id"), + resource.TestCheckResourceAttr(resName, "vpc_security_group_ids.#", "1"), + resource.TestCheckResourceAttr(resName, "tag_specifications.#", "1"), + resource.TestCheckResourceAttr(resName, "tags.#", "1"), + ), + }, + }, + }) +} + +func testAccCheckAWSLaunchTemplateExists(n string, t *ec2.LaunchTemplate) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Launch Template ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + resp, err := conn.DescribeLaunchTemplates(&ec2.DescribeLaunchTemplatesInput{ + LaunchTemplateIds: []*string{aws.String(rs.Primary.ID)}, + }) + if err != nil { + return err + } + + if len(resp.LaunchTemplates) != 1 || *resp.LaunchTemplates[0].LaunchTemplateId != rs.Primary.ID { + return fmt.Errorf("Launch Template not found") + } + + *t = *resp.LaunchTemplates[0] + + return nil + } +} + +func testAccCheckAWSLaunchTemplateDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_launch_template" { + continue + } + + resp, err := conn.DescribeLaunchTemplates(&ec2.DescribeLaunchTemplatesInput{ + LaunchTemplateIds: []*string{aws.String(rs.Primary.ID)}, + }) + + if err == nil { + if len(resp.LaunchTemplates) != 0 && *resp.LaunchTemplates[0].LaunchTemplateId == rs.Primary.ID { + return fmt.Errorf("Launch Template still exists") + } + } + + ae, ok := err.(awserr.Error) + if !ok { + return err + } + if ae.Code() != "InvalidLaunchTemplateId.NotFound" { + log.Printf("aws error code: %s", ae.Code()) + return err + } + } + + return nil +} + +const testAccAWSLaunchTemplateConfig_basic = ` +resource "aws_launch_template" "foo" { + name = "foo" +} +` + +const testAccAWSLaunchTemplateConfig_data = ` +resource "aws_launch_template" "foo" { + name = "foo" + + block_device_mappings { + device_name = "test" + } + + credit_specification { + cpu_credits = "standard" + } + + disable_api_termination = true + + ebs_optimized = true + + elastic_gpu_specifications { + type = "test" + } + + iam_instance_profile { + name = "test" + } + + image_id = "ami-test" + + instance_initiated_shutdown_behavior = "test" + + instance_market_options { + market_type = "test" + } + + instance_type = "t2.micro" + + kernel_id = "test" + + key_name = "test" + + monitoring = true + + network_interfaces { + associate_public_ip_address = true + } + + placement { + availability_zone = "test" + } + + ram_disk_id = "test" + + vpc_security_group_ids = ["test"] + + tag_specifications { + resource_type = "instance" + tags { + Name = "test" + } + } +} +` diff --git a/aws/validators.go b/aws/validators.go index a20a0321941..05b0ab4ef91 100644 --- a/aws/validators.go +++ b/aws/validators.go @@ -1740,3 +1740,15 @@ func validateDynamoDbTableAttributes(d *schema.ResourceDiff) error { return nil } + +func validateLaunchTemplateName(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if strings.HasSuffix(k, "prefix") && len(value) > 99 { + errors = append(errors, fmt.Errorf("%q cannot be longer than 99 characters, name is limited to 125", k)) + } else if !strings.HasSuffix(k, "prefix") && len(value) > 125 { + errors = append(errors, fmt.Errorf("%q cannot be longer than 125 characters", k)) + } else if !regexp.MustCompile(`^[0-9a-zA-Z()./_]+$`).MatchString(value) { + errors = append(errors, fmt.Errorf("%q can only alphanumeric characters and ()./_ symbols", k)) + } + return +} diff --git a/aws/validators_test.go b/aws/validators_test.go index e32679e35cc..3b52c162907 100644 --- a/aws/validators_test.go +++ b/aws/validators_test.go @@ -2521,3 +2521,45 @@ func TestValidateAmazonSideAsn(t *testing.T) { } } } + +func TestValidateLaunchTemplateName(t *testing.T) { + validNames := []string{ + "fooBAR123", + "(./_)", + } + for _, v := range validNames { + _, errors := validateLaunchTemplateName(v, "name") + if len(errors) != 0 { + t.Fatalf("%q should be a valid Launch Template name: %q", v, errors) + } + } + + invalidNames := []string{ + "tf", + strings.Repeat("W", 126), // > 125 + "invalid-", + "invalid*", + "invalid\name", + "inavalid&", + "invalid+", + "invalid!", + "invalid:", + "invalid;", + } + for _, v := range invalidNames { + _, errors := validateLaunchTemplateName(v, "name") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid Launch Template name: %q", v, errors) + } + } + + invalidNamePrefixes := []string{ + strings.Repeat("W", 100), // > 99 + } + for _, v := range invalidNamePrefixes { + _, errors := validateLaunchTemplateName(v, "name_prefix") + if len(errors) == 0 { + t.Fatalf("%q should be an invalid Launch Template name prefix: %q", v, errors) + } + } +} \ No newline at end of file From 237439ad59c47cfbd2af867cde5017b6b95d55f7 Mon Sep 17 00:00:00 2001 From: kl4w Date: Tue, 23 Jan 2018 22:03:21 -0500 Subject: [PATCH 2/7] implement update function --- aws/resource_aws_launch_template.go | 158 ++++++++++++----------- aws/resource_aws_launch_template_test.go | 1 - aws/validators_test.go | 2 +- 3 files changed, 83 insertions(+), 78 deletions(-) diff --git a/aws/resource_aws_launch_template.go b/aws/resource_aws_launch_template.go index b80303444ac..2f8c9338fd0 100644 --- a/aws/resource_aws_launch_template.go +++ b/aws/resource_aws_launch_template.go @@ -410,33 +410,10 @@ func resourceAwsLaunchTemplateCreate(d *schema.ResourceData, meta interface{}) e return err } - launchTemplateDataOpts := &ec2.RequestLaunchTemplateData{ - BlockDeviceMappings: launchTemplateData.BlockDeviceMappings, - CreditSpecification: launchTemplateData.CreditSpecification, - DisableApiTermination: launchTemplateData.DisableApiTermination, - EbsOptimized: launchTemplateData.EbsOptimized, - ElasticGpuSpecifications: launchTemplateData.ElasticGpuSpecifications, - IamInstanceProfile: launchTemplateData.IamInstanceProfile, - ImageId: launchTemplateData.ImageId, - InstanceInitiatedShutdownBehavior: launchTemplateData.InstanceInitiatedShutdownBehavior, - InstanceMarketOptions: launchTemplateData.InstanceMarketOptions, - InstanceType: launchTemplateData.InstanceType, - KernelId: launchTemplateData.KernelId, - KeyName: launchTemplateData.KeyName, - Monitoring: launchTemplateData.Monitoring, - NetworkInterfaces: launchTemplateData.NetworkInterfaces, - Placement: launchTemplateData.Placement, - RamDiskId: launchTemplateData.RamDiskId, - SecurityGroups: launchTemplateData.SecurityGroups, - SecurityGroupIds: launchTemplateData.SecurityGroupIds, - TagSpecifications: launchTemplateData.TagSpecifications, - UserData: launchTemplateData.UserData, - } - launchTemplateOpts := &ec2.CreateLaunchTemplateInput{ ClientToken: aws.String(resource.UniqueId()), LaunchTemplateName: aws.String(ltName), - LaunchTemplateData: launchTemplateDataOpts, + LaunchTemplateData: launchTemplateData, } resp, err := conn.CreateLaunchTemplate(launchTemplateOpts) @@ -540,6 +517,26 @@ func resourceAwsLaunchTemplateRead(d *schema.ResourceData, meta interface{}) err } func resourceAwsLaunchTemplateUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + if !d.IsNewResource() { + launchTemplateData, err := buildLaunchTemplateData(d, meta) + if err != nil { + return err + } + + launchTemplateVersionOpts := &ec2.CreateLaunchTemplateVersionInput{ + ClientToken: aws.String(resource.UniqueId()), + LaunchTemplateId: aws.String(d.Id()), + LaunchTemplateData: launchTemplateData, + } + + _, createErr := conn.CreateLaunchTemplateVersion(launchTemplateVersionOpts) + if createErr != nil { + return createErr + } + } + return resourceAwsLaunchTemplateRead(d, meta) } @@ -626,6 +623,9 @@ func getIamInstanceProfile(i *ec2.LaunchTemplateIamInstanceProfileSpecification) func getInstanceMarketOptions(m *ec2.LaunchTemplateInstanceMarketOptions) []interface{} { s := []interface{}{} if m != nil { + mo := map[string]interface{}{ + "market_type": *m.MarketType, + } spot := []interface{}{} so := m.SpotOptions if so != nil { @@ -636,11 +636,9 @@ func getInstanceMarketOptions(m *ec2.LaunchTemplateInstanceMarketOptions) []inte "spot_instance_type": *so.SpotInstanceType, "valid_until": *so.ValidUntil, }) + mo["spot_options"] = spot } - s = append(s, map[string]interface{}{ - "market_type": *m.MarketType, - "spot_options": spot, - }) + s = append(s, mo) } return s } @@ -666,12 +664,16 @@ func getNetworkInterfaces(n []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecifi for _, address := range v.Ipv6Addresses { ipv6Addresses = append(ipv6Addresses, *address.Ipv6Address) } - networkInterface["ipv6_addresses"] = ipv6Addresses + if len(ipv6Addresses) > 0 { + networkInterface["ipv6_addresses"] = ipv6Addresses + } for _, address := range v.PrivateIpAddresses { ipv4Addresses = append(ipv4Addresses, *address.PrivateIpAddress) } - networkInterface["ipv4_addresses"] = ipv4Addresses + if len(ipv4Addresses) > 0 { + networkInterface["ipv4_addresses"] = ipv4Addresses + } s = append(s, networkInterface) } @@ -697,47 +699,48 @@ func getTagSpecifications(t []*ec2.LaunchTemplateTagSpecification) []interface{} s := []interface{}{} for _, v := range t { s = append(s, map[string]interface{}{ - "resource_type": v.ResourceType, + "resource_type": *v.ResourceType, "tags": tagsToMap(v.Tags), }) } return s } -type launchTemplateOpts struct { - BlockDeviceMappings []*ec2.LaunchTemplateBlockDeviceMappingRequest - CreditSpecification *ec2.CreditSpecificationRequest - DisableApiTermination *bool - EbsOptimized *bool - ElasticGpuSpecifications []*ec2.ElasticGpuSpecification - IamInstanceProfile *ec2.LaunchTemplateIamInstanceProfileSpecificationRequest - ImageId *string - InstanceInitiatedShutdownBehavior *string - InstanceMarketOptions *ec2.LaunchTemplateInstanceMarketOptionsRequest - InstanceType *string - KernelId *string - KeyName *string - Monitoring *ec2.LaunchTemplatesMonitoringRequest - NetworkInterfaces []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest - Placement *ec2.LaunchTemplatePlacementRequest - RamDiskId *string - SecurityGroupIds []*string - SecurityGroups []*string - TagSpecifications []*ec2.LaunchTemplateTagSpecificationRequest - UserData *string -} +func buildLaunchTemplateData(d *schema.ResourceData, meta interface{}) (*ec2.RequestLaunchTemplateData, error) { + opts := &ec2.RequestLaunchTemplateData{ + UserData: aws.String(d.Get("user_data").(string)), + } + + if v, ok := d.GetOk("image_id"); ok { + opts.ImageId = aws.String(v.(string)) + } + + if v, ok := d.GetOk("instance_initiated_shutdown_behavior"); ok { + opts.InstanceInitiatedShutdownBehavior = aws.String(v.(string)) + } + + if v, ok := d.GetOk("instance_type"); ok { + opts.InstanceType = aws.String(v.(string)) + } + + if v, ok := d.GetOk("kernel_id"); ok { + opts.KernelId = aws.String(v.(string)) + } -func buildLaunchTemplateData(d *schema.ResourceData, meta interface{}) (*launchTemplateOpts, error) { - opts := &launchTemplateOpts{ - DisableApiTermination: aws.Bool(d.Get("disable_api_termination").(bool)), - EbsOptimized: aws.Bool(d.Get("ebs_optimized").(bool)), - ImageId: aws.String(d.Get("image_id").(string)), - InstanceInitiatedShutdownBehavior: aws.String(d.Get("instance_initiated_shutdown_behavior").(string)), - InstanceType: aws.String(d.Get("instance_type").(string)), - KernelId: aws.String(d.Get("kernel_id").(string)), - KeyName: aws.String(d.Get("key_name").(string)), - RamDiskId: aws.String(d.Get("ram_disk_id").(string)), - UserData: aws.String(d.Get("user_data").(string)), + if v, ok := d.GetOk("key_name"); ok { + opts.KeyName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ram_disk_id"); ok { + opts.RamDiskId = aws.String(v.(string)) + } + + if v, ok := d.GetOk("disable_api_termination"); ok { + opts.DisableApiTermination = aws.Bool(v.(bool)) + } + + if v, ok := d.GetOk("ebs_optimized"); ok { + opts.EbsOptimized = aws.Bool(v.(bool)) } if v, ok := d.GetOk("block_device_mappings"); ok { @@ -796,18 +799,21 @@ func buildLaunchTemplateData(d *schema.ResourceData, meta interface{}) (*launchT imoData := imo[0].(map[string]interface{}) spotOptions := &ec2.LaunchTemplateSpotMarketOptionsRequest{} - if v := imoData["spot_options"]; v != nil { - so := v.(map[string]interface{}) - spotOptions.BlockDurationMinutes = aws.Int64(int64(so["block_duration_minutes"].(int))) - spotOptions.InstanceInterruptionBehavior = aws.String(so["instance_interruption_behavior"].(string)) - spotOptions.MaxPrice = aws.String(so["max_price"].(string)) - spotOptions.SpotInstanceType = aws.String(so["spot_instance_type"].(string)) - - t, err := time.Parse(awsSpotInstanceTimeLayout, so["valid_until"].(string)) - if err != nil { - return nil, fmt.Errorf("Error Parsing Launch Template Spot Options valid until: %s", err.Error()) + if v, ok := imoData["spot_options"]; ok { + vL := v.(*schema.Set).List() + for _, v := range vL { + so := v.(map[string]interface{}) + spotOptions.BlockDurationMinutes = aws.Int64(int64(so["block_duration_minutes"].(int))) + spotOptions.InstanceInterruptionBehavior = aws.String(so["instance_interruption_behavior"].(string)) + spotOptions.MaxPrice = aws.String(so["max_price"].(string)) + spotOptions.SpotInstanceType = aws.String(so["spot_instance_type"].(string)) + + t, err := time.Parse(awsSpotInstanceTimeLayout, so["valid_until"].(string)) + if err != nil { + return nil, fmt.Errorf("Error Parsing Launch Template Spot Options valid until: %s", err.Error()) + } + spotOptions.ValidUntil = aws.Time(t) } - spotOptions.ValidUntil = aws.Time(t) } instanceMarketOptions := &ec2.LaunchTemplateInstanceMarketOptionsRequest{ @@ -894,7 +900,7 @@ func buildLaunchTemplateData(d *schema.ResourceData, meta interface{}) (*launchT for _, ts := range t { tsData := ts.(map[string]interface{}) - tags := tagsFromMap(tsData) + tags := tagsFromMap(tsData["tags"].(map[string]interface{})) tagSpecification := &ec2.LaunchTemplateTagSpecificationRequest{ ResourceType: aws.String(tsData["resource_type"].(string)), Tags: tags, diff --git a/aws/resource_aws_launch_template_test.go b/aws/resource_aws_launch_template_test.go index 68ad8fb7f3d..4f1a6970f84 100644 --- a/aws/resource_aws_launch_template_test.go +++ b/aws/resource_aws_launch_template_test.go @@ -63,7 +63,6 @@ func TestAccAWSLaunchTemplate_data(t *testing.T) { resource.TestCheckResourceAttrSet(resName, "ram_disk_id"), resource.TestCheckResourceAttr(resName, "vpc_security_group_ids.#", "1"), resource.TestCheckResourceAttr(resName, "tag_specifications.#", "1"), - resource.TestCheckResourceAttr(resName, "tags.#", "1"), ), }, }, diff --git a/aws/validators_test.go b/aws/validators_test.go index 3b52c162907..6ae30ec98aa 100644 --- a/aws/validators_test.go +++ b/aws/validators_test.go @@ -2562,4 +2562,4 @@ func TestValidateLaunchTemplateName(t *testing.T) { t.Fatalf("%q should be an invalid Launch Template name prefix: %q", v, errors) } } -} \ No newline at end of file +} From 50eae6dad3cef185bf14cfefd698af65574d7ce6 Mon Sep 17 00:00:00 2001 From: kl4w Date: Tue, 23 Jan 2018 22:18:52 -0500 Subject: [PATCH 3/7] name cannot be less than 3 chars --- aws/validators.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aws/validators.go b/aws/validators.go index 05b0ab4ef91..34fd7ab0232 100644 --- a/aws/validators.go +++ b/aws/validators.go @@ -1743,7 +1743,9 @@ func validateDynamoDbTableAttributes(d *schema.ResourceDiff) error { func validateLaunchTemplateName(v interface{}, k string) (ws []string, errors []error) { value := v.(string) - if strings.HasSuffix(k, "prefix") && len(value) > 99 { + if len(value) < 3 { + errors = append(errors, fmt.Errorf("%q cannot be less than 3 characters", k)) + } else if strings.HasSuffix(k, "prefix") && len(value) > 99 { errors = append(errors, fmt.Errorf("%q cannot be longer than 99 characters, name is limited to 125", k)) } else if !strings.HasSuffix(k, "prefix") && len(value) > 125 { errors = append(errors, fmt.Errorf("%q cannot be longer than 125 characters", k)) From aabb541850cdbb5754222683298a36ce55847963 Mon Sep 17 00:00:00 2001 From: Kash Date: Mon, 26 Feb 2018 10:17:20 -0500 Subject: [PATCH 4/7] add an update test --- aws/resource_aws_launch_template_test.go | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/aws/resource_aws_launch_template_test.go b/aws/resource_aws_launch_template_test.go index 4f1a6970f84..a58d752e1d0 100644 --- a/aws/resource_aws_launch_template_test.go +++ b/aws/resource_aws_launch_template_test.go @@ -69,6 +69,36 @@ func TestAccAWSLaunchTemplate_data(t *testing.T) { }) } +func TestAccAWSLaunchTemplate_update(t *testing.T) { + var template ec2.LaunchTemplate + resName := "aws_launch_template.foo" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLaunchTemplateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLaunchTemplateConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLaunchTemplateExists(resName, &template), + resource.TestCheckResourceAttr(resName, "default_version", "1"), + resource.TestCheckResourceAttr(resName, "latest_version", "1"), + ), + }, + { + Config: testAccAWSLaunchTemplateConfig_data, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSLaunchTemplateExists(resName, &template), + resource.TestCheckResourceAttr(resName, "default_version", "1"), + resource.TestCheckResourceAttr(resName, "latest_version", "2"), + resource.TestCheckResourceAttrSet(resName, "image_id"), + ), + }, + }, + }) +} + func testAccCheckAWSLaunchTemplateExists(n string, t *ec2.LaunchTemplate) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] From 4a26f38be86b7e38a1af6a1b00c93e859b5ca7ff Mon Sep 17 00:00:00 2001 From: Kash Date: Thu, 1 Mar 2018 10:46:24 -0500 Subject: [PATCH 5/7] add documentation for launch template resource --- website/aws.erb | 4 + website/docs/r/launch_template.html.markdown | 235 +++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 website/docs/r/launch_template.html.markdown diff --git a/website/aws.erb b/website/aws.erb index 39911e116cc..f7b183777ec 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -794,6 +794,10 @@ aws_launch_configuration + > + aws_launch_template + + > aws_lb_cookie_stickiness_policy diff --git a/website/docs/r/launch_template.html.markdown b/website/docs/r/launch_template.html.markdown new file mode 100644 index 00000000000..25b39af8901 --- /dev/null +++ b/website/docs/r/launch_template.html.markdown @@ -0,0 +1,235 @@ +--- +layout: "aws" +page_title: "AWS: aws_launch_template" +sidebar_current: "docs-aws-resource-launch-template" +description: |- + Provides an EC2 launch template resource. Can be used to create instances or auto scaling groups. +--- + +# aws_launch_template + +Provides an EC2 launch template resource. Can be used to create instances or auto scaling groups. + +-> **Note:** All arguments are optional except for either `name`, or `name_prefix`. + +## Example Usage + +```hcl +resource "aws_launch_template" "foo" { + name = "foo" + + block_device_mappings { + device_name = "test" + } + + credit_specification { + cpu_credits = "standard" + } + + disable_api_termination = true + + ebs_optimized = true + + elastic_gpu_specifications { + type = "test" + } + + iam_instance_profile { + name = "test" + } + + image_id = "ami-test" + + instance_initiated_shutdown_behavior = "test" + + instance_market_options { + market_type = "test" + } + + instance_type = "t2.micro" + + kernel_id = "test" + + key_name = "test" + + monitoring = true + + network_interfaces { + associate_public_ip_address = true + } + + placement { + availability_zone = "test" + } + + ram_disk_id = "test" + + vpc_security_group_ids = ["test"] + + tag_specifications { + resource_type = "instance" + tags { + Name = "test" + } + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - The name of the launch template. If you leave this blank, Terraform will auto-generate a unique name. +* `name_prefix` - Creates a unique name beginning with the specified prefix. Conflicts with `name`. +* `description` - Description of the launch template. +* `block_device_mappings` - Specify volumes to attach to the instance besides the volumes specified by the AMI. + See [Block Devices](#block-devices) below for details. +* `credit_specification` - Customize the credit specification of the instance. See [Credit + Specification](#credit-specification) below for more details. +* `disable_api_termination` - If `true`, enables [EC2 Instance + Termination Protection](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/terminating-instances.html#Using_ChangingDisableAPITermination) +* `ebs_optimized` - If `true`, the launched EC2 instance will be EBS-optimized. +* `elastic_gpu_specifications` - The elastic GPU to attach to the instance. See [Elastic GPU](#elastic-gpu) + below for more details. +* `iam_instance_profile` - The IAM Instance Profile to launch the instance with. See [Instance Profile](#instance-profile) + below for more details. +* `image_id` - The AMI from which to launch the instance. +* `instance_initiated_shutdown_behavior` - Shutdown behavior for the instance. Can be `stop` or `terminate`. + (Default: `stop`). +* `instance_market_options` - The market (purchasing) option for the instance. See [Market Options](#market-options) + below for details. +* `instance_type` - The type of the instance. +* `kernel_id` - The kernel ID. +* `key_name` - The key name to use for the instance. +* `monitoring` - If `true`, the launched EC2 instance will have detailed monitoring enabled. +* `network_interfaces` - Customize network interfaces to be attached at instance boot time. See [Network + Interfaces](#network-interfaces) below for more details. +* `placement` - The placement of the instance. See [Placement](#placement) below for more details. +* `ram_disk_id` - The ID of the RAM disk. +* `security_group_names` - A list of security group names to associate with. If you are creating Instances in a VPC, use + `vpc_security_group_ids` instead. +* `vpc_security_group_ids` - A list of security group IDs to associate with. +* `tag_specifications` - The tags to apply to the resources during launch. See [Tags](#tags) below for more details. +* `user_data` - The user data to provide when launching the instance. + +### Block devices + +Configure additional volumes of the instance besides specified by the AMI. It's a good idea to familiarize yourself with + [AWS's Block Device Mapping docs](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html) + to understand the implications of using these attributes. + +Each `block_device_mappings` supports the following: + +* `device_name` - The name of the device to mount. +* `ebs` - Configure EBS volume properties. +* `no_device` - Suppresses the specified device included in the AMI's block device mapping. +* `virtual_name` - The [Instance Store Device + Name](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html#InstanceStoreDeviceNames) + (e.g. `"ephemeral0"`). + +The `ebs` block supports the following: + +* `delete_on_termination` - Whether the volume should be destroyed on instance termination (Default: `true`). +* `encrypted` - Enables [EBS encryption](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html) + on the volume (Default: `false`). Cannot be used with `snapshot_id`. +* `iops` - The amount of provisioned + [IOPS](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html). + This must be set with a `volume_type` of `"io1"`. +* `kms_key_id` - AWS Key Management Service (AWS KMS) customer master key (CMK) to use when creating the encrypted volume. + `encrypted` must be set to `true` when this is set. +* `snapshot_id` - The Snapshot ID to mount. +* `volume_size` - The size of the volume in gigabytes. +* `volume_type` - The type of volume. Can be `"standard"`, `"gp2"`, or `"io1"`. (Default: `"standard"`). + +### Credit Specification + +Credit specification can be applied/modified to the EC2 Instance at any time. + +The `credit_specification` block supports the following: + +* `cpu_credits` - The credit option for CPU usage. Can be `"standard"` or `"unlimited"`. (Default: `"standard"`). + +### Elastic GPU + +Attach an elastic GPU the instance. + +The `elastic_gpu_specifications` block supports the following: + +* `type` - The [Elastic GPU Type](https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/elastic-gpus.html#elastic-gpus-basics) + +### Instance Profile + +The [IAM Instance Profile](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html) +to attach. + +The `iam_instance_profile` block supports the following: + +* `arn` - The Amazon Resource Name (ARN) of the instance profile. +* `name` - The name of the instance profile. + +### Market Options + +The market (purchasing) option for the instances. + +The `instance_market_options` block supports the following: + +* `market_type` - The market type. Can be `spot`. +* `spot_options` - The options for [Spot Instance](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-spot-instances.html) + +The `spot_options` block supports the following: + +* `block_duration_minutes` - The required duration in minutes. This value must be a multiple of 60. +* `instance_interruption_behavior` - The behavior when a Spot Instance is interrupted. Can be `hibernate`, + `stop`, or `terminate`. (Default: `terminate`). +* `max_price` - The maximum hourly price you're willing to pay for the Spot Instances. +* `spot_instance_type` - The Spot Instance request type. Can be `one-time`, or `persistent`. +* `valid_until` - The end date of the request. + + +### Network Interfaces + +Attaches one or more [Network Interfaces](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html) to the instance. + +Each `network_interfaces` block supports the following: + +* `associate_public_ip_address` - Associate a public ip address with the network interface. Boolean value. +* `delete_on_termination` - Whether the network interface should be destroyed on instance termination. +* `description` - Description of the network interface. +* `device_index` - The integer index of the network interface attachment. +* `ipv6_addresses` - One or more specific IPv6 addresses from the IPv6 CIDR block range of your subnet. +* `network_interface_id` - The ID of the network interface to attach. +* `private_ip_address` - The primary private IPv4 address. +* `ipv4_addresses` - One or more private IPv4 addresses to associate. +* `security_groups` - A list of security group IDs to associate. +* `subnet_id` - The VPC Subnet ID to associate. + +### Placement + +The [Placement Group](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html) of the instance. + +The `placement` block supports the following: + +* `affinity` - The affinity setting for an instance on a Dedicated Host. +* `availability_zone` - The Availability Zone for the instance. +* `group_name` - The name of the placement group for the instance. +* `host_id` - The ID of the Dedicated Host for the instance. +* `spread_domain` - Reserved for future use. +* `tenancy` - The tenancy of the instance (if the instance is running in a VPC). Can be `default`, `dedicated`, or `host`. + +### Tags + +The tags to apply to the resources during launch. You can tag instances and volumes. + +Each `tag_specifications` block supports the following: + +* `resource_type` - The type of resource to tag. +* `tags` - A mapping of tags to assign to the resource. + + +## Attributes Reference + +The following attributes are exported along with all argument references: + +* `id` - The ID of the launch template. +* `default_version` - The default version of the launch template. +* `latest_version` - The latest version of the launch template. From 053aaf3527ed2b1b1616ea99b4d1cf01c49e44b0 Mon Sep 17 00:00:00 2001 From: Kash Date: Fri, 13 Apr 2018 10:04:21 -0400 Subject: [PATCH 6/7] modifications after review --- aws/resource_aws_launch_template.go | 459 ++++++++++++++--------- aws/resource_aws_launch_template_test.go | 31 +- 2 files changed, 292 insertions(+), 198 deletions(-) diff --git a/aws/resource_aws_launch_template.go b/aws/resource_aws_launch_template.go index 2f8c9338fd0..7c26dc87159 100644 --- a/aws/resource_aws_launch_template.go +++ b/aws/resource_aws_launch_template.go @@ -10,10 +10,9 @@ import ( "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" ) -const awsSpotInstanceTimeLayout = "2006-01-02T15:04:05Z" - func resourceAwsLaunchTemplate() *schema.Resource { return &schema.Resource{ Create: resourceAwsLaunchTemplateCreate, @@ -42,21 +41,9 @@ func resourceAwsLaunchTemplate() *schema.Resource { }, "description": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { - value := v.(string) - if len(value) > 255 { - errors = append(errors, fmt.Errorf( - "%q cannot be longer than 255 characters", k)) - } - return - }, - }, - - "client_token": { - Type: schema.TypeString, - Computed: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 255), }, "default_version": { @@ -70,7 +57,7 @@ func resourceAwsLaunchTemplate() *schema.Resource { }, "block_device_mappings": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -87,7 +74,7 @@ func resourceAwsLaunchTemplate() *schema.Resource { Optional: true, }, "ebs": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, MaxItems: 1, Elem: &schema.Resource{ @@ -128,7 +115,7 @@ func resourceAwsLaunchTemplate() *schema.Resource { }, "credit_specification": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, MaxItems: 1, Elem: &schema.Resource{ @@ -152,20 +139,20 @@ func resourceAwsLaunchTemplate() *schema.Resource { }, "elastic_gpu_specifications": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "type": { Type: schema.TypeString, - Optional: true, + Required: true, }, }, }, }, "iam_instance_profile": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, MaxItems: 1, Elem: &schema.Resource{ @@ -190,20 +177,25 @@ func resourceAwsLaunchTemplate() *schema.Resource { "instance_initiated_shutdown_behavior": { Type: schema.TypeString, Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + ec2.ShutdownBehaviorStop, + ec2.ShutdownBehaviorTerminate, + }, false), }, "instance_market_options": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "market_type": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ec2.MarketTypeSpot}, false), }, "spot_options": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, MaxItems: 1, Elem: &schema.Resource{ @@ -225,8 +217,9 @@ func resourceAwsLaunchTemplate() *schema.Resource { Optional: true, }, "valid_until": { - Type: schema.TypeString, - Optional: true, + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.ValidateRFC3339TimeString, }, }, }, @@ -251,12 +244,21 @@ func resourceAwsLaunchTemplate() *schema.Resource { }, "monitoring": { - Type: schema.TypeBool, + Type: schema.TypeList, Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + }, + }, + }, }, "network_interfaces": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -277,34 +279,31 @@ func resourceAwsLaunchTemplate() *schema.Resource { Optional: true, }, "security_groups": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, "ipv6_address_count": { Type: schema.TypeInt, Computed: true, }, "ipv6_addresses": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, "network_interface_id": { Type: schema.TypeString, - Computed: true, + Optional: true, }, "private_ip_address": { Type: schema.TypeString, Optional: true, }, "ipv4_addresses": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, "ipv4_address_count": { Type: schema.TypeInt, @@ -319,7 +318,7 @@ func resourceAwsLaunchTemplate() *schema.Resource { }, "placement": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, MaxItems: 1, Elem: &schema.Resource{ @@ -347,6 +346,11 @@ func resourceAwsLaunchTemplate() *schema.Resource { "tenancy": { Type: schema.TypeString, Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + ec2.TenancyDedicated, + ec2.TenancyDefault, + ec2.TenancyHost, + }, false), }, }, }, @@ -358,21 +362,19 @@ func resourceAwsLaunchTemplate() *schema.Resource { }, "security_group_names": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, "vpc_security_group_ids": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, "tag_specifications": { - Type: schema.TypeSet, + Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -439,9 +441,15 @@ func resourceAwsLaunchTemplateRead(d *schema.ResourceData, meta interface{}) err LaunchTemplateIds: []*string{aws.String(d.Id())}, }) if err != nil { + if isAWSErr(err, ec2.LaunchTemplateErrorCodeLaunchTemplateIdDoesNotExist, "") { + log.Printf("[WARN] launch template (%s) not found - removing from state", d.Id()) + d.SetId("") + return nil + } return fmt.Errorf("Error getting launch template: %s", err) } if len(dlt.LaunchTemplates) == 0 { + log.Printf("[WARN] launch template (%s) not found - removing from state", d.Id()) d.SetId("") return nil } @@ -548,6 +556,9 @@ func resourceAwsLaunchTemplateDelete(d *schema.ResourceData, meta interface{}) e LaunchTemplateId: aws.String(d.Id()), }) if err != nil { + if isAWSErr(err, ec2.LaunchTemplateErrorCodeLaunchTemplateIdDoesNotExist, "") { + return nil + } return err } @@ -559,27 +570,27 @@ func getBlockDeviceMappings(m []*ec2.LaunchTemplateBlockDeviceMapping) []interfa s := []interface{}{} for _, v := range m { mapping := map[string]interface{}{ - "device_name": *v.DeviceName, - "virtual_name": *v.VirtualName, + "device_name": aws.StringValue(v.DeviceName), + "virtual_name": aws.StringValue(v.VirtualName), } if v.NoDevice != nil { mapping["no_device"] = *v.NoDevice } if v.Ebs != nil { ebs := map[string]interface{}{ - "delete_on_termination": *v.Ebs.DeleteOnTermination, - "encrypted": *v.Ebs.Encrypted, - "volume_size": *v.Ebs.VolumeSize, - "volume_type": *v.Ebs.VolumeType, + "delete_on_termination": aws.BoolValue(v.Ebs.DeleteOnTermination), + "encrypted": aws.BoolValue(v.Ebs.Encrypted), + "volume_size": aws.Int64Value(v.Ebs.VolumeSize), + "volume_type": aws.StringValue(v.Ebs.VolumeType), } if v.Ebs.Iops != nil { - ebs["iops"] = *v.Ebs.Iops + ebs["iops"] = aws.Int64Value(v.Ebs.Iops) } if v.Ebs.KmsKeyId != nil { - ebs["kms_key_id"] = *v.Ebs.KmsKeyId + ebs["kms_key_id"] = aws.StringValue(v.Ebs.KmsKeyId) } if v.Ebs.SnapshotId != nil { - ebs["snapshot_id"] = *v.Ebs.SnapshotId + ebs["snapshot_id"] = aws.StringValue(v.Ebs.SnapshotId) } mapping["ebs"] = ebs @@ -593,7 +604,7 @@ func getCreditSpecification(cs *ec2.CreditSpecification) []interface{} { s := []interface{}{} if cs != nil { s = append(s, map[string]interface{}{ - "cpu_credits": *cs.CpuCredits, + "cpu_credits": aws.StringValue(cs.CpuCredits), }) } return s @@ -603,7 +614,7 @@ func getElasticGpuSpecifications(e []*ec2.ElasticGpuSpecificationResponse) []int s := []interface{}{} for _, v := range e { s = append(s, map[string]interface{}{ - "type": *v.Type, + "type": aws.StringValue(v.Type), }) } return s @@ -613,8 +624,8 @@ func getIamInstanceProfile(i *ec2.LaunchTemplateIamInstanceProfileSpecification) s := []interface{}{} if i != nil { s = append(s, map[string]interface{}{ - "arn": *i.Arn, - "name": *i.Name, + "arn": aws.StringValue(i.Arn), + "name": aws.StringValue(i.Name), }) } return s @@ -624,17 +635,17 @@ func getInstanceMarketOptions(m *ec2.LaunchTemplateInstanceMarketOptions) []inte s := []interface{}{} if m != nil { mo := map[string]interface{}{ - "market_type": *m.MarketType, + "market_type": aws.StringValue(m.MarketType), } spot := []interface{}{} so := m.SpotOptions if so != nil { spot = append(spot, map[string]interface{}{ - "block_duration_minutes": *so.BlockDurationMinutes, - "instance_interruption_behavior": *so.InstanceInterruptionBehavior, - "max_price": *so.MaxPrice, - "spot_instance_type": *so.SpotInstanceType, - "valid_until": *so.ValidUntil, + "block_duration_minutes": aws.Int64Value(so.BlockDurationMinutes), + "instance_interruption_behavior": aws.StringValue(so.InstanceInterruptionBehavior), + "max_price": aws.StringValue(so.MaxPrice), + "spot_instance_type": aws.StringValue(so.SpotInstanceType), + "valid_until": aws.TimeValue(so.ValidUntil), }) mo["spot_options"] = spot } @@ -650,26 +661,26 @@ func getNetworkInterfaces(n []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecifi var ipv4Addresses []string networkInterface := map[string]interface{}{ - "associate_public_ip_address": *v.AssociatePublicIpAddress, - "delete_on_termination": *v.DeleteOnTermination, - "description": *v.Description, - "device_index": int(*v.DeviceIndex), - "ipv6_address_count": int(*v.Ipv6AddressCount), - "network_interface_id": *v.NetworkInterfaceId, - "private_ip_address": *v.PrivateIpAddress, - "ipv4_address_count": int(*v.SecondaryPrivateIpAddressCount), - "subnet_id": *v.SubnetId, + "associate_public_ip_address": aws.BoolValue(v.AssociatePublicIpAddress), + "delete_on_termination": aws.BoolValue(v.DeleteOnTermination), + "description": aws.StringValue(v.Description), + "device_index": aws.Int64Value(v.DeviceIndex), + "ipv6_address_count": aws.Int64Value(v.Ipv6AddressCount), + "network_interface_id": aws.StringValue(v.NetworkInterfaceId), + "private_ip_address": aws.StringValue(v.PrivateIpAddress), + "ipv4_address_count": aws.Int64Value(v.SecondaryPrivateIpAddressCount), + "subnet_id": aws.StringValue(v.SubnetId), } for _, address := range v.Ipv6Addresses { - ipv6Addresses = append(ipv6Addresses, *address.Ipv6Address) + ipv6Addresses = append(ipv6Addresses, aws.StringValue(address.Ipv6Address)) } if len(ipv6Addresses) > 0 { networkInterface["ipv6_addresses"] = ipv6Addresses } for _, address := range v.PrivateIpAddresses { - ipv4Addresses = append(ipv4Addresses, *address.PrivateIpAddress) + ipv4Addresses = append(ipv4Addresses, aws.StringValue(address.PrivateIpAddress)) } if len(ipv4Addresses) > 0 { networkInterface["ipv4_addresses"] = ipv4Addresses @@ -684,12 +695,12 @@ func getPlacement(p *ec2.LaunchTemplatePlacement) []interface{} { s := []interface{}{} if p != nil { s = append(s, map[string]interface{}{ - "affinity": *p.Affinity, - "availability_zone": *p.AvailabilityZone, - "group_name": *p.GroupName, - "host_id": *p.HostId, - "spread_domain": *p.SpreadDomain, - "tenancy": *p.Tenancy, + "affinity": aws.StringValue(p.Affinity), + "availability_zone": aws.StringValue(p.AvailabilityZone), + "group_name": aws.StringValue(p.GroupName), + "host_id": aws.StringValue(p.HostId), + "spread_domain": aws.StringValue(p.SpreadDomain), + "tenancy": aws.StringValue(p.Tenancy), }) } return s @@ -699,7 +710,7 @@ func getTagSpecifications(t []*ec2.LaunchTemplateTagSpecification) []interface{} s := []interface{}{} for _, v := range t { s = append(s, map[string]interface{}{ - "resource_type": *v.ResourceType, + "resource_type": aws.StringValue(v.ResourceType), "tags": tagsToMap(v.Tags), }) } @@ -745,158 +756,86 @@ func buildLaunchTemplateData(d *schema.ResourceData, meta interface{}) (*ec2.Req if v, ok := d.GetOk("block_device_mappings"); ok { var blockDeviceMappings []*ec2.LaunchTemplateBlockDeviceMappingRequest - bdms := v.(*schema.Set).List() + bdms := v.([]interface{}) for _, bdm := range bdms { - blockDeviceMap := bdm.(map[string]interface{}) - blockDeviceMappings = append(blockDeviceMappings, readBlockDeviceMappingFromConfig(blockDeviceMap)) + blockDeviceMappings = append(blockDeviceMappings, readBlockDeviceMappingFromConfig(bdm.(map[string]interface{}))) } opts.BlockDeviceMappings = blockDeviceMappings } if v, ok := d.GetOk("credit_specification"); ok { - cs := v.(*schema.Set).List() + cs := v.([]interface{}) if len(cs) > 0 { - csData := cs[0].(map[string]interface{}) - csr := &ec2.CreditSpecificationRequest{ - CpuCredits: aws.String(csData["cpu_credits"].(string)), - } - opts.CreditSpecification = csr + opts.CreditSpecification = readCreditSpecificationFromConfig(cs[0].(map[string]interface{})) } } if v, ok := d.GetOk("elastic_gpu_specifications"); ok { var elasticGpuSpecifications []*ec2.ElasticGpuSpecification - egsList := v.(*schema.Set).List() + egsList := v.([]interface{}) for _, egs := range egsList { - elasticGpuSpecification := egs.(map[string]interface{}) - elasticGpuSpecifications = append(elasticGpuSpecifications, &ec2.ElasticGpuSpecification{ - Type: aws.String(elasticGpuSpecification["type"].(string)), - }) + elasticGpuSpecifications = append(elasticGpuSpecifications, readElasticGpuSpecificationsFromConfig(egs.(map[string]interface{}))) } opts.ElasticGpuSpecifications = elasticGpuSpecifications } if v, ok := d.GetOk("iam_instance_profile"); ok { - iip := v.(*schema.Set).List() + iip := v.([]interface{}) if len(iip) > 0 { - iipData := iip[0].(map[string]interface{}) - iamInstanceProfile := &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{ - Arn: aws.String(iipData["arn"].(string)), - Name: aws.String(iipData["name"].(string)), - } - opts.IamInstanceProfile = iamInstanceProfile + opts.IamInstanceProfile = readIamInstanceProfileFromConfig(iip[0].(map[string]interface{})) } } if v, ok := d.GetOk("instance_market_options"); ok { - imo := v.(*schema.Set).List() + imo := v.([]interface{}) if len(imo) > 0 { - imoData := imo[0].(map[string]interface{}) - spotOptions := &ec2.LaunchTemplateSpotMarketOptionsRequest{} - - if v, ok := imoData["spot_options"]; ok { - vL := v.(*schema.Set).List() - for _, v := range vL { - so := v.(map[string]interface{}) - spotOptions.BlockDurationMinutes = aws.Int64(int64(so["block_duration_minutes"].(int))) - spotOptions.InstanceInterruptionBehavior = aws.String(so["instance_interruption_behavior"].(string)) - spotOptions.MaxPrice = aws.String(so["max_price"].(string)) - spotOptions.SpotInstanceType = aws.String(so["spot_instance_type"].(string)) - - t, err := time.Parse(awsSpotInstanceTimeLayout, so["valid_until"].(string)) - if err != nil { - return nil, fmt.Errorf("Error Parsing Launch Template Spot Options valid until: %s", err.Error()) - } - spotOptions.ValidUntil = aws.Time(t) - } + instanceMarketOptions, err := readInstanceMarketOptionsFromConfig(imo[0].(map[string]interface{})) + if err != nil { + return nil, err } - - instanceMarketOptions := &ec2.LaunchTemplateInstanceMarketOptionsRequest{ - MarketType: aws.String(imoData["market_type"].(string)), - SpotOptions: spotOptions, - } - opts.InstanceMarketOptions = instanceMarketOptions } } if v, ok := d.GetOk("monitoring"); ok { - monitoring := &ec2.LaunchTemplatesMonitoringRequest{ - Enabled: aws.Bool(v.(bool)), + m := v.([]interface{}) + if len(m) > 0 { + mData := m[0].(map[string]interface{}) + monitoring := &ec2.LaunchTemplatesMonitoringRequest{ + Enabled: aws.Bool(mData["enabled"].(bool)), + } + opts.Monitoring = monitoring } - opts.Monitoring = monitoring } if v, ok := d.GetOk("network_interfaces"); ok { var networkInterfaces []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest - niList := v.(*schema.Set).List() + niList := v.([]interface{}) for _, ni := range niList { - var ipv4Addresses []*ec2.PrivateIpAddressSpecification - var ipv6Addresses []*ec2.InstanceIpv6AddressRequest - ni := ni.(map[string]interface{}) - - privateIpAddress := ni["private_ip_address"].(string) - networkInterface := &ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{ - AssociatePublicIpAddress: aws.Bool(ni["associate_public_ip_address"].(bool)), - DeleteOnTermination: aws.Bool(ni["delete_on_termination"].(bool)), - Description: aws.String(ni["description"].(string)), - DeviceIndex: aws.Int64(int64(ni["device_index"].(int))), - NetworkInterfaceId: aws.String(ni["network_interface_id"].(string)), - PrivateIpAddress: aws.String(privateIpAddress), - SubnetId: aws.String(ni["subnet_id"].(string)), - } - - ipv6AddressList := ni["ipv6_addresses"].(*schema.Set).List() - for _, address := range ipv6AddressList { - ipv6Addresses = append(ipv6Addresses, &ec2.InstanceIpv6AddressRequest{ - Ipv6Address: aws.String(address.(string)), - }) - } - networkInterface.Ipv6AddressCount = aws.Int64(int64(len(ipv6AddressList))) - networkInterface.Ipv6Addresses = ipv6Addresses - - ipv4AddressList := ni["ipv4_addresses"].(*schema.Set).List() - for _, address := range ipv4AddressList { - privateIp := &ec2.PrivateIpAddressSpecification{ - Primary: aws.Bool(address.(string) == privateIpAddress), - PrivateIpAddress: aws.String(address.(string)), - } - ipv4Addresses = append(ipv4Addresses, privateIp) - } - networkInterface.SecondaryPrivateIpAddressCount = aws.Int64(int64(len(ipv4AddressList))) - networkInterface.PrivateIpAddresses = ipv4Addresses - + niData := ni.(map[string]interface{}) + networkInterface := readNetworkInterfacesFromConfig(niData) networkInterfaces = append(networkInterfaces, networkInterface) } opts.NetworkInterfaces = networkInterfaces } if v, ok := d.GetOk("placement"); ok { - p := v.(*schema.Set).List() + p := v.([]interface{}) if len(p) > 0 { - pData := p[0].(map[string]interface{}) - placement := &ec2.LaunchTemplatePlacementRequest{ - Affinity: aws.String(pData["affinity"].(string)), - AvailabilityZone: aws.String(pData["availability_zone"].(string)), - GroupName: aws.String(pData["group_name"].(string)), - HostId: aws.String(pData["host_id"].(string)), - SpreadDomain: aws.String(pData["spread_domain"].(string)), - Tenancy: aws.String(pData["tenancy"].(string)), - } - opts.Placement = placement + opts.Placement = readPlacementFromConfig(p[0].(map[string]interface{})) } } if v, ok := d.GetOk("tag_specifications"); ok { var tagSpecifications []*ec2.LaunchTemplateTagSpecificationRequest - t := v.(*schema.Set).List() + t := v.([]interface{}) for _, ts := range t { tsData := ts.(map[string]interface{}) @@ -928,16 +867,14 @@ func readBlockDeviceMappingFromConfig(bdm map[string]interface{}) *ec2.LaunchTem blockDeviceMapping.VirtualName = aws.String(v.(string)) } - if v := bdm["ebs"]; v.(*schema.Set).Len() > 0 { - ebs := v.(*schema.Set).List() + if v := bdm["ebs"]; len(v.([]interface{})) > 0 { + ebs := v.([]interface{}) if len(ebs) > 0 { ebsData := ebs[0] - //log.Printf("ebsData: %+v\n", ebsData) blockDeviceMapping.Ebs = readEbsBlockDeviceFromConfig(ebsData.(map[string]interface{})) } } - //log.Printf("block device mapping: %+v\n", *blockDeviceMapping) return blockDeviceMapping } @@ -974,3 +911,161 @@ func readEbsBlockDeviceFromConfig(ebs map[string]interface{}) *ec2.LaunchTemplat return ebsDevice } + +func readNetworkInterfacesFromConfig(ni map[string]interface{}) *ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest { + var ipv4Addresses []*ec2.PrivateIpAddressSpecification + var ipv6Addresses []*ec2.InstanceIpv6AddressRequest + var privateIpAddress string + networkInterface := &ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{ + AssociatePublicIpAddress: aws.Bool(ni["associate_public_ip_address"].(bool)), + DeleteOnTermination: aws.Bool(ni["delete_on_termination"].(bool)), + } + + if v, ok := ni["description"].(string); ok && v != "" { + networkInterface.Description = aws.String(v) + } + + if v, ok := ni["device_index"].(int); ok && v != 0 { + networkInterface.DeviceIndex = aws.Int64(int64(v)) + } + + if v, ok := ni["network_interface_id"].(string); ok && v != "" { + networkInterface.NetworkInterfaceId = aws.String(v) + } + + if v, ok := ni["private_ip_address"].(string); ok && v != "" { + privateIpAddress = v + networkInterface.PrivateIpAddress = aws.String(v) + } + + if v, ok := ni["subnet_id"].(string); ok && v != "" { + networkInterface.SubnetId = aws.String(v) + } + + ipv6AddressList := ni["ipv6_addresses"].([]interface{}) + for _, address := range ipv6AddressList { + ipv6Addresses = append(ipv6Addresses, &ec2.InstanceIpv6AddressRequest{ + Ipv6Address: aws.String(address.(string)), + }) + } + networkInterface.Ipv6AddressCount = aws.Int64(int64(len(ipv6AddressList))) + networkInterface.Ipv6Addresses = ipv6Addresses + + ipv4AddressList := ni["ipv4_addresses"].([]interface{}) + for _, address := range ipv4AddressList { + privateIp := &ec2.PrivateIpAddressSpecification{ + Primary: aws.Bool(address.(string) == privateIpAddress), + PrivateIpAddress: aws.String(address.(string)), + } + ipv4Addresses = append(ipv4Addresses, privateIp) + } + networkInterface.SecondaryPrivateIpAddressCount = aws.Int64(int64(len(ipv4AddressList))) + networkInterface.PrivateIpAddresses = ipv4Addresses + + return networkInterface +} + +func readIamInstanceProfileFromConfig(iip map[string]interface{}) *ec2.LaunchTemplateIamInstanceProfileSpecificationRequest { + iamInstanceProfile := &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{} + + if v, ok := iip["arn"].(string); ok && v != "" { + iamInstanceProfile.Arn = aws.String(v) + } + + if v, ok := iip["name"].(string); ok && v != "" { + iamInstanceProfile.Name = aws.String(v) + } + + return iamInstanceProfile +} + +func readCreditSpecificationFromConfig(cs map[string]interface{}) *ec2.CreditSpecificationRequest { + creditSpecification := &ec2.CreditSpecificationRequest{} + + if v, ok := cs["cpu_credits"].(string); ok && v != "" { + creditSpecification.CpuCredits = aws.String(v) + } + + return creditSpecification +} + +func readElasticGpuSpecificationsFromConfig(egs map[string]interface{}) *ec2.ElasticGpuSpecification { + elasticGpuSpecification := &ec2.ElasticGpuSpecification{} + + if v, ok := egs["type"].(string); ok && v != "" { + elasticGpuSpecification.Type = aws.String(v) + } + + return elasticGpuSpecification +} + +func readInstanceMarketOptionsFromConfig(imo map[string]interface{}) (*ec2.LaunchTemplateInstanceMarketOptionsRequest, error) { + instanceMarketOptions := &ec2.LaunchTemplateInstanceMarketOptionsRequest{} + spotOptions := &ec2.LaunchTemplateSpotMarketOptionsRequest{} + + if v, ok := imo["market_type"].(string); ok && v != "" { + instanceMarketOptions.MarketType = aws.String(v) + } + + if v, ok := imo["spot_options"]; ok { + vL := v.([]interface{}) + for _, v := range vL { + so := v.(map[string]interface{}) + + if v, ok := so["block_duration_minutes"].(int); ok && v != 0 { + spotOptions.BlockDurationMinutes = aws.Int64(int64(v)) + } + + if v, ok := so["instance_interruption_behavior"].(string); ok && v != "" { + spotOptions.InstanceInterruptionBehavior = aws.String(v) + } + + if v, ok := so["max_price"].(string); ok && v != "" { + spotOptions.MaxPrice = aws.String(v) + } + + if v, ok := so["spot_instance_type"].(string); ok && v != "" { + spotOptions.SpotInstanceType = aws.String(v) + } + + t, err := time.Parse(time.RFC3339, so["valid_until"].(string)) + if err != nil { + return nil, fmt.Errorf("Error Parsing Launch Template Spot Options valid until: %s", err.Error()) + } + spotOptions.ValidUntil = aws.Time(t) + } + instanceMarketOptions.SpotOptions = spotOptions + } + + return instanceMarketOptions, nil +} + +func readPlacementFromConfig(p map[string]interface{}) *ec2.LaunchTemplatePlacementRequest { + placement := &ec2.LaunchTemplatePlacementRequest{} + + if v, ok := p["affinity"].(string); ok && v != "" { + placement.Affinity = aws.String(v) + } + + if v, ok := p["availability_zone"].(string); ok && v != "" { + placement.AvailabilityZone = aws.String(v) + } + + if v, ok := p["group_name"].(string); ok && v != "" { + placement.GroupName = aws.String(v) + } + + if v, ok := p["host_id"].(string); ok && v != "" { + placement.HostId = aws.String(v) + } + + if v, ok := p["spread_domain"].(string); ok && v != "" { + placement.SpreadDomain = aws.String(v) + } + + if v, ok := p["tenancy"].(string); ok && v != "" { + placement.Tenancy = aws.String(v) + } + + return placement +} diff --git a/aws/resource_aws_launch_template_test.go b/aws/resource_aws_launch_template_test.go index a58d752e1d0..2ff0a4d7da0 100644 --- a/aws/resource_aws_launch_template_test.go +++ b/aws/resource_aws_launch_template_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" @@ -57,7 +56,7 @@ func TestAccAWSLaunchTemplate_data(t *testing.T) { resource.TestCheckResourceAttrSet(resName, "instance_type"), resource.TestCheckResourceAttrSet(resName, "kernel_id"), resource.TestCheckResourceAttrSet(resName, "key_name"), - resource.TestCheckResourceAttrSet(resName, "monitoring"), + resource.TestCheckResourceAttr(resName, "monitoring.#", "1"), resource.TestCheckResourceAttr(resName, "network_interfaces.#", "1"), resource.TestCheckResourceAttr(resName, "placement.#", "1"), resource.TestCheckResourceAttrSet(resName, "ram_disk_id"), @@ -147,14 +146,11 @@ func testAccCheckAWSLaunchTemplateDestroy(s *terraform.State) error { } } - ae, ok := err.(awserr.Error) - if !ok { - return err - } - if ae.Code() != "InvalidLaunchTemplateId.NotFound" { - log.Printf("aws error code: %s", ae.Code()) - return err + if isAWSErr(err, "InvalidLaunchTemplateId.NotFound", "") { + log.Printf("[WARN] launch template (%s) not found.", rs.Primary.ID) + continue } + return err } return nil @@ -190,31 +186,34 @@ resource "aws_launch_template" "foo" { name = "test" } - image_id = "ami-test" + image_id = "ami-12a3b456" - instance_initiated_shutdown_behavior = "test" + instance_initiated_shutdown_behavior = "terminate" instance_market_options { - market_type = "test" + market_type = "spot" } instance_type = "t2.micro" - kernel_id = "test" + kernel_id = "aki-a12bc3de" key_name = "test" - monitoring = true + monitoring { + enabled = true + } network_interfaces { associate_public_ip_address = true + network_interface_id = "eni-123456ab" } placement { - availability_zone = "test" + availability_zone = "us-west-2b" } - ram_disk_id = "test" + ram_disk_id = "ari-a12bc3de" vpc_security_group_ids = ["test"] From fa28f9a1fcc63aa526bc7186ffa21b3449f099a1 Mon Sep 17 00:00:00 2001 From: Kash Date: Fri, 13 Apr 2018 15:12:49 -0400 Subject: [PATCH 7/7] add missing pieces to correct import --- aws/import_aws_launch_template_test.go | 50 +++++++++++++++++++ aws/resource_aws_launch_template.go | 51 +++++++++++++++----- aws/resource_aws_launch_template_test.go | 30 +++++++----- website/docs/r/launch_template.html.markdown | 8 +++ 4 files changed, 116 insertions(+), 23 deletions(-) create mode 100644 aws/import_aws_launch_template_test.go diff --git a/aws/import_aws_launch_template_test.go b/aws/import_aws_launch_template_test.go new file mode 100644 index 00000000000..bc717276bfa --- /dev/null +++ b/aws/import_aws_launch_template_test.go @@ -0,0 +1,50 @@ +package aws + +import ( + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccAWSLaunchTemplate_importBasic(t *testing.T) { + resName := "aws_launch_template.foo" + rInt := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLaunchTemplateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLaunchTemplateConfig_basic(rInt), + }, + { + ResourceName: resName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSLaunchTemplate_importData(t *testing.T) { + resName := "aws_launch_template.foo" + rInt := acctest.RandInt() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSLaunchTemplateDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSLaunchTemplateConfig_data(rInt), + }, + { + ResourceName: resName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/aws/resource_aws_launch_template.go b/aws/resource_aws_launch_template.go index 7c26dc87159..fe88cddce40 100644 --- a/aws/resource_aws_launch_template.go +++ b/aws/resource_aws_launch_template.go @@ -279,7 +279,7 @@ func resourceAwsLaunchTemplate() *schema.Resource { Optional: true, }, "security_groups": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, @@ -288,7 +288,7 @@ func resourceAwsLaunchTemplate() *schema.Resource { Computed: true, }, "ipv6_addresses": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, @@ -301,7 +301,7 @@ func resourceAwsLaunchTemplate() *schema.Resource { Optional: true, }, "ipv4_addresses": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, @@ -362,13 +362,13 @@ func resourceAwsLaunchTemplate() *schema.Resource { }, "security_group_names": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, "vpc_security_group_ids": { - Type: schema.TypeList, + Type: schema.TypeSet, Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, @@ -463,7 +463,6 @@ func resourceAwsLaunchTemplateRead(d *schema.ResourceData, meta interface{}) err d.Set("name", lt.LaunchTemplateName) d.Set("latest_version", lt.LatestVersionNumber) d.Set("default_version", lt.DefaultVersionNumber) - d.Set("tags", tagsToMap(lt.Tags)) version := strconv.Itoa(int(*lt.LatestVersionNumber)) dltv, err := conn.DescribeLaunchTemplateVersions(&ec2.DescribeLaunchTemplateVersionsInput{ @@ -485,9 +484,10 @@ func resourceAwsLaunchTemplateRead(d *schema.ResourceData, meta interface{}) err d.Set("instance_type", ltData.InstanceType) d.Set("kernel_id", ltData.KernelId) d.Set("key_name", ltData.KeyName) - d.Set("monitoring", ltData.Monitoring) - d.Set("ram_dist_id", ltData.RamDiskId) + d.Set("ram_disk_id", ltData.RamDiskId) + d.Set("security_group_names", aws.StringValueSlice(ltData.SecurityGroups)) d.Set("user_data", ltData.UserData) + d.Set("vpc_security_group_ids", aws.StringValueSlice(ltData.SecurityGroupIds)) if err := d.Set("block_device_mappings", getBlockDeviceMappings(ltData.BlockDeviceMappings)); err != nil { return err @@ -509,6 +509,10 @@ func resourceAwsLaunchTemplateRead(d *schema.ResourceData, meta interface{}) err return err } + if err := d.Set("monitoring", getMonitoring(ltData.Monitoring)); err != nil { + return err + } + if err := d.Set("network_interfaces", getNetworkInterfaces(ltData.NetworkInterfaces)); err != nil { return err } @@ -580,7 +584,7 @@ func getBlockDeviceMappings(m []*ec2.LaunchTemplateBlockDeviceMapping) []interfa ebs := map[string]interface{}{ "delete_on_termination": aws.BoolValue(v.Ebs.DeleteOnTermination), "encrypted": aws.BoolValue(v.Ebs.Encrypted), - "volume_size": aws.Int64Value(v.Ebs.VolumeSize), + "volume_size": int(aws.Int64Value(v.Ebs.VolumeSize)), "volume_type": aws.StringValue(v.Ebs.VolumeType), } if v.Ebs.Iops != nil { @@ -654,6 +658,17 @@ func getInstanceMarketOptions(m *ec2.LaunchTemplateInstanceMarketOptions) []inte return s } +func getMonitoring(m *ec2.LaunchTemplatesMonitoring) []interface{} { + s := []interface{}{} + if m != nil { + mo := map[string]interface{}{ + "enabled": aws.BoolValue(m.Enabled), + } + s = append(s, mo) + } + return s +} + func getNetworkInterfaces(n []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecification) []interface{} { s := []interface{}{} for _, v := range n { @@ -665,10 +680,10 @@ func getNetworkInterfaces(n []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecifi "delete_on_termination": aws.BoolValue(v.DeleteOnTermination), "description": aws.StringValue(v.Description), "device_index": aws.Int64Value(v.DeviceIndex), + "ipv4_address_count": aws.Int64Value(v.SecondaryPrivateIpAddressCount), "ipv6_address_count": aws.Int64Value(v.Ipv6AddressCount), "network_interface_id": aws.StringValue(v.NetworkInterfaceId), "private_ip_address": aws.StringValue(v.PrivateIpAddress), - "ipv4_address_count": aws.Int64Value(v.SecondaryPrivateIpAddressCount), "subnet_id": aws.StringValue(v.SubnetId), } @@ -686,6 +701,10 @@ func getNetworkInterfaces(n []*ec2.LaunchTemplateInstanceNetworkInterfaceSpecifi networkInterface["ipv4_addresses"] = ipv4Addresses } + if len(v.Groups) > 0 { + networkInterface["security_groups"] = aws.StringValueSlice(v.Groups) + } + s = append(s, networkInterface) } return s @@ -754,6 +773,14 @@ func buildLaunchTemplateData(d *schema.ResourceData, meta interface{}) (*ec2.Req opts.EbsOptimized = aws.Bool(v.(bool)) } + if v, ok := d.GetOk("security_group_names"); ok { + opts.SecurityGroups = expandStringList(v.(*schema.Set).List()) + } + + if v, ok := d.GetOk("vpc_security_group_ids"); ok { + opts.SecurityGroupIds = expandStringList(v.(*schema.Set).List()) + } + if v, ok := d.GetOk("block_device_mappings"); ok { var blockDeviceMappings []*ec2.LaunchTemplateBlockDeviceMappingRequest bdms := v.([]interface{}) @@ -942,7 +969,7 @@ func readNetworkInterfacesFromConfig(ni map[string]interface{}) *ec2.LaunchTempl networkInterface.SubnetId = aws.String(v) } - ipv6AddressList := ni["ipv6_addresses"].([]interface{}) + ipv6AddressList := ni["ipv6_addresses"].(*schema.Set).List() for _, address := range ipv6AddressList { ipv6Addresses = append(ipv6Addresses, &ec2.InstanceIpv6AddressRequest{ Ipv6Address: aws.String(address.(string)), @@ -951,7 +978,7 @@ func readNetworkInterfacesFromConfig(ni map[string]interface{}) *ec2.LaunchTempl networkInterface.Ipv6AddressCount = aws.Int64(int64(len(ipv6AddressList))) networkInterface.Ipv6Addresses = ipv6Addresses - ipv4AddressList := ni["ipv4_addresses"].([]interface{}) + ipv4AddressList := ni["ipv4_addresses"].(*schema.Set).List() for _, address := range ipv4AddressList { privateIp := &ec2.PrivateIpAddressSpecification{ Primary: aws.Bool(address.(string) == privateIpAddress), diff --git a/aws/resource_aws_launch_template_test.go b/aws/resource_aws_launch_template_test.go index 2ff0a4d7da0..61b0018e5be 100644 --- a/aws/resource_aws_launch_template_test.go +++ b/aws/resource_aws_launch_template_test.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" ) @@ -14,6 +15,7 @@ import ( func TestAccAWSLaunchTemplate_basic(t *testing.T) { var template ec2.LaunchTemplate resName := "aws_launch_template.foo" + rInt := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -21,7 +23,7 @@ func TestAccAWSLaunchTemplate_basic(t *testing.T) { CheckDestroy: testAccCheckAWSLaunchTemplateDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSLaunchTemplateConfig_basic, + Config: testAccAWSLaunchTemplateConfig_basic(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckAWSLaunchTemplateExists(resName, &template), resource.TestCheckResourceAttr(resName, "default_version", "1"), @@ -35,6 +37,7 @@ func TestAccAWSLaunchTemplate_basic(t *testing.T) { func TestAccAWSLaunchTemplate_data(t *testing.T) { var template ec2.LaunchTemplate resName := "aws_launch_template.foo" + rInt := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -42,7 +45,7 @@ func TestAccAWSLaunchTemplate_data(t *testing.T) { CheckDestroy: testAccCheckAWSLaunchTemplateDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSLaunchTemplateConfig_data, + Config: testAccAWSLaunchTemplateConfig_data(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckAWSLaunchTemplateExists(resName, &template), resource.TestCheckResourceAttr(resName, "block_device_mappings.#", "1"), @@ -71,6 +74,7 @@ func TestAccAWSLaunchTemplate_data(t *testing.T) { func TestAccAWSLaunchTemplate_update(t *testing.T) { var template ec2.LaunchTemplate resName := "aws_launch_template.foo" + rInt := acctest.RandInt() resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -78,7 +82,7 @@ func TestAccAWSLaunchTemplate_update(t *testing.T) { CheckDestroy: testAccCheckAWSLaunchTemplateDestroy, Steps: []resource.TestStep{ { - Config: testAccAWSLaunchTemplateConfig_basic, + Config: testAccAWSLaunchTemplateConfig_basic(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckAWSLaunchTemplateExists(resName, &template), resource.TestCheckResourceAttr(resName, "default_version", "1"), @@ -86,7 +90,7 @@ func TestAccAWSLaunchTemplate_update(t *testing.T) { ), }, { - Config: testAccAWSLaunchTemplateConfig_data, + Config: testAccAWSLaunchTemplateConfig_data(rInt), Check: resource.ComposeTestCheckFunc( testAccCheckAWSLaunchTemplateExists(resName, &template), resource.TestCheckResourceAttr(resName, "default_version", "1"), @@ -156,15 +160,18 @@ func testAccCheckAWSLaunchTemplateDestroy(s *terraform.State) error { return nil } -const testAccAWSLaunchTemplateConfig_basic = ` +func testAccAWSLaunchTemplateConfig_basic(rInt int) string { + return fmt.Sprintf(` resource "aws_launch_template" "foo" { - name = "foo" + name = "foo_%d" +} +`, rInt) } -` -const testAccAWSLaunchTemplateConfig_data = ` +func testAccAWSLaunchTemplateConfig_data(rInt int) string { + return fmt.Sprintf(` resource "aws_launch_template" "foo" { - name = "foo" + name = "foo_%d" block_device_mappings { device_name = "test" @@ -215,7 +222,7 @@ resource "aws_launch_template" "foo" { ram_disk_id = "ari-a12bc3de" - vpc_security_group_ids = ["test"] + vpc_security_group_ids = ["sg-12a3b45c"] tag_specifications { resource_type = "instance" @@ -224,4 +231,5 @@ resource "aws_launch_template" "foo" { } } } -` +`, rInt) +} diff --git a/website/docs/r/launch_template.html.markdown b/website/docs/r/launch_template.html.markdown index 25b39af8901..48527b40a7a 100644 --- a/website/docs/r/launch_template.html.markdown +++ b/website/docs/r/launch_template.html.markdown @@ -233,3 +233,11 @@ The following attributes are exported along with all argument references: * `id` - The ID of the launch template. * `default_version` - The default version of the launch template. * `latest_version` - The latest version of the launch template. + +## Import + +Launch Templates can be imported using the `id`, e.g. + +``` +$ terraform import aws_launch_template.web lt-12345678 +``` \ No newline at end of file