diff --git a/.changelog/31802.txt b/.changelog/31802.txt new file mode 100644 index 00000000000..d811382f06f --- /dev/null +++ b/.changelog/31802.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_finspace_kx_environment +``` diff --git a/internal/service/finspace/generate.go b/internal/service/finspace/generate.go index f366292b691..9258825e365 100644 --- a/internal/service/finspace/generate.go +++ b/internal/service/finspace/generate.go @@ -1,4 +1,4 @@ -//go:generate go run ../../generate/tags/main.go -ServiceTagsMap -AWSSDKVersion=2 -KVTValues -ListTags -SkipTypesImp +//go:generate go run ../../generate/tags/main.go -ServiceTagsMap -AWSSDKVersion=2 -KVTValues -ListTags -CreateTags -UpdateTags -SkipTypesImp // ONLY generate directives and package declaration! Do not add anything else to this file. package finspace diff --git a/internal/service/finspace/kx_environment.go b/internal/service/finspace/kx_environment.go new file mode 100644 index 00000000000..234a47f3b15 --- /dev/null +++ b/internal/service/finspace/kx_environment.go @@ -0,0 +1,592 @@ +package finspace + +import ( + "context" + "errors" + "log" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/finspace" + "github.com/aws/aws-sdk-go-v2/service/finspace/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "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/create" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + 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_finspace_kx_environment", name="Kx Environment") +// @Tags(identifierAttribute="arn") +func ResourceKxEnvironment() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceKxEnvironmentCreate, + ReadWithoutTimeout: resourceKxEnvironmentRead, + UpdateWithoutTimeout: resourceKxEnvironmentUpdate, + DeleteWithoutTimeout: resourceKxEnvironmentDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "availability_zones": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + "created_timestamp": { + Type: schema.TypeString, + Computed: true, + }, + "custom_dns_configuration": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "custom_dns_server_name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(3, 255), + }, + "custom_dns_server_ip": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.IsIPAddress, + }, + }, + }, + }, + "description": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 1000), + }, + "id": { + Type: schema.TypeString, + Computed: true, + }, + "infrastructure_account_id": { + Type: schema.TypeString, + Computed: true, + }, + "kms_key_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidARN, + }, + "last_modified_timestamp": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 255), + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), + "transit_gateway_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "transit_gateway_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringLenBetween(1, 32), + }, + "routable_cidr_space": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.IsCIDR, + }, + }, + }, + }, + }, + CustomizeDiff: verify.SetTagsDiff, + } +} + +const ( + ResNameKxEnvironment = "Kx Environment" +) + +func resourceKxEnvironmentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).FinSpaceClient() + + in := &finspace.CreateKxEnvironmentInput{ + Name: aws.String(d.Get("name").(string)), + ClientToken: aws.String(id.UniqueId()), + } + + if v, ok := d.GetOk("description"); ok { + in.Description = aws.String(v.(string)) + } + + if v, ok := d.GetOk("kms_key_id"); ok { + in.KmsKeyId = aws.String(v.(string)) + } + + out, err := conn.CreateKxEnvironment(ctx, in) + if err != nil { + return append(diags, create.DiagError(names.FinSpace, create.ErrActionCreating, ResNameKxEnvironment, d.Get("name").(string), err)...) + } + + if out == nil || out.EnvironmentId == nil { + return append(diags, create.DiagError(names.FinSpace, create.ErrActionCreating, ResNameKxEnvironment, d.Get("name").(string), errors.New("empty output"))...) + } + + d.SetId(aws.ToString(out.EnvironmentId)) + + if _, err := waitKxEnvironmentCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return append(diags, create.DiagError(names.FinSpace, create.ErrActionWaitingForCreation, ResNameKxEnvironment, d.Id(), err)...) + } + + if err := updateKxEnvironmentNetwork(ctx, d, conn); err != nil { + return append(diags, create.DiagError(names.FinSpace, create.ErrActionCreating, ResNameKxEnvironment, d.Id(), err)...) + } + + // The CreateKxEnvironment API currently fails to tag the environment when the + // Tags field is set. Until the API is fixed, tag after creation instead. + if err := createTags(ctx, conn, aws.ToString(out.EnvironmentArn), GetTagsIn(ctx)); err != nil { + return append(diags, create.DiagError(names.FinSpace, create.ErrActionCreating, ResNameKxEnvironment, d.Id(), err)...) + } + + return append(diags, resourceKxEnvironmentRead(ctx, d, meta)...) +} + +func resourceKxEnvironmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).FinSpaceClient() + + out, err := findKxEnvironmentByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] FinSpace KxEnvironment (%s) not found, removing from state", d.Id()) + d.SetId("") + return diags + } + + if err != nil { + return append(diags, create.DiagError(names.FinSpace, create.ErrActionReading, ResNameKxEnvironment, d.Id(), err)...) + } + + d.Set("id", out.EnvironmentId) + d.Set("arn", out.EnvironmentArn) + d.Set("name", out.Name) + d.Set("description", out.Description) + d.Set("kms_key_id", out.KmsKeyId) + d.Set("status", out.Status) + d.Set("availability_zones", out.AvailabilityZoneIds) + d.Set("infrastructure_account_id", out.DedicatedServiceAccountId) + d.Set("created_timestamp", out.CreationTimestamp.String()) + d.Set("last_modified_timestamp", out.UpdateTimestamp.String()) + + if err := d.Set("transit_gateway_configuration", flattenTransitGatewayConfiguration(out.TransitGatewayConfiguration)); err != nil { + return append(diags, create.DiagError(names.FinSpace, create.ErrActionSetting, ResNameKxEnvironment, d.Id(), err)...) + } + + if err := d.Set("custom_dns_configuration", flattenCustomDNSConfigurations(out.CustomDNSConfiguration)); err != nil { + return append(diags, create.DiagError(names.FinSpace, create.ErrActionSetting, ResNameKxEnvironment, d.Id(), err)...) + } + + return diags +} + +func resourceKxEnvironmentUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).FinSpaceClient() + + update := false + + in := &finspace.UpdateKxEnvironmentInput{ + EnvironmentId: aws.String(d.Id()), + Name: aws.String(d.Get("name").(string)), + } + + if d.HasChanges("description") { + in.Description = aws.String(d.Get("description").(string)) + } + + if d.HasChanges("name") || d.HasChanges("description") { + update = true + log.Printf("[DEBUG] Updating FinSpace KxEnvironment (%s): %#v", d.Id(), in) + _, err := conn.UpdateKxEnvironment(ctx, in) + if err != nil { + return append(diags, create.DiagError(names.FinSpace, create.ErrActionUpdating, ResNameKxEnvironment, d.Id(), err)...) + } + } + + if d.HasChanges("transit_gateway_configuration") || d.HasChanges("custom_dns_configuration") { + update = true + if err := updateKxEnvironmentNetwork(ctx, d, conn); err != nil { + return append(diags, create.DiagError(names.FinSpace, create.ErrActionUpdating, ResNameKxEnvironment, d.Id(), err)...) + } + } + + if !update { + return diags + } + return append(diags, resourceKxEnvironmentRead(ctx, d, meta)...) +} + +func resourceKxEnvironmentDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).FinSpaceClient() + + log.Printf("[INFO] Deleting FinSpace KxEnvironment %s", d.Id()) + + _, err := conn.DeleteKxEnvironment(ctx, &finspace.DeleteKxEnvironmentInput{ + EnvironmentId: aws.String(d.Id()), + }) + if errs.IsA[*types.ResourceNotFoundException](err) || + errs.IsAErrorMessageContains[*types.ValidationException](err, "The Environment is in DELETED state") { + log.Printf("[DEBUG] FinSpace KxEnvironment %s already deleted. Nothing to delete.", d.Id()) + return diags + } + + if err != nil { + return append(diags, create.DiagError(names.FinSpace, create.ErrActionDeleting, ResNameKxEnvironment, d.Id(), err)...) + } + + if _, err := waitKxEnvironmentDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { + return append(diags, create.DiagError(names.FinSpace, create.ErrActionWaitingForDeletion, ResNameKxEnvironment, d.Id(), err)...) + } + + return diags +} + +// As of 2023-02-09, updating network configuration requires 2 separate requests if both DNS +// and transit gateway configurationtions are set. +func updateKxEnvironmentNetwork(ctx context.Context, d *schema.ResourceData, client *finspace.Client) error { + transitGatewayConfigIn := &finspace.UpdateKxEnvironmentNetworkInput{ + EnvironmentId: aws.String(d.Id()), + ClientToken: aws.String(id.UniqueId()), + } + + customDnsConfigIn := &finspace.UpdateKxEnvironmentNetworkInput{ + EnvironmentId: aws.String(d.Id()), + ClientToken: aws.String(id.UniqueId()), + } + + updateTransitGatewayConfig := false + updateCustomDnsConfig := false + + if v, ok := d.GetOk("transit_gateway_configuration"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil && + d.HasChanges("transit_gateway_configuration") { + transitGatewayConfigIn.TransitGatewayConfiguration = expandTransitGatewayConfiguration(v.([]interface{})) + updateTransitGatewayConfig = true + } + + if v, ok := d.GetOk("custom_dns_configuration"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil && + d.HasChanges("custom_dns_configuration") { + customDnsConfigIn.CustomDNSConfiguration = expandCustomDNSConfigurations(v.([]interface{})) + updateCustomDnsConfig = true + } + + if updateTransitGatewayConfig { + if _, err := client.UpdateKxEnvironmentNetwork(ctx, transitGatewayConfigIn); err != nil { + return err + } + + if _, err := waitTransitGatewayConfigurationUpdated(ctx, client, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { + return err + } + } + + if updateCustomDnsConfig { + if _, err := client.UpdateKxEnvironmentNetwork(ctx, customDnsConfigIn); err != nil { + return err + } + + if _, err := waitCustomDNSConfigurationUpdated(ctx, client, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil { + return err + } + } + + return nil +} + +func waitKxEnvironmentCreated(ctx context.Context, conn *finspace.Client, id string, timeout time.Duration) (*finspace.GetKxEnvironmentOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(types.EnvironmentStatusCreateRequested, types.EnvironmentStatusCreating), + Target: enum.Slice(types.EnvironmentStatusCreated), + Refresh: statusKxEnvironment(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*finspace.GetKxEnvironmentOutput); ok { + return out, err + } + + return nil, err +} + +func waitTransitGatewayConfigurationUpdated(ctx context.Context, conn *finspace.Client, id string, timeout time.Duration) (*finspace.GetKxEnvironmentOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(types.TgwStatusUpdateRequested, types.TgwStatusUpdating), + Target: enum.Slice(types.TgwStatusSuccessfullyUpdated), + Refresh: statusTransitGatewayConfiguration(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*finspace.GetKxEnvironmentOutput); ok { + return out, err + } + + return nil, err +} + +func waitCustomDNSConfigurationUpdated(ctx context.Context, conn *finspace.Client, id string, timeout time.Duration) (*finspace.GetKxEnvironmentOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(types.DnsStatusUpdateRequested, types.DnsStatusUpdating), + Target: enum.Slice(types.DnsStatusSuccessfullyUpdated), + Refresh: statusCustomDNSConfiguration(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*finspace.GetKxEnvironmentOutput); ok { + return out, err + } + + return nil, err +} + +func waitKxEnvironmentDeleted(ctx context.Context, conn *finspace.Client, id string, timeout time.Duration) (*finspace.GetKxEnvironmentOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(types.EnvironmentStatusDeleteRequested, types.EnvironmentStatusDeleting), + Target: []string{}, + Refresh: statusKxEnvironment(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*finspace.GetKxEnvironmentOutput); ok { + return out, err + } + + return nil, err +} + +func statusKxEnvironment(ctx context.Context, conn *finspace.Client, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := findKxEnvironmentByID(ctx, conn, id) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, string(out.Status), nil + } +} + +func statusTransitGatewayConfiguration(ctx context.Context, conn *finspace.Client, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := findKxEnvironmentByID(ctx, conn, id) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, string(out.TgwStatus), nil + } +} + +func statusCustomDNSConfiguration(ctx context.Context, conn *finspace.Client, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := findKxEnvironmentByID(ctx, conn, id) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, string(out.DnsStatus), nil + } +} + +func findKxEnvironmentByID(ctx context.Context, conn *finspace.Client, id string) (*finspace.GetKxEnvironmentOutput, error) { + in := &finspace.GetKxEnvironmentInput{ + EnvironmentId: aws.String(id), + } + out, err := conn.GetKxEnvironment(ctx, in) + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + return nil, err + } + // Treat DELETED status as NotFound + if out != nil && out.Status == types.EnvironmentStatusDeleted { + return nil, &retry.NotFoundError{ + LastError: errors.New("status is deleted"), + LastRequest: in, + } + } + + if out == nil || out.EnvironmentArn == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out, nil +} + +func expandTransitGatewayConfiguration(tfList []interface{}) *types.TransitGatewayConfiguration { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + tfMap := tfList[0].(map[string]interface{}) + + a := &types.TransitGatewayConfiguration{} + + if v, ok := tfMap["transit_gateway_id"].(string); ok && v != "" { + a.TransitGatewayID = aws.String(v) + } + + if v, ok := tfMap["routable_cidr_space"].(string); ok && v != "" { + a.RoutableCIDRSpace = aws.String(v) + } + + return a +} + +func expandCustomDNSConfiguration(tfMap map[string]interface{}) *types.CustomDNSServer { + if tfMap == nil { + return nil + } + + a := &types.CustomDNSServer{} + + if v, ok := tfMap["custom_dns_server_name"].(string); ok && v != "" { + a.CustomDNSServerName = aws.String(v) + } + + if v, ok := tfMap["custom_dns_server_ip"].(string); ok && v != "" { + a.CustomDNSServerIP = aws.String(v) + } + + return a +} + +func expandCustomDNSConfigurations(tfList []interface{}) []types.CustomDNSServer { + if len(tfList) == 0 { + return nil + } + + var s []types.CustomDNSServer + + for _, r := range tfList { + m, ok := r.(map[string]interface{}) + + if !ok { + continue + } + + a := expandCustomDNSConfiguration(m) + + if a == nil { + continue + } + + s = append(s, *a) + } + + return s +} + +func flattenTransitGatewayConfiguration(apiObject *types.TransitGatewayConfiguration) []interface{} { + if apiObject == nil { + return nil + } + + m := map[string]interface{}{} + + if v := apiObject.TransitGatewayID; v != nil { + m["transit_gateway_id"] = aws.ToString(v) + } + + if v := apiObject.RoutableCIDRSpace; v != nil { + m["routable_cidr_space"] = aws.ToString(v) + } + + return []interface{}{m} +} + +func flattenCustomDNSConfiguration(apiObject *types.CustomDNSServer) map[string]interface{} { + if apiObject == nil { + return nil + } + + m := map[string]interface{}{} + + if v := apiObject.CustomDNSServerName; v != nil { + m["custom_dns_server_name"] = aws.ToString(v) + } + + if v := apiObject.CustomDNSServerIP; v != nil { + m["custom_dns_server_ip"] = aws.ToString(v) + } + + return m +} + +func flattenCustomDNSConfigurations(apiObjects []types.CustomDNSServer) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var l []interface{} + + for _, apiObject := range apiObjects { + l = append(l, flattenCustomDNSConfiguration(&apiObject)) + } + + return l +} diff --git a/internal/service/finspace/kx_environment_test.go b/internal/service/finspace/kx_environment_test.go new file mode 100644 index 00000000000..088f1f565e1 --- /dev/null +++ b/internal/service/finspace/kx_environment_test.go @@ -0,0 +1,441 @@ +package finspace_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/finspace" + "github.com/aws/aws-sdk-go-v2/service/finspace/types" + 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" + tffinspace "github.com/hashicorp/terraform-provider-aws/internal/service/finspace" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccFinSpaceKxEnvironment_basic(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + ctx := acctest.Context(t) + var kxenvironment finspace.GetKxEnvironmentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_finspace_kx_environment.test" + kmsKeyResourceName := "aws_kms_key.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, finspace.ServiceID) + }, + ErrorCheck: acctest.ErrorCheck(t, finspace.ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckKxEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccKxEnvironmentConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckKxEnvironmentExists(ctx, resourceName, &kxenvironment), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttrPair(resourceName, "kms_key_id", kmsKeyResourceName, "arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccFinSpaceKxEnvironment_disappears(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + ctx := acctest.Context(t) + var kxenvironment finspace.GetKxEnvironmentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_finspace_kx_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, finspace.ServiceID) + }, + ErrorCheck: acctest.ErrorCheck(t, finspace.ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckKxEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccKxEnvironmentConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckKxEnvironmentExists(ctx, resourceName, &kxenvironment), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tffinspace.ResourceKxEnvironment(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccFinSpaceKxEnvironment_updateName(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + ctx := acctest.Context(t) + var kxenvironment finspace.GetKxEnvironmentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rName2 := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_finspace_kx_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, finspace.ServiceID) + }, + ErrorCheck: acctest.ErrorCheck(t, finspace.ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckKxEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccKxEnvironmentConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckKxEnvironmentExists(ctx, resourceName, &kxenvironment), + resource.TestCheckResourceAttr(resourceName, "name", rName), + ), + }, + { + Config: testAccKxEnvironmentConfig_basic(rName2), + Check: resource.ComposeTestCheckFunc( + testAccCheckKxEnvironmentExists(ctx, resourceName, &kxenvironment), + resource.TestCheckResourceAttr(resourceName, "name", rName2), + ), + }, + }, + }) +} + +func TestAccFinSpaceKxEnvironment_description(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + ctx := acctest.Context(t) + var kxenvironment finspace.GetKxEnvironmentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_finspace_kx_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, finspace.ServiceID) + }, + ErrorCheck: acctest.ErrorCheck(t, finspace.ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckKxEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccKxEnvironmentConfig_description(rName, "description 1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckKxEnvironmentExists(ctx, resourceName, &kxenvironment), + resource.TestCheckResourceAttr(resourceName, "description", "description 1"), + ), + }, + { + Config: testAccKxEnvironmentConfig_description(rName, "description 2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckKxEnvironmentExists(ctx, resourceName, &kxenvironment), + resource.TestCheckResourceAttr(resourceName, "description", "description 2"), + ), + }, + }, + }) +} + +func TestAccFinSpaceKxEnvironment_customDNS(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + ctx := acctest.Context(t) + var kxenvironment finspace.GetKxEnvironmentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_finspace_kx_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, finspace.ServiceID) + }, + ErrorCheck: acctest.ErrorCheck(t, finspace.ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckKxEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccKxEnvironmentConfig_dnsConfig(rName, "example.finspace.amazon.aws.com", "10.0.0.76"), + Check: resource.ComposeTestCheckFunc( + testAccCheckKxEnvironmentExists(ctx, resourceName, &kxenvironment), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "custom_dns_configuration.*", map[string]string{ + "custom_dns_server_name": "example.finspace.amazon.aws.com", + "custom_dns_server_ip": "10.0.0.76", + }), + ), + }, + { + Config: testAccKxEnvironmentConfig_dnsConfig(rName, "updated.finspace.amazon.com", "10.0.0.24"), + Check: resource.ComposeTestCheckFunc( + testAccCheckKxEnvironmentExists(ctx, resourceName, &kxenvironment), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "custom_dns_configuration.*", map[string]string{ + "custom_dns_server_name": "updated.finspace.amazon.com", + "custom_dns_server_ip": "10.0.0.24", + }), + ), + }, + }, + }) +} + +func TestAccFinSpaceKxEnvironment_transitGateway(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + ctx := acctest.Context(t) + var kxenvironment finspace.GetKxEnvironmentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_finspace_kx_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, finspace.ServiceID) + }, + ErrorCheck: acctest.ErrorCheck(t, finspace.ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckKxEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccKxEnvironmentConfig_tgwConfig(rName, "100.64.0.0/26"), + Check: resource.ComposeTestCheckFunc( + testAccCheckKxEnvironmentExists(ctx, resourceName, &kxenvironment), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "transit_gateway_configuration.*", map[string]string{ + "routable_cidr_space": "100.64.0.0/26", + }), + ), + }, + }, + }) +} + +func TestAccFinSpaceKxEnvironment_tags(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + ctx := acctest.Context(t) + var kxenvironment finspace.GetKxEnvironmentOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_finspace_kx_environment.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, finspace.ServiceID) + }, + ErrorCheck: acctest.ErrorCheck(t, finspace.ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckKxEnvironmentDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccKxEnvironmentConfig_tags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckKxEnvironmentExists(ctx, resourceName, &kxenvironment), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + Config: testAccKxEnvironmentConfig_tags2(rName, "key1", "value1", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckKxEnvironmentExists(ctx, resourceName, &kxenvironment), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccKxEnvironmentConfig_tags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckKxEnvironmentExists(ctx, resourceName, &kxenvironment), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func testAccCheckKxEnvironmentDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).FinSpaceClient() + ctx := context.Background() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_finspace_kx_environment" { + continue + } + + input := &finspace.GetKxEnvironmentInput{ + EnvironmentId: aws.String(rs.Primary.ID), + } + out, err := conn.GetKxEnvironment(ctx, input) + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil + } + return err + } + if out.Status == types.EnvironmentStatusDeleted { + return nil + } + return create.Error(names.FinSpace, create.ErrActionCheckingDestroyed, tffinspace.ResNameKxEnvironment, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckKxEnvironmentExists(ctx context.Context, name string, kxenvironment *finspace.GetKxEnvironmentOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.FinSpace, create.ErrActionCheckingExistence, tffinspace.ResNameKxEnvironment, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.FinSpace, create.ErrActionCheckingExistence, tffinspace.ResNameKxEnvironment, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).FinSpaceClient() + resp, err := conn.GetKxEnvironment(ctx, &finspace.GetKxEnvironmentInput{ + EnvironmentId: aws.String(rs.Primary.ID), + }) + + if err != nil { + return create.Error(names.FinSpace, create.ErrActionCheckingExistence, tffinspace.ResNameKxEnvironment, rs.Primary.ID, err) + } + + *kxenvironment = *resp + + return nil + } +} + +func testAccKxEnvironmentConfigBase() string { + return ` +resource "aws_kms_key" "test" { + deletion_window_in_days = 7 +} +` +} + +func testAccKxEnvironmentConfig_basic(rName string) string { + return acctest.ConfigCompose( + testAccKxEnvironmentConfigBase(), + fmt.Sprintf(` +resource "aws_finspace_kx_environment" "test" { + name = %[1]q + kms_key_id = aws_kms_key.test.arn +} +`, rName)) +} + +func testAccKxEnvironmentConfig_description(rName, desc string) string { + return acctest.ConfigCompose( + testAccKxEnvironmentConfigBase(), + fmt.Sprintf(` +resource "aws_finspace_kx_environment" "test" { + name = %[1]q + kms_key_id = aws_kms_key.test.arn + description = %[2]q +} +`, rName, desc)) +} + +func testAccKxEnvironmentConfig_tgwConfig(rName, cidr string) string { + return acctest.ConfigCompose( + testAccKxEnvironmentConfigBase(), + fmt.Sprintf(` +resource "aws_ec2_transit_gateway" "test" { + description = "test" +} + +resource "aws_finspace_kx_environment" "test" { + name = %[1]q + kms_key_id = aws_kms_key.test.arn + + transit_gateway_configuration { + transit_gateway_id = aws_ec2_transit_gateway.test.id + routable_cidr_space = %[2]q + } +} +`, rName, cidr)) +} + +func testAccKxEnvironmentConfig_dnsConfig(rName, serverName, serverIP string) string { + return acctest.ConfigCompose( + testAccKxEnvironmentConfigBase(), + fmt.Sprintf(` +resource "aws_finspace_kx_environment" "test" { + name = %[1]q + kms_key_id = aws_kms_key.test.arn + + custom_dns_configuration { + custom_dns_server_name = %[2]q + custom_dns_server_ip = %[3]q + } +} +`, rName, serverName, serverIP)) +} + +func testAccKxEnvironmentConfig_tags1(rName, tagKey1, tagValue1 string) string { + return acctest.ConfigCompose( + testAccKxEnvironmentConfigBase(), + fmt.Sprintf(` +resource "aws_finspace_kx_environment" "test" { + name = %[1]q + kms_key_id = aws_kms_key.test.arn + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1)) +} + +func testAccKxEnvironmentConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return acctest.ConfigCompose( + testAccKxEnvironmentConfigBase(), + fmt.Sprintf(` +resource "aws_finspace_kx_environment" "test" { + name = %[1]q + kms_key_id = aws_kms_key.test.arn + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2)) +} diff --git a/internal/service/finspace/service_package_gen.go b/internal/service/finspace/service_package_gen.go index 774ce1af3cb..8d807f4fd5b 100644 --- a/internal/service/finspace/service_package_gen.go +++ b/internal/service/finspace/service_package_gen.go @@ -24,7 +24,16 @@ 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: ResourceKxEnvironment, + TypeName: "aws_finspace_kx_environment", + Name: "Kx Environment", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "arn", + }, + }, + } } func (p *servicePackage) ServicePackageName() string { diff --git a/internal/service/finspace/tags_gen.go b/internal/service/finspace/tags_gen.go index de42795bd34..b69b7447c2c 100644 --- a/internal/service/finspace/tags_gen.go +++ b/internal/service/finspace/tags_gen.go @@ -3,12 +3,14 @@ package finspace import ( "context" + "fmt" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/finspace" "github.com/hashicorp/terraform-provider-aws/internal/conns" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/types" + "github.com/hashicorp/terraform-provider-aws/names" ) // ListTags lists finspace service tags. @@ -74,3 +76,58 @@ func SetTagsOut(ctx context.Context, tags map[string]string) { inContext.TagsOut = types.Some(KeyValueTags(ctx, tags)) } } + +// createTags creates finspace service tags for new resources. +func createTags(ctx context.Context, conn *finspace.Client, identifier string, tags map[string]string) error { + if len(tags) == 0 { + return nil + } + + return UpdateTags(ctx, conn, identifier, nil, tags) +} + +// UpdateTags updates finspace service tags. +// The identifier is typically the Amazon Resource Name (ARN), although +// it may also be a different identifier depending on the service. +func UpdateTags(ctx context.Context, conn *finspace.Client, identifier string, oldTagsMap, newTagsMap any) error { + oldTags := tftags.New(ctx, oldTagsMap) + newTags := tftags.New(ctx, newTagsMap) + + removedTags := oldTags.Removed(newTags) + removedTags = removedTags.IgnoreSystem(names.FinSpace) + if len(removedTags) > 0 { + input := &finspace.UntagResourceInput{ + ResourceArn: aws.String(identifier), + TagKeys: removedTags.Keys(), + } + + _, err := conn.UntagResource(ctx, input) + + if err != nil { + return fmt.Errorf("untagging resource (%s): %w", identifier, err) + } + } + + updatedTags := oldTags.Updated(newTags) + updatedTags = updatedTags.IgnoreSystem(names.FinSpace) + if len(updatedTags) > 0 { + input := &finspace.TagResourceInput{ + ResourceArn: aws.String(identifier), + Tags: Tags(updatedTags), + } + + _, err := conn.TagResource(ctx, input) + + if err != nil { + return fmt.Errorf("tagging resource (%s): %w", identifier, err) + } + } + + return nil +} + +// UpdateTags updates finspace service tags. +// It is called from outside this package. +func (p *servicePackage) UpdateTags(ctx context.Context, meta any, identifier string, oldTags, newTags any) error { + return UpdateTags(ctx, meta.(*conns.AWSClient).FinSpaceClient(), identifier, oldTags, newTags) +} diff --git a/website/docs/r/finspace_kx_environment.html.markdown b/website/docs/r/finspace_kx_environment.html.markdown new file mode 100644 index 00000000000..e6d5b1532ca --- /dev/null +++ b/website/docs/r/finspace_kx_environment.html.markdown @@ -0,0 +1,113 @@ +--- +subcategory: "FinSpace" +layout: "aws" +page_title: "AWS: aws_finspace_kx_environment" +description: |- + Terraform resource for managing an AWS FinSpace Kx Environment. +--- + +# Resource: aws_finspace_kx_environment + +Terraform resource for managing an AWS FinSpace Kx Environment. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_kms_key" "example" { + description = "Sample KMS Key" + deletion_window_in_days = 7 +} + +resource "aws_finspace_kx_environment" "example" { + name = "my-tf-kx-environment" + kms_key_id = aws_kms_key.example.arn +} +``` + +### With Network Setup + +```terraform +resource "aws_kms_key" "example" { + description = "Sample KMS Key" + deletion_window_in_days = 7 +} + +resource "aws_ec2_transit_gateway" "example" { + description = "example" +} + +resource "aws_finspace_kx_environment" "example_env" { + name = "my-tf-kx-environment" + description = "Environment description" + kms_key_id = aws_kms_key.example.arn + + transit_gateway_configuration { + transit_gateway_id = aws_ec2_transit_gateway.example.id + routable_cidr_space = "100.64.0.0/26" + } + + custom_dns_configuration { + custom_dns_server_name = "example.finspace.amazonaws.com" + custom_dns_server_ip = "10.0.0.76" + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `name` - (Required) Name of the KX environment that you want to create. +* `kms_key_id` - (Required) KMS key ID to encrypt your data in the FinSpace environment. + +The following arguments are optional: + +* `custom_dns_configuration` - (Optional) List of DNS server name and server IP. This is used to set up Route-53 outbound resolvers. Defined below. +* `description` - (Optional) Description for the KX environment. +* `tags` - (Optional) Key-value mapping of resource tags. 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. +* `transit_gateway_configuration` - (Optional) Transit gateway and network configuration that is used to connect the KX environment to an internal network. Defined below. + +### custom_dns_configuration + +The custom_dns_configuration block supports the following arguments: + +* `custom_dns_server_ip` - (Required) IP address of the DNS server. +* `custom_dns_server_name` - (Required) Name of the DNS server. + +### transit_gateway_configuration + +The transit_gateway_configuration block supports the following arguments: + +* `routable_cidr_space` - (Required) Routing CIDR on behalf of KX environment. It could be any “/26 range in the 100.64.0.0 CIDR space. After providing, it will be added to the customer’s transit gateway routing table so that the traffics could be routed to KX network. +* `transit_gateway_id` - (Required) Identifier of the transit gateway created by the customer to connect outbound traffics from KX network to your internal network. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - Amazon Resource Name (ARN) identifier of the KX environment. +* `availability_zones` - AWS Availability Zone IDs that this environment is available in. Important when selecting VPC subnets to use in cluster creation. +* `created_timestamp` - Timestamp at which the environment is created in FinSpace. Value determined as epoch time in seconds. For example, the value for Monday, November 1, 2021 12:00:00 PM UTC is specified as 1635768000. +* `id` - Unique identifier for the KX environment. +* `infrastructure_account_id` - Unique identifier for the AWS environment infrastructure account. +* `last_modified_timestamp` - Last timestamp at which the environment was updated in FinSpace. Value determined as epoch time in seconds. For example, the value for Monday, November 1, 2021 12:00:00 PM UTC is specified as 1635768000. +* `status` - Status of environment creation +* `tags_all` - 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). + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `30m`) +* `update` - (Default `30m`) +* `delete` - (Default `30m`) + +## Import + +An AWS FinSpace Kx Environment can be imported using the `id`, e.g., + +``` +$ terraform import aws_finspace_kx_environment.example n3ceo7wqxoxcti5tujqwzs +```