diff --git a/builtin/providers/aws/import_aws_sfn_activity_test.go b/builtin/providers/aws/import_aws_sfn_activity_test.go new file mode 100644 index 000000000..01a91143e --- /dev/null +++ b/builtin/providers/aws/import_aws_sfn_activity_test.go @@ -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, + }, + }, + }) +} diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 8d3d8f9a7..1554c7b47 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -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(), diff --git a/builtin/providers/aws/resource_aws_sfn_activity.go b/builtin/providers/aws/resource_aws_sfn_activity.go new file mode 100644 index 000000000..7ed65d1f4 --- /dev/null +++ b/builtin/providers/aws/resource_aws_sfn_activity.go @@ -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) + }) +} diff --git a/builtin/providers/aws/resource_aws_sfn_activity_test.go b/builtin/providers/aws/resource_aws_sfn_activity_test.go new file mode 100644 index 000000000..47a005717 --- /dev/null +++ b/builtin/providers/aws/resource_aws_sfn_activity_test.go @@ -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) +} diff --git a/builtin/providers/aws/resource_aws_sfn_state_machine.go b/builtin/providers/aws/resource_aws_sfn_state_machine.go new file mode 100644 index 000000000..9d0fc4ca7 --- /dev/null +++ b/builtin/providers/aws/resource_aws_sfn_state_machine.go @@ -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) + }) +} diff --git a/builtin/providers/aws/resource_aws_sfn_state_machine_test.go b/builtin/providers/aws/resource_aws_sfn_state_machine_test.go new file mode 100644 index 000000000..408717ac4 --- /dev/null +++ b/builtin/providers/aws/resource_aws_sfn_state_machine_test.go @@ -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 = < 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 +} diff --git a/builtin/providers/aws/validators_test.go b/builtin/providers/aws/validators_test.go index 68451fad3..0bf01c607 100644 --- a/builtin/providers/aws/validators_test.go +++ b/builtin/providers/aws/validators_test.go @@ -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", + "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) + } } } diff --git a/website/source/docs/providers/aws/r/sfn_activity.html.markdown b/website/source/docs/providers/aws/r/sfn_activity.html.markdown new file mode 100644 index 000000000..a5a52d517 --- /dev/null +++ b/website/source/docs/providers/aws/r/sfn_activity.html.markdown @@ -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 +``` diff --git a/website/source/docs/providers/aws/r/sfn_state_machine.html.markdown b/website/source/docs/providers/aws/r/sfn_state_machine.html.markdown new file mode 100644 index 000000000..633ab1dd5 --- /dev/null +++ b/website/source/docs/providers/aws/r/sfn_state_machine.html.markdown @@ -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 = < + > + Step Function Resources + + + + > SimpleDB Resources