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

Add instance shutdown behavior retrieval #18880

Merged
merged 9 commits into from
Apr 17, 2021
3 changes: 3 additions & 0 deletions .changelog/18880.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
resource/aws_instance: Make `instance_initiated_shutdown_behavior` also computed, allowing value to be read
```
64 changes: 56 additions & 8 deletions aws/resource_aws_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ func resourceAwsInstance() *schema.Resource {
"instance_initiated_shutdown_behavior": {
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"instance_state": {
Type: schema.TypeString,
Expand Down Expand Up @@ -940,6 +941,11 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
return err
}

// Retrieve instance shutdown behavior
if err := readInstanceShutdownBehavior(d, conn); err != nil {
return err
}

if err := readBlockDevices(d, instance, conn); err != nil {
return err
}
Expand Down Expand Up @@ -1318,14 +1324,10 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
}

if d.HasChange("disable_api_termination") && !d.IsNewResource() {
_, err := conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{
InstanceId: aws.String(d.Id()),
DisableApiTermination: &ec2.AttributeBooleanValue{
Value: aws.Bool(d.Get("disable_api_termination").(bool)),
},
})
err := resourceAwsInstanceDisableAPITermination(conn, d.Id(), d.Get("disable_api_termination").(bool))

if err != nil {
return err
return fmt.Errorf("error modifying instance (%s) attribute (%s): %w", d.Id(), ec2.InstanceAttributeNameDisableApiTermination, err)
}
}

Expand Down Expand Up @@ -1536,7 +1538,13 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn

err := awsTerminateInstance(conn, d.Id(), d.Timeout(schema.TimeoutDelete))
err := resourceAwsInstanceDisableAPITermination(conn, d.Id(), d.Get("disable_api_termination").(bool))

if err != nil {
log.Printf("[WARN] attempting to terminate EC2 instance (%s) despite error modifying attribute (%s): %s", d.Id(), ec2.InstanceAttributeNameDisableApiTermination, err)
}

err = awsTerminateInstance(conn, d.Id(), d.Timeout(schema.TimeoutDelete))

if err != nil {
return fmt.Errorf("error terminating EC2 Instance (%s): %s", d.Id(), err)
Expand All @@ -1545,6 +1553,29 @@ func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error {
return nil
}

func resourceAwsInstanceDisableAPITermination(conn *ec2.EC2, id string, disableAPITermination bool) error {
// false = enable api termination
// true = disable api termination (protected)

_, err := conn.ModifyInstanceAttribute(&ec2.ModifyInstanceAttributeInput{
InstanceId: aws.String(id),
DisableApiTermination: &ec2.AttributeBooleanValue{
Value: aws.Bool(disableAPITermination),
},
})

if tfawserr.ErrMessageContains(err, "UnsupportedOperation", "not supported for spot instances") {
log.Printf("[WARN] failed to modify instance (%s) attribute (%s): %s", id, ec2.InstanceAttributeNameDisableApiTermination, err)
return nil
}

if err != nil {
return fmt.Errorf("error modify instance (%s) attribute (%s) to value %t: %w", id, ec2.InstanceAttributeNameDisableApiTermination, disableAPITermination, err)
}

return nil
}

// InstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
// an EC2 instance.
func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string, failStates []string) resource.StateRefreshFunc {
Expand Down Expand Up @@ -2188,6 +2219,23 @@ func readSecurityGroups(d *schema.ResourceData, instance *ec2.Instance, conn *ec
return nil
}

func readInstanceShutdownBehavior(d *schema.ResourceData, conn *ec2.EC2) error {
output, err := conn.DescribeInstanceAttribute(&ec2.DescribeInstanceAttributeInput{
InstanceId: aws.String(d.Id()),
Attribute: aws.String(ec2.InstanceAttributeNameInstanceInitiatedShutdownBehavior),
})

if err != nil {
return fmt.Errorf("error while describing instance (%s) attribute (%s): %w", d.Id(), ec2.InstanceAttributeNameInstanceInitiatedShutdownBehavior, err)
}

if output != nil && output.InstanceInitiatedShutdownBehavior != nil {
d.Set("instance_initiated_shutdown_behavior", output.InstanceInitiatedShutdownBehavior.Value)
}

return nil
}

func getAwsEc2InstancePasswordData(instanceID string, conn *ec2.EC2) (string, error) {
log.Printf("[INFO] Reading password data for instance %s", instanceID)

Expand Down
62 changes: 30 additions & 32 deletions aws/resource_aws_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -42,18 +43,25 @@ func testAccErrorCheckSkipEC2(t *testing.T) resource.ErrorCheckFunc {

func testSweepInstances(region string) error {
client, err := sharedClientForRegion(region)

if err != nil {
return fmt.Errorf("error getting client: %s", err)
}

conn := client.(*AWSClient).ec2conn
sweepResources := make([]*testSweepResource, 0)
var errs *multierror.Error

err = conn.DescribeInstancesPages(&ec2.DescribeInstancesInput{}, func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
if len(page.Reservations) == 0 {
log.Print("[DEBUG] No EC2 Instances to sweep")
return false
if page == nil {
return !lastPage
}

for _, reservation := range page.Reservations {
if reservation == nil {
continue
}

for _, instance := range reservation.Instances {
id := aws.StringValue(instance.InstanceId)

Expand All @@ -62,42 +70,31 @@ func testSweepInstances(region string) error {
continue
}

log.Printf("[INFO] Terminating EC2 Instance: %s", id)
err := awsTerminateInstance(conn, id, 5*time.Minute)

if isAWSErr(err, "OperationNotPermitted", "Modify its 'disableApiTermination' instance attribute and try again.") {
log.Printf("[INFO] Enabling API Termination on EC2 Instance: %s", id)
r := resourceAwsInstance()
d := r.Data(nil)
d.SetId(id)
d.Set("disable_api_termination", false)

input := &ec2.ModifyInstanceAttributeInput{
InstanceId: instance.InstanceId,
DisableApiTermination: &ec2.AttributeBooleanValue{
Value: aws.Bool(false),
},
}

_, err = conn.ModifyInstanceAttribute(input)

if err == nil {
err = awsTerminateInstance(conn, id, 5*time.Minute)
}
}

if err != nil {
log.Printf("[ERROR] Error terminating EC2 Instance (%s): %s", id, err)
}
sweepResources = append(sweepResources, NewTestSweepResource(r, d, client))
}
}
return !lastPage
})

if err != nil {
if testSweepSkipSweepError(err) {
log.Printf("[WARN] Skipping EC2 Instance sweep for %s: %s", region, err)
return nil
}
return fmt.Errorf("Error retrieving EC2 Instances: %s", err)
errs = multierror.Append(errs, fmt.Errorf("error describing EC2 Instances for %s: %w", region, err))
}

return nil
if err = testSweepResourceOrchestrator(sweepResources); err != nil {
errs = multierror.Append(errs, fmt.Errorf("error sweeping EC2 Instances for %s: %w", region, err))
}

if testSweepSkipSweepError(errs.ErrorOrNil()) {
log.Printf("[WARN] Skipping EC2 Instance sweep for %s: %s", region, errs)
return nil
}

return errs.ErrorOrNil()
}

func TestFetchRootDevice(t *testing.T) {
Expand Down Expand Up @@ -258,6 +255,7 @@ func TestAccAWSInstance_basic(t *testing.T) {
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists(resourceName, &v),
testAccMatchResourceAttrRegionalARN(resourceName, "arn", "ec2", regexp.MustCompile(`instance/i-[a-z0-9]+`)),
resource.TestCheckResourceAttr(resourceName, "instance_initiated_shutdown_behavior", "stop"),
),
},
{
Expand Down Expand Up @@ -3713,7 +3711,7 @@ func testAccInstanceConfigBasic() string {
return composeConfig(
testAccLatestAmazonLinuxHvmEbsAmiConfig(),
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-classic-platform.html#ec2-classic-instance-types
testAccAvailableEc2InstanceTypeForRegion("t1.micro", "m1.small", "t3.micro", "t2.micro"),
testAccAvailableEc2InstanceTypeForRegion("t3.micro", "t2.micro", "t1.micro", "m1.small"),
`
resource "aws_instance" "test" {
ami = data.aws_ami.amzn-ami-minimal-hvm-ebs.id
Expand Down
40 changes: 29 additions & 11 deletions aws/resource_aws_spot_fleet_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
Expand All @@ -24,12 +25,20 @@ func init() {

func testSweepSpotFleetRequests(region string) error {
client, err := sharedClientForRegion(region)

if err != nil {
return fmt.Errorf("error getting client: %s", err)
}

conn := client.(*AWSClient).ec2conn
sweepResources := make([]*testSweepResource, 0)
var errs *multierror.Error

err = conn.DescribeSpotFleetRequestsPages(&ec2.DescribeSpotFleetRequestsInput{}, func(page *ec2.DescribeSpotFleetRequestsOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

if len(page.SpotFleetRequestConfigs) == 0 {
log.Print("[DEBUG] No Spot Fleet Requests to sweep")
return false
Expand All @@ -38,22 +47,31 @@ func testSweepSpotFleetRequests(region string) error {
for _, config := range page.SpotFleetRequestConfigs {
id := aws.StringValue(config.SpotFleetRequestId)

log.Printf("[INFO] Deleting Spot Fleet Request: %s", id)
err := deleteSpotFleetRequest(id, true, 15*time.Minute, conn)
if err != nil {
log.Printf("[ERROR] Failed to delete Spot Fleet Request (%s): %s", id, err)
}
r := resourceAwsSpotFleetRequest()
d := r.Data(nil)
d.SetId(id)
d.Set("terminate_instances_with_expiration", true)

sweepResources = append(sweepResources, NewTestSweepResource(r, d, client))
}

return !lastPage
})

if err != nil {
if testSweepSkipSweepError(err) {
log.Printf("[WARN] Skipping EC2 Spot Fleet Requests sweep for %s: %s", region, err)
return nil
}
return fmt.Errorf("Error retrieving EC2 Spot Fleet Requests: %s", err)
errs = multierror.Append(errs, fmt.Errorf("error describing EC2 Spot Fleet Requests for %s: %w", region, err))
}
return nil

if err = testSweepResourceOrchestrator(sweepResources); err != nil {
errs = multierror.Append(errs, fmt.Errorf("error sweeping EC2 Spot Fleet Requests for %s: %w", region, err))
}

if testSweepSkipSweepError(errs.ErrorOrNil()) {
log.Printf("[WARN] Skipping EC2 Spot Fleet Requests sweep for %s: %s", region, errs)
return nil
}

return errs.ErrorOrNil()
}

func TestAccAWSSpotFleetRequest_basic(t *testing.T) {
Expand Down