From 0afd07858dcb0bf69c32186500ced6a8baef8d21 Mon Sep 17 00:00:00 2001 From: Jared Baker Date: Thu, 22 Jun 2023 15:12:09 -0400 Subject: [PATCH 1/2] r/aws_lb_target_registration: initial implementation --- .github/labeler-pr-triage.yml | 1 + internal/service/elbv2/exports_test.go | 9 + internal/service/elbv2/service_package_gen.go | 7 +- internal/service/elbv2/target_registration.go | 387 +++++++++++++++ .../service/elbv2/target_registration_test.go | 442 ++++++++++++++++++ names/names_data.csv | 2 +- .../r/lb_target_registration.html.markdown | 49 ++ 7 files changed, 895 insertions(+), 2 deletions(-) create mode 100644 internal/service/elbv2/exports_test.go create mode 100644 internal/service/elbv2/target_registration.go create mode 100644 internal/service/elbv2/target_registration_test.go create mode 100644 website/docs/r/lb_target_registration.html.markdown diff --git a/.github/labeler-pr-triage.yml b/.github/labeler-pr-triage.yml index 5c251c6f36f..b683f22f64e 100644 --- a/.github/labeler-pr-triage.yml +++ b/.github/labeler-pr-triage.yml @@ -425,6 +425,7 @@ service/elbv2: - 'website/**/lb_listener*' - 'website/**/lb_target_group*' - 'website/**/lb_hosted*' + - 'website/**/lb_target_registration*' service/emr: - 'internal/service/emr/**/*' - 'website/**/emr_*' diff --git a/internal/service/elbv2/exports_test.go b/internal/service/elbv2/exports_test.go new file mode 100644 index 00000000000..81d2e9cbd8f --- /dev/null +++ b/internal/service/elbv2/exports_test.go @@ -0,0 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package elbv2 + +// Exports for use in tests only. +var ( + ResourceTargetRegistration = newResourceTargetRegistration +) diff --git a/internal/service/elbv2/service_package_gen.go b/internal/service/elbv2/service_package_gen.go index 57e5abc0a71..98b71eab462 100644 --- a/internal/service/elbv2/service_package_gen.go +++ b/internal/service/elbv2/service_package_gen.go @@ -20,7 +20,12 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv } func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { - return []*types.ServicePackageFrameworkResource{} + return []*types.ServicePackageFrameworkResource{ + { + Factory: newResourceTargetRegistration, + Name: "Target Registration", + }, + } } func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePackageSDKDataSource { diff --git a/internal/service/elbv2/target_registration.go b/internal/service/elbv2/target_registration.go new file mode 100644 index 00000000000..546728273f6 --- /dev/null +++ b/internal/service/elbv2/target_registration.go @@ -0,0 +1,387 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package elbv2 + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="Target Registration") +func newResourceTargetRegistration(_ context.Context) (resource.ResourceWithConfigure, error) { + return &resourceTargetRegistration{}, nil +} + +const ( + ResNameTargetRegistration = "Target Registration" +) + +type resourceTargetRegistration struct { + framework.ResourceWithConfigure + framework.WithTimeouts +} + +func (r *resourceTargetRegistration) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "aws_lb_target_registration" +} + +func (r *resourceTargetRegistration) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttribute(), + "target_group_arn": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "target": schema.SetNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "availability_zone": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "port": schema.Int64Attribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + "target_id": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + } +} + +func (r *resourceTargetRegistration) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().ELBV2Conn(ctx) + + var plan resourceTargetRegistrationData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var tfList []targetData + resp.Diagnostics.Append(plan.Target.ElementsAs(ctx, &tfList, false)...) + if resp.Diagnostics.HasError() { + return + } + + in := &elbv2.RegisterTargetsInput{ + TargetGroupArn: aws.String(plan.TargetGroupARN.ValueString()), + Targets: expandTargets(tfList), + } + + _, err := conn.RegisterTargetsWithContext(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.ELBV2, create.ErrActionCreating, ResNameTargetRegistration, plan.TargetGroupARN.String(), err), + err.Error(), + ) + return + } + + // Targets must be read after create to set computed attributes + out, err := findTargetRegistrationByMultipartKey(ctx, conn, plan.TargetGroupARN.ValueString(), tfList) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.ELBV2, create.ErrActionReading, ResNameTargetRegistration, plan.TargetGroupARN.String(), err), + err.Error(), + ) + return + } + targets, d := flattenTargets(ctx, out) + resp.Diagnostics.Append(d...) + plan.Target = targets + + plan.ID = types.StringValue(plan.TargetGroupARN.ValueString()) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceTargetRegistration) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().ELBV2Conn(ctx) + + var state resourceTargetRegistrationData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var tfList []targetData + resp.Diagnostics.Append(state.Target.ElementsAs(ctx, &tfList, false)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := findTargetRegistrationByMultipartKey(ctx, conn, state.TargetGroupARN.ValueString(), tfList) + if tfresource.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.ELBV2, create.ErrActionSetting, ResNameTargetRegistration, state.ID.String(), err), + err.Error(), + ) + return + } + + targets, d := flattenTargets(ctx, out) + resp.Diagnostics.Append(d...) + state.Target = targets + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceTargetRegistration) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().ELBV2Conn(ctx) + + var plan, state resourceTargetRegistrationData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if !plan.Target.Equal(state.Target) { + var stateTargets []targetData + var planTargets []targetData + resp.Diagnostics.Append(state.Target.ElementsAs(ctx, &stateTargets, false)...) + resp.Diagnostics.Append(plan.Target.ElementsAs(ctx, &planTargets, false)...) + if resp.Diagnostics.HasError() { + return + } + + stateSet := flex.Set[targetData](stateTargets) + planSet := flex.Set[targetData](planTargets) + + if dereg := stateSet.Difference(planSet); len(dereg) > 0 { + in := &elbv2.DeregisterTargetsInput{ + TargetGroupArn: aws.String(state.TargetGroupARN.ValueString()), + Targets: expandTargets(dereg), + } + + _, err := conn.DeregisterTargetsWithContext(ctx, in) + if err != nil { + if tfawserr.ErrCodeEquals(err, elbv2.ErrCodeTargetGroupNotFoundException) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.ELBV2, create.ErrActionUpdating, ResNameTargetRegistration, state.ID.String(), err), + err.Error(), + ) + return + } + } + + if reg := planSet.Difference(stateSet); len(reg) > 0 { + in := &elbv2.RegisterTargetsInput{ + TargetGroupArn: aws.String(plan.TargetGroupARN.ValueString()), + Targets: expandTargets(reg), + } + + _, err := conn.RegisterTargetsWithContext(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.ELBV2, create.ErrActionUpdating, ResNameTargetRegistration, state.ID.String(), err), + err.Error(), + ) + return + } + } + + // Targets must be read after update to set computed attributes + out, err := findTargetRegistrationByMultipartKey(ctx, conn, plan.TargetGroupARN.ValueString(), planTargets) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.ELBV2, create.ErrActionReading, ResNameTargetRegistration, plan.TargetGroupARN.String(), err), + err.Error(), + ) + return + } + targets, d := flattenTargets(ctx, out) + resp.Diagnostics.Append(d...) + plan.Target = targets + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceTargetRegistration) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().ELBV2Conn(ctx) + + var state resourceTargetRegistrationData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + var tfList []targetData + resp.Diagnostics.Append(state.Target.ElementsAs(ctx, &tfList, false)...) + if resp.Diagnostics.HasError() { + return + } + if len(tfList) == 0 { + // No active targets, nothing to do + return + } + + in := &elbv2.DeregisterTargetsInput{ + TargetGroupArn: aws.String(state.TargetGroupARN.ValueString()), + Targets: expandTargets(tfList), + } + + _, err := conn.DeregisterTargetsWithContext(ctx, in) + if err != nil { + if tfawserr.ErrCodeEquals(err, elbv2.ErrCodeTargetGroupNotFoundException) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.ELBV2, create.ErrActionDeleting, ResNameTargetRegistration, state.ID.String(), err), + err.Error(), + ) + return + } +} + +func findTargetRegistrationByMultipartKey(ctx context.Context, conn *elbv2.ELBV2, targetGroupARN string, targets []targetData) ([]*elbv2.TargetHealthDescription, error) { + in := &elbv2.DescribeTargetHealthInput{ + TargetGroupArn: aws.String(targetGroupARN), + Targets: expandTargets(targets), + } + + out, err := conn.DescribeTargetHealthWithContext(ctx, in) + if tfawserr.ErrCodeEquals(err, elbv2.ErrCodeTargetGroupNotFoundException, elbv2.ErrCodeInvalidTargetException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + if err != nil { + return nil, err + } + + if out == nil || len(out.TargetHealthDescriptions) == 0 { + return nil, tfresource.NewEmptyResultError(in) + } + + return out.TargetHealthDescriptions, nil +} + +func flattenTargets(ctx context.Context, apiObjects []*elbv2.TargetHealthDescription) (types.Set, diag.Diagnostics) { + var diags diag.Diagnostics + elemType := types.ObjectType{AttrTypes: targetAttrTypes} + + if len(apiObjects) == 0 { + return types.SetNull(elemType), diags + } + + elems := []attr.Value{} + for _, apiObject := range apiObjects { + if apiObject == nil || apiObject.Target == nil || apiObject.TargetHealth == nil { + continue + } + + // Unregistered targets, or targets in the process of deregistering should not be included + reason := aws.StringValue(apiObject.TargetHealth.Reason) + if reason == elbv2.TargetHealthReasonEnumTargetNotRegistered || reason == elbv2.TargetHealthReasonEnumTargetDeregistrationInProgress { + continue + } + + target := apiObject.Target + obj := map[string]attr.Value{ + "availability_zone": flex.StringToFramework(ctx, target.AvailabilityZone), + "port": flex.Int64ToFramework(ctx, target.Port), + "target_id": flex.StringToFramework(ctx, target.Id), + } + objVal, d := types.ObjectValue(targetAttrTypes, obj) + diags.Append(d...) + + elems = append(elems, objVal) + } + + // check resulting elem length in case none of the returned API objects are + // actively registered + if len(elems) == 0 { + return types.SetNull(elemType), diags + } + + setVal, d := types.SetValue(elemType, elems) + diags.Append(d...) + + return setVal, diags +} + +func expandTargets(tfList []targetData) []*elbv2.TargetDescription { + if len(tfList) == 0 { + return nil + } + + var apiObject []*elbv2.TargetDescription + + for _, tfObj := range tfList { + item := &elbv2.TargetDescription{ + Id: aws.String(tfObj.TargetID.ValueString()), + } + if !tfObj.AvailabilityZone.IsNull() && !tfObj.AvailabilityZone.IsUnknown() { + item.AvailabilityZone = aws.String(tfObj.AvailabilityZone.ValueString()) + } + if !tfObj.Port.IsNull() && !tfObj.Port.IsUnknown() { + item.Port = aws.Int64(tfObj.Port.ValueInt64()) + } + + apiObject = append(apiObject, item) + } + + return apiObject +} + +type resourceTargetRegistrationData struct { + ID types.String `tfsdk:"id"` + Target types.Set `tfsdk:"target"` + TargetGroupARN types.String `tfsdk:"target_group_arn"` +} + +type targetData struct { + AvailabilityZone types.String `tfsdk:"availability_zone"` + Port types.Int64 `tfsdk:"port"` + TargetID types.String `tfsdk:"target_id"` +} + +var targetAttrTypes = map[string]attr.Type{ + "availability_zone": types.StringType, + "port": types.Int64Type, + "target_id": types.StringType, +} diff --git a/internal/service/elbv2/target_registration_test.go b/internal/service/elbv2/target_registration_test.go new file mode 100644 index 00000000000..fc8f05680d3 --- /dev/null +++ b/internal/service/elbv2/target_registration_test.go @@ -0,0 +1,442 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package elbv2_test + +import ( + "context" + "errors" + "fmt" + "strconv" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + 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" + tfelbv2 "github.com/hashicorp/terraform-provider-aws/internal/service/elbv2" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccELBV2TargetRegistration_basic(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lb_target_registration.test" + targetGroupResourceName := "aws_lb_target_group.test" + instanceResourceName := "aws_instance.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, elbv2.EndpointsID) + }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTargetRegistrationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetRegistrationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTargetRegistrationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "target_group_arn", targetGroupResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "target.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "target.0.target_id", instanceResourceName, "id"), + ), + }, + }, + }) +} + +func TestAccELBV2TargetRegistration_disappears(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lb_target_registration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, elbv2.EndpointsID) + }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTargetRegistrationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetRegistrationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTargetRegistrationExists(ctx, resourceName), + testAccCheckTargetRegistrationDisappears(ctx, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccELBV2TargetRegistration_update(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lb_target_registration.test" + targetGroupResourceName := "aws_lb_target_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, elbv2.EndpointsID) + }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTargetRegistrationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetRegistrationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTargetRegistrationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "target_group_arn", targetGroupResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "target.#", "1"), + ), + }, + { + Config: testAccTargetRegistrationConfig_update(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTargetRegistrationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "target_group_arn", targetGroupResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "target.#", "3"), + ), + }, + { + Config: testAccTargetRegistrationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTargetRegistrationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "target_group_arn", targetGroupResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "target.#", "1"), + ), + }, + }, + }) +} + +func TestAccELBV2TargetRegistration_Type_ipAddress(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lb_target_registration.test" + targetGroupResourceName := "aws_lb_target_group.test" + instanceResourceName := "aws_instance.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, elbv2.EndpointsID) + }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTargetRegistrationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetRegistrationConfig_Type_ipAddress(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTargetRegistrationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "target_group_arn", targetGroupResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "target.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "target.0.availability_zone", instanceResourceName, "availability_zone"), + resource.TestCheckResourceAttrPair(resourceName, "target.0.target_id", instanceResourceName, "private_ip"), + ), + }, + }, + }) +} + +func TestAccELBV2TargetRegistration_Type_lambdaFunction(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lb_target_registration.test" + targetGroupResourceName := "aws_lb_target_group.test" + lambdaAliasResourceName := "aws_lambda_alias.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, elbv2.EndpointsID) + }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTargetRegistrationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetRegistrationConfig_Type_lambdaFunction(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckTargetRegistrationExists(ctx, resourceName), + resource.TestCheckResourceAttrPair(resourceName, "target_group_arn", targetGroupResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "target.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "target.0.target_id", lambdaAliasResourceName, "arn"), + ), + }, + }, + }) +} + +// testAccCheckTargetRegistrationDisappears is a custom variant of the shared acctest +// disappears helper. The shared function does not copy nested arguments into the resource +// to be deleted, resulting in an "InvalidParameter" exception as the nested Targets argument +// required for de-registration is empty. +func testAccCheckTargetRegistrationDisappears(ctx context.Context, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("resource not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("resource ID missing: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).ELBV2Conn(ctx) + in := &elbv2.DeregisterTargetsInput{ + TargetGroupArn: aws.String(rs.Primary.Attributes["target_group_arn"]), + Targets: []*elbv2.TargetDescription{ + { + Id: aws.String(rs.Primary.Attributes["target.0.target_id"]), + }, + }, + } + + _, err := conn.DeregisterTargetsWithContext(ctx, in) + if err != nil && !tfawserr.ErrCodeEquals(err, elbv2.ErrCodeTargetGroupNotFoundException) { + return fmt.Errorf("deregistering targets: %s", err) + } + + return err + } +} + +func testAccCheckTargetRegistrationDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).ELBV2Conn(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_lb_target_registration" && rs.Type != "aws_alb_target_registration" { + continue + } + + targetGroupArn := rs.Primary.Attributes["target_group_arn"] + + // Extracting target data from nested object string attributes is complicated, so + // lazily describe with only the target group ARN input and check the resulting + // output count instead. + out, err := conn.DescribeTargetHealthWithContext(ctx, &elbv2.DescribeTargetHealthInput{ + TargetGroupArn: aws.String(targetGroupArn), + }) + if err == nil { + if len(out.TargetHealthDescriptions) != 0 { + return fmt.Errorf("Target Group %q still has registered targets", rs.Primary.ID) + } + } + + if tfawserr.ErrCodeEquals(err, elbv2.ErrCodeTargetGroupNotFoundException) || tfawserr.ErrCodeEquals(err, elbv2.ErrCodeInvalidTargetException) { + return nil + } + + return create.Error(names.ELBV2, create.ErrActionCheckingDestroyed, tfelbv2.ResNameTargetRegistration, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckTargetRegistrationExists(ctx context.Context, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.ELBV2, create.ErrActionCheckingExistence, tfelbv2.ResNameTargetRegistration, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.ELBV2, create.ErrActionCheckingExistence, tfelbv2.ResNameTargetRegistration, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).ELBV2Conn(ctx) + targetGroupArn := rs.Primary.Attributes["target_group_arn"] + targetCount := rs.Primary.Attributes["target.#"] + want, err := strconv.Atoi(targetCount) + if err != nil { + return create.Error(names.ELBV2, create.ErrActionCheckingExistence, tfelbv2.ResNameTargetRegistration, name, errors.New("converting target count")) + } + + // Extracting target data from nested object string attributes is complicated, so + // lazily describe with only the target group ARN input and check the resulting + // output count instead. + out, err := conn.DescribeTargetHealthWithContext(ctx, &elbv2.DescribeTargetHealthInput{ + TargetGroupArn: aws.String(targetGroupArn), + }) + if err != nil { + return create.Error(names.ELBV2, create.ErrActionCheckingExistence, tfelbv2.ResNameTargetRegistration, rs.Primary.ID, err) + } + if out.TargetHealthDescriptions == nil { + return create.Error(names.ELBV2, create.ErrActionCheckingExistence, tfelbv2.ResNameTargetRegistration, rs.Primary.ID, errors.New("empty response")) + } + if got := len(out.TargetHealthDescriptions); got != want { + return create.Error(names.ELBV2, create.ErrActionCheckingExistence, tfelbv2.ResNameTargetRegistration, rs.Primary.ID, fmt.Errorf("unexpected target count. got %d, want %d", got, want)) + } + + return nil + } +} + +func testAccTargetRegistrationConfigBase(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigLatestAmazonLinuxHVMEBSAMI(), + acctest.AvailableEC2InstanceTypeForRegion("t3.micro", "t2.micro", "t1.micro", "m1.small"), + acctest.ConfigVPCWithSubnets(rName, 1), + ` +resource "aws_instance" "test" { + ami = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = data.aws_ec2_instance_type_offering.available.instance_type + subnet_id = aws_subnet.test[0].id +} +`) +} + +func testAccTargetRegistrationConfig_basic(rName string) string { + return acctest.ConfigCompose( + testAccTargetRegistrationConfigBase(rName), + fmt.Sprintf(` +resource "aws_lb_target_group" "test" { + name = %[1]q + port = 80 + protocol = "HTTP" + vpc_id = aws_vpc.test.id +} + +resource "aws_lb_target_registration" "test" { + target_group_arn = aws_lb_target_group.test.arn + + target { + target_id = aws_instance.test.id + } +} +`, rName)) +} + +func testAccTargetRegistrationConfig_update(rName string) string { + return acctest.ConfigCompose( + testAccTargetRegistrationConfigBase(rName), + fmt.Sprintf(` +resource "aws_instance" "test2" { + ami = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = data.aws_ec2_instance_type_offering.available.instance_type + subnet_id = aws_subnet.test[0].id +} + +resource "aws_lb_target_group" "test" { + name = %[1]q + port = 80 + protocol = "HTTP" + vpc_id = aws_vpc.test.id +} + +resource "aws_lb_target_registration" "test" { + target_group_arn = aws_lb_target_group.test.arn + + target { + target_id = aws_instance.test.id + } + target { + target_id = aws_instance.test2.id + port = 80 + } + target { + target_id = aws_instance.test2.id + port = 8080 + } +} +`, rName)) +} + +func testAccTargetRegistrationConfig_Type_ipAddress(rName string) string { + return acctest.ConfigCompose( + testAccTargetRegistrationConfigBase(rName), + fmt.Sprintf(` +resource "aws_lb_target_group" "test" { + name = %[1]q + port = 80 + protocol = "HTTP" + target_type = "ip" + vpc_id = aws_vpc.test.id +} + +resource "aws_lb_target_registration" "test" { + target_group_arn = aws_lb_target_group.test.arn + + target { + availability_zone = aws_instance.test.availability_zone + target_id = aws_instance.test.private_ip + } +} +`, rName)) +} + +func testAccTargetRegistrationConfig_Type_lambdaFunction(rName string) string { + return acctest.ConfigCompose( + testAccTargetRegistrationConfigBase(rName), + fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + assume_role_policy = < Date: Wed, 5 Jul 2023 14:48:16 -0400 Subject: [PATCH 2/2] r/aws_lb_target_group_attachment: deprecate --- internal/service/elbv2/target_group_attachment.go | 2 ++ website/docs/r/lb_target_group_attachment.html.markdown | 2 ++ 2 files changed, 4 insertions(+) diff --git a/internal/service/elbv2/target_group_attachment.go b/internal/service/elbv2/target_group_attachment.go index 70433ac3a2b..6603c8800dc 100644 --- a/internal/service/elbv2/target_group_attachment.go +++ b/internal/service/elbv2/target_group_attachment.go @@ -29,6 +29,8 @@ func ResourceTargetGroupAttachment() *schema.Resource { ReadWithoutTimeout: resourceAttachmentRead, DeleteWithoutTimeout: resourceAttachmentDelete, + DeprecationMessage: "Use the aws_lb_target_registration resource instead.", + Schema: map[string]*schema.Schema{ "target_group_arn": { Type: schema.TypeString, diff --git a/website/docs/r/lb_target_group_attachment.html.markdown b/website/docs/r/lb_target_group_attachment.html.markdown index 494b4a2c77a..be3f5bed137 100644 --- a/website/docs/r/lb_target_group_attachment.html.markdown +++ b/website/docs/r/lb_target_group_attachment.html.markdown @@ -11,6 +11,8 @@ description: |- Provides the ability to register instances and containers with an Application Load Balancer (ALB) or Network Load Balancer (NLB) target group. For attaching resources with Elastic Load Balancer (ELB), see the [`aws_elb_attachment` resource](/docs/providers/aws/r/elb_attachment.html). +~> **Note:** This resource is deprecated. Use the [`aws_lb_target_registration`](lb_target_registration.html.markdown) resource instead. + ~> **Note:** `aws_alb_target_group_attachment` is known as `aws_lb_target_group_attachment`. The functionality is identical. ## Example Usage