diff --git a/.changelog/13140.txt b/.changelog/13140.txt new file mode 100644 index 000000000000..c15a0e0c7525 --- /dev/null +++ b/.changelog/13140.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_synthetics_canary +``` \ No newline at end of file diff --git a/aws/internal/service/synthetics/finder/finder.go b/aws/internal/service/synthetics/finder/finder.go new file mode 100644 index 000000000000..c7ed5179088a --- /dev/null +++ b/aws/internal/service/synthetics/finder/finder.go @@ -0,0 +1,20 @@ +package finder + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/synthetics" +) + +// CanaryByName returns the Canary corresponding to the specified Name. +func CanaryByName(conn *synthetics.Synthetics, name string) (*synthetics.GetCanaryOutput, error) { + input := &synthetics.GetCanaryInput{ + Name: aws.String(name), + } + + output, err := conn.GetCanary(input) + if err != nil { + return nil, err + } + + return output, nil +} diff --git a/aws/internal/service/synthetics/waiter/status.go b/aws/internal/service/synthetics/waiter/status.go new file mode 100644 index 000000000000..b25b869f6ba6 --- /dev/null +++ b/aws/internal/service/synthetics/waiter/status.go @@ -0,0 +1,26 @@ +package waiter + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/synthetics" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/synthetics/finder" +) + +func CanaryStatus(conn *synthetics.Synthetics, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := finder.CanaryByName(conn, name) + + if err != nil { + return nil, synthetics.CanaryStateError, err + } + + if aws.StringValue(output.Canary.Status.State) == synthetics.CanaryStateError { + return output, synthetics.CanaryStateError, fmt.Errorf("%s: %s", aws.StringValue(output.Canary.Status.StateReasonCode), aws.StringValue(output.Canary.Status.StateReason)) + } + + return output, aws.StringValue(output.Canary.Status.State), nil + } +} diff --git a/aws/internal/service/synthetics/waiter/waiter.go b/aws/internal/service/synthetics/waiter/waiter.go new file mode 100644 index 000000000000..39501896d6f0 --- /dev/null +++ b/aws/internal/service/synthetics/waiter/waiter.go @@ -0,0 +1,90 @@ +package waiter + +import ( + "time" + + "github.com/aws/aws-sdk-go/service/synthetics" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +const ( + // Maximum amount of time to wait for a Canary to return Ready + CanaryCreatedTimeout = 5 * time.Minute + CanaryRunningTimeout = 5 * time.Minute + CanaryStoppedTimeout = 5 * time.Minute + CanaryDeletedTimeout = 5 * time.Minute +) + +// CanaryReady waits for a Canary to return Ready +func CanaryReady(conn *synthetics.Synthetics, name string) (*synthetics.GetCanaryOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{synthetics.CanaryStateCreating, synthetics.CanaryStateUpdating}, + Target: []string{synthetics.CanaryStateReady}, + Refresh: CanaryStatus(conn, name), + Timeout: CanaryCreatedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*synthetics.GetCanaryOutput); ok { + return v, err + } + + return nil, err +} + +// CanaryReady waits for a Canary to return Stopped +func CanaryStopped(conn *synthetics.Synthetics, name string) (*synthetics.GetCanaryOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{synthetics.CanaryStateStopping, synthetics.CanaryStateUpdating, + synthetics.CanaryStateRunning, synthetics.CanaryStateReady, synthetics.CanaryStateStarting}, + Target: []string{synthetics.CanaryStateStopped}, + Refresh: CanaryStatus(conn, name), + Timeout: CanaryStoppedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*synthetics.GetCanaryOutput); ok { + return v, err + } + + return nil, err +} + +// CanaryReady waits for a Canary to return Running +func CanaryRunning(conn *synthetics.Synthetics, name string) (*synthetics.GetCanaryOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{synthetics.CanaryStateStarting, synthetics.CanaryStateUpdating, + synthetics.CanaryStateStarting, synthetics.CanaryStateReady}, + Target: []string{synthetics.CanaryStateRunning}, + Refresh: CanaryStatus(conn, name), + Timeout: CanaryRunningTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*synthetics.GetCanaryOutput); ok { + return v, err + } + + return nil, err +} + +// CanaryReady waits for a Canary to return Ready +func CanaryDeleted(conn *synthetics.Synthetics, name string) (*synthetics.GetCanaryOutput, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{synthetics.CanaryStateDeleting, synthetics.CanaryStateStopped}, + Target: []string{}, + Refresh: CanaryStatus(conn, name), + Timeout: CanaryDeletedTimeout, + } + + outputRaw, err := stateConf.WaitForState() + + if v, ok := outputRaw.(*synthetics.GetCanaryOutput); ok { + return v, err + } + + return nil, err +} diff --git a/aws/provider.go b/aws/provider.go index b99c34da705f..d07e1c7deca5 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -994,6 +994,7 @@ func Provider() *schema.Provider { "aws_default_subnet": resourceAwsDefaultSubnet(), "aws_subnet": resourceAwsSubnet(), "aws_swf_domain": resourceAwsSwfDomain(), + "aws_synthetics_canary": resourceAwsSyntheticsCanary(), "aws_transfer_server": resourceAwsTransferServer(), "aws_transfer_ssh_key": resourceAwsTransferSshKey(), "aws_transfer_user": resourceAwsTransferUser(), diff --git a/aws/resource_aws_synthetics_canary.go b/aws/resource_aws_synthetics_canary.go new file mode 100644 index 000000000000..ee730dabcd92 --- /dev/null +++ b/aws/resource_aws_synthetics_canary.go @@ -0,0 +1,649 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/synthetics" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/synthetics/finder" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/synthetics/waiter" +) + +const awsMutexCanary = `aws_synthetics_canary` + +func resourceAwsSyntheticsCanary() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsSyntheticsCanaryCreate, + Read: resourceAwsSyntheticsCanaryRead, + Update: resourceAwsSyntheticsCanaryUpdate, + Delete: resourceAwsSyntheticsCanaryDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "artifact_s3_location": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return strings.TrimPrefix(new, "s3://") == old + }, + }, + "engine_arn": { + Type: schema.TypeString, + Computed: true, + }, + "execution_role_arn": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validateArn, + }, + "failure_retention_period": { + Type: schema.TypeInt, + Optional: true, + Default: 31, + ValidateFunc: validation.IntBetween(1, 455), + }, + "handler": { + Type: schema.TypeString, + Required: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 21), + validation.StringMatch(regexp.MustCompile(`^[0-9a-z_\-]+$`), "must contain only alphanumeric, hyphen, underscore."), + ), + }, + "run_config": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "active_tracing": { + Type: schema.TypeBool, + Optional: true, + }, + "memory_in_mb": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validation.All( + validation.IntDivisibleBy(64), + validation.IntAtLeast(960), + ), + }, + "timeout_in_seconds": { + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(60, 14*60), + Default: 840, + }, + }, + }, + }, + "runtime_version": { + Type: schema.TypeString, + Required: true, + }, + "s3_bucket": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"zip_file"}, + RequiredWith: []string{"s3_key"}, + }, + "s3_key": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"zip_file"}, + RequiredWith: []string{"s3_bucket"}, + }, + "s3_version": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"zip_file"}, + }, + "schedule": { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "duration_in_seconds": { + Type: schema.TypeInt, + Optional: true, + }, + "expression": { + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + return new == "rate(0 minute)" && old == "rate(0 hour)" + }, + }, + }, + }, + }, + "source_location_arn": { + Type: schema.TypeString, + Computed: true, + }, + "start_canary": { + Type: schema.TypeBool, + Default: false, + Optional: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "success_retention_period": { + Type: schema.TypeInt, + Optional: true, + Default: 31, + ValidateFunc: validation.IntBetween(1, 455), + }, + "tags": tagsSchema(), + "timeline": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "created": { + Type: schema.TypeString, + Computed: true, + }, + "last_modified": { + Type: schema.TypeString, + Computed: true, + }, + "last_started": { + Type: schema.TypeString, + Computed: true, + }, + "last_stopped": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "vpc_config": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "security_group_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "subnet_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + "vpc_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "zip_file": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"s3_bucket", "s3_key", "s3_version"}, + }, + }, + } +} + +func resourceAwsSyntheticsCanaryCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).syntheticsconn + + input := &synthetics.CreateCanaryInput{ + Name: aws.String(d.Get("name").(string)), + ArtifactS3Location: aws.String(d.Get("artifact_s3_location").(string)), + ExecutionRoleArn: aws.String(d.Get("execution_role_arn").(string)), + RuntimeVersion: aws.String(d.Get("runtime_version").(string)), + } + + code, err := expandAwsSyntheticsCanaryCode(d) + if err != nil { + return err + } + + input.Code = code + + if v := d.Get("tags").(map[string]interface{}); len(v) > 0 { + input.Tags = keyvaluetags.New(v).IgnoreAws().SyntheticsTags() + } + + if v, ok := d.GetOk("run_config"); ok { + input.RunConfig = expandAwsSyntheticsCanaryRunConfig(v.([]interface{})) + } + + if v, ok := d.GetOk("schedule"); ok { + input.Schedule = expandAwsSyntheticsCanarySchedule(v.([]interface{})) + } + + if v, ok := d.GetOk("vpc_config"); ok { + input.VpcConfig = expandAwsSyntheticsCanaryVpcConfig(v.([]interface{})) + } + + if v, ok := d.GetOk("failure_retention_period"); ok { + input.FailureRetentionPeriodInDays = aws.Int64(int64(v.(int))) + } + + if v, ok := d.GetOk("success_retention_period"); ok { + input.SuccessRetentionPeriodInDays = aws.Int64(int64(v.(int))) + } + + log.Printf("[DEBUG] creating Synthetics Canary: %#v", input) + + resp, err := conn.CreateCanary(input) + if err != nil { + return fmt.Errorf("error creating Synthetics Canary: %w", err) + } + + d.SetId(aws.StringValue(resp.Canary.Name)) + + if _, err := waiter.CanaryReady(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for Synthetics Canary (%s) creation: %w", d.Id(), err) + } + + if v := d.Get("start_canary"); v.(bool) { + if err := syntheticsStartCanary(d.Id(), conn); err != nil { + return err + } + } + + return resourceAwsSyntheticsCanaryRead(d, meta) +} + +func resourceAwsSyntheticsCanaryRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).syntheticsconn + ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig + + resp, err := finder.CanaryByName(conn, d.Id()) + if err != nil { + if isAWSErr(err, synthetics.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] Synthetics Canary (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("error reading Synthetics Canary: %w", err) + } + + canary := resp.Canary + d.Set("artifact_s3_location", canary.ArtifactS3Location) + d.Set("engine_arn", canary.EngineArn) + d.Set("execution_role_arn", canary.ExecutionRoleArn) + d.Set("failure_retention_period", canary.FailureRetentionPeriodInDays) + d.Set("handler", canary.Code.Handler) + d.Set("name", canary.Name) + d.Set("runtime_version", canary.RuntimeVersion) + d.Set("source_location_arn", canary.Code.SourceLocationArn) + d.Set("status", canary.Status.State) + d.Set("success_retention_period", canary.SuccessRetentionPeriodInDays) + + canaryArn := arn.ARN{ + Partition: meta.(*AWSClient).partition, + Service: synthetics.ServiceName, + Region: meta.(*AWSClient).region, + AccountID: meta.(*AWSClient).accountid, + Resource: fmt.Sprintf("canary:%s", aws.StringValue(canary.Name)), + }.String() + + d.Set("arn", canaryArn) + + if err := d.Set("vpc_config", flattenAwsSyntheticsCanaryVpcConfig(canary.VpcConfig)); err != nil { + return fmt.Errorf("error setting vpc config: %w", err) + } + + if err := d.Set("run_config", flattenAwsSyntheticsCanaryRunConfig(canary.RunConfig)); err != nil { + return fmt.Errorf("error setting run config: %w", err) + } + + if err := d.Set("schedule", flattenAwsSyntheticsCanarySchedule(canary.Schedule)); err != nil { + return fmt.Errorf("error setting schedule: %w", err) + } + + if err := d.Set("timeline", flattenAwsSyntheticsCanaryTimeline(canary.Timeline)); err != nil { + return fmt.Errorf("error setting schedule: %w", err) + } + + if err := d.Set("tags", keyvaluetags.SyntheticsKeyValueTags(canary.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + return nil +} + +func resourceAwsSyntheticsCanaryUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).syntheticsconn + + input := &synthetics.UpdateCanaryInput{ + Name: aws.String(d.Id()), + } + + updateFlag := false + + if d.HasChange("vpc_config") { + input.VpcConfig = expandAwsSyntheticsCanaryVpcConfig(d.Get("vpc_config").([]interface{})) + updateFlag = true + } + + if d.HasChange("runtime_version") { + input.RuntimeVersion = aws.String(d.Get("runtime_version").(string)) + updateFlag = true + } + + if d.HasChanges("handler", "zip_file", "s3_bucket", "s3_key", "s3_version") { + code, err := expandAwsSyntheticsCanaryCode(d) + if err != nil { + return err + } + input.Code = code + updateFlag = true + } + + if d.HasChange("run_config") { + input.RunConfig = expandAwsSyntheticsCanaryRunConfig(d.Get("run_config").([]interface{})) + updateFlag = true + } + + if d.HasChange("schedule") { + input.Schedule = expandAwsSyntheticsCanarySchedule(d.Get("schedule").([]interface{})) + updateFlag = true + } + + if d.HasChange("success_retention_period") { + _, n := d.GetChange("success_retention_period") + input.SuccessRetentionPeriodInDays = aws.Int64(int64(n.(int))) + updateFlag = true + } + + if d.HasChange("failure_retention_period") { + _, n := d.GetChange("failure_retention_period") + input.FailureRetentionPeriodInDays = aws.Int64(int64(n.(int))) + updateFlag = true + } + + if d.HasChange("execution_role_arn") { + _, n := d.GetChange("execution_role_arn") + input.ExecutionRoleArn = aws.String(n.(string)) + updateFlag = true + } + + if updateFlag { + if status := d.Get("status"); status.(string) == synthetics.CanaryStateRunning { + if err := syntheticsStopCanary(d.Id(), conn); err != nil { + return err + } + } + + _, err := conn.UpdateCanary(input) + if err != nil { + return fmt.Errorf("error updating Synthetics Canary: %w", err) + } + + if _, err := waiter.CanaryReady(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for Synthetics Canary (%s) updating: %w", d.Id(), err) + } + } + + status := d.Get("status") + if v := d.Get("start_canary"); v.(bool) { + if status.(string) != synthetics.CanaryStateRunning { + if err := syntheticsStartCanary(d.Id(), conn); err != nil { + return err + } + } + } else { + if status.(string) == synthetics.CanaryStateRunning { + if err := syntheticsStopCanary(d.Id(), conn); err != nil { + return err + } + } + } + + if d.HasChange("tags") { + o, n := d.GetChange("tags") + + if err := keyvaluetags.SyntheticsUpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return fmt.Errorf("error updating Synthetics Canary (%s) tags: %w", d.Id(), err) + } + } + + return resourceAwsSyntheticsCanaryRead(d, meta) +} + +func resourceAwsSyntheticsCanaryDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).syntheticsconn + + if status := d.Get("status"); status.(string) == synthetics.CanaryStateRunning { + if err := syntheticsStopCanary(d.Id(), conn); err != nil { + log.Print("could not stop canary before delete") + } + } + + input := &synthetics.DeleteCanaryInput{ + Name: aws.String(d.Id()), + } + + _, err := conn.DeleteCanary(input) + if err != nil { + if isAWSErr(err, synthetics.ErrCodeResourceNotFoundException, "") { + return nil + } + return fmt.Errorf("error deleting Synthetics Canary: %w", err) + } + + if _, err := waiter.CanaryDeleted(conn, d.Id()); err != nil { + if isAWSErr(err, synthetics.ErrCodeResourceNotFoundException, "") { + return nil + } + return fmt.Errorf("error waiting for Synthetics Canary (%s) deletion: %w", d.Id(), err) + } + + return nil +} + +func expandAwsSyntheticsCanaryCode(d *schema.ResourceData) (*synthetics.CanaryCodeInput, error) { + codeConfig := &synthetics.CanaryCodeInput{ + Handler: aws.String(d.Get("handler").(string)), + } + + if v, ok := d.GetOk("zip_file"); ok { + awsMutexKV.Lock(awsMutexCanary) + defer awsMutexKV.Unlock(awsMutexCanary) + file, err := loadFileContent(v.(string)) + if err != nil { + return nil, fmt.Errorf("unable to load %q: %w", v.(string), err) + } + codeConfig.ZipFile = file + } else { + codeConfig.S3Bucket = aws.String(d.Get("s3_bucket").(string)) + codeConfig.S3Key = aws.String(d.Get("s3_key").(string)) + + if v, ok := d.GetOk("s3_version"); ok { + codeConfig.S3Version = aws.String(v.(string)) + } + } + + return codeConfig, nil +} + +func expandAwsSyntheticsCanarySchedule(l []interface{}) *synthetics.CanaryScheduleInput { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + codeConfig := &synthetics.CanaryScheduleInput{ + Expression: aws.String(m["expression"].(string)), + } + + if v, ok := m["duration_in_seconds"]; ok { + codeConfig.DurationInSeconds = aws.Int64(int64(v.(int))) + } + + return codeConfig +} + +func flattenAwsSyntheticsCanarySchedule(canarySchedule *synthetics.CanaryScheduleOutput) []interface{} { + if canarySchedule == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "expression": aws.StringValue(canarySchedule.Expression), + "duration_in_seconds": aws.Int64Value(canarySchedule.DurationInSeconds), + } + + return []interface{}{m} +} + +func expandAwsSyntheticsCanaryRunConfig(l []interface{}) *synthetics.CanaryRunConfigInput { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + codeConfig := &synthetics.CanaryRunConfigInput{ + TimeoutInSeconds: aws.Int64(int64(m["timeout_in_seconds"].(int))), + } + + if v, ok := m["memory_in_mb"].(int); ok && v > 0 { + codeConfig.MemoryInMB = aws.Int64(int64(v)) + } + + if v, ok := m["active_tracing"].(bool); ok { + codeConfig.ActiveTracing = aws.Bool(v) + } + + return codeConfig +} + +func flattenAwsSyntheticsCanaryRunConfig(canaryCodeOut *synthetics.CanaryRunConfigOutput) []interface{} { + if canaryCodeOut == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "timeout_in_seconds": aws.Int64Value(canaryCodeOut.TimeoutInSeconds), + "memory_in_mb": aws.Int64Value(canaryCodeOut.MemoryInMB), + "active_tracing": aws.BoolValue(canaryCodeOut.ActiveTracing), + } + + return []interface{}{m} +} + +func flattenAwsSyntheticsCanaryVpcConfig(canaryVpcOutput *synthetics.VpcConfigOutput) []interface{} { + if canaryVpcOutput == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "subnet_ids": flattenStringSet(canaryVpcOutput.SubnetIds), + "security_group_ids": flattenStringSet(canaryVpcOutput.SecurityGroupIds), + "vpc_id": aws.StringValue(canaryVpcOutput.VpcId), + } + + return []interface{}{m} +} + +func expandAwsSyntheticsCanaryVpcConfig(l []interface{}) *synthetics.VpcConfigInput { + if len(l) == 0 || l[0] == nil { + return nil + } + + m := l[0].(map[string]interface{}) + + codeConfig := &synthetics.VpcConfigInput{ + SubnetIds: expandStringSet(m["subnet_ids"].(*schema.Set)), + SecurityGroupIds: expandStringSet(m["security_group_ids"].(*schema.Set)), + } + + return codeConfig +} + +func flattenAwsSyntheticsCanaryTimeline(timeline *synthetics.CanaryTimeline) []interface{} { + if timeline == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "created": aws.TimeValue(timeline.Created).Format(time.RFC3339), + } + + if timeline.LastModified != nil { + m["last_modified"] = aws.TimeValue(timeline.LastModified).Format(time.RFC3339) + } + + if timeline.LastStarted != nil { + m["last_started"] = aws.TimeValue(timeline.LastStarted).Format(time.RFC3339) + } + + if timeline.LastStopped != nil { + m["last_stopped"] = aws.TimeValue(timeline.LastStopped).Format(time.RFC3339) + } + + return []interface{}{m} +} + +func syntheticsStartCanary(name string, conn *synthetics.Synthetics) error { + startInput := &synthetics.StartCanaryInput{ + Name: aws.String(name), + } + + log.Print("starting Canary") + _, err := conn.StartCanary(startInput) + if err != nil { + return fmt.Errorf("error starting Synthetics Canary: %w", err) + } + + if _, err := waiter.CanaryRunning(conn, name); err != nil { + return fmt.Errorf("error waiting for Synthetics Canary (%s) to be running: %w", name, err) + } + + return nil +} + +func syntheticsStopCanary(name string, conn *synthetics.Synthetics) error { + stopInput := &synthetics.StopCanaryInput{ + Name: aws.String(name), + } + + _, err := conn.StopCanary(stopInput) + if err != nil { + return fmt.Errorf("error stopping Synthetics Canary: %w", err) + } + + if _, err := waiter.CanaryStopped(conn, name); err != nil { + return fmt.Errorf("error waiting for Synthetics Canary (%s) to be stopped: %w", name, err) + } + + return nil +} diff --git a/aws/resource_aws_synthetics_canary_test.go b/aws/resource_aws_synthetics_canary_test.go new file mode 100644 index 000000000000..5202928fe732 --- /dev/null +++ b/aws/resource_aws_synthetics_canary_test.go @@ -0,0 +1,1036 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "strings" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/lambda" + "github.com/aws/aws-sdk-go/service/synthetics" + "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" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/synthetics/finder" +) + +func init() { + resource.AddTestSweepers("aws_synthetics_canary", &resource.Sweeper{ + Name: "aws_synthetics_canary", + F: testSweepSyntheticsCanaries, + Dependencies: []string{ + "aws_lambda_function", + "aws_lambda_layer", + "aws_cloudwatch_log_group", + }, + }) +} + +func testSweepSyntheticsCanaries(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.(*AWSClient).syntheticsconn + input := &synthetics.DescribeCanariesInput{} + var sweeperErrs *multierror.Error + + for { + output, err := conn.DescribeCanaries(input) + if testSweepSkipSweepError(err) { + log.Printf("[WARN] Skipping Synthetics Canary sweep for %s: %s", region, err) + return nil + } + if err != nil { + return fmt.Errorf("error retrieving Synthetics Canaries: %w", err) + } + + for _, canary := range output.Canaries { + name := aws.StringValue(canary.Name) + log.Printf("[INFO] Deleting Synthetics Canary: %s", name) + + r := resourceAwsSyntheticsCanary() + d := r.Data(nil) + d.SetId(name) + err := r.Delete(d, client) + + if err != nil { + log.Printf("[ERROR] %s", err) + sweeperErrs = multierror.Append(sweeperErrs, err) + continue + } + } + + if aws.StringValue(output.NextToken) == "" { + break + } + input.NextToken = output.NextToken + } + + return sweeperErrs.ErrorOrNil() +} + +func TestAccAWSSyntheticsCanary_basic(t *testing.T) { + var conf1, conf2 synthetics.Canary + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_synthetics_canary.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsSyntheticsCanaryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSyntheticsCanaryBasicConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf1), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", synthetics.ServiceName, regexp.MustCompile(`canary:.+`)), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "runtime_version", "syn-1.0"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "run_config.0.memory_in_mb", "1000"), + resource.TestCheckResourceAttr(resourceName, "run_config.0.timeout_in_seconds", "840"), + resource.TestCheckResourceAttr(resourceName, "failure_retention_period", "31"), + resource.TestCheckResourceAttr(resourceName, "success_retention_period", "31"), + resource.TestCheckResourceAttr(resourceName, "handler", "exports.handler"), + resource.TestCheckResourceAttr(resourceName, "vpc_config.#", "0"), + resource.TestCheckResourceAttr(resourceName, "schedule.0.duration_in_seconds", "0"), + resource.TestCheckResourceAttr(resourceName, "schedule.0.expression", "rate(0 hour)"), + testAccMatchResourceAttrRegionalARN(resourceName, "engine_arn", "lambda", regexp.MustCompile(fmt.Sprintf(`function:cwsyn-%s.+`, rName))), + testAccMatchResourceAttrRegionalARN(resourceName, "source_location_arn", "lambda", regexp.MustCompile(fmt.Sprintf(`layer:cwsyn-%s.+`, rName))), + resource.TestCheckResourceAttrPair(resourceName, "execution_role_arn", "aws_iam_role.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "artifact_s3_location", fmt.Sprintf("%s/", rName)), + resource.TestCheckResourceAttr(resourceName, "timeline.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "timeline.0.created"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"zip_file", "start_canary"}, + }, + { + Config: testAccAWSSyntheticsCanaryZipUpdatedConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf2), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", synthetics.ServiceName, regexp.MustCompile(`canary:.+`)), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "runtime_version", "syn-1.0"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "run_config.0.memory_in_mb", "1000"), + resource.TestCheckResourceAttr(resourceName, "run_config.0.timeout_in_seconds", "840"), + resource.TestCheckResourceAttr(resourceName, "failure_retention_period", "31"), + resource.TestCheckResourceAttr(resourceName, "success_retention_period", "31"), + resource.TestCheckResourceAttr(resourceName, "handler", "exports.handler"), + resource.TestCheckResourceAttr(resourceName, "vpc_config.#", "0"), + resource.TestCheckResourceAttr(resourceName, "schedule.0.duration_in_seconds", "0"), + resource.TestCheckResourceAttr(resourceName, "schedule.0.expression", "rate(0 hour)"), + testAccMatchResourceAttrRegionalARN(resourceName, "engine_arn", "lambda", regexp.MustCompile(fmt.Sprintf(`function:cwsyn-%s.+`, rName))), + testAccMatchResourceAttrRegionalARN(resourceName, "source_location_arn", "lambda", regexp.MustCompile(fmt.Sprintf(`layer:cwsyn-%s.+`, rName))), + resource.TestCheckResourceAttrPair(resourceName, "execution_role_arn", "aws_iam_role.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "artifact_s3_location", fmt.Sprintf("%s/", rName)), + resource.TestCheckResourceAttr(resourceName, "timeline.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "timeline.0.created"), + resource.TestCheckResourceAttrSet(resourceName, "timeline.0.last_modified"), + testAccCheckAwsSyntheticsCanaryIsUpdated(&conf1, &conf2), + ), + }, + }, + }) +} + +func TestAccAWSSyntheticsCanary_runtimeVersion(t *testing.T) { + var conf1 synthetics.Canary + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_synthetics_canary.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsSyntheticsCanaryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSyntheticsCanaryRuntimeVersionConfig(rName, "syn-nodejs-2.1"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf1), + resource.TestCheckResourceAttr(resourceName, "runtime_version", "syn-nodejs-2.1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"zip_file", "start_canary"}, + }, + { + Config: testAccAWSSyntheticsCanaryRuntimeVersionConfig(rName, "syn-nodejs-2.2"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf1), + resource.TestCheckResourceAttr(resourceName, "runtime_version", "syn-nodejs-2.2"), + ), + }, + }, + }) +} + +func TestAccAWSSyntheticsCanary_startCanary(t *testing.T) { + var conf1, conf2, conf3 synthetics.Canary + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_synthetics_canary.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsSyntheticsCanaryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSyntheticsCanaryStartCanaryConfig(rName, true), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf1), + resource.TestCheckResourceAttr(resourceName, "timeline.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "timeline.0.last_started"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"zip_file", "start_canary"}, + }, + { + Config: testAccAWSSyntheticsCanaryStartCanaryConfig(rName, false), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf2), + resource.TestCheckResourceAttr(resourceName, "timeline.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "timeline.0.last_started"), + resource.TestCheckResourceAttrSet(resourceName, "timeline.0.last_stopped"), + ), + }, + { + Config: testAccAWSSyntheticsCanaryStartCanaryConfig(rName, true), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf3), + resource.TestCheckResourceAttr(resourceName, "timeline.#", "1"), + resource.TestCheckResourceAttrSet(resourceName, "timeline.0.last_started"), + testAccCheckAwsSyntheticsCanaryIsStartedAfter(&conf2, &conf3), + ), + }, + }, + }) +} + +func TestAccAWSSyntheticsCanary_s3(t *testing.T) { + var conf synthetics.Canary + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_synthetics_canary.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsSyntheticsCanaryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSyntheticsCanaryBasicS3CodeConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", synthetics.ServiceName, regexp.MustCompile(`canary:.+`)), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "runtime_version", "syn-1.0"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "run_config.0.memory_in_mb", "1000"), + resource.TestCheckResourceAttr(resourceName, "run_config.0.timeout_in_seconds", "840"), + resource.TestCheckResourceAttr(resourceName, "run_config.0.active_tracing", "false"), + resource.TestCheckResourceAttr(resourceName, "failure_retention_period", "31"), + resource.TestCheckResourceAttr(resourceName, "success_retention_period", "31"), + resource.TestCheckResourceAttr(resourceName, "handler", "exports.handler"), + resource.TestCheckResourceAttr(resourceName, "vpc_config.#", "0"), + resource.TestCheckResourceAttr(resourceName, "schedule.0.duration_in_seconds", "0"), + resource.TestCheckResourceAttr(resourceName, "schedule.0.expression", "rate(0 hour)"), + testAccMatchResourceAttrRegionalARN(resourceName, "engine_arn", "lambda", regexp.MustCompile(fmt.Sprintf(`function:cwsyn-%s.+`, rName))), + testAccMatchResourceAttrRegionalARN(resourceName, "source_location_arn", "lambda", regexp.MustCompile(fmt.Sprintf(`layer:cwsyn-%s.+`, rName))), + resource.TestCheckResourceAttrPair(resourceName, "execution_role_arn", "aws_iam_role.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "artifact_s3_location", fmt.Sprintf("%s/", rName)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"s3_bucket", "s3_key", "s3_version", "start_canary"}, + }, + }, + }) +} + +func TestAccAWSSyntheticsCanary_runConfig(t *testing.T) { + var conf synthetics.Canary + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_synthetics_canary.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsSyntheticsCanaryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSyntheticsCanaryRunConfigConfig1(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "run_config.0.memory_in_mb", "1000"), + resource.TestCheckResourceAttr(resourceName, "run_config.0.timeout_in_seconds", "60"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"zip_file", "start_canary"}, + }, + { + Config: testAccAWSSyntheticsCanaryRunConfigConfig2(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "run_config.0.memory_in_mb", "960"), + resource.TestCheckResourceAttr(resourceName, "run_config.0.timeout_in_seconds", "120"), + ), + }, + { + Config: testAccAWSSyntheticsCanaryRunConfigConfig1(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "run_config.0.memory_in_mb", "960"), + resource.TestCheckResourceAttr(resourceName, "run_config.0.timeout_in_seconds", "60"), + ), + }, + }, + }) +} + +func TestAccAWSSyntheticsCanary_runConfigTracing(t *testing.T) { + var conf synthetics.Canary + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_synthetics_canary.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsSyntheticsCanaryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSyntheticsCanaryRunConfigTracingConfig(rName, true), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "run_config.0.active_tracing", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"zip_file", "start_canary"}, + }, + { + Config: testAccAWSSyntheticsCanaryRunConfigTracingConfig(rName, false), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "run_config.0.active_tracing", "false"), + ), + }, + { + Config: testAccAWSSyntheticsCanaryRunConfigTracingConfig(rName, true), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "run_config.0.active_tracing", "true"), + ), + }, + }, + }) +} + +func TestAccAWSSyntheticsCanary_vpc(t *testing.T) { + var conf synthetics.Canary + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_synthetics_canary.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsSyntheticsCanaryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSyntheticsCanaryVPCConfig1(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "vpc_config.0.subnet_ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "vpc_config.0.security_group_ids.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "vpc_config.0.vpc_id", "aws_vpc.test", "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"zip_file", "start_canary"}, + }, + { + Config: testAccAWSSyntheticsCanaryVPCConfig2(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "vpc_config.0.subnet_ids.#", "2"), + resource.TestCheckResourceAttr(resourceName, "vpc_config.0.security_group_ids.#", "2"), + resource.TestCheckResourceAttrPair(resourceName, "vpc_config.0.vpc_id", "aws_vpc.test", "id"), + ), + }, + { + Config: testAccAWSSyntheticsCanaryVPCConfig3(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "vpc_config.0.subnet_ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "vpc_config.0.security_group_ids.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "vpc_config.0.vpc_id", "aws_vpc.test", "id"), + testAccCheckAwsSyntheticsCanaryDeleteImplicitResources(resourceName), + ), + }, + }, + }) +} + +func TestAccAWSSyntheticsCanary_tags(t *testing.T) { + var conf synthetics.Canary + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_synthetics_canary.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsSyntheticsCanaryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSyntheticsCanaryConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"zip_file", "start_canary"}, + }, + { + Config: testAccAWSSyntheticsCanaryConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAWSSyntheticsCanaryConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccAWSSyntheticsCanary_disappears(t *testing.T) { + var conf synthetics.Canary + rName := fmt.Sprintf("tf-acc-test-%s", acctest.RandString(8)) + resourceName := "aws_synthetics_canary.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsSyntheticsCanaryDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSyntheticsCanaryBasicConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAwsSyntheticsCanaryExists(resourceName, &conf), + testAccCheckResourceDisappears(testAccProvider, resourceAwsSyntheticsCanary(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckAwsSyntheticsCanaryDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).syntheticsconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_synthetics_canary" { + continue + } + + name := rs.Primary.ID + _, err := finder.CanaryByName(conn, name) + if err != nil { + if isAWSErr(err, synthetics.ErrCodeResourceNotFoundException, "") { + return nil + } + return err + } + } + + return nil +} + +func testAccCheckAwsSyntheticsCanaryExists(n string, canary *synthetics.Canary) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("synthetics Canary not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("synthetics Canary name not set") + } + + name := rs.Primary.ID + conn := testAccProvider.Meta().(*AWSClient).syntheticsconn + + out, err := finder.CanaryByName(conn, name) + if err != nil { + return fmt.Errorf("syntherics Canary %s not found in AWS", name) + } + + *canary = *out.Canary + + return nil + } +} + +func testAccCheckAwsSyntheticsCanaryDeleteImplicitResources(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("synthetics Canary not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("synthetics Canary name not set") + } + + lambdaConn := testAccProvider.Meta().(*AWSClient).lambdaconn + + layerArn := rs.Primary.Attributes["source_location_arn"] + layerArnObj, err := arn.Parse(layerArn) + if err != nil { + return fmt.Errorf("synthetics Canary Lambda Layer %s incorrect arn format: %w", layerArn, err) + } + + layerName := strings.Split(layerArnObj.Resource, ":") + + deleteLayerVersionInput := &lambda.DeleteLayerVersionInput{ + LayerName: aws.String(layerName[1]), + VersionNumber: aws.Int64(1), + } + + _, err = lambdaConn.DeleteLayerVersion(deleteLayerVersionInput) + if err != nil { + return fmt.Errorf("synthetics Canary Lambda Layer %s could not be deleted: %w", layerArn, err) + } + + lambdaArn := rs.Primary.Attributes["engine_arn"] + lambdaArnObj, err := arn.Parse(layerArn) + if err != nil { + return fmt.Errorf("synthetics Canary Lambda %s incorrect arn format: %w", lambdaArn, err) + } + lambdaArnParts := strings.Split(lambdaArnObj.Resource, ":") + + deleteLambdaInput := &lambda.DeleteFunctionInput{ + FunctionName: aws.String(lambdaArnParts[1]), + } + + _, err = lambdaConn.DeleteFunction(deleteLambdaInput) + if err != nil { + return fmt.Errorf("synthetics Canary Lambda %s could not be deleted: %w", lambdaArn, err) + } + + return nil + } +} + +func testAccCheckAwsSyntheticsCanaryIsUpdated(first, second *synthetics.Canary) resource.TestCheckFunc { + return func(s *terraform.State) error { + if !second.Timeline.LastModified.After(*first.Timeline.LastModified) { + return fmt.Errorf("synthetics Canary not updated") + + } + + return nil + } +} + +func testAccCheckAwsSyntheticsCanaryIsStartedAfter(first, second *synthetics.Canary) resource.TestCheckFunc { + return func(s *terraform.State) error { + if !second.Timeline.LastStarted.After(*first.Timeline.LastStarted) { + return fmt.Errorf("synthetics Canary not updated") + + } + + return nil + } +} + +func testAccAWSSyntheticsCanaryConfigBase(rName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q + acl = "private" + force_destroy = true + + versioning { + enabled = true + } + + tags = { + Name = %[1]q + } +} + +resource "aws_iam_role" "test" { + name = %[1]q + + assume_role_policy = < **NOTE:** When you create a canary, AWS creates supporting implicit resources. See the Amazon CloudWatch Synthetics documentation on [DeleteCanary](https://docs.aws.amazon.com/AmazonSynthetics/latest/APIReference/API_DeleteCanary.html) for a full list. Neither AWS nor Terraform deletes these implicit resources automatically when the canary is deleted. Before deleting a canary, ensure you have all the information about the canary that you need to delete the implicit resources using Terraform shell commands, the AWS Console, or AWS CLI. + +## Example Usage + +```hcl +resource "aws_synthetics_canary" "some" { + name = "some-canary" + artifact_s3_location = "s3://some-bucket/" + execution_role_arn = "some-role" + handler = "exports.handler" + zip_file = "test-fixtures/lambdatest.zip" + runtime_version = "syn-1.0" + + schedule { + expression = "rate(0 minute)" + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `artifact_s3_location` - (Required) Location in Amazon S3 where Synthetics stores artifacts from the test runs of this canary. +* `execution_role_arn` - (Required) ARN of the IAM role to be used to run the canary. see [AWS Docs](https://docs.aws.amazon.com/AmazonSynthetics/latest/APIReference/API_CreateCanary.html#API_CreateCanary_RequestSyntax) for permissions needs for IAM Role. +* `handler` - (Required) Entry point to use for the source code when running the canary. This value must end with the string `.handler` . +* `name` - (Required) Name for this canary. +* `runtime_version` - (Required) Runtime version to use for the canary. Versions change often so consult the [Amazon CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_Library.html) for the latest valid versions. Values include `syn-python-selenium-1.0`, `syn-nodejs-puppeteer-3.0`, `syn-nodejs-2.2`, `syn-nodejs-2.1`, `syn-nodejs-2.0`, and `syn-1.0`. +* `schedule` - (Required) Configuration block providing how often the canary is to run and when these test runs are to stop. Detailed below. + +The following arguments are optional: + +* `failure_retention_period` - (Optional) Number of days to retain data about failed runs of this canary. If you omit this field, the default of 31 days is used. The valid range is 1 to 455 days. +* `run_config` - (Optional) Configuration block for individual canary runs. Detailed below. +* `s3_bucket` - (Optional) Full bucket name which is used if your canary script is located in S3. The bucket must already exist. Specify the full bucket name including s3:// as the start of the bucket name. **Conflicts with `zip_file`.** +* `s3_key` - (Optional) S3 key of your script. **Conflicts with `zip_file`.** +* `s3_version` - (Optional) S3 version ID of your script. **Conflicts with `zip_file`.** +* `start_canary` - (Optional) Whether to run or stop the canary. +* `success_retention_period` - (Optional) Number of days to retain data about successful runs of this canary. If you omit this field, the default of 31 days is used. The valid range is 1 to 455 days. +* `tags` - (Optional) Key-value map of resource tags +* `vpc_config` - (Optional) Configuration block. Detailed below. +* `zip_file` - (Optional) ZIP file that contains the script, if you input your canary script directly into the canary instead of referring to an S3 location. It can be up to 5 MB. **Conflicts with `s3_bucket`, `s3_key`, and `s3_version`.** + +### schedule + +* `expression` - (Required) Rate expression that defines how often the canary is to run. The syntax is rate(number unit). unit can be minute, minutes, or hour. +* `duration_in_seconds` - (Optional) Duration in seconds, for the canary to continue making regular runs according to the schedule in the Expression value. + +### run_config + +* `timeout_in_seconds` - (Optional) Number of seconds the canary is allowed to run before it must stop. If you omit this field, the frequency of the canary is used, up to a maximum of 840 (14 minutes). +* `memory_in_mb` - (Optional) Maximum amount of memory available to the canary while it is running, in MB. The value you specify must be a multiple of 64. +* `active_tracing` - (Optional) Whether this canary is to use active AWS X-Ray tracing when it runs. You can enable active tracing only for canaries that use version syn-nodejs-2.0 or later for their canary runtime. + +### vpc_config + +If this canary tests an endpoint in a VPC, this structure contains information about the subnet and security groups of the VPC endpoint. For more information, see [Running a Canary in a VPC](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_VPC.html). + +* `subnet_ids` - (Required) IDs of the subnets where this canary is to run. +* `security_group_ids` - (Required) IDs of the security groups for this canary. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - Amazon Resource Name (ARN) of the Canary. +* `engine_arn` - ARN of the Lambda function that is used as your canary's engine. +* `id` - Name for this canary. +* `source_location_arn` - ARN of the Lambda layer where Synthetics stores the canary script code. +* `status` - Canary status. +* `timeline` - Structure that contains information about when the canary was created, modified, and most recently run. see [Timeline](#timeline). + +### vpc_config + +* `vpc_id` - ID of the VPC where this canary is to run. + +### timeline + +* `created` - Date and time the canary was created. +* `last_modified` - Date and time the canary was most recently modified. +* `last_started` - Date and time that the canary's most recent run started. +* `last_stopped` - Date and time that the canary's most recent run ended. + +## Import + +Synthetics Canaries can be imported using the `name`, e.g. + +``` +$ terraform import aws_synthetics_canary.some some-canary +```