Added Step Function resources (State Machine & Activity) (#11420)

* Added Step Function Activity & Step Function State Machine

* Added SFN State Machine documentation

* Added aws_sfn_activity & documentation

* Allowed import of sfn resources

* Added more checks on tests, fixed documentation

* Handled the update case of a SFN function (might be already deleting)

* Removed the State Machine import test file

* Fixed the eventual consistency of the read after delete for SFN functions
This commit is contained in:
Gauthier Wallet 2017-01-31 21:17:38 +01:00 committed by Paul Stack
parent d36680116c
commit 4da1451971
11 changed files with 817 additions and 0 deletions

View File

@ -0,0 +1,30 @@
package aws
import (
"testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
)
func TestAccAWSSfnActivity_importBasic(t *testing.T) {
resourceName := "aws_sfn_activity.foo"
rName := acctest.RandString(10)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSfnActivityDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSSfnActivityBasicConfig(rName),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

View File

@ -382,6 +382,8 @@ func Provider() terraform.ResourceProvider {
"aws_sns_topic": resourceAwsSnsTopic(),
"aws_sns_topic_policy": resourceAwsSnsTopicPolicy(),
"aws_sns_topic_subscription": resourceAwsSnsTopicSubscription(),
"aws_sfn_activity": resourceAwsSfnActivity(),
"aws_sfn_state_machine": resourceAwsSfnStateMachine(),
"aws_subnet": resourceAwsSubnet(),
"aws_volume_attachment": resourceAwsVolumeAttachment(),
"aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(),

View File

@ -0,0 +1,97 @@
package aws
import (
"fmt"
"log"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/sfn"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsSfnActivity() *schema.Resource {
return &schema.Resource{
Create: resourceAwsSfnActivityCreate,
Read: resourceAwsSfnActivityRead,
Delete: resourceAwsSfnActivityDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateSfnActivityName,
},
"creation_date": {
Type: schema.TypeString,
Computed: true,
},
},
}
}
func resourceAwsSfnActivityCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).sfnconn
log.Print("[DEBUG] Creating Step Function Activity")
params := &sfn.CreateActivityInput{
Name: aws.String(d.Get("name").(string)),
}
activity, err := conn.CreateActivity(params)
if err != nil {
return fmt.Errorf("Error creating Step Function Activity: %s", err)
}
d.SetId(*activity.ActivityArn)
return resourceAwsSfnActivityRead(d, meta)
}
func resourceAwsSfnActivityRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).sfnconn
log.Printf("[DEBUG] Reading Step Function Activity: %s", d.Id())
sm, err := conn.DescribeActivity(&sfn.DescribeActivityInput{
ActivityArn: aws.String(d.Id()),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ActivityDoesNotExist" {
d.SetId("")
return nil
}
return err
}
d.Set("name", sm.Name)
if err := d.Set("creation_date", sm.CreationDate.Format(time.RFC3339)); err != nil {
log.Printf("[DEBUG] Error setting creation_date: %s", err)
}
return nil
}
func resourceAwsSfnActivityDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).sfnconn
log.Printf("[DEBUG] Deleting Step Functions Activity: %s", d.Id())
return resource.Retry(5*time.Minute, func() *resource.RetryError {
_, err := conn.DeleteActivity(&sfn.DeleteActivityInput{
ActivityArn: aws.String(d.Id()),
})
if err == nil {
return nil
}
return resource.NonRetryableError(err)
})
}

View File

@ -0,0 +1,106 @@
package aws
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/sfn"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
"time"
)
func TestAccAWSSfnActivity_basic(t *testing.T) {
name := acctest.RandString(10)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSfnActivityDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSSfnActivityBasicConfig(name),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSfnActivityExists("aws_sfn_activity.foo"),
resource.TestCheckResourceAttr("aws_sfn_activity.foo", "name", name),
resource.TestCheckResourceAttrSet("aws_sfn_activity.foo", "creation_date"),
),
},
},
})
}
func testAccCheckAWSSfnActivityExists(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 fmt.Errorf("No Step Function ID set")
}
conn := testAccProvider.Meta().(*AWSClient).sfnconn
_, err := conn.DescribeActivity(&sfn.DescribeActivityInput{
ActivityArn: aws.String(rs.Primary.ID),
})
if err != nil {
return err
}
return nil
}
}
func testAccCheckAWSSfnActivityDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).sfnconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_sfn_activity" {
continue
}
// Retrying as Read after Delete is not always consistent
retryErr := resource.Retry(1*time.Minute, func() *resource.RetryError {
var err error
_, err = conn.DescribeActivity(&sfn.DescribeActivityInput{
ActivityArn: aws.String(rs.Primary.ID),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "ActivityDoesNotExist" {
return nil
}
return resource.NonRetryableError(err)
}
// If there are no errors, the removal failed
// and the object is not yet removed.
return resource.RetryableError(fmt.Errorf("Expected AWS Step Function Activity to be destroyed, but was still found, retrying"))
})
if retryErr != nil {
return retryErr
}
return nil
}
return fmt.Errorf("Default error in Step Function Test")
}
func testAccAWSSfnActivityBasicConfig(rName string) string {
return fmt.Sprintf(`
resource "aws_sfn_activity" "foo" {
name = "%s"
}
`, rName)
}

