Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rd/aws_instance and rd/aws_launch_template: Add support for metadata_options #12491

Merged
merged 7 commits into from
Mar 27, 2020
24 changes: 24 additions & 0 deletions aws/data_source_aws_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,26 @@ func dataSourceAwsInstance() *schema.Resource {
},
},
},
"metadata_options": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"http_endpoint": {
Type: schema.TypeString,
Computed: true,
},
"http_tokens": {
Type: schema.TypeString,
Computed: true,
},
"http_put_response_hop_limit": {
Type: schema.TypeInt,
Computed: true,
},
},
},
},
"disable_api_termination": {
Type: schema.TypeBool,
Computed: true,
Expand Down Expand Up @@ -486,5 +506,9 @@ func instanceDescriptionAttributes(d *schema.ResourceData, instance *ec2.Instanc
return fmt.Errorf("error setting credit_specification: %s", err)
}

if err := d.Set("metadata_options", flattenEc2InstanceMetadataOptions(instance.MetadataOptions)); err != nil {
return fmt.Errorf("error setting metadata_options: %s", err)
}

return nil
}
51 changes: 51 additions & 0 deletions aws/data_source_aws_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,34 @@ func TestAccAWSInstanceDataSource_creditSpecification(t *testing.T) {
})
}

func TestAccAWSInstanceDataSource_metadataOptions(t *testing.T) {
resourceName := "aws_instance.test"
datasourceName := "data.aws_instance.test"
rName := acctest.RandomWithPrefix("tf-acc-test")
instanceType := "m1.small"

resource.ParallelTest(t, resource.TestCase{
// No subnet_id specified requires default VPC or EC2-Classic.
PreCheck: func() {
testAccPreCheck(t)
testAccPreCheckHasDefaultVpcOrEc2Classic(t)
testAccPreCheckOffersEc2InstanceType(t, instanceType)
},
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccInstanceDataSourceConfig_metadataOptions(rName, instanceType),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrPair(datasourceName, "metadata_options.#", resourceName, "metadata_options.#"),
resource.TestCheckResourceAttrPair(datasourceName, "metadata_options.0.http_endpoint", resourceName, "metadata_options.0.http_endpoint"),
resource.TestCheckResourceAttrPair(datasourceName, "metadata_options.0.http_tokens", resourceName, "metadata_options.0.http_tokens"),
resource.TestCheckResourceAttrPair(datasourceName, "metadata_options.0.http_put_response_hop_limit", resourceName, "metadata_options.0.http_put_response_hop_limit"),
),
},
},
})
}

// Lookup based on InstanceID
const testAccInstanceDataSourceConfig = `
resource "aws_instance" "test" {
Expand Down Expand Up @@ -835,3 +863,26 @@ data "aws_instance" "test" {
}
`)
}

func testAccInstanceDataSourceConfig_metadataOptions(rName, instanceType string) string {
return testAccLatestAmazonLinuxHvmEbsAmiConfig() + fmt.Sprintf(`
resource "aws_instance" "test" {
ami = data.aws_ami.amzn-ami-minimal-hvm-ebs.id
instance_type = %[2]q

tags = {
Name = %[1]q
}

metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 2
}
}

data "aws_instance" "test" {
instance_id = aws_instance.test.id
}
`, rName, instanceType)
}
24 changes: 24 additions & 0 deletions aws/data_source_aws_launch_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,26 @@ func dataSourceAwsLaunchTemplate() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},
"metadata_options": {
Type: schema.TypeList,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"http_endpoint": {
Type: schema.TypeString,
Computed: true,
},
"http_tokens": {
Type: schema.TypeString,
Computed: true,
},
"http_put_response_hop_limit": {
Type: schema.TypeInt,
Computed: true,
},
},
},
},
"monitoring": {
Type: schema.TypeList,
Computed: true,
Expand Down Expand Up @@ -456,6 +476,10 @@ func dataSourceAwsLaunchTemplateRead(d *schema.ResourceData, meta interface{}) e
return fmt.Errorf("error setting instance_market_options: %s", err)
}

if err := d.Set("metadata_options", flattenLaunchTemplateInstanceMetadataOptions(ltData.MetadataOptions)); err != nil {
return fmt.Errorf("error setting metadata_options: %s", err)
}

if err := d.Set("monitoring", getMonitoring(ltData.Monitoring)); err != nil {
return fmt.Errorf("error setting monitoring: %s", err)
}
Expand Down
41 changes: 41 additions & 0 deletions aws/data_source_aws_launch_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,29 @@ func TestAccAWSLaunchTemplateDataSource_filter_tags(t *testing.T) {
})
}

