Skip to content

Commit

Permalink
Merge pull request #28639 from brittandeyoung/f-aws_instance_state
Browse files Browse the repository at this point in the history
New Resource: `aws_instance_state`
  • Loading branch information
YakDriver authored Jan 4, 2023
2 parents 61d7bcc + 31f4526 commit 0b7be5b
Show file tree
Hide file tree
Showing 8 changed files with 524 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/28639.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
aws_ec2_instance_state
```
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,7 @@ func New(ctx context.Context) (*schema.Provider, error) {
"aws_ec2_client_vpn_route": ec2.ResourceClientVPNRoute(),
"aws_ec2_fleet": ec2.ResourceFleet(),
"aws_ec2_host": ec2.ResourceHost(),
"aws_ec2_instance_state": ec2.ResourceInstanceState(),
"aws_ec2_local_gateway_route": ec2.ResourceLocalGatewayRoute(),
"aws_ec2_local_gateway_route_table_vpc_association": ec2.ResourceLocalGatewayRouteTableVPCAssociation(),
"aws_ec2_managed_prefix_list": ec2.ResourceManagedPrefixList(),
Expand Down
5 changes: 5 additions & 0 deletions internal/service/ec2/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,3 +276,8 @@ func securityGroupRuleType_Values() []string {
securityGroupRuleTypeIngress,
}
}

const (
ResInstance = "Instance"
ResInstanceState = "Instance State"
)
179 changes: 179 additions & 0 deletions internal/service/ec2/ec2_instance_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package ec2

import (
"context"
"log"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
"github.com/hashicorp/terraform-provider-aws/internal/create"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/names"
)

func ResourceInstanceState() *schema.Resource {
return &schema.Resource{
CreateWithoutTimeout: resourceInstanceStateCreate,
ReadWithoutTimeout: resourceInstanceStateRead,
UpdateWithoutTimeout: resourceInstanceStateUpdate,
DeleteWithoutTimeout: resourceInstanceStateDelete,

Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(10 * time.Minute),
Update: schema.DefaultTimeout(10 * time.Minute),
Delete: schema.DefaultTimeout(1 * time.Minute),
},

Schema: map[string]*schema.Schema{
"force": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"instance_id": {
Type: schema.TypeString,
ForceNew: true,
Required: true,
},
"state": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{ec2.InstanceStateNameRunning, ec2.InstanceStateNameStopped}, false),
},
},
}
}

func resourceInstanceStateCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).EC2Conn()
instanceId := d.Get("instance_id").(string)

instance, instanceErr := WaitInstanceReadyWithContext(ctx, conn, instanceId, d.Timeout(schema.TimeoutCreate))

if instanceErr != nil {
return create.DiagError(names.EC2, create.ErrActionReading, ResInstance, instanceId, instanceErr)
}

err := UpdateInstanceState(ctx, conn, instanceId, aws.StringValue(instance.State.Name), d.Get("state").(string), d.Get("force").(bool))

if err != nil {
return err
}

d.SetId(d.Get("instance_id").(string))

return resourceInstanceStateRead(ctx, d, meta)
}

func resourceInstanceStateRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).EC2Conn()

state, err := FindInstanceStateById(ctx, conn, d.Id())

if !d.IsNewResource() && tfresource.NotFound(err) {
create.LogNotFoundRemoveState(names.EC2, create.ErrActionReading, ResInstanceState, d.Id())
d.SetId("")
return nil
}

if err != nil {
return create.DiagError(names.EC2, create.ErrActionReading, ResInstanceState, d.Id(), err)
}

d.Set("instance_id", d.Id())
d.Set("state", state.Name)
d.Set("force", d.Get("force").(bool))

return nil
}

func resourceInstanceStateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
conn := meta.(*conns.AWSClient).EC2Conn()

instance, instanceErr := WaitInstanceReadyWithContext(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate))

if instanceErr != nil {
return create.DiagError(names.EC2, create.ErrActionReading, ResInstance, aws.StringValue(instance.InstanceId), instanceErr)
}

if d.HasChange("state") {
o, n := d.GetChange("state")
err := UpdateInstanceState(ctx, conn, d.Id(), o.(string), n.(string), d.Get("force").(bool))

if err != nil {
return err
}
}

return resourceInstanceStateRead(ctx, d, meta)
}

func resourceInstanceStateDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
log.Printf("[DEBUG] %s %s deleting an aws_ec2_instance_state resource only stops managing instance state, The Instance is left in its current state.: %s", names.EC2, ResInstanceState, d.Id())

return nil
}

func UpdateInstanceState(ctx context.Context, conn *ec2.EC2, id string, currentState string, configuredState string, force bool) diag.Diagnostics {
if currentState == configuredState {
return nil
}

if configuredState == "stopped" {
if err := StopInstanceWithContext(ctx, conn, id, force, InstanceStopTimeout); err != nil {
return err
}
}

if configuredState == "running" {
if err := StartInstanceWithContext(ctx, conn, id, InstanceStartTimeout); err != nil {
return err
}
}

return nil
}

func StopInstanceWithContext(ctx context.Context, conn *ec2.EC2, id string, force bool, timeout time.Duration) diag.Diagnostics {
log.Printf("[INFO] Stopping EC2 Instance: %s, force: %t", id, force)
_, err := conn.StopInstancesWithContext(ctx, &ec2.StopInstancesInput{
InstanceIds: aws.StringSlice([]string{id}),
Force: aws.Bool(force),
})

if err != nil {
return create.DiagError(names.EC2, "stopping Instance", ResInstance, id, err)
}

if _, err := WaitInstanceStoppedWithContext(ctx, conn, id, timeout); err != nil {
return create.DiagError(names.EC2, "waiting for instance to stop", ResInstance, id, err)
}

return nil
}

func StartInstanceWithContext(ctx context.Context, conn *ec2.EC2, id string, timeout time.Duration) diag.Diagnostics {
log.Printf("[INFO] Starting EC2 Instance: %s", id)
_, err := conn.StartInstancesWithContext(ctx, &ec2.StartInstancesInput{
InstanceIds: aws.StringSlice([]string{id}),
})

if err != nil {
return create.DiagError(names.EC2, "starting Instance", ResInstance, id, err)
}

if _, err := WaitInstanceStartedWithContext(ctx, conn, id, timeout); err != nil {
return create.DiagError(names.EC2, "waiting for instance to start", ResInstance, id, err)
}

return nil
}
143 changes: 143 additions & 0 deletions internal/service/ec2/ec2_instance_state_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package ec2_test

import (
"context"
"errors"
"fmt"
"testing"

"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/hashicorp/terraform-provider-aws/internal/acctest"
"github.com/hashicorp/terraform-provider-aws/internal/conns"
tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2"
)

func TestAccEC2InstanceState_basic(t *testing.T) {
resourceName := "aws_ec2_instance_state.test"
state := "stopped"
force := "false"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccInstanceStateConfig_basic(state, force),
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceStateExists(resourceName),
resource.TestCheckResourceAttrSet(resourceName, "instance_id"),
resource.TestCheckResourceAttr(resourceName, "state", state),
),
},
},
})
}

func TestAccEC2InstanceState_state(t *testing.T) {
resourceName := "aws_ec2_instance_state.test"
stateStopped := "stopped"
stateRunning := "running"
force := "false"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccInstanceStateConfig_basic(stateStopped, force),
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceStateExists(resourceName),
resource.TestCheckResourceAttrSet(resourceName, "instance_id"),
resource.TestCheckResourceAttr(resourceName, "state", stateStopped),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
Config: testAccInstanceStateConfig_basic(stateRunning, force),
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceStateExists(resourceName),
resource.TestCheckResourceAttrSet(resourceName, "instance_id"),
resource.TestCheckResourceAttr(resourceName, "state", stateRunning),
),
},
},
})
}
func TestAccEC2InstanceState_disappears_Instance(t *testing.T) {
resourceName := "aws_ec2_instance_state.test"
parentResourceName := "aws_instance.test"
state := "stopped"
force := "false"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccInstanceStateConfig_basic(state, force),
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceStateExists(resourceName),
acctest.CheckResourceDisappears(acctest.Provider, tfec2.ResourceInstance(), parentResourceName),
),
ExpectNonEmptyPlan: true,
},
},
})
}

func testAccCheckInstanceStateExists(n string) 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 errors.New("No EC2InstanceState ID is set")
}

conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn()

out, err := tfec2.FindInstanceStateById(context.Background(), conn, rs.Primary.ID)

if err != nil {
return err
}

if out == nil {
return fmt.Errorf("Instance State %q does not exist", rs.Primary.ID)
}

return nil
}
}

func testAccInstanceStateConfig_basic(state string, force string) string {
return acctest.ConfigCompose(
acctest.ConfigLatestAmazonLinuxHVMEBSAMI(),
acctest.AvailableEC2InstanceTypeForRegion("t3.micro", "t2.micro", "t1.micro", "m1.small"),
fmt.Sprintf(`
resource "aws_instance" "test" {
ami = data.aws_ami.amzn-ami-minimal-hvm-ebs.id
instance_type = data.aws_ec2_instance_type_offering.available.instance_type
}
resource "aws_ec2_instance_state" "test" {
instance_id = aws_instance.test.id
state = %[1]q
force = %[2]s
}
`, state, force))
}
39 changes: 39 additions & 0 deletions internal/service/ec2/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -6539,3 +6539,42 @@ func FindNetworkPerformanceMetricSubscriptionByFourPartKey(ctx context.Context,

return nil, &resource.NotFoundError{}
}

func FindInstanceStateById(ctx context.Context, conn *ec2.EC2, id string) (*ec2.InstanceState, error) {
in := &ec2.DescribeInstanceStatusInput{
InstanceIds: aws.StringSlice([]string{id}),
IncludeAllInstances: aws.Bool(true),
}

out, err := conn.DescribeInstanceStatusWithContext(ctx, in)

if tfawserr.ErrCodeEquals(err, errCodeInvalidInstanceIDNotFound) {
return nil, &resource.NotFoundError{
LastError: err,
LastRequest: in,
}
}

if err != nil {
return nil, err
}

if out == nil || len(out.InstanceStatuses) == 0 {
return nil, tfresource.NewEmptyResultError(in)
}

instanceState := out.InstanceStatuses[0].InstanceState

if instanceState == nil || aws.StringValue(instanceState.Name) == ec2.InstanceStateNameTerminated {
return nil, tfresource.NewEmptyResultError(in)
}

// Eventual consistency check.
if aws.StringValue(out.InstanceStatuses[0].InstanceId) != id {
return nil, &resource.NotFoundError{
LastRequest: in,
}
}

return instanceState, nil
}
Loading

0 comments on commit 0b7be5b

Please sign in to comment.