diff --git a/.changelog/27159.txt b/.changelog/27159.txt new file mode 100644 index 00000000000..c3c363bf876 --- /dev/null +++ b/.changelog/27159.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_evidently_segment +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 19cd98c8f60..0f291089218 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1517,6 +1517,7 @@ func New(_ context.Context) (*schema.Provider, error) { "aws_emrserverless_application": emrserverless.ResourceApplication(), "aws_evidently_project": evidently.ResourceProject(), + "aws_evidently_segment": evidently.ResourceSegment(), "aws_kinesis_firehose_delivery_stream": firehose.ResourceDeliveryStream(), diff --git a/internal/service/evidently/find.go b/internal/service/evidently/find.go index 0f6b556f019..43ebb8f57b5 100644 --- a/internal/service/evidently/find.go +++ b/internal/service/evidently/find.go @@ -34,3 +34,28 @@ func FindProjectByNameOrARN(ctx context.Context, conn *cloudwatchevidently.Cloud return output.Project, nil } + +func FindSegmentByNameOrARN(ctx context.Context, conn *cloudwatchevidently.CloudWatchEvidently, nameOrARN string) (*cloudwatchevidently.Segment, error) { + input := &cloudwatchevidently.GetSegmentInput{ + Segment: aws.String(nameOrARN), + } + + output, err := conn.GetSegmentWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, cloudwatchevidently.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || output.Segment == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Segment, nil +} diff --git a/internal/service/evidently/segment.go b/internal/service/evidently/segment.go new file mode 100644 index 00000000000..a2f60499b6e --- /dev/null +++ b/internal/service/evidently/segment.go @@ -0,0 +1,192 @@ +package evidently + +import ( + "context" + "log" + "regexp" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudwatchevidently" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "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/structure" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +func ResourceSegment() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceSegmentCreate, + ReadWithoutTimeout: resourceSegmentRead, + UpdateWithoutTimeout: resourceSegmentUpdate, + DeleteWithoutTimeout: resourceSegmentDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "created_time": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 160), + }, + "experiment_count": { + Type: schema.TypeInt, + Computed: true, + }, + "last_updated_time": { + Type: schema.TypeString, + Computed: true, + }, + "launch_count": { + Type: schema.TypeInt, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 64), + validation.StringMatch(regexp.MustCompile(`^[-a-zA-Z0-9._]*$`), "alphanumeric and can contain hyphens, underscores, and periods"), + ), + }, + "pattern": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 1024), + validation.StringIsJSON, + ), + DiffSuppressFunc: verify.SuppressEquivalentJSONDiffs, + StateFunc: func(v interface{}) string { + json, _ := structure.NormalizeJsonString(v) + return json + }, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + }, + + CustomizeDiff: verify.SetTagsDiff, + } +} + +func resourceSegmentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).EvidentlyConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) + + name := d.Get("name").(string) + input := &cloudwatchevidently.CreateSegmentInput{ + Name: aws.String(name), + Pattern: aws.String(d.Get("pattern").(string)), + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if len(tags) > 0 { + input.Tags = Tags(tags.IgnoreAWS()) + } + + log.Printf("[DEBUG] Creating CloudWatch Evidently Segment: %s", input) + output, err := conn.CreateSegmentWithContext(ctx, input) + + if err != nil { + return diag.Errorf("creating CloudWatch Evidently Segment (%s): %s", name, err) + } + + d.SetId(aws.StringValue(output.Segment.Arn)) + + return resourceSegmentRead(ctx, d, meta) +} + +func resourceSegmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).EvidentlyConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + segment, err := FindSegmentByNameOrARN(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] CloudWatch Evidently Segment (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return diag.Errorf("reading CloudWatch Evidently Segment (%s): %s", d.Id(), err) + } + + d.Set("arn", segment.Arn) + d.Set("created_time", aws.TimeValue(segment.CreatedTime).Format(time.RFC3339)) + d.Set("description", segment.Description) + d.Set("experiment_count", segment.ExperimentCount) + d.Set("last_updated_time", aws.TimeValue(segment.LastUpdatedTime).Format(time.RFC3339)) + d.Set("launch_count", segment.LaunchCount) + d.Set("name", segment.Name) + d.Set("pattern", segment.Pattern) + + tags := KeyValueTags(segment.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.Errorf("setting tags: %s", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return diag.Errorf("setting tags_all: %s", err) + } + + return nil +} + +func resourceSegmentUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).EvidentlyConn + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + + if err := UpdateTagsWithContext(ctx, conn, d.Id(), o, n); err != nil { + return diag.Errorf("updating tags: %s", err) + } + } + + return resourceSegmentRead(ctx, d, meta) +} + +func resourceSegmentDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).EvidentlyConn + + log.Printf("[DEBUG] Deleting CloudWatch Evidently Segment: %s", d.Id()) + _, err := conn.DeleteSegmentWithContext(ctx, &cloudwatchevidently.DeleteSegmentInput{ + Segment: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, cloudwatchevidently.ErrCodeResourceNotFoundException) { + return nil + } + + if err != nil { + return diag.Errorf("deleting CloudWatch Evidently Segment (%s): %s", d.Id(), err) + } + + return nil +} diff --git a/internal/service/evidently/segment_test.go b/internal/service/evidently/segment_test.go new file mode 100644 index 00000000000..ff1476ad54c --- /dev/null +++ b/internal/service/evidently/segment_test.go @@ -0,0 +1,310 @@ +package evidently_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/cloudwatchevidently" + sdkacctest "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/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfcloudwatchevidently "github.com/hashicorp/terraform-provider-aws/internal/service/evidently" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccEvidentlySegment_basic(t *testing.T) { + var segment cloudwatchevidently.Segment + + rName := sdkacctest.RandomWithPrefix("resource-test-terraform") + resourceName := "aws_evidently_segment.test" + pattern := "{\"Price\":[{\"numeric\":[\">\",10,\"<=\",20]}]}" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(cloudwatchevidently.EndpointsID, t) + }, + ErrorCheck: acctest.ErrorCheck(t, cloudwatchevidently.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckSegmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSegmentConfig_basic(rName, pattern), + Check: resource.ComposeTestCheckFunc( + testAccCheckSegmentExists(resourceName, &segment), + acctest.CheckResourceAttrRegionalARN(resourceName, "arn", "evidently", fmt.Sprintf("segment/%s", rName)), + resource.TestCheckResourceAttrSet(resourceName, "created_time"), + resource.TestCheckResourceAttrSet(resourceName, "experiment_count"), + acctest.CheckResourceAttrRegionalARN(resourceName, "id", "evidently", fmt.Sprintf("segment/%s", rName)), + resource.TestCheckResourceAttrSet(resourceName, "last_updated_time"), + resource.TestCheckResourceAttrSet(resourceName, "launch_count"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "pattern", pattern), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccEvidentlySegment_description(t *testing.T) { + var segment cloudwatchevidently.Segment + + rName := sdkacctest.RandomWithPrefix("resource-test-terraform") + description := "example description" + resourceName := "aws_evidently_segment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(cloudwatchevidently.EndpointsID, t) + }, + ErrorCheck: acctest.ErrorCheck(t, cloudwatchevidently.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckSegmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSegmentConfig_description(rName, description), + Check: resource.ComposeTestCheckFunc( + testAccCheckSegmentExists(resourceName, &segment), + resource.TestCheckResourceAttr(resourceName, "description", description), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccEvidentlySegment_patternJSON(t *testing.T) { + var segment cloudwatchevidently.Segment + + rName := sdkacctest.RandomWithPrefix("resource-test-terraform") + resourceName := "aws_evidently_segment.test" + pattern := " {\n\t \"Price\": [\n\t\t {\n\t\t\t \"numeric\": [\">\",10,\"<=\",20]\n\t\t }\n\t ]\n }\n" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(cloudwatchevidently.EndpointsID, t) + }, + ErrorCheck: acctest.ErrorCheck(t, cloudwatchevidently.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckSegmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSegmentConfig_patternJSON(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckSegmentExists(resourceName, &segment), + resource.TestCheckResourceAttr(resourceName, "pattern", pattern), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccEvidentlySegment_tags(t *testing.T) { + var segment cloudwatchevidently.Segment + + rName := sdkacctest.RandomWithPrefix("resource-test-terraform") + resourceName := "aws_evidently_segment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(cloudwatchevidently.EndpointsID, t) + }, + ErrorCheck: acctest.ErrorCheck(t, cloudwatchevidently.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckSegmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSegmentConfig_tags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckSegmentExists(resourceName, &segment), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccSegmentConfig_tags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckSegmentExists(resourceName, &segment), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccSegmentConfig_tags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckSegmentExists(resourceName, &segment), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccEvidentlySegment_disappears(t *testing.T) { + var segment cloudwatchevidently.Segment + + rName := sdkacctest.RandomWithPrefix("resource-test-terraform") + pattern := "{\"Price\":[{\"numeric\":[\">\",10,\"<=\",20]}]}" + resourceName := "aws_evidently_segment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, cloudwatchevidently.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckSegmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSegmentConfig_basic(rName, pattern), + Check: resource.ComposeTestCheckFunc( + testAccCheckSegmentExists(resourceName, &segment), + acctest.CheckResourceDisappears(acctest.Provider, tfcloudwatchevidently.ResourceSegment(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckSegmentDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).EvidentlyConn + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_evidently_segment" { + continue + } + + _, err := tfcloudwatchevidently.FindSegmentByNameOrARN(context.Background(), conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("CloudWatch Evidently Segment %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCheckSegmentExists(n string, v *cloudwatchevidently.Segment) 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 CloudWatch Evidently Segment ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).EvidentlyConn + + output, err := tfcloudwatchevidently.FindSegmentByNameOrARN(context.Background(), conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccSegmentConfig_basic(rName, pattern string) string { + return fmt.Sprintf(` +resource "aws_evidently_segment" "test" { + name = %[1]q + pattern = %[2]q +} +`, rName, pattern) +} + +func testAccSegmentConfig_description(rName, description string) string { + return fmt.Sprintf(` +resource "aws_evidently_segment" "test" { + name = %[1]q + pattern = "{\"Price\":[{\"numeric\":[\">\",10,\"<=\",20]}]}" + description = %[2]q +} +`, rName, description) +} + +func testAccSegmentConfig_patternJSON(rName string) string { + return fmt.Sprintf(` +resource "aws_evidently_segment" "test" { + name = %[1]q + pattern = <",10,"<=",20] + } + ] + } + JSON +} +`, rName) +} + +func testAccSegmentConfig_tags1(rName, tag, value string) string { + return fmt.Sprintf(` +resource "aws_evidently_segment" "test" { + name = %[1]q + pattern = "{\"Price\":[{\"numeric\":[\">\",10,\"<=\",20]}]}" + + tags = { + %[2]q = %[3]q + } +} +`, rName, tag, value) +} + +func testAccSegmentConfig_tags2(rName, tag1, value1, tag2, value2 string) string { + return fmt.Sprintf(` +resource "aws_evidently_segment" "test" { + name = %[1]q + pattern = "{\"Price\":[{\"numeric\":[\">\",10,\"<=\",20]}]}" + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tag1, value1, tag2, value2) +} diff --git a/website/docs/r/evidently_project.html.markdown b/website/docs/r/evidently_project.html.markdown index 35bff978c26..b12353dae45 100644 --- a/website/docs/r/evidently_project.html.markdown +++ b/website/docs/r/evidently_project.html.markdown @@ -107,7 +107,7 @@ In addition to all arguments above, the following attributes are exported: * `created_time` - The date and time that the project is created. * `experiment_count` - The number of experiments currently in the project. This includes all experiments that have been created and not deleted, whether they are ongoing or not. * `feature_count` - The number of features currently in the project. -* `id` - The ID has the same value as the name of the project. +* `id` - The ID has the same value as the arn of the project. * `last_updated_time` - The date and time that the project was most recently updated. * `launch_count` - The number of launches currently in the project. This includes all launches that have been created and not deleted, whether they are ongoing or not. * `status` - The current state of the project. Valid values are `AVAILABLE` and `UPDATING`. @@ -115,8 +115,8 @@ In addition to all arguments above, the following attributes are exported: ## Import -CloudWatch Evidently Project can be imported using the `name`, e.g., +CloudWatch Evidently Project can be imported using the `arn`, e.g., ``` -$ terraform import aws_evidently_project.example example +$ terraform import aws_evidently_project.example arn:aws:evidently:us-east-1:123456789012:segment/example ``` diff --git a/website/docs/r/evidently_segment.html.markdown b/website/docs/r/evidently_segment.html.markdown new file mode 100644 index 00000000000..d62a166da75 --- /dev/null +++ b/website/docs/r/evidently_segment.html.markdown @@ -0,0 +1,86 @@ +--- +subcategory: "CloudWatch Evidently" +layout: "aws" +page_title: "AWS: aws_evidently_segment" +description: |- + Provides a CloudWatch Evidently Segment resource. +--- + +# Resource: aws_evidently_segment + +Provides a CloudWatch Evidently Segment resource. + +## Example Usage + +### Basic + +```terraform +resource "aws_evidently_segment" "example" { + name = "example" + pattern = "{\"Price\":[{\"numeric\":[\">\",10,\"<=\",20]}]}" + + tags = { + "Key1" = "example Segment" + } +} +``` + +### With JSON object in pattern + +```terraform +resource "aws_evidently_segment" "example" { + name = "example" + pattern = <",10,"<=",20] + } + ] + } + JSON + + tags = { + "Key1" = "example Segment" + } +} +``` + +### With Description + +```terraform +resource "aws_evidently_segment" "example" { + name = "example" + pattern = "{\"Price\":[{\"numeric\":[\">\",10,\"<=\",20]}]}" + description = "example" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `description` - (Optional, Forces new resource) Specifies the description of the segment. +* `name` - (Required, Forces new resource) A name for the segment. +* `pattern` - (Required, Forces new resource) The pattern to use for the segment. For more information about pattern syntax, see [Segment rule pattern syntax](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Evidently-segments.html#CloudWatch-Evidently-segments-syntax.html). +* `tags` - (Optional) Tags to apply to the segment. If configured with a provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - The ARN of the segment. +* `created_time` - The date and time that the segment is created. +* `experiment_count` - The number of experiments that this segment is used in. This count includes all current experiments, not just those that are currently running. +* `id` - The ID has the same value as the name of the segment. +* `last_updated_time` - The date and time that this segment was most recently updated. +* `launch_count` - The number of launches that this segment is used in. This count includes all current launches, not just those that are currently running. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](/docs/providers/aws/index.html#default_tags-configuration-block). + +## Import + +CloudWatch Evidently Segment can be imported using the `name`, e.g., + +``` +$ terraform import aws_evidently_segment.example example +```