View File

@ -0,0 +1,140 @@
package aws
import (
"log"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/sfn"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsSfnStateMachine() *schema.Resource {
return &schema.Resource{
Create: resourceAwsSfnStateMachineCreate,
Read: resourceAwsSfnStateMachineRead,
Delete: resourceAwsSfnStateMachineDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"definition": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateSfnStateMachineDefinition,
},
"name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateSfnStateMachineName,
},
"role_arn": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validateArn,
},
"creation_date": {
Type: schema.TypeString,
Computed: true,
},
"status": {
Type: schema.TypeString,
Computed: true,
},
},
}
}
func resourceAwsSfnStateMachineCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).sfnconn
log.Print("[DEBUG] Creating Step Function State Machine")
params := &sfn.CreateStateMachineInput{
Definition: aws.String(d.Get("definition").(string)),
Name: aws.String(d.Get("name").(string)),
RoleArn: aws.String(d.Get("role_arn").(string)),
}
var activity *sfn.CreateStateMachineOutput
err := resource.Retry(5*time.Minute, func() *resource.RetryError {
var err error
activity, err = conn.CreateStateMachine(params)
if err != nil {
// Note: the instance may be in a deleting mode, hence the retry
// when creating the step function. This can happen when we are
// updating the resource (since there is no update API call).
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "StateMachineDeleting" {
return resource.RetryableError(err)
}
return resource.NonRetryableError(err)
}
return nil
})
if err != nil {
return errwrap.Wrapf("Error creating Step Function State Machine: {{err}}", err)
}
d.SetId(*activity.StateMachineArn)
return resourceAwsSfnStateMachineRead(d, meta)
}
func resourceAwsSfnStateMachineRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).sfnconn
log.Printf("[DEBUG] Reading Step Function State Machine: %s", d.Id())
sm, err := conn.DescribeStateMachine(&sfn.DescribeStateMachineInput{
StateMachineArn: aws.String(d.Id()),
})
if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NotFoundException" {
d.SetId("")
return nil
}
return err
}
d.Set("definition", sm.Definition)
d.Set("name", sm.Name)
d.Set("role_arn", sm.RoleArn)
d.Set("status", sm.Status)
if err := d.Set("creation_date", sm.CreationDate.Format(time.RFC3339)); err != nil {
log.Printf("[DEBUG] Error setting creation_date: %s", err)
}
return nil
}
func resourceAwsSfnStateMachineDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).sfnconn
log.Printf("[DEBUG] Deleting Step Function State Machine: %s", d.Id())
return resource.Retry(5*time.Minute, func() *resource.RetryError {
_, err := conn.DeleteStateMachine(&sfn.DeleteStateMachineInput{
StateMachineArn: aws.String(d.Id()),
})
if err == nil {
return nil
}
return resource.NonRetryableError(err)
})
}

View File