func TestAccAWSLaunchTemplateDataSource_metadataOptions(t *testing.T) {
rName := acctest.RandomWithPrefix("tf-acc-test")
dataSourceName := "data.aws_launch_template.test"
resourceName := "aws_launch_template.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLaunchTemplateDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSLaunchTemplateDataSourceConfig_metadataOptions(rName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrPair(dataSourceName, "metadata_options.#", resourceName, "metadata_options.#"),
resource.TestCheckResourceAttrPair(dataSourceName, "metadata_options.0.http_endpoint", resourceName, "metadata_options.0.http_endpoint"),
resource.TestCheckResourceAttrPair(dataSourceName, "metadata_options.0.http_tokens", resourceName, "metadata_options.0.http_tokens"),
resource.TestCheckResourceAttrPair(dataSourceName, "metadata_options.0.http_put_response_hop_limit", resourceName, "metadata_options.0.http_put_response_hop_limit"),
),
},
},
})
}

func testAccAWSLaunchTemplateDataSourceConfig_Basic(rName string) string {
return fmt.Sprintf(`
resource "aws_launch_template" "test" {
Expand Down Expand Up @@ -124,3 +147,21 @@ data "aws_launch_template" "test" {
}
`, rName, rInt)
}

func testAccAWSLaunchTemplateDataSourceConfig_metadataOptions(rName string) string {
return fmt.Sprintf(`
resource "aws_launch_template" "test" {
name = %[1]q

metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 2
}
}

data "aws_launch_template" "test" {
name = aws_launch_template.test.name
}
`, rName)
}
49 changes: 49 additions & 0 deletions aws/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import (
"strings"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/organizations"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
Expand Down Expand Up @@ -1137,6 +1139,53 @@ func testAccCheckAWSProviderPartition(providers *[]*schema.Provider, expectedPar
}
}

// testAccPreCheckHasDefaultVpcOrEc2Classic checks that the test region has a default VPC or has the EC2-Classic platform.
// This check is useful to ensure that an instance can be launched without specifying a subnet.
func testAccPreCheckHasDefaultVpcOrEc2Classic(t *testing.T) {
client := testAccProvider.Meta().(*AWSClient)

if !testAccHasDefaultVpc(t) && !hasEc2Classic(client.supportedplatforms) {
t.Skipf("skipping tests; %s does not have a default VPC or EC2-Classic", client.region)
}
}

func testAccHasDefaultVpc(t *testing.T) bool {
conn := testAccProvider.Meta().(*AWSClient).ec2conn

resp, err := conn.DescribeAccountAttributes(&ec2.DescribeAccountAttributesInput{
AttributeNames: aws.StringSlice([]string{ec2.AccountAttributeNameDefaultVpc}),
})
if testAccPreCheckSkipError(err) ||
len(resp.AccountAttributes) == 0 ||
len(resp.AccountAttributes[0].AttributeValues) == 0 ||
aws.StringValue(resp.AccountAttributes[0].AttributeValues[0].AttributeValue) == "none" {
return false
}
if err != nil {
t.Fatalf("error describing EC2 account attributes: %s", err)
}

return true
}

// testAccPreCheckOffersEc2InstanceType checks that the test region offers the specified EC2 instance type.
func testAccPreCheckOffersEc2InstanceType(t *testing.T, instanceType string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that there is the aws_ec2_isntance_type_offering data source, we can use that to selectively pick out of a list of preferred instance types. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's probably a simpler way.

client := testAccProvider.Meta().(*AWSClient)

resp, err := client.ec2conn.DescribeInstanceTypeOfferings(&ec2.DescribeInstanceTypeOfferingsInput{
Filters: buildEC2AttributeFilterList(map[string]string{
"instance-type": instanceType,
}),
LocationType: aws.String(ec2.LocationTypeRegion),
})
if testAccPreCheckSkipError(err) || len(resp.InstanceTypeOfferings) == 0 {
t.Skipf("skipping tests; %s does not offer EC2 instance type: %s", client.region, instanceType)
}
if err != nil {
t.Fatalf("error describing EC2 instance type offerings: %s", err)
}
}

