diff --git a/.changelog/33285.txt b/.changelog/33285.txt new file mode 100644 index 00000000000..2320fd04668 --- /dev/null +++ b/.changelog/33285.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_rds_custom_db_engine_version +``` diff --git a/internal/service/rds/custom_db_engine_version.go b/internal/service/rds/custom_db_engine_version.go new file mode 100644 index 00000000000..42845da7ce2 --- /dev/null +++ b/internal/service/rds/custom_db_engine_version.go @@ -0,0 +1,469 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package rds + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "strings" + "time" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/rds" + "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/structure" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "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" + "github.com/mitchellh/go-homedir" +) + +const cevMutexKey = `aws_rds_custom_engine_version` + +// @SDKResource("aws_rds_custom_db_engine_version", name="Custom DB Engine Version") +// @Tags(identifierAttribute="arn") +func ResourceCustomDBEngineVersion() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceCustomDBEngineVersionCreate, + ReadWithoutTimeout: resourceCustomDBEngineVersionRead, + UpdateWithoutTimeout: resourceCustomDBEngineVersionUpdate, + DeleteWithoutTimeout: resourceCustomDBEngineVersionDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(240 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(60 * time.Minute), + }, + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + }, + "database_installation_files_s3_bucket_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(3, 63), + }, + "database_installation_files_s3_prefix": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + "db_parameter_group_family": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 1000), + }, + "engine": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringMatch(regexache.MustCompile(fmt.Sprintf(`^%s.*$`, InstanceEngineCustomPrefix)), fmt.Sprintf("must begin with %s", InstanceEngineCustomPrefix)), + validation.StringLenBetween(1, 35), + ), + }, + "engine_version": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 60), + }, + "filename": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"manifest"}, + }, + //API returns created image_id of the newly created image. + "image_id": { + Type: schema.TypeString, + Computed: true, + }, + "kms_key_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: verify.ValidARN, + }, + "major_engine_version": { + Type: schema.TypeString, + Computed: true, + }, + "manifest": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.All( + validation.StringIsJSON, + validation.StringLenBetween(1, 100000), + ), + ConflictsWith: []string{"filename"}, + DiffSuppressFunc: verify.SuppressEquivalentJSONDiffs, + StateFunc: func(v interface{}) string { + json, _ := structure.NormalizeJsonString(v) + return json + }, + }, + //API returns manifest with service added additions, non-determinestic. + "manifest_computed": { + Type: schema.TypeString, + Computed: true, + }, + "manifest_hash": { + Type: schema.TypeString, + Optional: true, + }, + "status": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice(rds.CustomEngineVersionStatus_Values(), false), + }, + // Allow CEV creation from a source AMI ID. + // implicit state passthrough, virtual attribute + "source_image_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), + }, + CustomizeDiff: verify.SetTagsDiff, + } +} + +const ( + ResNameCustomDBEngineVersion = "Custom DB Engine Version" +) + +func resourceCustomDBEngineVersionCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).RDSConn(ctx) + + input := rds.CreateCustomDBEngineVersionInput{ + Engine: aws.String(d.Get("engine").(string)), + EngineVersion: aws.String(d.Get("engine_version").(string)), + Tags: getTagsIn(ctx), + } + + if v, ok := d.GetOk("database_installation_files_s3_bucket_name"); ok { + input.DatabaseInstallationFilesS3BucketName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("database_installation_files_s3_prefix"); ok { + input.DatabaseInstallationFilesS3Prefix = aws.String(v.(string)) + } + + if v, ok := d.GetOk("description"); ok { + input.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("source_image_id"); ok { + input.ImageId = aws.String(v.(string)) + } + + if v, ok := d.GetOk("kms_key_id"); ok { + input.KMSKeyId = aws.String(v.(string)) + } + + if v, ok := d.GetOk("filename"); ok { + filename := v.(string) + // Grab an exclusive lock so that we're only reading one contact flow into + // memory at a time. + // See https://github.com/hashicorp/terraform/issues/9364 + conns.GlobalMutexKV.Lock(cevMutexKey) + defer conns.GlobalMutexKV.Unlock(cevMutexKey) + file, err := resourceCustomDBEngineVersionLoadFileContent(filename) + if err != nil { + return diag.Errorf("unable to load %q: %s", filename, err) + } + input.Manifest = aws.String(file) + } else if v, ok := d.GetOk("manifest"); ok { + input.Manifest = aws.String(v.(string)) + } + + output, err := conn.CreateCustomDBEngineVersionWithContext(ctx, &input) + if err != nil { + return append(diags, create.DiagError(names.RDS, create.ErrActionCreating, ResNameCustomDBEngineVersion, fmt.Sprintf("%s:%s", aws.StringValue(output.Engine), aws.StringValue(output.EngineVersion)), err)...) + } + + if output == nil { + return append(diags, create.DiagError(names.RDS, create.ErrActionCreating, ResNameCustomDBEngineVersion, fmt.Sprintf("%s:%s", aws.StringValue(output.Engine), aws.StringValue(output.EngineVersion)), errors.New("empty output"))...) + } + + d.SetId(fmt.Sprintf("%s:%s", aws.StringValue(output.Engine), aws.StringValue(output.EngineVersion))) + + if _, err := waitCustomDBEngineVersionCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return append(diags, create.DiagError(names.RDS, create.ErrActionWaitingForCreation, ResNameCustomDBEngineVersion, d.Id(), err)...) + } + + return append(diags, resourceCustomDBEngineVersionRead(ctx, d, meta)...) +} + +func resourceCustomDBEngineVersionRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).RDSConn(ctx) + + out, err := FindCustomDBEngineVersionByID(ctx, conn, d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] RDS CustomDBEngineVersion (%s) not found, removing from state", d.Id()) + d.SetId("") + return diags + } + + if err != nil { + return append(diags, create.DiagError(names.RDS, create.ErrActionReading, ResNameCustomDBEngineVersion, d.Id(), err)...) + } + + d.Set("arn", out.DBEngineVersionArn) + if out.CreateTime != nil { + d.Set("create_time", out.CreateTime.Format(time.RFC3339)) + } + d.Set("database_installation_files_s3_bucket_name", out.DatabaseInstallationFilesS3BucketName) + d.Set("database_installation_files_s3_prefix", out.DatabaseInstallationFilesS3Prefix) + d.Set("db_parameter_group_family", out.DBParameterGroupFamily) + d.Set("description", out.DBEngineVersionDescription) + d.Set("engine", out.Engine) + d.Set("engine_version", out.EngineVersion) + d.Set("image_id", out.Image.ImageId) + d.Set("kms_key_id", out.KMSKeyId) + d.Set("major_engine_version", out.MajorEngineVersion) + d.Set("manifest_computed", out.CustomDBEngineVersionManifest) + d.Set("status", out.Status) + + setTagsOut(ctx, out.TagList) + + return diags +} + +func resourceCustomDBEngineVersionUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).RDSConn(ctx) + + if d.HasChangesExcept("description", "status") { + return append(diags, create.DiagError(names.RDS, create.ErrActionUpdating, ResNameCustomDBEngineVersion, d.Id(), errors.New("only description and status can be updated"))...) + } + + update := false + engine, engineVersion, e := customEngineVersionParseID(d.Id()) + if e != nil { + return append(diags, create.DiagError(names.RDS, create.ErrActionUpdating, ResNameCustomDBEngineVersion, d.Id(), e)...) + } + input := &rds.ModifyCustomDBEngineVersionInput{ + Engine: aws.String(engine), + EngineVersion: aws.String(engineVersion), + } + + if d.HasChanges("description") { + input.Description = aws.String(d.Get("description").(string)) + update = true + } + if d.HasChanges("status") { + input.Status = aws.String(d.Get("status").(string)) + update = true + } + + if !update { + return diags + } + + log.Printf("[DEBUG] Updating RDS CustomDBEngineVersion (%s): %#v", d.Id(), input) + output, err := conn.ModifyCustomDBEngineVersionWithContext(ctx, input) + if err != nil { + return append(diags, create.DiagError(names.RDS, create.ErrActionUpdating, ResNameCustomDBEngineVersion, d.Id(), err)...) + } + if output == nil { + return append(diags, create.DiagError(names.RDS, create.ErrActionUpdating, ResNameCustomDBEngineVersion, d.Id(), errors.New("empty output"))...) + } + + if _, err := waitCustomDBEngineVersionUpdated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { + return append(diags, create.DiagError(names.RDS, create.ErrActionWaitingForUpdate, ResNameCustomDBEngineVersion, d.Id(), err)...) + } + + return append(diags, resourceCustomDBEngineVersionRead(ctx, d, meta)...) +} + +func resourceCustomDBEngineVersionDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).RDSConn(ctx) + + log.Printf("[INFO] Deleting RDS CustomDBEngineVersion %s", d.Id()) + + engine, engineVersion, e := customEngineVersionParseID(d.Id()) + if e != nil { + return append(diags, create.DiagError(names.RDS, create.ErrActionUpdating, ResNameCustomDBEngineVersion, d.Id(), e)...) + } + _, err := conn.DeleteCustomDBEngineVersionWithContext(ctx, &rds.DeleteCustomDBEngineVersionInput{ + Engine: aws.String(engine), + EngineVersion: aws.String(engineVersion), + }) + + if tfawserr.ErrCodeEquals(err, rds.ErrCodeCustomDBEngineVersionNotFoundFault) { + return diags + } + + if err != nil { + return append(diags, create.DiagError(names.RDS, create.ErrActionDeleting, ResNameCustomDBEngineVersion, d.Id(), err)...) + } + + if _, err := waitCustomDBEngineVersionDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { + return append(diags, create.DiagError(names.RDS, create.ErrActionWaitingForDeletion, ResNameCustomDBEngineVersion, d.Id(), err)...) + } + + return diags +} + +const ( + statusAvailable = "available" + statusCreating = "creating" + statusDeleting = "deleting" + statusDeprecated = "deprecated" + statusFailed = "failed" + statusPendingValidation = "pending-validation" // Custom for SQL Server, ready for validation by an instance +) + +func waitCustomDBEngineVersionCreated(ctx context.Context, conn *rds.RDS, id string, timeout time.Duration) (*rds.DBEngineVersion, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{statusCreating}, + Target: []string{statusAvailable, statusPendingValidation}, + Refresh: statusCustomDBEngineVersion(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*rds.DBEngineVersion); ok { + return out, err + } + + return nil, err +} + +func waitCustomDBEngineVersionUpdated(ctx context.Context, conn *rds.RDS, id string, timeout time.Duration) (*rds.DBEngineVersion, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{statusAvailable}, + Target: []string{statusAvailable, statusPendingValidation}, + Refresh: statusCustomDBEngineVersion(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*rds.DBEngineVersion); ok { + return out, err + } + + return nil, err +} + +func waitCustomDBEngineVersionDeleted(ctx context.Context, conn *rds.RDS, id string, timeout time.Duration) (*rds.DBEngineVersion, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{statusDeleting}, + Target: []string{}, + Refresh: statusCustomDBEngineVersion(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*rds.DBEngineVersion); ok { + return out, err + } + + return nil, err +} + +func statusCustomDBEngineVersion(ctx context.Context, conn *rds.RDS, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := FindCustomDBEngineVersionByID(ctx, conn, id) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, aws.StringValue(out.Status), nil + } +} + +func FindCustomDBEngineVersionByID(ctx context.Context, conn *rds.RDS, id string) (*rds.DBEngineVersion, error) { + engine, engineVersion, e := customEngineVersionParseID(id) + if e != nil { + return nil, e + } + input := &rds.DescribeDBEngineVersionsInput{ + Engine: aws.String(engine), + EngineVersion: aws.String(engineVersion), + IncludeAll: aws.Bool(true), // Required to return CEVs that are in `creating` state + } + + output, err := conn.DescribeDBEngineVersionsWithContext(ctx, input) + if tfawserr.ErrCodeEquals(err, rds.ErrCodeCustomDBEngineVersionNotFoundFault) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { + return nil, err + } + if output == nil || len(output.DBEngineVersions) == 0 { + return nil, &retry.NotFoundError{ + LastRequest: input, + } + } + + return output.DBEngineVersions[0], nil +} + +func customEngineVersionParseID(id string) (string, string, error) { + parts := strings.SplitN(id, ":", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("unexpected format of ID (%s), expected engine:engineversion", id) + } + + return parts[0], parts[1], nil +} + +func resourceCustomDBEngineVersionLoadFileContent(filename string) (string, error) { + filename, err := homedir.Expand(filename) + if err != nil { + return "", err + } + fileContent, err := os.ReadFile(filename) + if err != nil { + return "", err + } + return string(fileContent), nil +} diff --git a/internal/service/rds/custom_db_engine_version_test.go b/internal/service/rds/custom_db_engine_version_test.go new file mode 100644 index 00000000000..c3a7e9d3cb5 --- /dev/null +++ b/internal/service/rds/custom_db_engine_version_test.go @@ -0,0 +1,457 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package rds_test + +import ( + "context" + "errors" + "fmt" + "os" + "testing" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go/service/rds" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "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" + "github.com/hashicorp/terraform-provider-aws/internal/create" + tfrds "github.com/hashicorp/terraform-provider-aws/internal/service/rds" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccRDSCustomDBEngineVersion_sqlServer(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + // Requires an existing Windows SQL Server AMI owned by operating account set as environmental variable + // Blog: https://aws.amazon.com/blogs/database/persist-your-os-level-customization-within-amazon-rds-custom-for-sql-server-using-custom-engine-version-cev/ + key := "RDS_CUSTOM_WINDOWS_SQLSERVER_AMI" + ami := os.Getenv(key) + if ami == "" { + t.Skipf("Environment variable %s is not set", key) + } + var customdbengineversion rds.DBEngineVersion + rName := fmt.Sprintf("%s%s%d", "15.00.4249.2.", acctest.ResourcePrefix, sdkacctest.RandIntRange(100, 999)) + resourceName := "aws_rds_custom_db_engine_version.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, rds.EndpointsID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, rds.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomDBEngineVersionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCustomDBEngineVersionConfig_sqlServer(rName, ami), + Check: resource.ComposeTestCheckFunc( + testAccCheckCustomDBEngineVersionExists(ctx, resourceName, &customdbengineversion), + resource.TestCheckResourceAttr(resourceName, "engine_version", rName), + resource.TestCheckResourceAttrSet(resourceName, "create_time"), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "rds", + regexache.MustCompile(fmt.Sprintf(`cev:custom-sqlserver.+%s.+`, rName))), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"filename", "manifest_hash", "manifest", "source_image_id"}, + }, + }, + }) +} + +func TestAccRDSCustomDBEngineVersion_sqlServerUpdate(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + // Requires an existing Windows SQL Server AMI owned by operating account set as environmental variable + key := "RDS_CUSTOM_WINDOWS_SQLSERVER_AMI" + ami := os.Getenv(key) + if ami == "" { + t.Skipf("Environment variable %s is not set", key) + } + var customdbengineversion rds.DBEngineVersion + rName := fmt.Sprintf("%s%s%d", "15.00.4249.2.", acctest.ResourcePrefix, sdkacctest.RandIntRange(100, 999)) + resourceName := "aws_rds_custom_db_engine_version.test" + status := "pending-validation" + description2 := "inactive" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, rds.EndpointsID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, rds.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomDBEngineVersionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCustomDBEngineVersionConfig_sqlServer(rName, ami), + Check: resource.ComposeTestCheckFunc( + testAccCheckCustomDBEngineVersionExists(ctx, resourceName, &customdbengineversion), + resource.TestCheckResourceAttr(resourceName, "engine_version", rName), + resource.TestCheckResourceAttr(resourceName, "status", status), + resource.TestCheckResourceAttrSet(resourceName, "create_time"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"filename", "manifest_hash", "manifest", "source_image_id"}, + }, + { + Config: testAccCustomDBEngineVersionConfig_sqlServerUpdate(rName, ami, description2), + Check: resource.ComposeTestCheckFunc( + testAccCheckCustomDBEngineVersionExists(ctx, resourceName, &customdbengineversion), + resource.TestCheckResourceAttr(resourceName, "engine_version", rName), + resource.TestCheckResourceAttr(resourceName, "description", description2), + ), + }, + }, + }) +} + +func TestAccRDSCustomDBEngineVersion_oracle(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + // Requires an existing Oracle installation media in S3 bucket owned (bucket must be in operating region) by operating account set as environmental variable + // Pre-requisite: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/custom-cev.preparing.html + key := "RDS_CUSTOM_ORACLE_S3_BUCKET" + bucket := os.Getenv(key) + if bucket == "" { + t.Skipf("Environment variable %s is not set", key) + } + var customdbengineversion rds.DBEngineVersion + rName := fmt.Sprintf("%s%s%d", "19.19.ee.", acctest.ResourcePrefix, sdkacctest.RandIntRange(100, 999)) + resourceName := "aws_rds_custom_db_engine_version.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, rds.EndpointsID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, rds.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomDBEngineVersionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCustomDBEngineVersionConfig_oracle(rName, bucket), + Check: resource.ComposeTestCheckFunc( + testAccCheckCustomDBEngineVersionExists(ctx, resourceName, &customdbengineversion), + resource.TestCheckResourceAttr(resourceName, "engine_version", rName), + resource.TestCheckResourceAttrSet(resourceName, "create_time"), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "rds", regexache.MustCompile(fmt.Sprintf(`cev:custom-oracle.+%s.+`, rName))), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"filename", "manifest_hash", "manifest"}, + }, + }, + }) +} + +func TestAccRDSCustomDBEngineVersion_manifestFile(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + // Requires an existing Oracle installation media in S3 bucket owned (bucket must be in operating region) by operating account set as environmental variable + // Pre-requisite: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/custom-cev.preparing.html + key := "RDS_CUSTOM_ORACLE_S3_BUCKET" + bucket := os.Getenv(key) + if bucket == "" { + t.Skipf("Environment variable %s is not set", key) + } + var customdbengineversion rds.DBEngineVersion + rName := fmt.Sprintf("%s%s%d", "19.19.ee.", acctest.ResourcePrefix, sdkacctest.RandIntRange(100, 999)) + filename := "test-fixtures/custom-oracle-manifest.json" + resourceName := "aws_rds_custom_db_engine_version.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, rds.EndpointsID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, rds.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomDBEngineVersionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCustomDBEngineVersionConfig_manifestFile(rName, bucket, filename), + Check: resource.ComposeTestCheckFunc( + testAccCheckCustomDBEngineVersionExists(ctx, resourceName, &customdbengineversion), + resource.TestCheckResourceAttr(resourceName, "engine_version", rName), + resource.TestCheckResourceAttrSet(resourceName, "create_time"), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "rds", regexache.MustCompile(fmt.Sprintf(`cev:custom-oracle.+%s.+`, rName))), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"filename", "manifest_hash", "manifest"}, + }, + }, + }) +} + +func TestAccRDSCustomDBEngineVersion_tags(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + // Requires an existing Windows SQL Server AMI owned by operating account set as environmental variable + key := "RDS_CUSTOM_WINDOWS_SQLSERVER_AMI" + ami := os.Getenv(key) + if ami == "" { + t.Skipf("Environment variable %s is not set", key) + } + var customdbengineversion rds.DBEngineVersion + rName := fmt.Sprintf("%s%s%d", "15.00.4249.2.", acctest.ResourcePrefix, sdkacctest.RandIntRange(100, 999)) + resourceName := "aws_rds_custom_db_engine_version.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, rds.EndpointsID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, rds.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomDBEngineVersionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCustomDBEngineVersionConfig_tags(rName, ami, "key1", "value1"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckCustomDBEngineVersionExists(ctx, resourceName, &customdbengineversion), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + }, + }) +} + +func TestAccRDSCustomDBEngineVersion_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + // Requires an existing Windows SQL Server AMI owned by operating account set as environmental variable + key := "RDS_CUSTOM_WINDOWS_SQLSERVER_AMI" + ami := os.Getenv(key) + if ami == "" { + t.Skipf("Environment variable %s is not set", key) + } + var customdbengineversion rds.DBEngineVersion + rName := fmt.Sprintf("%s%s%d", "15.00.4249.2.", acctest.ResourcePrefix, sdkacctest.RandIntRange(100, 999)) + resourceName := "aws_rds_custom_db_engine_version.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, rds.EndpointsID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, rds.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCustomDBEngineVersionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCustomDBEngineVersionConfig_sqlServer(rName, ami), + Check: resource.ComposeTestCheckFunc( + testAccCheckCustomDBEngineVersionExists(ctx, resourceName, &customdbengineversion), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfrds.ResourceCustomDBEngineVersion(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckCustomDBEngineVersionDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).RDSConn(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_rds_custom_db_engine_version" { + continue + } + + _, err := tfrds.FindCustomDBEngineVersionByID(ctx, conn, rs.Primary.ID) + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return create.Error(names.RDS, create.ErrActionCheckingDestroyed, tfrds.ResNameCustomDBEngineVersion, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckCustomDBEngineVersionExists(ctx context.Context, name string, customdbengineversion *rds.DBEngineVersion) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.RDS, create.ErrActionCheckingExistence, tfrds.ResNameCustomDBEngineVersion, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.RDS, create.ErrActionCheckingExistence, tfrds.ResNameCustomDBEngineVersion, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).RDSConn(ctx) + + output, err := tfrds.FindCustomDBEngineVersionByID(ctx, conn, rs.Primary.ID) + if err != nil { + return create.Error(names.RDS, create.ErrActionCheckingExistence, tfrds.ResNameCustomDBEngineVersion, rs.Primary.ID, err) + } + + *customdbengineversion = *output + + return nil + } +} + +func testAccPreCheck(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).RDSConn(ctx) + + input := &rds.DescribeDBEngineVersionsInput{} + _, err := conn.DescribeDBEngineVersionsWithContext(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccCustomDBEngineVersionConfig_sqlServer(rName, ami string) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +# Copy the Amazon AMI for Windows SQL Server, CEV creation requires an AMI owned by the operator +resource "aws_ami_copy" "test" { + name = %[1]q + source_ami_id = %[2]q + source_ami_region = data.aws_region.current.name +} + +resource "aws_rds_custom_db_engine_version" "test" { + engine = "custom-sqlserver-se" + engine_version = %[1]q + source_image_id = aws_ami_copy.test.id +} +`, rName, ami) +} + +func testAccCustomDBEngineVersionConfig_sqlServerUpdate(rName, ami, description string) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +# Copy the Amazon AMI for Windows SQL Server, CEV creation requires an AMI owned by the operator +resource "aws_ami_copy" "test" { + name = %[1]q + source_ami_id = %[2]q + source_ami_region = data.aws_region.current.name +} + +resource "aws_rds_custom_db_engine_version" "test" { + description = %[3]q + engine = "custom-sqlserver-se" + engine_version = %[1]q + source_image_id = aws_ami_copy.test.id +} +`, rName, ami, description) +} + +func testAccCustomDBEngineVersionConfig_oracle(rName, bucket string) string { + return fmt.Sprintf(` +resource "aws_kms_key" "rdscfo_kms_key" { + description = "KMS symmetric key for RDS Custom for Oracle" +} + +resource "aws_rds_custom_db_engine_version" "test" { + database_installation_files_s3_bucket_name = %[2]q + engine = "custom-oracle-ee-cdb" + engine_version = %[1]q + kms_key_id = aws_kms_key.rdscfo_kms_key.arn + manifest = <