@ -0,0 +1,201 @@
package aws
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/sfn"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSSfnStateMachine_basic(t *testing.T) {
name := acctest.RandString(10)
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSSfnStateMachineDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSSfnStateMachineBasicConfig(name),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSSfnExists("aws_sfn_state_machine.foo"),
resource.TestCheckResourceAttr("aws_sfn_state_machine.foo", "status", sfn.StateMachineStatusActive),
resource.TestCheckResourceAttrSet("aws_sfn_state_machine.foo", "name"),
resource.TestCheckResourceAttrSet("aws_sfn_state_machine.foo", "creation_date"),
resource.TestCheckResourceAttrSet("aws_sfn_state_machine.foo", "definition"),
resource.TestCheckResourceAttrSet("aws_sfn_state_machine.foo", "role_arn"),
),
},
},
})
}
func testAccCheckAWSSfnExists(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 fmt.Errorf("No Step Function ID set")
}
conn := testAccProvider.Meta().(*AWSClient).sfnconn
_, err := conn.DescribeStateMachine(&sfn.DescribeStateMachineInput{
StateMachineArn: aws.String(rs.Primary.ID),
})
if err != nil {
return err
}
return nil
}
}
func testAccCheckAWSSfnStateMachineDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).sfnconn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_sfn_state_machine" {
continue
}
out, err := conn.DescribeStateMachine(&sfn.DescribeStateMachineInput{
StateMachineArn: aws.String(rs.Primary.ID),
})
if err != nil {
if wserr, ok := err.(awserr.Error); ok && wserr.Code() == "StateMachineDoesNotExist" {
return nil
}
return err
}
if out != nil && *out.Status != sfn.StateMachineStatusDeleting {
return fmt.Errorf("Expected AWS Step Function State Machine to be destroyed, but was still found")
}
return nil
}
return fmt.Errorf("Default error in Step Function Test")
}
func testAccAWSSfnStateMachineBasicConfig(rName string) string {
return fmt.Sprintf(`
data "aws_region" "current" {
current = true
}
resource "aws_iam_role_policy" "iam_policy_for_lambda" {
name = "iam_policy_for_lambda_%s"
role = "${aws_iam_role.iam_for_lambda.id}"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}]
}
EOF
}
resource "aws_iam_role" "iam_for_lambda" {
name = "iam_for_lambda_%s"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
EOF
}
resource "aws_iam_role_policy" "iam_policy_for_sfn" {
name = "iam_policy_for_sfn_%s"
role = "${aws_iam_role.iam_for_sfn.id}"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": "*"
}
]
}
EOF
}
resource "aws_iam_role" "iam_for_sfn" {
name = "iam_for_sfn_%s"
assume_role_policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "states.${data.aws_region.current.name}.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
}
resource "aws_lambda_function" "lambda_function_test" {
filename = "test-fixtures/lambdatest.zip"
function_name = "sfn-%s"
role = "${aws_iam_role.iam_for_lambda.arn}"
handler = "exports.example"
runtime = "nodejs4.3"
}
resource "aws_sfn_state_machine" "foo" {
name = "test_sfn_%s"
role_arn = "${aws_iam_role.iam_for_sfn.arn}"
definition = <<EOF
{
"Comment": "A Hello World example of the Amazon States Language using an AWS Lambda Function",
"StartAt": "HelloWorld",
"States": {
"HelloWorld": {
"Type": "Task",
"Resource": "${aws_lambda_function.lambda_function_test.arn}",
"End": true
}
}
}
EOF
}
`, rName, rName, rName, rName, rName, rName)
}

View File

@ -743,3 +743,33 @@ func validateAwsEmrEbsVolumeType(v interface{}, k string) (ws []string, errors [
}
return
}
func validateSfnActivityName(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if len(value) > 80 {
errors = append(errors, fmt.Errorf("%q cannot be longer than 80 characters", k))
}
return
}
func validateSfnStateMachineDefinition(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if len(value) > 1048576 {
errors = append(errors, fmt.Errorf("%q cannot be longer than 1048576 characters", k))
}
return
}
func validateSfnStateMachineName(v interface{}, k string) (ws []string, errors []error) {
value := v.(string)
if len(value) > 80 {
errors = append(errors, fmt.Errorf("%q cannot be longer than 80 characters", k))
}
if !regexp.MustCompile(`^[a-zA-Z0-9-_]+$`).MatchString(value) {
errors = append(errors, fmt.Errorf(
"%q must be composed with only these characters [a-zA-Z0-9-_]: %v", k, value))
}
return
}

View File

