diff --git a/.changelog/22042.txt b/.changelog/22042.txt new file mode 100644 index 00000000000..4565457e663 --- /dev/null +++ b/.changelog/22042.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_detective_graph +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 7240ab1490b..d4cae9563ca 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -51,6 +51,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/service/datapipeline" "github.com/hashicorp/terraform-provider-aws/internal/service/datasync" "github.com/hashicorp/terraform-provider-aws/internal/service/dax" + "github.com/hashicorp/terraform-provider-aws/internal/service/detective" "github.com/hashicorp/terraform-provider-aws/internal/service/devicefarm" "github.com/hashicorp/terraform-provider-aws/internal/service/directconnect" "github.com/hashicorp/terraform-provider-aws/internal/service/dlm" @@ -970,6 +971,8 @@ func Provider() *schema.Provider { "aws_devicefarm_project": devicefarm.ResourceProject(), + "aws_detective_graph": detective.ResourceGraph(), + "aws_dx_bgp_peer": directconnect.ResourceBGPPeer(), "aws_dx_connection": directconnect.ResourceConnection(), "aws_dx_connection_association": directconnect.ResourceConnectionAssociation(), diff --git a/internal/service/detective/find.go b/internal/service/detective/find.go new file mode 100644 index 00000000000..27a7496d118 --- /dev/null +++ b/internal/service/detective/find.go @@ -0,0 +1,52 @@ +package detective + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/detective" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func FindDetectiveGraphByArn(conn *detective.Detective, ctx context.Context, arn string) (*detective.Graph, error) { + input := &detective.ListGraphsInput{} + var result *detective.Graph + + err := conn.ListGraphsPagesWithContext(ctx, input, func(page *detective.ListGraphsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, graph := range page.GraphList { + if graph == nil { + continue + } + + if aws.StringValue(graph.Arn) == arn { + result = graph + return false + } + } + return !lastPage + }) + if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { + return nil, err + } + + if result == nil { + return nil, &resource.NotFoundError{ + Message: fmt.Sprintf("No detective graph with arn %q", arn), + LastRequest: input, + } + } + + return result, nil +} diff --git a/internal/service/detective/generate.go b/internal/service/detective/generate.go new file mode 100644 index 00000000000..9284016ebb4 --- /dev/null +++ b/internal/service/detective/generate.go @@ -0,0 +1,4 @@ +//go:generate go run ../../generate/tags/main.go -ServiceTagsMap -ListTags -UpdateTags +// ONLY generate directives and package declaration! Do not add anything else to this file. + +package detective diff --git a/internal/service/detective/graph.go b/internal/service/detective/graph.go new file mode 100644 index 00000000000..f8fe6eb65ca --- /dev/null +++ b/internal/service/detective/graph.go @@ -0,0 +1,151 @@ +package detective + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/detective" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "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 ResourceGraph() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceGraphCreate, + ReadContext: resourceGraphRead, + UpdateContext: resourceGraphUpdate, + DeleteContext: resourceGraphDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "created_time": { + Type: schema.TypeString, + Computed: true, + }, + "graph_arn": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + }, + CustomizeDiff: verify.SetTagsDiff, + } +} + +func resourceGraphCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DetectiveConn + + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) + + input := &detective.CreateGraphInput{} + + if len(tags) > 0 { + input.Tags = Tags(tags.IgnoreAWS()) + } + + var output *detective.CreateGraphOutput + var err error + err = resource.RetryContext(ctx, DetectiveOperationTimeout, func() *resource.RetryError { + output, err = conn.CreateGraphWithContext(ctx, input) + if err != nil { + if tfawserr.ErrCodeEquals(err, detective.ErrCodeInternalServerException) { + return resource.RetryableError(err) + } + + return resource.NonRetryableError(err) + } + + return nil + }) + + if tfresource.TimedOut(err) { + output, err = conn.CreateGraphWithContext(ctx, input) + } + + if err != nil { + return diag.Errorf("error creating detective Graph: %s", err) + } + + d.SetId(aws.StringValue(output.GraphArn)) + + return resourceGraphRead(ctx, d, meta) +} + +func resourceGraphRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DetectiveConn + + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + resp, err := FindDetectiveGraphByArn(conn, ctx, d.Id()) + + if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) || resp == nil { + d.SetId("") + return nil + } + if err != nil { + return diag.Errorf("error reading detective Graph (%s): %s", d.Id(), err) + } + + d.Set("created_time", aws.TimeValue(resp.CreatedTime).Format(time.RFC3339)) + d.Set("graph_arn", resp.Arn) + + tags, err := ListTags(conn, aws.StringValue(resp.Arn)) + + if err != nil { + return diag.Errorf("error listing tags for Detective Graph (%s): %s", d.Id(), err) + } + + tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + if err = d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return diag.Errorf("error setting `%s` for Detective Graph (%s): %s", "tags", d.Id(), err) + } + + if err = d.Set("tags_all", tags.Map()); err != nil { + return diag.Errorf("error setting `%s` for Detective Graph (%s): %s", "tags_all", d.Id(), err) + } + + return nil +} + +func resourceGraphUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DetectiveConn + + if d.HasChange("tags") { + o, n := d.GetChange("tags") + if err := UpdateTags(conn, d.Id(), o, n); err != nil { + return diag.Errorf("error updating detective Graph tags (%s): %s", d.Id(), err) + } + } + + return resourceGraphRead(ctx, d, meta) +} + +func resourceGraphDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).DetectiveConn + + input := &detective.DeleteGraphInput{ + GraphArn: aws.String(d.Id()), + } + + _, err := conn.DeleteGraphWithContext(ctx, input) + if err != nil { + if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) { + return nil + } + return diag.Errorf("error deleting detective Graph (%s): %s", d.Id(), err) + } + + return nil +} diff --git a/internal/service/detective/graph_test.go b/internal/service/detective/graph_test.go new file mode 100644 index 00000000000..5ad4cd2942d --- /dev/null +++ b/internal/service/detective/graph_test.go @@ -0,0 +1,208 @@ +package detective_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/detective" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "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" + tfdetective "github.com/hashicorp/terraform-provider-aws/internal/service/detective" +) + +func TestAccDetectiveGraph_basic(t *testing.T) { + var graphOutput detective.Graph + resourceName := "aws_detective_graph.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckDetectiveGraphDestroy, + ErrorCheck: acctest.ErrorCheck(t, detective.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccDetectiveGraphConfigBasic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckDetectiveGraphExists(resourceName, &graphOutput), + acctest.CheckResourceAttrRFC3339(resourceName, "created_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccDetectiveGraph_tags(t *testing.T) { + var graph1, graph2 detective.Graph + resourceName := "aws_detective_graph.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckDetectiveGraphDestroy, + ErrorCheck: acctest.ErrorCheck(t, detective.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccDetectiveGraphConfigTags1("key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckDetectiveGraphExists(resourceName, &graph1), + acctest.CheckResourceAttrRFC3339(resourceName, "created_time"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1"), + ), + }, + { + Config: testAccDetectiveGraphConfigTags2("key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckDetectiveGraphExists(resourceName, &graph2), + testAccCheckDetectiveGraphNotRecreated(&graph1, &graph2), + acctest.CheckResourceAttrRFC3339(resourceName, "created_time"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key2", "value2"), + ), + }, + { + Config: testAccDetectiveGraphConfigTags1("key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckDetectiveGraphExists(resourceName, &graph2), + testAccCheckDetectiveGraphNotRecreated(&graph1, &graph2), + acctest.CheckResourceAttrRFC3339(resourceName, "created_time"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + resource.TestCheckResourceAttr(resourceName, "tags_all.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags_all.key2", "value2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccDetectiveGraph_disappears(t *testing.T) { + var graphOutput detective.Graph + resourceName := "aws_detective_graph.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckDetectiveGraphDestroy, + ErrorCheck: acctest.ErrorCheck(t, detective.EndpointsID), + Steps: []resource.TestStep{ + { + Config: testAccDetectiveGraphConfigBasic(), + Check: resource.ComposeTestCheckFunc( + testAccCheckDetectiveGraphExists(resourceName, &graphOutput), + acctest.CheckResourceDisappears(acctest.Provider, tfdetective.ResourceGraph(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckDetectiveGraphDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).DetectiveConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_detective_graph" { + continue + } + + resp, err := tfdetective.FindDetectiveGraphByArn(conn, context.Background(), rs.Primary.ID) + + if tfawserr.ErrCodeEquals(err, detective.ErrCodeResourceNotFoundException) || resp == nil { + continue + } + + if err != nil { + return err + } + + if resp != nil { + return fmt.Errorf("detective graph %q still exists", rs.Primary.ID) + } + } + + return nil + +} + +func testAccCheckDetectiveGraphExists(resourceName string, graph *detective.Graph) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).DetectiveConn + resp, err := tfdetective.FindDetectiveGraphByArn(conn, context.Background(), rs.Primary.ID) + + if err != nil { + return err + } + + if resp == nil { + return fmt.Errorf("detective graph %q does not exist", rs.Primary.ID) + } + + *graph = *resp + + return nil + } +} + +func testAccCheckDetectiveGraphNotRecreated(before, after *detective.Graph) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.StringValue(before.Arn), aws.StringValue(after.Arn); before != after { + return fmt.Errorf("detective graph (%s/%s) recreated", before, after) + } + + return nil + } +} + +func testAccDetectiveGraphConfigBasic() string { + return ` +resource "aws_detective_graph" "test" {} +` +} + +func testAccDetectiveGraphConfigTags1(tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_detective_graph" "test" { + tags = { + %[1]q = %[2]q + } +} +`, tagKey1, tagValue1) +} + +func testAccDetectiveGraphConfigTags2(tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_detective_graph" "test" { + tags = { + %[1]q = %[2]q + %[3]q = %[4]q + } +} +`, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/internal/service/detective/tags_gen.go b/internal/service/detective/tags_gen.go new file mode 100644 index 00000000000..434fb6f4264 --- /dev/null +++ b/internal/service/detective/tags_gen.go @@ -0,0 +1,75 @@ +// Code generated by internal/generate/tags/main.go; DO NOT EDIT. +package detective + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/detective" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" +) + +// ListTags lists detective service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func ListTags(conn *detective.Detective, identifier string) (tftags.KeyValueTags, error) { + input := &detective.ListTagsForResourceInput{ + ResourceArn: aws.String(identifier), + } + + output, err := conn.ListTagsForResource(input) + + if err != nil { + return tftags.New(nil), err + } + + return KeyValueTags(output.Tags), nil +} + +// map[string]*string handling + +// Tags returns detective service tags. +func Tags(tags tftags.KeyValueTags) map[string]*string { + return aws.StringMap(tags.Map()) +} + +// KeyValueTags creates KeyValueTags from detective service tags. +func KeyValueTags(tags map[string]*string) tftags.KeyValueTags { + return tftags.New(tags) +} + +// UpdateTags updates detective service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func UpdateTags(conn *detective.Detective, identifier string, oldTagsMap interface{}, newTagsMap interface{}) error { + oldTags := tftags.New(oldTagsMap) + newTags := tftags.New(newTagsMap) + + if removedTags := oldTags.Removed(newTags); len(removedTags) > 0 { + input := &detective.UntagResourceInput{ + ResourceArn: aws.String(identifier), + TagKeys: aws.StringSlice(removedTags.IgnoreAWS().Keys()), + } + + _, err := conn.UntagResource(input) + + if err != nil { + return fmt.Errorf("error untagging resource (%s): %w", identifier, err) + } + } + + if updatedTags := oldTags.Updated(newTags); len(updatedTags) > 0 { + input := &detective.TagResourceInput{ + ResourceArn: aws.String(identifier), + Tags: Tags(updatedTags.IgnoreAWS()), + } + + _, err := conn.TagResource(input) + + if err != nil { + return fmt.Errorf("error tagging resource (%s): %w", identifier, err) + } + } + + return nil +} diff --git a/internal/service/detective/wait.go b/internal/service/detective/wait.go new file mode 100644 index 00000000000..ab5759cba06 --- /dev/null +++ b/internal/service/detective/wait.go @@ -0,0 +1,8 @@ +package detective + +import "time" + +const ( + // DetectiveOperationTimeout Maximum amount of time to wait for a detective graph to be created, deleted + DetectiveOperationTimeout = 4 * time.Minute +) diff --git a/website/docs/r/detective_graph.html.markdown b/website/docs/r/detective_graph.html.markdown new file mode 100644 index 00000000000..ba62c9e7c59 --- /dev/null +++ b/website/docs/r/detective_graph.html.markdown @@ -0,0 +1,43 @@ +--- +subcategory: "Detective" +layout: "aws" +page_title: "AWS: aws_detective_graph" +description: |- + Provides a resource to manage Amazon Detective on a Graph. +--- + +# Resource: aws_detective_graph + +Provides a resource to manage an [AWS Detective Graph](https://docs.aws.amazon.com/detective/latest/APIReference/API_CreateGraph.html). As an AWS account may own only one Detective graph per region, provisioning multiple Detective graphs requires a separate provider configuration for each graph. + +## Example Usage + +```terraform +resource "aws_detective_graph" "example" { + tags = { + Name = "example-detective-graph" + } +} +``` + +## Argument Reference + +The following arguments are optional: + +* `tags` - (Optional) A map of tags to assign to the instance. 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: + +* `id` - ARN of the Detective Graph. +* `graph_arn` - ARN of the Detective Graph. +* `created_time` - Date and time, in UTC and extended RFC 3339 format, when the Amazon Detective Graph was created. + +## Import + +`aws_detective_graph` can be imported using the arn, e.g. + +``` +$ terraform import aws_detective_graph.example arn:aws:detective:us-east-1:123456789101:graph:231684d34gh74g4bae1dbc7bd807d02d +```