diff --git a/.changelog/29670.txt b/.changelog/29670.txt new file mode 100644 index 00000000000..dcb1a9999da --- /dev/null +++ b/.changelog/29670.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_oam_sink +``` diff --git a/internal/service/oam/service_package_gen.go b/internal/service/oam/service_package_gen.go index f3131dd5d5b..cbf276562fd 100644 --- a/internal/service/oam/service_package_gen.go +++ b/internal/service/oam/service_package_gen.go @@ -24,7 +24,12 @@ func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePac } func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePackageSDKResource { - return []*types.ServicePackageSDKResource{} + return []*types.ServicePackageSDKResource{ + { + Factory: ResourceSink, + TypeName: "aws_oam_sink", + }, + } } func (p *servicePackage) ServicePackageName() string { diff --git a/internal/service/oam/sink.go b/internal/service/oam/sink.go new file mode 100644 index 00000000000..2198829751b --- /dev/null +++ b/internal/service/oam/sink.go @@ -0,0 +1,193 @@ +package oam + +import ( + "context" + "errors" + "log" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/oam" + "github.com/aws/aws-sdk-go-v2/service/oam/types" + "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" + "github.com/hashicorp/terraform-provider-aws/internal/create" + 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" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @SDKResource("aws_oam_sink") +func ResourceSink() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceSinkCreate, + ReadWithoutTimeout: resourceSinkRead, + UpdateWithoutTimeout: resourceSinkUpdate, + DeleteWithoutTimeout: resourceSinkDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(1 * time.Minute), + Update: schema.DefaultTimeout(1 * time.Minute), + Delete: schema.DefaultTimeout(1 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "sink_id": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tftags.TagsSchema(), + "tags_all": tftags.TagsSchemaComputed(), + }, + + CustomizeDiff: verify.SetTagsDiff, + } +} + +const ( + ResNameSink = "Sink" +) + +func resourceSinkCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).ObservabilityAccessManagerClient() + + in := &oam.CreateSinkInput{ + Name: aws.String(d.Get("name").(string)), + } + + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + tags := defaultTagsConfig.MergeTags(tftags.New(ctx, d.Get("tags").(map[string]interface{}))) + + if len(tags) > 0 { + in.Tags = Tags(tags.IgnoreAWS()) + } + + out, err := conn.CreateSink(ctx, in) + if err != nil { + return create.DiagError(names.ObservabilityAccessManager, create.ErrActionCreating, ResNameSink, d.Get("name").(string), err) + } + + if out == nil { + return create.DiagError(names.ObservabilityAccessManager, create.ErrActionCreating, ResNameSink, d.Get("name").(string), errors.New("empty output")) + } + + d.SetId(aws.ToString(out.Arn)) + + return resourceSinkRead(ctx, d, meta) +} + +func resourceSinkRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).ObservabilityAccessManagerClient() + + out, err := findSinkByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] ObservabilityAccessManager Sink (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return create.DiagError(names.ObservabilityAccessManager, create.ErrActionReading, ResNameSink, d.Id(), err) + } + + d.Set("arn", out.Arn) + d.Set("name", out.Name) + d.Set("sink_id", out.Id) + + tags, err := ListTags(ctx, conn, d.Id()) + if err != nil { + return create.DiagError(names.ObservabilityAccessManager, create.ErrActionReading, ResNameSink, d.Id(), err) + } + + defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return create.DiagError(names.ObservabilityAccessManager, create.ErrActionSetting, ResNameSink, d.Id(), err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return create.DiagError(names.ObservabilityAccessManager, create.ErrActionSetting, ResNameSink, d.Id(), err) + } + + return nil +} + +func resourceSinkUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).ObservabilityAccessManagerClient() + + if d.HasChange("tags_all") { + log.Printf("[DEBUG] Updating ObservabilityAccessManager Sink Tags (%s): %#v", d.Id(), d.Get("tags_all")) + oldTags, newTags := d.GetChange("tags_all") + if err := UpdateTags(ctx, conn, d.Get("arn").(string), oldTags, newTags); err != nil { + return create.DiagError(names.ObservabilityAccessManager, create.ErrActionUpdating, ResNameSink, d.Id(), err) + } + + return resourceSinkRead(ctx, d, meta) + } + + return nil +} + +func resourceSinkDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).ObservabilityAccessManagerClient() + + log.Printf("[INFO] Deleting ObservabilityAccessManager Sink %s", d.Id()) + + _, err := conn.DeleteSink(ctx, &oam.DeleteSinkInput{ + Identifier: aws.String(d.Id()), + }) + + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil + } + + return create.DiagError(names.ObservabilityAccessManager, create.ErrActionDeleting, ResNameSink, d.Id(), err) + } + + return nil +} + +func findSinkByID(ctx context.Context, conn *oam.Client, id string) (*oam.GetSinkOutput, error) { + in := &oam.GetSinkInput{ + Identifier: aws.String(id), + } + out, err := conn.GetSink(ctx, in) + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + return nil, err + } + + if out == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out, nil +} diff --git a/internal/service/oam/sink_test.go b/internal/service/oam/sink_test.go new file mode 100644 index 00000000000..7048b7a0573 --- /dev/null +++ b/internal/service/oam/sink_test.go @@ -0,0 +1,247 @@ +package oam_test + +import ( + "context" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/oam" + "github.com/aws/aws-sdk-go-v2/service/oam/types" + 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" + "github.com/hashicorp/terraform-provider-aws/internal/create" + tfoam "github.com/hashicorp/terraform-provider-aws/internal/service/oam" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccObservabilityAccessManagerSink_basic(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var sink oam.GetSinkOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_oam_sink.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(t, names.ObservabilityAccessManagerEndpointID) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ObservabilityAccessManagerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckSinkDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSinkConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckSinkExists(resourceName, &sink), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "oam", regexp.MustCompile(`sink/+.`)), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrSet(resourceName, "sink_id"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccObservabilityAccessManagerSink_disappears(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + ctx := acctest.Context(t) + var sink oam.GetSinkOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_oam_sink.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(t, names.ObservabilityAccessManagerEndpointID) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ObservabilityAccessManagerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckSinkDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSinkConfigBasic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckSinkExists(resourceName, &sink), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfoam.ResourceSink(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccObservabilityAccessManagerSink_tags(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var sink oam.GetSinkOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_oam_sink.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(t) + acctest.PreCheckPartitionHasService(t, names.ObservabilityAccessManagerEndpointID) + testAccPreCheck(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.ObservabilityAccessManagerEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckSinkDestroy, + Steps: []resource.TestStep{ + { + Config: testAccSinkConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckSinkExists(resourceName, &sink), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccSinkConfigTags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckSinkExists(resourceName, &sink), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccSinkConfigTags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckSinkExists(resourceName, &sink), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckSinkDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).ObservabilityAccessManagerClient() + ctx := context.Background() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_oam_sink" { + continue + } + + input := &oam.GetSinkInput{ + Identifier: aws.String(rs.Primary.ID), + } + _, err := conn.GetSink(ctx, input) + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil + } + return err + } + + return create.Error(names.ObservabilityAccessManager, create.ErrActionCheckingDestroyed, tfoam.ResNameSink, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil +} + +func testAccCheckSinkExists(name string, sink *oam.GetSinkOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.ObservabilityAccessManager, create.ErrActionCheckingExistence, tfoam.ResNameSink, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.ObservabilityAccessManager, create.ErrActionCheckingExistence, tfoam.ResNameSink, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).ObservabilityAccessManagerClient() + ctx := context.Background() + resp, err := conn.GetSink(ctx, &oam.GetSinkInput{ + Identifier: aws.String(rs.Primary.ID), + }) + + if err != nil { + return create.Error(names.ObservabilityAccessManager, create.ErrActionCheckingExistence, tfoam.ResNameSink, rs.Primary.ID, err) + } + + *sink = *resp + + return nil + } +} + +func testAccPreCheck(t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).ObservabilityAccessManagerClient() + ctx := context.Background() + + input := &oam.ListSinksInput{} + _, err := conn.ListSinks(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccSinkConfigBasic(rName string) string { + return fmt.Sprintf(` +resource "aws_oam_sink" "test" { + name = %[1]q +} +`, rName) +} + +func testAccSinkConfigTags1(rName, tag1Key, tag1Value string) string { + return fmt.Sprintf(` +resource "aws_oam_sink" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + } +} +`, rName, tag1Key, tag1Value) +} + +func testAccSinkConfigTags2(rName, tag1Key, tag1Value, tag2Key, tag2Value string) string { + return fmt.Sprintf(` +resource "aws_oam_sink" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tag1Key, tag1Value, tag2Key, tag2Value) +} diff --git a/names/names.go b/names/names.go index 9dddf3aef1e..fc027a092d5 100644 --- a/names/names.go +++ b/names/names.go @@ -23,26 +23,27 @@ import ( // This "should" be defined by the AWS Go SDK v2, but currently isn't. const ( - AuditManagerEndpointID = "auditmanager" - CloudWatchLogsEndpointID = "logs" - ComprehendEndpointID = "comprehend" - ComputeOptimizerEndpointID = "computeoptimizer" - IdentityStoreEndpointID = "identitystore" - Inspector2EndpointID = "inspector2" - IVSChatEndpointID = "ivschat" - KendraEndpointID = "kendra" - LambdaEndpointID = "lambda" - MediaLiveEndpointID = "medialive" - OpenSearchServerlessEndpointID = "aoss" - PipesEndpointID = "pipes" - ResourceExplorer2EndpointID = "resource-explorer-2" - RolesAnywhereEndpointID = "rolesanywhere" - Route53DomainsEndpointID = "route53domains" - SchedulerEndpointID = "scheduler" - SESV2EndpointID = "sesv2" - SSMEndpointID = "ssm" - SSMContactsEndpointId = "ssm-contacts" - TranscribeEndpointID = "transcribe" + AuditManagerEndpointID = "auditmanager" + CloudWatchLogsEndpointID = "logs" + ComprehendEndpointID = "comprehend" + ComputeOptimizerEndpointID = "computeoptimizer" + IdentityStoreEndpointID = "identitystore" + Inspector2EndpointID = "inspector2" + IVSChatEndpointID = "ivschat" + KendraEndpointID = "kendra" + LambdaEndpointID = "lambda" + MediaLiveEndpointID = "medialive" + ObservabilityAccessManagerEndpointID = "oam" + OpenSearchServerlessEndpointID = "aoss" + PipesEndpointID = "pipes" + ResourceExplorer2EndpointID = "resource-explorer-2" + RolesAnywhereEndpointID = "rolesanywhere" + Route53DomainsEndpointID = "route53domains" + SchedulerEndpointID = "scheduler" + SESV2EndpointID = "sesv2" + SSMEndpointID = "ssm" + SSMContactsEndpointId = "ssm-contacts" + TranscribeEndpointID = "transcribe" ) // Type ServiceDatum corresponds closely to columns in `names_data.csv` and are diff --git a/website/docs/r/oam_sink.html.markdown b/website/docs/r/oam_sink.html.markdown new file mode 100644 index 00000000000..b19a0a448ea --- /dev/null +++ b/website/docs/r/oam_sink.html.markdown @@ -0,0 +1,58 @@ +--- +subcategory: "CloudWatch Observability Access Manager" +layout: "aws" +page_title: "AWS: aws_oam_sink" +description: |- + Terraform resource for managing an AWS CloudWatch Observability Access Manager Sink. +--- + +# Resource: aws_oam_sink + +Terraform resource for managing an AWS CloudWatch Observability Access Manager Sink. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_oam_sink" "example" { + name = "ExampleSink" + + tags = { + Env = "prod" + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `name` - (Required) Name for the sink. + +The following arguments are optional: + +* `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#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` - ARN of the Sink. +* `sink_id` - ID string that AWS generated as part of the sink ARN. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `1m`) +* `update` - (Default `1m`) +* `delete` - (Default `1m`) + +## Import + +CloudWatch Observability Access Manager Sink can be imported using the `arn`, e.g., + +``` +$ terraform import aws_oam_sink.example arn:aws:oam:us-west-2:123456789012:sink/sink-id +```