diff --git a/.changelog/22933.txt b/.changelog/22933.txt new file mode 100644 index 000000000000..a2aead0d78a2 --- /dev/null +++ b/.changelog/22933.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_dataexchange_revision +``` diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 33cd8969176b..a7ea10fd7e9a 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1046,6 +1046,7 @@ func Provider() *schema.Provider { "aws_cur_report_definition": cur.ResourceReportDefinition(), "aws_dataexchange_data_set": dataexchange.ResourceDataSet(), + "aws_dataexchange_revision": dataexchange.ResourceRevision(), "aws_datapipeline_pipeline": datapipeline.ResourcePipeline(), "aws_datapipeline_pipeline_definition": datapipeline.ResourcePipelineDefinition(), diff --git a/internal/service/dataexchange/find.go b/internal/service/dataexchange/find.go index 6f873e5322ae..ee8d956f31f2 100644 --- a/internal/service/dataexchange/find.go +++ b/internal/service/dataexchange/find.go @@ -27,3 +27,25 @@ func FindDataSetById(conn *dataexchange.DataExchange, id string) (*dataexchange. return output, nil } + +func FindRevisionById(conn *dataexchange.DataExchange, dataSetId, revisionId string) (*dataexchange.GetRevisionOutput, error) { + + input := &dataexchange.GetRevisionInput{ + DataSetId: aws.String(dataSetId), + RevisionId: aws.String(revisionId), + } + output, err := conn.GetRevision(input) + + if tfawserr.ErrCodeEquals(err, dataexchange.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} diff --git a/internal/service/dataexchange/revision.go b/internal/service/dataexchange/revision.go new file mode 100644 index 000000000000..7bcf691c7198 --- /dev/null +++ b/internal/service/dataexchange/revision.go @@ -0,0 +1,178 @@ +package dataexchange + +import ( + "fmt" + "log" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/dataexchange" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "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 ResourceRevision() *schema.Resource { + return &schema.Resource{ + Create: resourceRevisionCreate, + Read: resourceRevisionRead, + Update: resourceRevisionUpdate, + Delete: resourceRevisionDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "comment": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 16348), + }, + "data_set_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "revision_id": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + }, + CustomizeDiff: verify.SetTagsDiff, + } +} + +func resourceRevisionCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).DataExchangeConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) + + input := &dataexchange.CreateRevisionInput{ + DataSetId: aws.String(d.Get("data_set_id").(string)), + Comment: aws.String(d.Get("comment").(string)), + } + + if len(tags) > 0 { + input.Tags = Tags(tags.IgnoreAWS()) + } + + out, err := conn.CreateRevision(input) + if err != nil { + return fmt.Errorf("Error creating DataExchange Revision: %w", err) + } + + d.SetId(fmt.Sprintf("%s:%s", aws.StringValue(out.DataSetId), aws.StringValue(out.Id))) + + return resourceRevisionRead(d, meta) +} + +func resourceRevisionRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).DataExchangeConn + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + dataSetId, revisionId, err := RevisionParseResourceID(d.Id()) + if err != nil { + return err + } + + revision, err := FindRevisionById(conn, dataSetId, revisionId) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] DataExchange Revision (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading DataExchange Revision (%s): %w", d.Id(), err) + } + + d.Set("data_set_id", revision.DataSetId) + d.Set("comment", revision.Comment) + d.Set("arn", revision.Arn) + d.Set("revision_id", revision.Id) + + tags := KeyValueTags(revision.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + + return nil +} + +func resourceRevisionUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).DataExchangeConn + + if d.HasChangesExcept("tags", "tags_all") { + input := &dataexchange.UpdateRevisionInput{ + RevisionId: aws.String(d.Get("revision_id").(string)), + DataSetId: aws.String(d.Get("data_set_id").(string)), + } + + if d.HasChange("comment") { + input.Comment = aws.String(d.Get("comment").(string)) + } + + log.Printf("[DEBUG] Updating DataExchange Revision: %s", d.Id()) + _, err := conn.UpdateRevision(input) + if err != nil { + return fmt.Errorf("Error Updating DataExchange Revision: %w", err) + } + } + + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + + if err := UpdateTags(conn, d.Get("arn").(string), o, n); err != nil { + return fmt.Errorf("error updating DataExchange Revision (%s) tags: %w", d.Get("arn").(string), err) + } + } + + return resourceRevisionRead(d, meta) +} + +func resourceRevisionDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).DataExchangeConn + + input := &dataexchange.DeleteRevisionInput{ + RevisionId: aws.String(d.Get("revision_id").(string)), + DataSetId: aws.String(d.Get("data_set_id").(string)), + } + + log.Printf("[DEBUG] Deleting DataExchange Revision: %s", d.Id()) + _, err := conn.DeleteRevision(input) + if err != nil { + if tfawserr.ErrCodeEquals(err, dataexchange.ErrCodeResourceNotFoundException) { + return nil + } + return fmt.Errorf("Error deleting DataExchange Revision: %w", err) + } + + return nil +} + +func RevisionParseResourceID(id string) (string, string, error) { + parts := strings.Split(id, ":") + + if len(parts) == 2 && parts[0] != "" && parts[1] != "" { + return parts[0], parts[1], nil + } + + return "", "", fmt.Errorf("unexpected format for ID (%s), expected DATA-SET_ID:REVISION-ID", id) +} diff --git a/internal/service/dataexchange/revision_test.go b/internal/service/dataexchange/revision_test.go new file mode 100644 index 000000000000..cfb290730fef --- /dev/null +++ b/internal/service/dataexchange/revision_test.go @@ -0,0 +1,250 @@ +package dataexchange_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/dataexchange" + 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" + tfdataexchange "github.com/hashicorp/terraform-provider-aws/internal/service/dataexchange" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccDataExchangeRevision_basic(t *testing.T) { + var proj dataexchange.GetRevisionOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_dataexchange_revision.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(dataexchange.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, dataexchange.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckDataExchangeRevisionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataExchangeRevisionConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDataExchangeRevisionExists(resourceName, &proj), + resource.TestCheckResourceAttrPair(resourceName, "data_set_id", "aws_dataexchange_data_set.test", "id"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "dataexchange", regexp.MustCompile(`data-sets/.+/revisions/.+`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccDataExchangeRevision_tags(t *testing.T) { + var proj dataexchange.GetRevisionOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_dataexchange_revision.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(dataexchange.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, dataexchange.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckDataExchangeRevisionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataExchangeRevisionConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckDataExchangeRevisionExists(resourceName, &proj), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccDataExchangeRevisionConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckDataExchangeRevisionExists(resourceName, &proj), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccDataExchangeRevisionConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckDataExchangeRevisionExists(resourceName, &proj), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccDataExchangeRevision_disappears(t *testing.T) { + var proj dataexchange.GetRevisionOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_dataexchange_revision.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(dataexchange.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, dataexchange.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckDataExchangeRevisionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataExchangeRevisionConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDataExchangeRevisionExists(resourceName, &proj), + acctest.CheckResourceDisappears(acctest.Provider, tfdataexchange.ResourceRevision(), resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfdataexchange.ResourceRevision(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccDataExchangeRevision_disappears_dataSet(t *testing.T) { + var proj dataexchange.GetRevisionOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_dataexchange_revision.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); acctest.PreCheckPartitionHasService(dataexchange.EndpointsID, t) }, + ErrorCheck: acctest.ErrorCheck(t, dataexchange.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckDataExchangeRevisionDestroy, + Steps: []resource.TestStep{ + { + Config: testAccDataExchangeRevisionConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckDataExchangeRevisionExists(resourceName, &proj), + acctest.CheckResourceDisappears(acctest.Provider, tfdataexchange.ResourceDataSet(), "aws_dataexchange_data_set.test"), + acctest.CheckResourceDisappears(acctest.Provider, tfdataexchange.ResourceRevision(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckDataExchangeRevisionExists(n string, v *dataexchange.GetRevisionOutput) 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 ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).DataExchangeConn + + dataSetId, revisionId, err := tfdataexchange.RevisionParseResourceID(rs.Primary.ID) + if err != nil { + return err + } + + resp, err := tfdataexchange.FindRevisionById(conn, dataSetId, revisionId) + if err != nil { + return err + } + if resp == nil { + return fmt.Errorf("DataExchange Revision not found") + } + + *v = *resp + + return nil + } +} + +func testAccCheckDataExchangeRevisionDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).DataExchangeConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_dataexchange_revision" { + continue + } + + dataSetId, revisionId, err := tfdataexchange.RevisionParseResourceID(rs.Primary.ID) + if err != nil { + return err + } + + // Try to find the resource + _, err = tfdataexchange.FindRevisionById(conn, dataSetId, revisionId) + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("DataExchange Revision %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccDataExchangeRevisionConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_dataexchange_data_set" "test" { + asset_type = "S3_SNAPSHOT" + description = %[1]q + name = %[1]q +} + +resource "aws_dataexchange_revision" "test" { + data_set_id = aws_dataexchange_data_set.test.id +} +`, rName) +} + +func testAccDataExchangeRevisionConfigTags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_dataexchange_data_set" "test" { + asset_type = "S3_SNAPSHOT" + description = %[1]q + name = %[1]q +} + +resource "aws_dataexchange_revision" "test" { + data_set_id = aws_dataexchange_data_set.test.id + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccDataExchangeRevisionConfigTags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_dataexchange_data_set" "test" { + asset_type = "S3_SNAPSHOT" + description = %[1]q + name = %[1]q +} + +resource "aws_dataexchange_revision" "test" { + data_set_id = aws_dataexchange_data_set.test.id + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/website/docs/r/dataexchange_revision.html.markdown b/website/docs/r/dataexchange_revision.html.markdown new file mode 100644 index 000000000000..aad26fd7da63 --- /dev/null +++ b/website/docs/r/dataexchange_revision.html.markdown @@ -0,0 +1,44 @@ +--- +subcategory: "Data Exchange" +layout: "aws" +page_title: "AWS: aws_dataexchange_revision" +description: |- + Provides a DataExchange Revision +--- + +# Resource: aws_dataexchange_revision + +Provides a resource to manage AWS Data Exchange Revisions. + +## Example Usage + + +```terraform +resource "aws_dataexchange_revision" "example" { + data_set_id = aws_dataexchange_data_set.example.id +} +``` + +## Argument Reference + +* `data_set_id` - (Required) The dataset id. +* `comment` - (Required) An optional comment about the revision. +* `tags` - (Optional) A map of tags to assign to the resource. 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` - The Id of the data set. +* `revision_id` - The Id of the revision. +* `arn` - The Amazon Resource Name of this data set. +* `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 + +DataExchange Revisions can be imported by their `data-set-id:revision-id`: + +``` +$ terraform import aws_dataexchange_revision.example 4fa784c7-ccb4-4dbf-ba4f-02198320daa1:4fa784c7-ccb4-4dbf-ba4f-02198320daa1 +```