func testAccAWSProviderConfigEndpoints(endpoints string) string {
//lintignore:AT004
return fmt.Sprintf(`
Expand Down
97 changes: 97 additions & 0 deletions aws/resource_aws_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,35 @@ func resourceAwsInstance() *schema.Resource {
},
},
},

"metadata_options": {
Type: schema.TypeList,
Optional: true,
Computed: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"http_endpoint": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.StringInSlice([]string{ec2.InstanceMetadataEndpointStateEnabled, ec2.InstanceMetadataEndpointStateDisabled}, false),
},
"http_tokens": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.StringInSlice([]string{ec2.HttpTokensStateOptional, ec2.HttpTokensStateRequired}, false),
},
"http_put_response_hop_limit": {
Type: schema.TypeInt,
Optional: true,
Computed: true,
ValidateFunc: validation.IntBetween(1, 64),
},
},
},
},
},
}
}
Expand Down Expand Up @@ -572,6 +601,7 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
CreditSpecification: instanceOpts.CreditSpecification,
CpuOptions: instanceOpts.CpuOptions,
HibernationOptions: instanceOpts.HibernationOptions,
MetadataOptions: instanceOpts.MetadataOptions,
TagSpecifications: tagSpecifications,
}

Expand Down Expand Up @@ -723,6 +753,10 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("hibernation", instance.HibernationOptions.Configured)
}

if err := d.Set("metadata_options", flattenEc2InstanceMetadataOptions(instance.MetadataOptions)); err != nil {
return fmt.Errorf("error setting metadata_options: %s", err)
}

d.Set("ami", instance.ImageId)
d.Set("instance_type", instance.InstanceType)
d.Set("key_name", instance.KeyName)
Expand Down Expand Up @@ -1211,6 +1245,27 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
}
}

if d.HasChange("metadata_options") && !d.IsNewResource() {
if v, ok := d.GetOk("metadata_options"); ok {
if mo, ok := v.([]interface{})[0].(map[string]interface{}); ok {
log.Printf("[DEBUG] Modifying metadata options for Instance (%s)", d.Id())
input := &ec2.ModifyInstanceMetadataOptionsInput{
InstanceId: aws.String(d.Id()),
HttpEndpoint: aws.String(mo["http_endpoint"].(string)),
}
if mo["http_endpoint"].(string) == ec2.InstanceMetadataEndpointStateEnabled {
// These parameters are not allowed unless HttpEndpoint is enabled
input.HttpTokens = aws.String(mo["http_tokens"].(string))
input.HttpPutResponseHopLimit = aws.Int64(int64(mo["http_put_response_hop_limit"].(int)))
}
_, err := conn.ModifyInstanceMetadataOptions(input)
if err != nil {
return fmt.Errorf("Error updating metadata options: %s", err)
}
}
}
}

// TODO(mitchellh): wait for the attributes we modified to
// persist the change...

Expand Down Expand Up @@ -1815,6 +1870,7 @@ type awsInstanceOpts struct {
CreditSpecification *ec2.CreditSpecificationRequest
CpuOptions *ec2.CpuOptionsRequest
HibernationOptions *ec2.HibernationOptionsRequest
MetadataOptions *ec2.InstanceMetadataOptionsRequest
}

func buildAwsInstanceOpts(
Expand All @@ -1827,6 +1883,7 @@ func buildAwsInstanceOpts(
EBSOptimized: aws.Bool(d.Get("ebs_optimized").(bool)),
ImageID: aws.String(d.Get("ami").(string)),
InstanceType: aws.String(instanceType),
MetadataOptions: expandEc2InstanceMetadataOptions(d.Get("metadata_options").([]interface{})),
}

// Set default cpu_credits as Unlimited for T3 instance type
Expand Down Expand Up @@ -2101,3 +2158,43 @@ func getCreditSpecifications(conn *ec2.EC2, instanceId string) ([]map[string]int

return creditSpecifications, nil
}

func expandEc2InstanceMetadataOptions(l []interface{}) *ec2.InstanceMetadataOptionsRequest {
if len(l) == 0 || l[0] == nil {
return nil
}

m := l[0].(map[string]interface{})

opts := &ec2.InstanceMetadataOptionsRequest{
HttpEndpoint: aws.String(m["http_endpoint"].(string)),
}

if m["http_endpoint"].(string) == ec2.InstanceMetadataEndpointStateEnabled {
// These parameters are not allowed unless HttpEndpoint is enabled

if v, ok := m["http_tokens"].(string); ok && v != "" {
opts.HttpTokens = aws.String(v)
}

if v, ok := m["http_put_response_hop_limit"].(int); ok && v != 0 {
opts.HttpPutResponseHopLimit = aws.Int64(int64(v))
}
}

return opts
}

func flattenEc2InstanceMetadataOptions(opts *ec2.InstanceMetadataOptionsResponse) []interface{} {
if opts == nil {
return nil
}

m := map[string]interface{}{
"http_endpoint": aws.StringValue(opts.HttpEndpoint),
"http_put_response_hop_limit": aws.Int64Value(opts.HttpPutResponseHopLimit),
"http_tokens": aws.StringValue(opts.HttpTokens),
}

return []interface{}{m}
}
Loading