diff --git a/.changelog/33227.txt b/.changelog/33227.txt new file mode 100644 index 000000000000..e46d348e9463 --- /dev/null +++ b/.changelog/33227.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_opensearch_package +``` + +```release-note:new-resource +aws_opensearch_package_association +``` diff --git a/internal/service/opensearch/package.go b/internal/service/opensearch/package.go new file mode 100644 index 000000000000..c474ba0a44cc --- /dev/null +++ b/internal/service/opensearch/package.go @@ -0,0 +1,236 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package opensearch + +import ( + "context" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/opensearchservice" + "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/retry" + "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" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +// @SDKResource("aws_opensearch_package") +func ResourcePackage() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourcePackageCreate, + ReadWithoutTimeout: resourcePackageRead, + UpdateWithoutTimeout: resourcePackageUpdate, + DeleteWithoutTimeout: resourcePackageDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "available_package_version": { + Type: schema.TypeString, + Computed: true, + }, + "package_description": { + Type: schema.TypeString, + Optional: true, + }, + "package_id": { + Type: schema.TypeString, + Computed: true, + }, + "package_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 32), + }, + "package_source": { + Type: schema.TypeList, + Required: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "s3_bucket_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "s3_key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + }, + }, + "package_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(opensearchservice.PackageType_Values(), false), + }, + }, + } +} + +func resourcePackageCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).OpenSearchConn(ctx) + + name := d.Get("package_name").(string) + input := &opensearchservice.CreatePackageInput{ + PackageDescription: aws.String(d.Get("package_description").(string)), + PackageName: aws.String(name), + PackageType: aws.String(d.Get("package_type").(string)), + } + + if v, ok := d.GetOk("package_source"); ok { + input.PackageSource = expandPackageSource(v.([]interface{})[0].(map[string]interface{})) + } + + output, err := conn.CreatePackageWithContext(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "creating OpenSearch Package (%s): %s", name, err) + } + + d.SetId(aws.StringValue(output.PackageDetails.PackageID)) + + return append(diags, resourcePackageRead(ctx, d, meta)...) +} + +func resourcePackageRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).OpenSearchConn(ctx) + + pkg, err := FindPackageByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] OpenSearch Package (%s) not found, removing from state", d.Id()) + d.SetId("") + return diags + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading OpenSearch Package (%s): %s", d.Id(), err) + } + + d.Set("available_package_version", pkg.AvailablePackageVersion) + d.Set("package_description", pkg.PackageDescription) + d.Set("package_id", pkg.PackageID) + d.Set("package_name", pkg.PackageName) + d.Set("package_type", pkg.PackageType) + + return diags +} + +func resourcePackageUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).OpenSearchConn(ctx) + + input := &opensearchservice.UpdatePackageInput{ + PackageID: aws.String(d.Id()), + PackageDescription: aws.String(d.Get("package_description").(string)), + PackageSource: expandPackageSource(d.Get("package_source").([]interface{})[0].(map[string]interface{})), + } + + _, err := conn.UpdatePackageWithContext(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "updating OpenSearch Package (%s): %s", d.Id(), err) + } + + return append(diags, resourcePackageRead(ctx, d, meta)...) +} + +func resourcePackageDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).OpenSearchConn(ctx) + + log.Printf("[DEBUG] Deleting OpenSearch Package: %s", d.Id()) + _, err := conn.DeletePackageWithContext(ctx, &opensearchservice.DeletePackageInput{ + PackageID: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, opensearchservice.ErrCodeResourceNotFoundException) { + return diags + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "deleting OpenSearch Package (%s): %s", d.Id(), err) + } + + return diags +} + +func FindPackageByID(ctx context.Context, conn *opensearchservice.OpenSearchService, id string) (*opensearchservice.PackageDetails, error) { + input := &opensearchservice.DescribePackagesInput{ + Filters: []*opensearchservice.DescribePackagesFilter{ + { + Name: aws.String("PackageID"), + Value: aws.StringSlice([]string{id}), + }, + }, + } + + return findPackage(ctx, conn, input) +} + +func findPackage(ctx context.Context, conn *opensearchservice.OpenSearchService, input *opensearchservice.DescribePackagesInput) (*opensearchservice.PackageDetails, error) { + output, err := findPackages(ctx, conn, input) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findPackages(ctx context.Context, conn *opensearchservice.OpenSearchService, input *opensearchservice.DescribePackagesInput) ([]*opensearchservice.PackageDetails, error) { + var output []*opensearchservice.PackageDetails + + err := conn.DescribePackagesPagesWithContext(ctx, input, func(page *opensearchservice.DescribePackagesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.PackageDetailsList { + if v != nil { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, opensearchservice.ErrCodeResourceNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} + +func expandPackageSource(v interface{}) *opensearchservice.PackageSource { + if v == nil { + return nil + } + + return &opensearchservice.PackageSource{ + S3BucketName: aws.String(v.(map[string]interface{})["s3_bucket_name"].(string)), + S3Key: aws.String(v.(map[string]interface{})["s3_key"].(string)), + } +} diff --git a/internal/service/opensearch/package_association.go b/internal/service/opensearch/package_association.go new file mode 100644 index 000000000000..49aa138b25ca --- /dev/null +++ b/internal/service/opensearch/package_association.go @@ -0,0 +1,248 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package opensearch + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/opensearchservice" + "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/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +// @SDKResource("aws_opensearch_package_association") +func ResourcePackageAssociation() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourcePackageAssociationCreate, + ReadWithoutTimeout: resourcePackageAssociationRead, + DeleteWithoutTimeout: resourcePackageAssociationDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "domain_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "package_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "reference_path": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourcePackageAssociationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).OpenSearchConn(ctx) + + domainName := d.Get("domain_name").(string) + packageID := d.Get("package_id").(string) + id := fmt.Sprintf("%s-%s", domainName, packageID) + input := &opensearchservice.AssociatePackageInput{ + DomainName: aws.String(domainName), + PackageID: aws.String(packageID), + } + + _, err := conn.AssociatePackageWithContext(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "creating OpenSearch Package Association (%s): %s", id, err) + } + + d.SetId(id) + + if _, err := waitPackageAssociationCreated(ctx, conn, domainName, packageID, d.Timeout(schema.TimeoutCreate)); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for OpenSearch Package Association (%s) create: %s", d.Id(), err) + } + + return append(diags, resourcePackageAssociationRead(ctx, d, meta)...) +} + +func resourcePackageAssociationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).OpenSearchConn(ctx) + + domainName := d.Get("domain_name").(string) + packageID := d.Get("package_id").(string) + pkgAssociation, err := FindPackageAssociationByTwoPartKey(ctx, conn, domainName, packageID) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] OpenSearch Package Association (%s) not found, removing from state", d.Id()) + d.SetId("") + return diags + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading OpenSearch Package Association (%s): %s", d.Id(), err) + } + + d.Set("domain_name", pkgAssociation.DomainName) + d.Set("package_id", pkgAssociation.PackageID) + d.Set("reference_path", pkgAssociation.ReferencePath) + + return diags +} + +func resourcePackageAssociationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).OpenSearchConn(ctx) + + log.Printf("[DEBUG] Deleting OpenSearch Package Association: %s", d.Id()) + domainName := d.Get("domain_name").(string) + packageID := d.Get("package_id").(string) + _, err := conn.DissociatePackageWithContext(ctx, &opensearchservice.DissociatePackageInput{ + DomainName: aws.String(domainName), + PackageID: aws.String(packageID), + }) + + if tfawserr.ErrCodeEquals(err, opensearchservice.ErrCodeResourceNotFoundException) { + return diags + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "deleting OpenSearch Package Association (%s): %s", d.Id(), err) + } + + if _, err := waitPackageAssociationDeleted(ctx, conn, domainName, packageID, d.Timeout(schema.TimeoutDelete)); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for OpenSearch Package Association (%s) delete: %s", d.Id(), err) + } + + return diags +} + +func FindPackageAssociationByTwoPartKey(ctx context.Context, conn *opensearchservice.OpenSearchService, domainName, packageID string) (*opensearchservice.DomainPackageDetails, error) { + input := &opensearchservice.ListPackagesForDomainInput{ + DomainName: aws.String(domainName), + } + filter := func(v *opensearchservice.DomainPackageDetails) bool { + return aws.StringValue(v.PackageID) == packageID + } + + return findPackageAssociation(ctx, conn, input, filter) +} + +func findPackageAssociation(ctx context.Context, conn *opensearchservice.OpenSearchService, input *opensearchservice.ListPackagesForDomainInput, filter tfslices.Predicate[*opensearchservice.DomainPackageDetails]) (*opensearchservice.DomainPackageDetails, error) { + output, err := findPackageAssociations(ctx, conn, input, filter) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findPackageAssociations(ctx context.Context, conn *opensearchservice.OpenSearchService, input *opensearchservice.ListPackagesForDomainInput, filter tfslices.Predicate[*opensearchservice.DomainPackageDetails]) ([]*opensearchservice.DomainPackageDetails, error) { + var output []*opensearchservice.DomainPackageDetails + + err := conn.ListPackagesForDomainPagesWithContext(ctx, input, func(page *opensearchservice.ListPackagesForDomainOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.DomainPackageDetailsList { + if v != nil && filter(v) { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, opensearchservice.ErrCodeResourceNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} + +func statusPackageAssociation(ctx context.Context, conn *opensearchservice.OpenSearchService, domainName, packageID string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindPackageAssociationByTwoPartKey(ctx, conn, domainName, packageID) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.DomainPackageStatus), nil + } +} + +func waitPackageAssociationCreated(ctx context.Context, conn *opensearchservice.OpenSearchService, domainName, packageID string, timeout time.Duration) (*opensearchservice.DomainPackageDetails, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{opensearchservice.DomainPackageStatusAssociating}, + Target: []string{opensearchservice.DomainPackageStatusActive}, + Refresh: statusPackageAssociation(ctx, conn, domainName, packageID), + Timeout: timeout, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*opensearchservice.DomainPackageDetails); ok { + if status, details := aws.StringValue(output.DomainPackageStatus), output.ErrorDetails; status == opensearchservice.DomainPackageStatusAssociationFailed && details != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(details.ErrorType), aws.StringValue(details.ErrorMessage))) + } + + return output, err + } + + return nil, err +} + +func waitPackageAssociationDeleted(ctx context.Context, conn *opensearchservice.OpenSearchService, domainName, packageID string, timeout time.Duration) (*opensearchservice.DomainPackageDetails, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{opensearchservice.DomainPackageStatusDissociating}, + Target: []string{}, + Refresh: statusPackageAssociation(ctx, conn, domainName, packageID), + Timeout: timeout, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*opensearchservice.DomainPackageDetails); ok { + if status, details := aws.StringValue(output.DomainPackageStatus), output.ErrorDetails; status == opensearchservice.DomainPackageStatusDissociationFailed && details != nil { + tfresource.SetLastError(err, fmt.Errorf("%s: %s", aws.StringValue(details.ErrorType), aws.StringValue(details.ErrorMessage))) + } + + return output, err + } + + return nil, err +} diff --git a/internal/service/opensearch/package_association_test.go b/internal/service/opensearch/package_association_test.go new file mode 100644 index 000000000000..38cfcce2fb19 --- /dev/null +++ b/internal/service/opensearch/package_association_test.go @@ -0,0 +1,151 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package opensearch_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/opensearchservice" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfopensearch "github.com/hashicorp/terraform-provider-aws/internal/service/opensearch" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccOpenSearchPackageAssociation_basic(t *testing.T) { + ctx := acctest.Context(t) + domainName := testAccRandomDomainName() + pkgName := testAccRandomDomainName() + resourceName := "aws_opensearch_package_association.test" + packageResourceName := "aws_opensearch_package.test" + domainResourceName := "aws_opensearch_domain.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, opensearchservice.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPackageAssociationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPackageAssociationConfig_basic(pkgName, domainName), + Check: resource.ComposeTestCheckFunc( + testAccCheckPackageAssociationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "domain_name", domainResourceName, "domain_name"), + resource.TestCheckResourceAttrPair(resourceName, "package_id", packageResourceName, "id"), + ), + }, + }, + }) +} + +func TestAccOpenSearchPackageAssociation_disappears(t *testing.T) { + ctx := acctest.Context(t) + domainName := testAccRandomDomainName() + pkgName := testAccRandomDomainName() + resourceName := "aws_opensearch_package_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, opensearchservice.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPackageAssociationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPackageAssociationConfig_basic(pkgName, domainName), + Check: resource.ComposeTestCheckFunc( + testAccCheckPackageAssociationExists(ctx, resourceName), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfopensearch.ResourcePackageAssociation(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckPackageAssociationExists(ctx context.Context, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).OpenSearchConn(ctx) + + _, err := tfopensearch.FindPackageAssociationByTwoPartKey(ctx, conn, rs.Primary.Attributes["domain_name"], rs.Primary.Attributes["package_id"]) + + return err + } +} + +func testAccCheckPackageAssociationDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_opensearch_package_association" { + continue + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).OpenSearchConn(ctx) + + _, err := tfopensearch.FindPackageAssociationByTwoPartKey(ctx, conn, rs.Primary.Attributes["domain_name"], rs.Primary.Attributes["package_id"]) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("OpenSearch Package Association %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccPackageAssociationConfig_basic(pkgName, domainName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q +} + +resource "aws_s3_object" "test" { + bucket = aws_s3_bucket.test.bucket + key = %[1]q + source = "./test-fixtures/example-opensearch-custom-package.txt" + etag = filemd5("./test-fixtures/example-opensearch-custom-package.txt") +} + +resource "aws_opensearch_package" "test" { + package_name = %[1]q + package_source { + s3_bucket_name = aws_s3_bucket.test.bucket + s3_key = aws_s3_object.test.key + } + package_type = "TXT-DICTIONARY" +} + +resource "aws_opensearch_domain" "test" { + domain_name = %[2]q + + cluster_config { + instance_type = "t3.small.search" # supported in both aws and aws-us-gov + } + + ebs_options { + ebs_enabled = true + volume_size = 10 + } +} + +resource "aws_opensearch_package_association" "test" { + package_id = aws_opensearch_package.test.id + domain_name = aws_opensearch_domain.test.domain_name +} +`, pkgName, domainName) +} diff --git a/internal/service/opensearch/package_test.go b/internal/service/opensearch/package_test.go new file mode 100644 index 000000000000..d31f9fdb907c --- /dev/null +++ b/internal/service/opensearch/package_test.go @@ -0,0 +1,141 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package opensearch_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/opensearchservice" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfopensearch "github.com/hashicorp/terraform-provider-aws/internal/service/opensearch" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccOpenSearchPackage_basic(t *testing.T) { + ctx := acctest.Context(t) + pkgName := testAccRandomDomainName() + resourceName := "aws_opensearch_package.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, opensearchservice.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPackageDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPackageConfig_basic(pkgName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckPackageExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "available_package_version", ""), + resource.TestCheckResourceAttr(resourceName, "package_description", ""), + resource.TestCheckResourceAttrSet(resourceName, "package_id"), + resource.TestCheckResourceAttr(resourceName, "package_name", pkgName), + resource.TestCheckResourceAttr(resourceName, "package_source.#", "1"), + resource.TestCheckResourceAttr(resourceName, "package_type", "TXT-DICTIONARY"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "package_source", // This isn't returned by the API + }, + }, + }, + }) +} + +func TestAccOpenSearchPackage_disappears(t *testing.T) { + ctx := acctest.Context(t) + pkgName := testAccRandomDomainName() + resourceName := "aws_opensearch_package.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, opensearchservice.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckPackageDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccPackageConfig_basic(pkgName), + Check: resource.ComposeTestCheckFunc( + testAccCheckPackageExists(ctx, resourceName), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfopensearch.ResourcePackage(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckPackageExists(ctx context.Context, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).OpenSearchConn(ctx) + + _, err := tfopensearch.FindPackageByID(ctx, conn, rs.Primary.ID) + + return err + } +} + +func testAccCheckPackageDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_opensearch_package" { + continue + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).OpenSearchConn(ctx) + + _, err := tfopensearch.FindPackageByID(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("OpenSearch Package %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccPackageConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = %[1]q +} + +resource "aws_s3_object" "test" { + bucket = aws_s3_bucket.test.bucket + key = %[1]q + source = "./test-fixtures/example-opensearch-custom-package.txt" + etag = filemd5("./test-fixtures/example-opensearch-custom-package.txt") +} + +resource "aws_opensearch_package" "test" { + package_name = %[1]q + package_source { + s3_bucket_name = aws_s3_bucket.test.bucket + s3_key = aws_s3_object.test.key + } + package_type = "TXT-DICTIONARY" +} +`, rName) +} diff --git a/internal/service/opensearch/service_package_gen.go b/internal/service/opensearch/service_package_gen.go index be066c6c9dec..a10a8bd9ebe6 100644 --- a/internal/service/opensearch/service_package_gen.go +++ b/internal/service/opensearch/service_package_gen.go @@ -58,6 +58,14 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka Factory: ResourceOutboundConnection, TypeName: "aws_opensearch_outbound_connection", }, + { + Factory: ResourcePackage, + TypeName: "aws_opensearch_package", + }, + { + Factory: ResourcePackageAssociation, + TypeName: "aws_opensearch_package_association", + }, { Factory: ResourceVPCEndpoint, TypeName: "aws_opensearch_vpc_endpoint", diff --git a/internal/service/opensearch/test-fixtures/example-opensearch-custom-package.txt b/internal/service/opensearch/test-fixtures/example-opensearch-custom-package.txt new file mode 100644 index 000000000000..8249df1efa47 --- /dev/null +++ b/internal/service/opensearch/test-fixtures/example-opensearch-custom-package.txt @@ -0,0 +1,4 @@ +danish, croissant, pastry +ice cream, gelato, frozen custard +sneaker, tennis shoe, running shoe +basketball shoe, hightop diff --git a/website/docs/r/opensearch_package.html.markdown b/website/docs/r/opensearch_package.html.markdown new file mode 100644 index 000000000000..4360da88dce7 --- /dev/null +++ b/website/docs/r/opensearch_package.html.markdown @@ -0,0 +1,75 @@ +--- +subcategory: "OpenSearch" +layout: "aws" +page_title: "AWS: aws_opensearch_package" +description: |- + Terraform resource for managing an AWS OpenSearch package. +--- + +# Resource: aws_opensearch_package + +Manages an AWS Opensearch Package. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_s3_bucket" "my_opensearch_packages" { + bucket = "my-opensearch-packages" +} + +resource "aws_s3_object" "example" { + bucket = aws_s3_bucket.my_opensearch_packages.bucket + key = "example.txt" + source = "./example.txt" + etag = filemd5("./example.txt") +} + +resource "aws_opensearch_package" "example" { + package_name = "example-txt" + package_source { + s3_bucket_name = aws_s3_bucket.my_opensearch_packages.bucket + s3_key = aws_s3_object.example.key + } + package_type = "TXT-DICTIONARY" +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `package_name` - (Required, Forces new resource) Unique name for the package. +* `package_type` - (Required, Forces new resource) The type of package. +* `package_source` - (Required, Forces new resource) Configuration block for the package source options. +* `package_description` - (Optional, Forces new resource) Description of the package. + +### package_source + +* `s3_bucket_name` - (Required, Forces new resource) The name of the Amazon S3 bucket containing the package. +* `s3_key` - (Required, Forces new resource) Key (file name) of the package. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `id` - The Id of the package. +* `available_package_version` - The current version of the package. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import AWS Opensearch Packages using the Package ID. For example: + +```terraform +import { + to = aws_opensearch_package.example + id = "package-id" +} +``` + +Using `terraform import`, import AWS Opensearch Packages using the Package ID. For example: + +```console +% terraform import aws_opensearch_package.example package-id +``` diff --git a/website/docs/r/opensearch_package_association.html.markdown b/website/docs/r/opensearch_package_association.html.markdown new file mode 100644 index 000000000000..db6ce09f93a8 --- /dev/null +++ b/website/docs/r/opensearch_package_association.html.markdown @@ -0,0 +1,60 @@ +--- +subcategory: "OpenSearch" +layout: "aws" +page_title: "AWS: aws_opensearch_package_association" +description: |- + Terraform resource for managing an AWS OpenSearch package association. +--- + +# Resource: aws_opensearch_package_association + +Manages an AWS Opensearch Package Association. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_opensearch_domain" "my_domain" { + domain_name = "my-opensearch-domain" + engine_version = "Elasticsearch_7.10" + + cluster_config { + instance_type = "r4.large.search" + } +} + +resource "aws_opensearch_package" "example" { + package_name = "example-txt" + package_source { + s3_bucket_name = aws_s3_bucket.my_opensearch_packages.bucket + s3_key = aws_s3_object.example.key + } + package_type = "TXT-DICTIONARY" +} + +resource "aws_opensearch_package_association" "example" { + package_id = aws_opensearch_package.example.id + domain_name = aws_opensearch_domain.my_domain.domain_name +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `package_id` - (Required, Forces new resource) Internal ID of the package to associate with a domain. +* `domain_name` - (Required, Forces new resource) Name of the domain to associate the package with. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `id` - The Id of the package association. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `10m`) +* `delete` - (Default `10m`)