diff --git a/.changelog/30455.txt b/.changelog/30455.txt new file mode 100644 index 000000000000..feace80c8971 --- /dev/null +++ b/.changelog/30455.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_vpclattice_target_group +``` \ No newline at end of file diff --git a/internal/service/vpclattice/service_package_gen.go b/internal/service/vpclattice/service_package_gen.go index ad122f13a1f0..726c2a1fdc42 100644 --- a/internal/service/vpclattice/service_package_gen.go +++ b/internal/service/vpclattice/service_package_gen.go @@ -60,6 +60,14 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka IdentifierAttribute: "arn", }, }, + { + Factory: ResourceTargetGroup, + TypeName: "aws_vpclattice_target_group", + Name: "Target Group", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "arn", + }, + }, } } diff --git a/internal/service/vpclattice/target_group.go b/internal/service/vpclattice/target_group.go new file mode 100644 index 000000000000..473e0efa1957 --- /dev/null +++ b/internal/service/vpclattice/target_group.go @@ -0,0 +1,602 @@ +package vpclattice + +import ( + "context" + "errors" + "fmt" + "log" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/vpclattice" + "github.com/aws/aws-sdk-go-v2/service/vpclattice/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "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" + 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_vpclattice_target_group", name="Target Group") +// @Tags(identifierAttribute="arn") +func ResourceTargetGroup() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceTargetGroupCreate, + ReadWithoutTimeout: resourceTargetGroupRead, + UpdateWithoutTimeout: resourceTargetGroupUpdate, + DeleteWithoutTimeout: resourceTargetGroupDelete, + + 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, + }, + "config": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "health_check": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "health_check_interval_seconds": { + Type: schema.TypeInt, + Optional: true, + Default: 30, + ValidateFunc: validation.IntBetween(5, 300), + }, + "health_check_timeout_seconds": { + Type: schema.TypeInt, + Optional: true, + Default: 5, + ValidateFunc: validation.IntBetween(1, 120), + }, + "healthy_threshold_count": { + Type: schema.TypeInt, + Optional: true, + Default: 5, + ValidateFunc: validation.IntBetween(2, 10), + }, + "matcher": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "value": { + Type: schema.TypeString, + Optional: true, + Default: "200", + }, + }, + }, + DiffSuppressFunc: verify.SuppressMissingOptionalConfigurationBlock, + }, + "path": { + Type: schema.TypeString, + Optional: true, + Default: "/", + }, + "port": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validation.IsPortNumber, + }, + "protocol": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateDiagFunc: enum.Validate[types.TargetGroupProtocol](), + }, + "protocol_version": { + Type: schema.TypeString, + Optional: true, + Default: types.HealthCheckProtocolVersionHttp1, + StateFunc: func(v interface{}) string { + return strings.ToUpper(v.(string)) + }, + ValidateDiagFunc: enum.Validate[types.HealthCheckProtocolVersion](), + }, + "unhealthy_threshold_count": { + Type: schema.TypeInt, + Optional: true, + Default: 2, + ValidateFunc: validation.IntBetween(2, 10), + }, + }, + }, + DiffSuppressFunc: verify.SuppressMissingOptionalConfigurationBlock, + }, + "ip_address_type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateDiagFunc: enum.Validate[types.IpAddressType](), + }, + "port": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + ValidateFunc: validation.IsPortNumber, + }, + "protocol": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: enum.Validate[types.TargetGroupProtocol](), + }, + "protocol_version": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: types.TargetGroupProtocolVersionHttp1, + StateFunc: func(v interface{}) string { + return strings.ToUpper(v.(string)) + }, + ValidateDiagFunc: enum.Validate[types.TargetGroupProtocolVersion](), + }, + "vpc_identifier": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + }, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(3, 128), + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: enum.Validate[types.TargetGroupType](), + }, + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), + }, + + CustomizeDiff: customdiff.All( + verify.SetTagsDiff, + func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { + targetGroupType := types.TargetGroupType(d.Get("type").(string)) + + if v, ok := d.GetOk("config"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + if targetGroupType == types.TargetGroupTypeLambda { + return fmt.Errorf(`config not supported for type = %q`, targetGroupType) + } + } else { + if targetGroupType != types.TargetGroupTypeLambda { + return fmt.Errorf(`config required for type = %q`, targetGroupType) + } + } + + return nil + }, + ), + } +} + +const ( + ResNameTargetGroup = "Target Group" +) + +func resourceTargetGroupCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).VPCLatticeClient() + + name := d.Get("name").(string) + in := &vpclattice.CreateTargetGroupInput{ + ClientToken: aws.String(id.UniqueId()), + Name: aws.String(name), + Tags: GetTagsIn(ctx), + Type: types.TargetGroupType(d.Get("type").(string)), + } + + if v, ok := d.GetOk("config"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + in.Config = expandTargetGroupConfig(v.([]interface{})[0].(map[string]interface{})) + } + + out, err := conn.CreateTargetGroup(ctx, in) + + if err != nil { + return create.DiagError(names.VPCLattice, create.ErrActionCreating, ResNameService, name, err) + } + + d.SetId(aws.ToString(out.Id)) + + if _, err := waitTargetGroupCreated(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil { + return create.DiagError(names.VPCLattice, create.ErrActionWaitingForCreation, ResNameTargetGroup, d.Id(), err) + } + + return resourceTargetGroupRead(ctx, d, meta) +} + +func resourceTargetGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).VPCLatticeClient() + + out, err := FindTargetGroupByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] VpcLattice Target Group (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return create.DiagError(names.VPCLattice, create.ErrActionReading, ResNameTargetGroup, d.Id(), err) + } + + d.Set("arn", out.Arn) + if out.Config != nil { + if err := d.Set("config", []interface{}{flattenTargetGroupConfig(out.Config)}); err != nil { + return create.DiagError(names.VPCLattice, create.ErrActionSetting, ResNameTargetGroup, d.Id(), err) + } + } else { + d.Set("config", nil) + } + d.Set("name", out.Name) + d.Set("status", out.Status) + d.Set("type", out.Type) + + return nil +} + +func resourceTargetGroupUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).VPCLatticeClient() + + if d.HasChangesExcept("tags", "tags_all") { + in := &vpclattice.UpdateTargetGroupInput{ + TargetGroupIdentifier: aws.String(d.Id()), + } + + if d.HasChange("config") { + if v, ok := d.GetOk("config"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + config := expandTargetGroupConfig(v.([]interface{})[0].(map[string]interface{})) + + if v := config.HealthCheck; v != nil { + in.HealthCheck = v + } + } + } + + if in.HealthCheck == nil { + return nil + } + + out, err := conn.UpdateTargetGroup(ctx, in) + + if err != nil { + return create.DiagError(names.VPCLattice, create.ErrActionUpdating, ResNameTargetGroup, d.Id(), err) + } + + if _, err := waitTargetGroupUpdated(ctx, conn, aws.ToString(out.Id), d.Timeout(schema.TimeoutUpdate)); err != nil { + return create.DiagError(names.VPCLattice, create.ErrActionWaitingForUpdate, ResNameTargetGroup, d.Id(), err) + } + } + + return resourceTargetGroupRead(ctx, d, meta) +} + +func resourceTargetGroupDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + conn := meta.(*conns.AWSClient).VPCLatticeClient() + + log.Printf("[INFO] Deleting VpcLattice TargetGroup: %s", d.Id()) + _, err := conn.DeleteTargetGroup(ctx, &vpclattice.DeleteTargetGroupInput{ + TargetGroupIdentifier: aws.String(d.Id()), + }) + + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil + } + + return create.DiagError(names.VPCLattice, create.ErrActionDeleting, ResNameTargetGroup, d.Id(), err) + } + + if _, err := waitTargetGroupDeleted(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { + return create.DiagError(names.VPCLattice, create.ErrActionWaitingForDeletion, ResNameTargetGroup, d.Id(), err) + } + + return nil +} + +func waitTargetGroupCreated(ctx context.Context, conn *vpclattice.Client, id string, timeout time.Duration) (*vpclattice.CreateTargetGroupOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(types.TargetGroupStatusCreateInProgress), + Target: enum.Slice(types.TargetGroupStatusActive), + Refresh: statusTargetGroup(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*vpclattice.CreateTargetGroupOutput); ok { + return out, err + } + + return nil, err +} + +func waitTargetGroupUpdated(ctx context.Context, conn *vpclattice.Client, id string, timeout time.Duration) (*vpclattice.UpdateTargetGroupOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(types.TargetGroupStatusCreateInProgress), + Target: enum.Slice(types.TargetGroupStatusActive), + Refresh: statusTargetGroup(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*vpclattice.UpdateTargetGroupOutput); ok { + return out, err + } + + return nil, err +} + +func waitTargetGroupDeleted(ctx context.Context, conn *vpclattice.Client, id string, timeout time.Duration) (*vpclattice.DeleteTargetGroupOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(types.TargetGroupStatusDeleteInProgress, types.TargetGroupStatusActive), + Target: []string{}, + Refresh: statusTargetGroup(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*vpclattice.DeleteTargetGroupOutput); ok { + return out, err + } + + return nil, err +} + +func statusTargetGroup(ctx context.Context, conn *vpclattice.Client, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := FindTargetGroupByID(ctx, conn, id) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, string(out.Status), nil + } +} + +func FindTargetGroupByID(ctx context.Context, conn *vpclattice.Client, id string) (*vpclattice.GetTargetGroupOutput, error) { + in := &vpclattice.GetTargetGroupInput{ + TargetGroupIdentifier: aws.String(id), + } + out, err := conn.GetTargetGroup(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 + } + + if out == nil || out.Id == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out, nil +} + +func flattenTargetGroupConfig(apiObject *types.TargetGroupConfig) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{ + "ip_address_type": apiObject.IpAddressType, + "protocol": apiObject.Protocol, + "protocol_version": apiObject.ProtocolVersion, + } + + if v := apiObject.HealthCheck; v != nil { + tfMap["health_check"] = []interface{}{flattenHealthCheckConfig(v)} + } + + if v := apiObject.Port; v != nil { + tfMap["port"] = aws.ToInt32(v) + } + + if v := apiObject.VpcIdentifier; v != nil { + tfMap["vpc_identifier"] = aws.ToString(v) + } + + return tfMap +} + +func flattenHealthCheckConfig(apiObject *types.HealthCheckConfig) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{ + "protocol": apiObject.Protocol, + "protocol_version": apiObject.ProtocolVersion, + } + + if v := apiObject.Enabled; v != nil { + tfMap["enabled"] = aws.ToBool(v) + } + + if v := apiObject.HealthCheckIntervalSeconds; v != nil { + tfMap["health_check_interval_seconds"] = aws.ToInt32(v) + } + + if v := apiObject.HealthCheckTimeoutSeconds; v != nil { + tfMap["health_check_timeout_seconds"] = aws.ToInt32(v) + } + + if v := apiObject.HealthyThresholdCount; v != nil { + tfMap["healthy_threshold_count"] = aws.ToInt32(v) + } + + if v := apiObject.Matcher; v != nil { + tfMap["matcher"] = []interface{}{flattenMatcherMemberHTTPCode(v.(*types.MatcherMemberHttpCode))} + } + + if v := apiObject.Path; v != nil { + tfMap["path"] = aws.ToString(v) + } + + if v := apiObject.Port; v != nil { + tfMap["port"] = aws.ToInt32(v) + } + + if v := apiObject.UnhealthyThresholdCount; v != nil { + tfMap["unhealthy_threshold_count"] = aws.ToInt32(v) + } + + return tfMap +} + +func flattenMatcherMemberHTTPCode(apiObject *types.MatcherMemberHttpCode) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{ + "value": apiObject.Value, + } + + return tfMap +} + +func expandTargetGroupConfig(tfMap map[string]interface{}) *types.TargetGroupConfig { + if tfMap == nil { + return nil + } + + apiObject := &types.TargetGroupConfig{} + + if v, ok := tfMap["health_check"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + apiObject.HealthCheck = expandHealthCheckConfig(v[0].(map[string]interface{})) + } + + if v, ok := tfMap["ip_address_type"].(string); ok && v != "" { + apiObject.IpAddressType = types.IpAddressType(v) + } + + if v, ok := tfMap["port"].(int); ok && v != 0 { + apiObject.Port = aws.Int32(int32(v)) + } + + if v, ok := tfMap["protocol"].(string); ok && v != "" { + apiObject.Protocol = types.TargetGroupProtocol(v) + } + + if v, ok := tfMap["vpc_identifier"].(string); ok && v != "" { + apiObject.VpcIdentifier = aws.String(v) + } + + if v, ok := tfMap["protocol_version"].(string); ok && v != "" { + apiObject.ProtocolVersion = types.TargetGroupProtocolVersion(v) + } + + return apiObject +} + +func expandHealthCheckConfig(tfMap map[string]interface{}) *types.HealthCheckConfig { + apiObject := &types.HealthCheckConfig{} + + if v, ok := tfMap["enabled"].(bool); ok { + apiObject.Enabled = aws.Bool(v) + } + + if v, ok := tfMap["health_check_interval_seconds"].(int); ok && v != 0 { + apiObject.HealthCheckIntervalSeconds = aws.Int32(int32(v)) + } + + if v, ok := tfMap["health_check_timeout_seconds"].(int); ok && v != 0 { + apiObject.HealthCheckTimeoutSeconds = aws.Int32(int32(v)) + } + + if v, ok := tfMap["healthy_threshold_count"].(int); ok && v != 0 { + apiObject.HealthyThresholdCount = aws.Int32(int32(v)) + } + + if v, ok := tfMap["matcher"].([]interface{}); ok && len(v) > 0 && v[0] != nil { + apiObject.Matcher = expandMatcherMemberHTTPCode(v[0].(map[string]interface{})) + } + + if v, ok := tfMap["path"].(string); ok && v != "" { + apiObject.Path = aws.String(v) + } + + if v, ok := tfMap["port"].(int); ok && v != 0 { + apiObject.Port = aws.Int32(int32(v)) + } + + if v, ok := tfMap["protocol"].(string); ok && v != "" { + apiObject.Protocol = types.TargetGroupProtocol(v) + } + + if v, ok := tfMap["protocol_version"].(string); ok && v != "" { + apiObject.ProtocolVersion = types.HealthCheckProtocolVersion(v) + } + + if v, ok := tfMap["unhealthy_threshold_count"].(int); ok && v != 0 { + apiObject.UnhealthyThresholdCount = aws.Int32(int32(v)) + } + + return apiObject +} + +func expandMatcherMemberHTTPCode(tfMap map[string]interface{}) types.Matcher { + apiObject := &types.MatcherMemberHttpCode{} + + if v, ok := tfMap["value"].(string); ok && v != "" { + apiObject.Value = v + } + return apiObject +} diff --git a/internal/service/vpclattice/target_group_test.go b/internal/service/vpclattice/target_group_test.go new file mode 100644 index 000000000000..526b832534eb --- /dev/null +++ b/internal/service/vpclattice/target_group_test.go @@ -0,0 +1,492 @@ +package vpclattice_test + +import ( + "context" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/vpclattice" + "github.com/aws/aws-sdk-go-v2/service/vpclattice/types" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + tfvpclattice "github.com/hashicorp/terraform-provider-aws/internal/service/vpclattice" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccVPCLatticeTargetGroup_basic(t *testing.T) { + ctx := acctest.Context(t) + var targetGroup vpclattice.GetTargetGroupOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_vpclattice_target_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VPCLatticeEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VPCLatticeEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTargetGroupDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetGroupConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTargetGroupExists(ctx, resourceName, &targetGroup), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "vpc-lattice", regexp.MustCompile("targetgroup/.+$")), + resource.TestCheckResourceAttr(resourceName, "config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.#", "1"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.health_check_interval_seconds", "30"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.health_check_timeout_seconds", "5"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.healthy_threshold_count", "5"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.matcher.#", "1"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.matcher.0.value", "200"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.path", "/"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.port", "0"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.protocol", "HTTP"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.protocol_version", "HTTP1"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.unhealthy_threshold_count", "2"), + resource.TestCheckResourceAttr(resourceName, "config.0.ip_address_type", ""), + resource.TestCheckResourceAttr(resourceName, "config.0.port", "80"), + resource.TestCheckResourceAttr(resourceName, "config.0.protocol", "HTTP"), + resource.TestCheckResourceAttr(resourceName, "config.0.protocol_version", "HTTP1"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "status", "ACTIVE"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "INSTANCE"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccVPCLatticeTargetGroup_disappears(t *testing.T) { + ctx := acctest.Context(t) + var targetGroup vpclattice.GetTargetGroupOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_vpclattice_target_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VPCLatticeEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VPCLatticeEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckServiceDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetGroupConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTargetGroupExists(ctx, resourceName, &targetGroup), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfvpclattice.ResourceTargetGroup(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccVPCLatticeTargetGroup_tags(t *testing.T) { + ctx := acctest.Context(t) + var targetGroup vpclattice.GetTargetGroupOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_vpclattice_target_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.VPCLatticeEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTargetGroupDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetGroupConfig_tags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckTargetGroupExists(ctx, resourceName, &targetGroup), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccTargetGroupConfig_tags2(rName, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckTargetGroupExists(ctx, resourceName, &targetGroup), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccTargetGroupConfig_tags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckTargetGroupExists(ctx, resourceName, &targetGroup), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccVPCLatticeTargetGroup_lambda(t *testing.T) { + ctx := acctest.Context(t) + var targetGroup vpclattice.GetTargetGroupOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_vpclattice_target_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VPCLatticeEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VPCLatticeEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTargetGroupDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetGroupConfig_lambda(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTargetGroupExists(ctx, resourceName, &targetGroup), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "vpc-lattice", regexp.MustCompile("targetgroup/.+$")), + resource.TestCheckResourceAttr(resourceName, "config.#", "0"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "status", "ACTIVE"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "LAMBDA"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccVPCLatticeTargetGroup_ip(t *testing.T) { + ctx := acctest.Context(t) + var targetGroup vpclattice.GetTargetGroupOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_vpclattice_target_group.test" + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VPCLatticeEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VPCLatticeEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTargetGroupDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetGroupConfig_ip(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTargetGroupExists(ctx, resourceName, &targetGroup), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "vpc-lattice", regexp.MustCompile("targetgroup/.+$")), + resource.TestCheckResourceAttr(resourceName, "config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.#", "1"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.health_check_interval_seconds", "60"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.health_check_timeout_seconds", "10"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.healthy_threshold_count", "6"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.matcher.#", "1"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.matcher.0.value", "200-299"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.path", "/health"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.port", "8443"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.protocol", "HTTPS"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.protocol_version", "HTTP1"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.unhealthy_threshold_count", "4"), + resource.TestCheckResourceAttr(resourceName, "config.0.ip_address_type", "IPV6"), + resource.TestCheckResourceAttr(resourceName, "config.0.port", "443"), + resource.TestCheckResourceAttr(resourceName, "config.0.protocol", "HTTPS"), + resource.TestCheckResourceAttr(resourceName, "config.0.protocol_version", "HTTP2"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "status", "ACTIVE"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "IP"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccTargetGroupConfig_ipUpdated(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTargetGroupExists(ctx, resourceName, &targetGroup), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "vpc-lattice", regexp.MustCompile("targetgroup/.+$")), + resource.TestCheckResourceAttr(resourceName, "config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.#", "1"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.health_check_interval_seconds", "180"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.health_check_timeout_seconds", "90"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.healthy_threshold_count", "8"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.matcher.#", "1"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.matcher.0.value", "202"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.path", "/health"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.port", "8443"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.protocol", "HTTPS"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.protocol_version", "HTTP2"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.0.unhealthy_threshold_count", "3"), + resource.TestCheckResourceAttr(resourceName, "config.0.ip_address_type", "IPV6"), + resource.TestCheckResourceAttr(resourceName, "config.0.port", "443"), + resource.TestCheckResourceAttr(resourceName, "config.0.protocol", "HTTPS"), + resource.TestCheckResourceAttr(resourceName, "config.0.protocol_version", "HTTP2"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "status", "ACTIVE"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "IP"), + ), + }, + }, + }) +} + +func TestAccVPCLatticeTargetGroup_alb(t *testing.T) { + ctx := acctest.Context(t) + var targetGroup vpclattice.GetTargetGroupOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_vpclattice_target_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.VPCLatticeEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.VPCLatticeEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTargetGroupDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetGroupConfig_alb(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTargetGroupExists(ctx, resourceName, &targetGroup), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "vpc-lattice", regexp.MustCompile("targetgroup/.+$")), + resource.TestCheckResourceAttr(resourceName, "config.#", "1"), + resource.TestCheckResourceAttr(resourceName, "config.0.health_check.#", "0"), + resource.TestCheckResourceAttr(resourceName, "config.0.ip_address_type", ""), + resource.TestCheckResourceAttr(resourceName, "config.0.port", "80"), + resource.TestCheckResourceAttr(resourceName, "config.0.protocol", "HTTP"), + resource.TestCheckResourceAttr(resourceName, "config.0.protocol_version", "HTTP1"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "status", "ACTIVE"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "ALB"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckTargetGroupDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).VPCLatticeClient() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpclattice_target_group" { + continue + } + + _, err := conn.GetTargetGroup(ctx, &vpclattice.GetTargetGroupInput{ + TargetGroupIdentifier: aws.String(rs.Primary.ID), + }) + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil + } + return err + } + + return create.Error(names.VPCLattice, create.ErrActionCheckingDestroyed, tfvpclattice.ResNameService, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckTargetGroupExists(ctx context.Context, name string, targetGroup *vpclattice.GetTargetGroupOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.VPCLattice, create.ErrActionCheckingExistence, tfvpclattice.ResNameService, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.VPCLattice, create.ErrActionCheckingExistence, tfvpclattice.ResNameService, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).VPCLatticeClient() + resp, err := conn.GetTargetGroup(ctx, &vpclattice.GetTargetGroupInput{ + TargetGroupIdentifier: aws.String(rs.Primary.ID), + }) + + if err != nil { + return create.Error(names.VPCLattice, create.ErrActionCheckingExistence, tfvpclattice.ResNameService, rs.Primary.ID, err) + } + + *targetGroup = *resp + + return nil + } +} + +func testAccTargetGroupConfig_basic(rName string) string { + return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 0), fmt.Sprintf(` +resource "aws_vpclattice_target_group" "test" { + name = %[1]q + type = "INSTANCE" + + config { + port = 80 + protocol = "HTTP" + vpc_identifier = aws_vpc.test.id + } +} +`, rName)) +} + +func testAccTargetGroupConfig_tags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_vpclattice_target_group" "test" { + name = %[1]q + type = "LAMBDA" + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccTargetGroupConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_vpclattice_target_group" "test" { + name = %[1]q + type = "LAMBDA" + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} + +func testAccTargetGroupConfig_lambda(rName string) string { + return fmt.Sprintf(` +resource "aws_vpclattice_target_group" "test" { + name = %[1]q + type = "LAMBDA" +} +`, rName) +} + +func testAccTargetGroupConfig_ip(rName string) string { + return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 0), fmt.Sprintf(` +resource "aws_vpclattice_target_group" "test" { + name = %[1]q + type = "IP" + + config { + port = 443 + protocol = "HTTPS" + vpc_identifier = aws_vpc.test.id + ip_address_type = "IPV6" + protocol_version = "HTTP2" + + health_check { + health_check_interval_seconds = 60 + health_check_timeout_seconds = 10 + healthy_threshold_count = 6 + unhealthy_threshold_count = 4 + + matcher { + value = "200-299" + } + + path = "/health" + port = 8443 + protocol = "HTTPS" + protocol_version = "HTTP1" + } + } +} +`, rName)) +} + +func testAccTargetGroupConfig_ipUpdated(rName string) string { + return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 0), fmt.Sprintf(` +resource "aws_vpclattice_target_group" "test" { + name = %[1]q + type = "IP" + + config { + port = 443 + protocol = "HTTPS" + vpc_identifier = aws_vpc.test.id + ip_address_type = "IPV6" + protocol_version = "HTTP2" + + health_check { + health_check_interval_seconds = 180 + health_check_timeout_seconds = 90 + healthy_threshold_count = 8 + unhealthy_threshold_count = 3 + + matcher { + value = "202" + } + + path = "/health" + port = 8443 + protocol = "HTTPS" + protocol_version = "HTTP2" + } + } +} +`, rName)) +} + +func testAccTargetGroupConfig_alb(rName string) string { + return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 0), fmt.Sprintf(` +resource "aws_vpclattice_target_group" "test" { + name = %[1]q + type = "ALB" + + config { + port = 80 + protocol = "HTTP" + vpc_identifier = aws_vpc.test.id + } +} +`, rName)) +} diff --git a/website/docs/r/vpclattice_target_group.html.markdown b/website/docs/r/vpclattice_target_group.html.markdown new file mode 100644 index 000000000000..a657e65737cc --- /dev/null +++ b/website/docs/r/vpclattice_target_group.html.markdown @@ -0,0 +1,130 @@ +--- +subcategory: "VPC Lattice" +layout: "aws" +page_title: "AWS: aws_vpclattice_target_group" +description: |- + Terraform resource for managing an AWS VPC Lattice Target Group. +--- + +# Resource: aws_vpclattice_target_group + +Terraform resource for managing an AWS VPC Lattice Target Group. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_vpclattice_target_group" "example" { + name = "example" + type = "INSTANCE" + config { + port = 443 + protocol = "HTTPS" + vpc_identifier = aws_vpc.example.id + } +} +``` + +### Basic usage with Health check + +```terraform +resource "aws_vpclattice_target_group" "example" { + name = "example" + type = "ALB" + + config { + port = 443 + protocol = "HTTPS" + vpc_identifier = aws_vpc.example.id + protocol_version = "HPPT1" + + health_check { + enabled = true + health_check_interval_seconds = 20 + health_check_timeout_seconds = 10 + healthy_threshold_count = 7 + unhealthy_threshold_count = 3 + + matcher { + value = "200-299" + } + + path = "/instance" + port = 80 + protocol = "HTTP" + protocol_version = "HTTP1" + } + } +} +``` + +### Lambda + +If the type is Lambda, `config` block is not supported. + +```terraform +resource "aws_vpclattice_target_group" "example" { + name = "example" + type = "LAMBDA" +} +``` + +## Argument Reference + +The following arguments are required: + +* `name` - (Required) The name of the target group. The name must be unique within the account. The valid characters are a-z, 0-9, and hyphens (-). You can't use a hyphen as the first or last character, or immediately after another hyphen. +* `type` - (Required) The type of target group. Valid Values are `IP` | `LAMBDA` | `INSTANCE` | `ALB` + +The following arguments are optional: + +* `config` - (Optional) The target group configuration. If type is set to `LAMBDA,` this parameter should not be specified. +* `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. + +Config (`config`) supports the following: + +* `health_check` - (Optional) The health check configuration. +* `ip_address_type` - (Optional) The type of IP address used for the target group. Valid values: `IPV4` | `IPV6` +* `port` - (Required) The port on which the targets are listening. +* `protocol` - (Required) The protocol to use for routing traffic to the targets. Valid Values are `HTTP` | `HTTPS` +* `protocol_version` - (Optional) The protocol version. Valid Values are `HTTP1` | `HTTP2` | `GRPC`. Default value is `HTTP1`. +* `vpc_identifier` - (Required) The ID of the VPC. + +Health Check (`health_check`) supports the following: + +* `enabled` - (Optional) Indicates whether health checking is enabled. Defaults to `true`. +* `health_check_interval_seconds` - (Optional) The approximate amount of time, in seconds, between health checks of an individual target. The range is 5–300 seconds. The default is 30 seconds. +* `health_check_timeout_seconds` - (Optional) The amount of time, in seconds, to wait before reporting a target as unhealthy. The range is 1–120 seconds. The default is 5 seconds. +* `healthy_threshold_count ` - (Optional) The number of consecutive successful health checks required before considering an unhealthy target healthy. The range is 2–10. The default is 5. +* `matcher` - (Optional) The codes to use when checking for a successful response from a target. These are called _Success codes_ in the console. + * `value` - (Optional) The HTTP codes to use when checking for a successful response from a target. +* `path` - (Optional) The destination for health checks on the targets. If the protocol version is HTTP/1.1 or HTTP/2, specify a valid URI (for example, /path?query). The default path is `/`. Health checks are not supported if the protocol version is gRPC, however, you can choose HTTP/1.1 or HTTP/2 and specify a valid URI. +* `port` - (Optional) The port used when performing health checks on targets. The default setting is the port that a target receives traffic on. +* `protocol` - (Optional) The protocol used when performing health checks on targets. The possible protocols are `HTTP` and `HTTPS`. +* `protocol_version` - (Optional) The protocol version used when performing health checks on targets. The possible protocol versions are `HTTP1` and `HTTP2`. The default is `HTTP1`. +* `unhealthy_threshold_count` - (Optional) The number of consecutive failed health checks required before considering a target unhealthy. The range is 2–10. The default is 2. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - ARN of the target group. +* `id` - Unique identifier for the target group. +* `status` - Status of the target group. +* `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 `5m`) +* `delete` - (Default `5m`) + +## Import + +VPC Lattice Target Group can be imported using the `id`, e.g., + +``` +$ terraform import aws_vpclattice_target_group.example tg-0c11d4dc16ed96bdb +```