@ -1171,7 +1171,101 @@ func TestValidateEcsPlacementStrategy(t *testing.T) {
t.Fatalf("Unexpected validation error for \"%s:%s\": %s",
tc.stratType, tc.stratField, err)
}
}
}
func TestValidateStepFunctionActivityName(t *testing.T) {
validTypes := []string{
"foo",
"FooBar123",
}
invalidTypes := []string{
strings.Repeat("W", 81), // length > 80
}
for _, v := range validTypes {
_, errors := validateSfnActivityName(v, "name")
if len(errors) != 0 {
t.Fatalf("%q should be a valid Step Function Activity name: %v", v, errors)
}
}
for _, v := range invalidTypes {
_, errors := validateSfnActivityName(v, "name")
if len(errors) == 0 {
t.Fatalf("%q should not be a valid Step Function Activity name", v)
}
}
}
func TestValidateStepFunctionStateMachineDefinition(t *testing.T) {
validDefinitions := []string{
"foobar",
strings.Repeat("W", 1048576),
}
invalidDefinitions := []string{
strings.Repeat("W", 1048577), // length > 1048576
}
for _, v := range validDefinitions {
_, errors := validateSfnStateMachineDefinition(v, "definition")
if len(errors) != 0 {
t.Fatalf("%q should be a valid Step Function State Machine definition: %v", v, errors)
}
}
for _, v := range invalidDefinitions {
_, errors := validateSfnStateMachineDefinition(v, "definition")
if len(errors) == 0 {
t.Fatalf("%q should not be a valid Step Function State Machine definition", v)
}
}
}
func TestValidateStepFunctionStateMachineName(t *testing.T) {
validTypes := []string{
"foo",
"BAR",
"FooBar123",
"FooBar123Baz-_",
}
invalidTypes := []string{
"foo bar",
"foo<bar>",
"foo{bar}",
"foo[bar]",
"foo*bar",
"foo?bar",
"foo#bar",
"foo%bar",
"foo\bar",
"foo^bar",
"foo|bar",
"foo~bar",
"foo$bar",
"foo&bar",
"foo,bar",
"foo:bar",
"foo;bar",
"foo/bar",
strings.Repeat("W", 81), // length > 80
}
for _, v := range validTypes {
_, errors := validateSfnStateMachineName(v, "name")
if len(errors) != 0 {
t.Fatalf("%q should be a valid Step Function State Machine name: %v", v, errors)
}
}
for _, v := range invalidTypes {
_, errors := validateSfnStateMachineName(v, "name")
if len(errors) == 0 {
t.Fatalf("%q should not be a valid Step Function State Machine name", v)
}
}
}

View File

@ -0,0 +1,41 @@
---
layout: "aws"
page_title: "AWS: sfn_activity"
sidebar_current: "docs-aws-resource-sfn-activity"
description: |-
Provides a Step Function Activity resource.
---
# sfn\_activity
Provides a Step Function Activity resource
## Example Usage
```
resource "aws_sfn_activity" "sfn_activity" {
name = "my-activity"
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) The name of the activity to create.
## Attributes Reference
The following attributes are exported:
* `id` - The Amazon Resource Name (ARN) that identifies the created activity.
* `name` - The name of the activity.
* `creation_date` - The date the activity was created.
## Import
Activities can be imported using the `arn`, e.g.
```
$ terraform import aws_sfn_activity.foo arn:aws:states:eu-west-1:123456789098:activity:bar
```

View File

@ -0,0 +1,60 @@
---
layout: "aws"
page_title: "AWS: sfn_state_machine"
sidebar_current: "docs-aws-resource-sfn-state-machine"
description: |-
Provides a Step Function State Machine resource.
---
# sfn\_state\_machine
Provides a Step Function State Machine resource
## Example Usage
```
...
resource "aws_sfn_state_machine" "sfn_state_machine" {
name = "my-state-machine"
role_arn = "${aws_iam_role.iam_for_sfn.arn}"
definition = <<EOF
{
"Comment": "A Hello World example of the Amazon States Language using an AWS Lambda Function",
"StartAt": "HelloWorld",
"States": {
"HelloWorld": {
"Type": "Task",
"Resource": "${aws_lambda_function.lambda.arn}",
"End": true
}
}
}
EOF
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) The name of the state machine.
* `definition` - (Required) The Amazon States Language definition of the state machine.
* `role_arn` - (Required) The Amazon Resource Name (ARN) of the IAM role to use for this state machine.
## Attributes Reference
The following attributes are exported:
* `id` - The ARN of the state machine.
* `creation_date` - The date the state machine was created.
* `status` - The current status of the state machine. Either "ACTIVE" or "DELETING".
## Import
State Machines can be imported using the `arn`, e.g.
```
$ terraform import aws_sfn_state_machine.foo arn:aws:states:eu-west-1:123456789098:stateMachine:bar
```

View File

@ -1024,6 +1024,22 @@
</li>
<li<%= sidebar_current(/^docs-aws-resource-sfn/) %>>
<a href="#">Step Function Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-aws-resource-sfn-activity") %>>
<a href="/docs/providers/aws/r/sfn_activity.html">aws_sfn_activity</a>
</li>
<li<%= sidebar_current("docs-aws-resource-sfn-state-machine") %>>
<a href="/docs/providers/aws/r/sfn_state_machine.html">aws_sfn_state_machine</a>
</li>
</ul>
</li>
<li<%= sidebar_current(/^docs-aws-resource-simpledb/) %>>
<a href="#">SimpleDB Resources</a>
<ul class="nav nav-visible">