diff --git a/.changelog/26555.txt b/.changelog/26555.txt new file mode 100644 index 00000000000..5eb948b3f93 --- /dev/null +++ b/.changelog/26555.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_redshiftserverless_endpoint_access +``` + +```release-note:enhancement +resource/aws_redshiftserverless_endpoint_workgroup: Add `endpoint` attribute +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 9774baf9220..b0e38d32f87 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -1861,8 +1861,9 @@ func New(_ context.Context) (*schema.Provider, error) { "aws_redshiftdata_statement": redshiftdata.ResourceStatement(), - "aws_redshiftserverless_namespace": redshiftserverless.ResourceNamespace(), - "aws_redshiftserverless_workgroup": redshiftserverless.ResourceWorkgroup(), + "aws_redshiftserverless_endpoint_access": redshiftserverless.ResourceEndpointAccess(), + "aws_redshiftserverless_namespace": redshiftserverless.ResourceNamespace(), + "aws_redshiftserverless_workgroup": redshiftserverless.ResourceWorkgroup(), "aws_resourcegroups_group": resourcegroups.ResourceGroup(), diff --git a/internal/service/redshiftserverless/endpoint_access.go b/internal/service/redshiftserverless/endpoint_access.go new file mode 100644 index 00000000000..6f8f6f458af --- /dev/null +++ b/internal/service/redshiftserverless/endpoint_access.go @@ -0,0 +1,223 @@ +package redshiftserverless + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/redshiftserverless" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func ResourceEndpointAccess() *schema.Resource { + return &schema.Resource{ + Create: resourceEndpointAccessCreate, + Read: resourceEndpointAccessRead, + Update: resourceEndpointAccessUpdate, + Delete: resourceEndpointAccessDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "address": { + Type: schema.TypeString, + Computed: true, + }, + "port": { + Type: schema.TypeInt, + Computed: true, + }, + "vpc_endpoint": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "vpc_endpoint_id": { + Type: schema.TypeString, + Computed: true, + }, + "vpc_id": { + Type: schema.TypeString, + Computed: true, + }, + "network_interface": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "availability_zone": { + Type: schema.TypeString, + Computed: true, + }, + "network_interface_id": { + Type: schema.TypeString, + Computed: true, + }, + "private_ip_address": { + Type: schema.TypeString, + Computed: true, + }, + "subnet_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + "workgroup_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "vpc_security_group_ids": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "subnet_ids": { + Type: schema.TypeSet, + Required: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "endpoint_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(1, 30), + }, + }, + } +} + +func resourceEndpointAccessCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).RedshiftServerlessConn + + input := redshiftserverless.CreateEndpointAccessInput{ + WorkgroupName: aws.String(d.Get("workgroup_name").(string)), + EndpointName: aws.String(d.Get("endpoint_name").(string)), + } + + if v, ok := d.GetOk("vpc_security_group_ids"); ok && v.(*schema.Set).Len() > 0 { + input.VpcSecurityGroupIds = flex.ExpandStringSet(v.(*schema.Set)) + } + + if v, ok := d.GetOk("subnet_ids"); ok && v.(*schema.Set).Len() > 0 { + input.SubnetIds = flex.ExpandStringSet(v.(*schema.Set)) + } + + out, err := conn.CreateEndpointAccess(&input) + + if err != nil { + return fmt.Errorf("error creating Redshift Serverless Endpoint Access: %w", err) + } + + d.SetId(aws.StringValue(out.Endpoint.EndpointName)) + + if _, err := waitEndpointAccessActive(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for Redshift Serverless Endpoint Access (%s) to be created: %w", d.Id(), err) + } + + return resourceEndpointAccessRead(d, meta) +} + +func resourceEndpointAccessRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).RedshiftServerlessConn + + out, err := FindEndpointAccessByName(conn, d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] Redshift Serverless EndpointAccess (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("error reading Redshift Serverless Endpoint Access (%s): %w", d.Id(), err) + } + + d.Set("address", out.Address) + d.Set("port", out.Port) + d.Set("arn", out.EndpointArn) + d.Set("endpoint_name", out.EndpointName) + d.Set("workgroup_name", out.WorkgroupName) + d.Set("subnet_ids", flex.FlattenStringSet(out.SubnetIds)) + + result := make([]*string, 0, len(out.VpcSecurityGroups)) + + for _, v := range out.VpcSecurityGroups { + result = append(result, v.VpcSecurityGroupId) + } + d.Set("vpc_security_group_ids", flex.FlattenStringSet(result)) + + if err := d.Set("vpc_endpoint", []interface{}{flattenVPCEndpoint(out.VpcEndpoint)}); err != nil { + return fmt.Errorf("setting vpc_endpoint: %w", err) + } + + return nil +} + +func resourceEndpointAccessUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).RedshiftServerlessConn + + input := &redshiftserverless.UpdateEndpointAccessInput{ + EndpointName: aws.String(d.Id()), + } + + if v, ok := d.GetOk("vpc_security_group_ids"); ok && v.(*schema.Set).Len() > 0 { + input.VpcSecurityGroupIds = flex.ExpandStringSet(v.(*schema.Set)) + } + + _, err := conn.UpdateEndpointAccess(input) + if err != nil { + return fmt.Errorf("error updating Redshift Serverless Endpoint Access (%s): %w", d.Id(), err) + } + + if _, err := waitEndpointAccessActive(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for Redshift Serverless Endpoint Access (%s) to be updated: %w", d.Id(), err) + } + + return resourceEndpointAccessRead(d, meta) +} + +func resourceEndpointAccessDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).RedshiftServerlessConn + + deleteInput := redshiftserverless.DeleteEndpointAccessInput{ + EndpointName: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Deleting Redshift Serverless EndpointAccess: %s", d.Id()) + _, err := conn.DeleteEndpointAccess(&deleteInput) + + if err != nil { + if tfawserr.ErrCodeEquals(err, redshiftserverless.ErrCodeResourceNotFoundException) { + return nil + } + return err + } + + if _, err := waitEndpointAccessDeleted(conn, d.Id()); err != nil { + return fmt.Errorf("error waiting for Redshift Serverless Endpoint Access (%s) delete: %w", d.Id(), err) + } + + return nil +} diff --git a/internal/service/redshiftserverless/endpoint_access_test.go b/internal/service/redshiftserverless/endpoint_access_test.go new file mode 100644 index 00000000000..7b072fa18f1 --- /dev/null +++ b/internal/service/redshiftserverless/endpoint_access_test.go @@ -0,0 +1,212 @@ +package redshiftserverless_test + +import ( + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/service/redshiftserverless" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfredshiftserverless "github.com/hashicorp/terraform-provider-aws/internal/service/redshiftserverless" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccRedshiftServerlessEndpointAccess_basic(t *testing.T) { + resourceName := "aws_redshiftserverless_endpoint_access.test" + rName := sdkacctest.RandStringFromCharSet(30, sdkacctest.CharSetAlpha) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, redshiftserverless.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckEndpointAccessDestroy, + Steps: []resource.TestStep{ + { + Config: testAccEndpointAccessConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEndpointAccessExists(resourceName), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "redshift-serverless", regexp.MustCompile("managedvpcendpoint/.+$")), + resource.TestCheckResourceAttr(resourceName, "workgroup_name", rName), + resource.TestCheckResourceAttr(resourceName, "endpoint_name", rName), + resource.TestCheckResourceAttrSet(resourceName, "port"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "subnet_ids.*", "aws_subnet.test", "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccEndpointAccessConfig_updated(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEndpointAccessExists(resourceName), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "redshift-serverless", regexp.MustCompile("managedvpcendpoint/.+$")), + resource.TestCheckResourceAttr(resourceName, "workgroup_name", rName), + resource.TestCheckResourceAttr(resourceName, "endpoint_name", rName), + resource.TestCheckResourceAttrSet(resourceName, "port"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "subnet_ids.*", "aws_subnet.test", "id"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "vpc_security_group_ids.*", "aws_security_group.test", "id"), + ), + }, + }, + }) +} + +func TestAccRedshiftServerlessEndpointAccess_disappears_workgroup(t *testing.T) { + resourceName := "aws_redshiftserverless_endpoint_access.test" + rName := sdkacctest.RandStringFromCharSet(30, sdkacctest.CharSetAlpha) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, redshiftserverless.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckEndpointAccessDestroy, + Steps: []resource.TestStep{ + { + Config: testAccEndpointAccessConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEndpointAccessExists(resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfredshiftserverless.ResourceWorkgroup(), "aws_redshiftserverless_workgroup.test"), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccRedshiftServerlessEndpointAccess_disappears(t *testing.T) { + resourceName := "aws_redshiftserverless_endpoint_access.test" + rName := sdkacctest.RandStringFromCharSet(30, sdkacctest.CharSetAlpha) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, redshiftserverless.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckEndpointAccessDestroy, + Steps: []resource.TestStep{ + { + Config: testAccEndpointAccessConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckEndpointAccessExists(resourceName), + acctest.CheckResourceDisappears(acctest.Provider, tfredshiftserverless.ResourceEndpointAccess(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckEndpointAccessDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).RedshiftServerlessConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_redshiftserverless_endpoint_access" { + continue + } + _, err := tfredshiftserverless.FindEndpointAccessByName(conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("Redshift Serverless EndpointAccess %s still exists", rs.Primary.ID) + } + + return nil +} + +func testAccCheckEndpointAccessExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("Redshift Serverless EndpointAccess ID is not set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).RedshiftServerlessConn + + _, err := tfredshiftserverless.FindEndpointAccessByName(conn, rs.Primary.ID) + + return err + } +} + +func testAccEndpointAccessConfig_basic(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "test" { + availability_zone = data.aws_availability_zones.available.names[0] + cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, 0) + vpc_id = aws_vpc.test.id +} + +resource "aws_redshiftserverless_namespace" "test" { + namespace_name = %[1]q +} + +resource "aws_redshiftserverless_workgroup" "test" { + namespace_name = aws_redshiftserverless_namespace.test.namespace_name + workgroup_name = %[1]q +} + +resource "aws_redshiftserverless_endpoint_access" "test" { + workgroup_name = aws_redshiftserverless_workgroup.test.workgroup_name + endpoint_name = %[1]q + subnet_ids = [aws_subnet.test.id] +} +`, rName)) +} + +func testAccEndpointAccessConfig_updated(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_security_group" "test" { + name = %[1]q + vpc_id = aws_vpc.test.id +} + +resource "aws_subnet" "test" { + availability_zone = data.aws_availability_zones.available.names[0] + cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, 0) + vpc_id = aws_vpc.test.id +} + +resource "aws_redshiftserverless_namespace" "test" { + namespace_name = %[1]q +} + +resource "aws_redshiftserverless_workgroup" "test" { + namespace_name = aws_redshiftserverless_namespace.test.namespace_name + workgroup_name = %[1]q +} + +resource "aws_redshiftserverless_endpoint_access" "test" { + workgroup_name = aws_redshiftserverless_workgroup.test.workgroup_name + endpoint_name = %[1]q + subnet_ids = [aws_subnet.test.id] + vpc_security_group_ids = [aws_security_group.test.id] +} +`, rName)) +} diff --git a/internal/service/redshiftserverless/find.go b/internal/service/redshiftserverless/find.go index dc78ea7b3a8..23fb5183f3f 100644 --- a/internal/service/redshiftserverless/find.go +++ b/internal/service/redshiftserverless/find.go @@ -57,3 +57,28 @@ func FindWorkgroupByName(conn *redshiftserverless.RedshiftServerless, name strin return output.Workgroup, nil } + +func FindEndpointAccessByName(conn *redshiftserverless.RedshiftServerless, name string) (*redshiftserverless.EndpointAccess, error) { + input := &redshiftserverless.GetEndpointAccessInput{ + EndpointName: aws.String(name), + } + + output, err := conn.GetEndpointAccess(input) + + if tfawserr.ErrCodeEquals(err, redshiftserverless.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Endpoint, nil +} diff --git a/internal/service/redshiftserverless/namespace.go b/internal/service/redshiftserverless/namespace.go index 2a7f9714089..16533e71ef8 100644 --- a/internal/service/redshiftserverless/namespace.go +++ b/internal/service/redshiftserverless/namespace.go @@ -3,6 +3,7 @@ package redshiftserverless import ( "fmt" "log" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/redshiftserverless" @@ -251,18 +252,22 @@ func resourceNamespaceUpdate(d *schema.ResourceData, meta interface{}) error { func resourceNamespaceDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).RedshiftServerlessConn - deleteInput := redshiftserverless.DeleteNamespaceInput{ - NamespaceName: aws.String(d.Id()), - } - log.Printf("[DEBUG] Deleting Redshift Serverless Namespace: %s", d.Id()) - _, err := conn.DeleteNamespace(&deleteInput) + _, err := tfresource.RetryWhenAWSErrMessageContains(10*time.Minute, + func() (interface{}, error) { + return conn.DeleteNamespace(&redshiftserverless.DeleteNamespaceInput{ + NamespaceName: aws.String(d.Id()), + }) + }, + // "ConflictException: There is an operation running on the namespace. Try deleting the namespace again later." + redshiftserverless.ErrCodeConflictException, "operation running") + + if tfawserr.ErrCodeEquals(err, redshiftserverless.ErrCodeResourceNotFoundException) { + return nil + } if err != nil { - if tfawserr.ErrCodeEquals(err, redshiftserverless.ErrCodeResourceNotFoundException) { - return nil - } - return err + return fmt.Errorf("error deleting Redshift Serverless Namespace (%s): %w", d.Id(), err) } if _, err := waitNamespaceDeleted(conn, d.Id()); err != nil { diff --git a/internal/service/redshiftserverless/status.go b/internal/service/redshiftserverless/status.go index 32e45aa1e6c..f32b4b4dd1a 100644 --- a/internal/service/redshiftserverless/status.go +++ b/internal/service/redshiftserverless/status.go @@ -38,3 +38,19 @@ func statusWorkgroup(conn *redshiftserverless.RedshiftServerless, name string) r return output, aws.StringValue(output.Status), nil } } + +func statusEndpointAccess(conn *redshiftserverless.RedshiftServerless, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindEndpointAccessByName(conn, name) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.EndpointStatus), nil + } +} diff --git a/internal/service/redshiftserverless/sweep.go b/internal/service/redshiftserverless/sweep.go index b25148fcf7c..9bbc9128a21 100644 --- a/internal/service/redshiftserverless/sweep.go +++ b/internal/service/redshiftserverless/sweep.go @@ -19,6 +19,14 @@ func init() { resource.AddTestSweepers("aws_redshiftserverless_namespace", &resource.Sweeper{ Name: "aws_redshiftserverless_namespace", F: sweepNamespaces, + Dependencies: []string{ + "aws_redshiftserverless_workgroup", + }, + }) + + resource.AddTestSweepers("aws_redshiftserverless_workgroup", &resource.Sweeper{ + Name: "aws_redshiftserverless_workgroup", + F: sweepWorkgroups, }) } @@ -64,3 +72,46 @@ func sweepNamespaces(region string) error { return nil } + +func sweepWorkgroups(region string) error { + client, err := sweep.SharedRegionalSweepClient(region) + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + conn := client.(*conns.AWSClient).RedshiftServerlessConn + input := &redshiftserverless.ListWorkgroupsInput{} + sweepResources := make([]*sweep.SweepResource, 0) + var errs *multierror.Error + + err = conn.ListWorkgroupsPages(input, func(page *redshiftserverless.ListWorkgroupsOutput, lastPage bool) bool { + if len(page.Workgroups) == 0 { + log.Print("[DEBUG] No Redshift Serverless Workgroups to sweep") + return !lastPage + } + + for _, workgroup := range page.Workgroups { + r := ResourceWorkgroup() + d := r.Data(nil) + d.SetId(aws.StringValue(workgroup.WorkgroupName)) + + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) + } + + return !lastPage + }) + + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("error describing Redshift Serverless Workgroups: %w", err)) + } + + if err = sweep.SweepOrchestrator(sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping Redshift Serverless Workgroups for %s: %w", region, err)) + } + + if sweep.SkipSweepError(errs.ErrorOrNil()) { + log.Printf("[WARN] Skipping Redshift Serverless Workgroups sweep for %s: %s", region, errs) + return nil + } + + return nil +} diff --git a/internal/service/redshiftserverless/wait.go b/internal/service/redshiftserverless/wait.go index d368f54dc95..e219ff3ef24 100644 --- a/internal/service/redshiftserverless/wait.go +++ b/internal/service/redshiftserverless/wait.go @@ -87,3 +87,44 @@ func waitWorkgroupDeleted(conn *redshiftserverless.RedshiftServerless, name stri return nil, err } + +func waitEndpointAccessActive(conn *redshiftserverless.RedshiftServerless, name string) (*redshiftserverless.EndpointAccess, error) { //nolint:unparam + stateConf := &resource.StateChangeConf{ + Pending: []string{ + "CREATING", + "MODIFYING", + }, + Target: []string{ + "ACTIVE", + }, + Refresh: statusEndpointAccess(conn, name), + Timeout: 10 * time.Minute, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*redshiftserverless.EndpointAccess); ok { + return output, err + } + + return nil, err +} + +func waitEndpointAccessDeleted(conn *redshiftserverless.RedshiftServerless, name string) (*redshiftserverless.EndpointAccess, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ + "DELETING", + }, + Target: []string{}, + Refresh: statusEndpointAccess(conn, name), + Timeout: 10 * time.Minute, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*redshiftserverless.EndpointAccess); ok { + return output, err + } + + return nil, err +} diff --git a/internal/service/redshiftserverless/workgroup.go b/internal/service/redshiftserverless/workgroup.go index f3c5b6f6712..8f24ff379e7 100644 --- a/internal/service/redshiftserverless/workgroup.go +++ b/internal/service/redshiftserverless/workgroup.go @@ -55,6 +55,62 @@ func ResourceWorkgroup() *schema.Resource { }, }, }, + "endpoint": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "address": { + Type: schema.TypeString, + Computed: true, + }, + "port": { + Type: schema.TypeInt, + Computed: true, + }, + "vpc_endpoint": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "vpc_endpoint_id": { + Type: schema.TypeString, + Computed: true, + }, + "vpc_id": { + Type: schema.TypeString, + Computed: true, + }, + "network_interface": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "availability_zone": { + Type: schema.TypeString, + Computed: true, + }, + "network_interface_id": { + Type: schema.TypeString, + Computed: true, + }, + "private_ip_address": { + Type: schema.TypeString, + Computed: true, + }, + "subnet_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, "enhanced_vpc_routing": { Type: schema.TypeBool, Optional: true, @@ -182,6 +238,10 @@ func resourceWorkgroupRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("setting config_parameter: %w", err) } + if err := d.Set("endpoint", []interface{}{flattenEndpoint(out.Endpoint)}); err != nil { + return fmt.Errorf("setting endpoint: %w", err) + } + tags, err := ListTags(conn, arn) if err != nil { @@ -361,3 +421,106 @@ func flattenConfigParameters(apiObjects []*redshiftserverless.ConfigParameter) [ return tfList } + +func flattenEndpoint(apiObject *redshiftserverless.Endpoint) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + if v := apiObject.Address; v != nil { + tfMap["address"] = aws.StringValue(v) + } + + if v := apiObject.Port; v != nil { + tfMap["port"] = aws.Int64Value(v) + } + + if v := apiObject.VpcEndpoints; v != nil { + tfMap["vpc_endpoint"] = flattenVPCEndpoints(v) + } + + return tfMap +} + +func flattenVPCEndpoints(apiObjects []*redshiftserverless.VpcEndpoint) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenVPCEndpoint(apiObject)) + } + + return tfList +} + +func flattenVPCEndpoint(apiObject *redshiftserverless.VpcEndpoint) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.VpcEndpointId; v != nil { + tfMap["vpc_endpoint_id"] = aws.StringValue(v) + } + + if v := apiObject.VpcId; v != nil { + tfMap["vpc_id"] = aws.StringValue(v) + } + + if v := apiObject.NetworkInterfaces; v != nil { + tfMap["network_interface"] = flattenNetworkInterfaces(v) + } + return tfMap +} + +func flattenNetworkInterfaces(apiObjects []*redshiftserverless.NetworkInterface) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenNetworkInterface(apiObject)) + } + + return tfList +} + +func flattenNetworkInterface(apiObject *redshiftserverless.NetworkInterface) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if v := apiObject.AvailabilityZone; v != nil { + tfMap["availability_zone"] = aws.StringValue(v) + } + + if v := apiObject.NetworkInterfaceId; v != nil { + tfMap["network_interface_id"] = aws.StringValue(v) + } + + if v := apiObject.PrivateIpAddress; v != nil { + tfMap["private_ip_address"] = aws.StringValue(v) + } + + if v := apiObject.SubnetId; v != nil { + tfMap["subnet_id"] = aws.StringValue(v) + } + return tfMap +} diff --git a/internal/service/redshiftserverless/workgroup_test.go b/internal/service/redshiftserverless/workgroup_test.go index 2eae3be0a4f..b92ef4efe4e 100644 --- a/internal/service/redshiftserverless/workgroup_test.go +++ b/internal/service/redshiftserverless/workgroup_test.go @@ -114,7 +114,7 @@ func testAccCheckWorkgroupDestroy(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).RedshiftServerlessConn for _, rs := range s.RootModule().Resources { - if rs.Type != "aws_redshiftserverless_namespace" { + if rs.Type != "aws_redshiftserverless_workgroup" { continue } _, err := tfredshiftserverless.FindWorkgroupByName(conn, rs.Primary.ID) diff --git a/website/docs/r/redshiftserverless_endpoint_access.html.markdown b/website/docs/r/redshiftserverless_endpoint_access.html.markdown new file mode 100644 index 00000000000..17dd073e17b --- /dev/null +++ b/website/docs/r/redshiftserverless_endpoint_access.html.markdown @@ -0,0 +1,61 @@ +--- +subcategory: "Redshift Serverless" +layout: "aws" +page_title: "AWS: aws_redshiftserverless_endpoint_access" +description: |- + Provides a Redshift Serverless Endpoint Access resource. +--- + +# Resource: aws_redshiftserverless_endpoint_access + +Creates a new Amazon Redshift Serverless Endpoint Access. + +## Example Usage + +```terraform +resource "aws_redshiftserverless_endpoint_access" "example" { + endpoint_name = "example" + workgroup_name = "example" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `endpoint_name` - (Required) The name of the endpoint. +* `security_group_ids` - (Optional) An array of security group IDs to associate with the endpoint. +* `subnet_ids` - (Required) An array of VPC subnet IDs to associate with the endpoint. +* `vpc_security_group_ids` - (Optional) An array of security group IDs to associate with the workgroup. +* `workgroup_name` - (Required) The name of the workgroup. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - Amazon Resource Name (ARN) of the Redshift Serverless Endpoint Access. +* `id` - The Redshift Endpoint Access Name. +* `address` - The DNS address of the VPC endpoint. +* `port` - The port that Amazon Redshift Serverless listens on. +* `vpc_endpoint` - The VPC endpoint or the Redshift Serverless workgroup. See `VPC Endpoint` below. + +#### VPC Endpoint + +* `vpc_endpoint_id` - The DNS address of the VPC endpoint. +* `vpc_id` - The port that Amazon Redshift Serverless listens on. +* `network_interface` - The network interfaces of the endpoint.. See `Network Interface` below. + +##### Network Interface + +* `availability_zone` - The availability Zone. +* `network_interface_id` - The unique identifier of the network interface. +* `private_ip_address` - The IPv4 address of the network interface within the subnet. +* `subnet_id` - The unique identifier of the subnet. + +## Import + +Redshift Serverless Endpoint Access can be imported using the `endpoint_name`, e.g., + +``` +$ terraform import aws_redshiftserverless_endpoint_access.example example +``` diff --git a/website/docs/r/redshiftserverless_workgroup.html.markdown b/website/docs/r/redshiftserverless_workgroup.html.markdown index 28be490b13a..84afb4516d4 100644 --- a/website/docs/r/redshiftserverless_workgroup.html.markdown +++ b/website/docs/r/redshiftserverless_workgroup.html.markdown @@ -45,8 +45,28 @@ In addition to all arguments above, the following attributes are exported: * `arn` - Amazon Resource Name (ARN) of the Redshift Serverless Workgroup. * `id` - The Redshift Workgroup Name. * `workgroup_id` - The Redshift Workgroup ID. +* `endpoint` - The endpoint that is created from the workgroup. See `Endpoint` below. * `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). +### Endpoint + +* `address` - The DNS address of the VPC endpoint. +* `port` - The port that Amazon Redshift Serverless listens on. +* `vpc_endpoint` - The VPC endpoint or the Redshift Serverless workgroup. See `VPC Endpoint` below. + +#### VPC Endpoint + +* `vpc_endpoint_id` - The DNS address of the VPC endpoint. +* `vpc_id` - The port that Amazon Redshift Serverless listens on. +* `network_interface` - The network interfaces of the endpoint.. See `Network Interface` below. + +##### Network Interface + +* `availability_zone` - The availability Zone. +* `network_interface_id` - The unique identifier of the network interface. +* `private_ip_address` - The IPv4 address of the network interface within the subnet. +* `subnet_id` - The unique identifier of the subnet. + ## Import Redshift Serverless Workgroups can be imported using the `workgroup_name`, e.g.,