diff --git a/.changelog/46939.txt b/.changelog/46939.txt new file mode 100644 index 000000000000..a84c1e560f80 --- /dev/null +++ b/.changelog/46939.txt @@ -0,0 +1,31 @@ +```release-note:new-resource +aws_networkfirewall_proxy +``` + +```release-note:new-resource +aws_networkfirewall_proxy_configuration +``` + +```release-note:new-resource +aws_networkfirewall_proxy_rule_group +``` + +```release-note:new-resource +aws_networkfirewall_proxy_rules_exclusive +``` + +```release-note:new-resource +aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive +``` + +```release-note:new-list-resource +aws_networkfirewall_proxy +``` + +```release-note:new-list-resource +aws_networkfirewall_proxy_configuration +``` + +```release-note:new-list-resource +aws_networkfirewall_proxy_rule_group +``` diff --git a/internal/service/networkfirewall/exports_test.go b/internal/service/networkfirewall/exports_test.go index d5e1e86bfa05..35c1828cb275 100644 --- a/internal/service/networkfirewall/exports_test.go +++ b/internal/service/networkfirewall/exports_test.go @@ -5,18 +5,26 @@ package networkfirewall // Exports for use in tests only. var ( - ResourceFirewall = resourceFirewall - ResourceFirewallPolicy = resourceFirewallPolicy - ResourceFirewallTransitGatewayAttachmentAccepter = newFirewallTransitGatewayAttachmentAccepterResource - ResourceLoggingConfiguration = resourceLoggingConfiguration - ResourceResourcePolicy = resourceResourcePolicy - ResourceRuleGroup = resourceRuleGroup - ResourceTLSInspectionConfiguration = newTLSInspectionConfigurationResource - ResourceVPCEndpointAssociation = newVPCEndpointAssociationResource + ResourceFirewall = resourceFirewall + ResourceFirewallPolicy = resourceFirewallPolicy + ResourceFirewallTransitGatewayAttachmentAccepter = newFirewallTransitGatewayAttachmentAccepterResource + ResourceLoggingConfiguration = resourceLoggingConfiguration + ResourceProxy = newResourceProxy + ResourceProxyConfiguration = newResourceProxyConfiguration + ResourceProxyConfigurationRuleGroupAttachmentsExclusive = newResourceProxyConfigurationRuleGroupAttachmentsExclusive + ResourceProxyRuleGroup = newResourceProxyRuleGroup + ResourceProxyRulesExclusive = newResourceProxyRulesExclusive + ResourceResourcePolicy = resourceResourcePolicy + ResourceRuleGroup = resourceRuleGroup + ResourceTLSInspectionConfiguration = newTLSInspectionConfigurationResource + ResourceVPCEndpointAssociation = newVPCEndpointAssociationResource FindFirewallByARN = findFirewallByARN FindFirewallPolicyByARN = findFirewallPolicyByARN FindLoggingConfigurationByARN = findLoggingConfigurationByARN + FindProxyByARN = findProxyByARN + FindProxyConfigurationByARN = findProxyConfigurationByARN + FindProxyRuleGroupByARN = findProxyRuleGroupByARN FindResourcePolicyByARN = findResourcePolicyByARN FindRuleGroupByARN = findRuleGroupByARN FindTLSInspectionConfigurationByARN = findTLSInspectionConfigurationByARN diff --git a/internal/service/networkfirewall/firewall_test.go b/internal/service/networkfirewall/firewall_test.go index a236eaff0d3c..b6d42b0ff53a 100644 --- a/internal/service/networkfirewall/firewall_test.go +++ b/internal/service/networkfirewall/firewall_test.go @@ -638,6 +638,14 @@ func testAccPreCheck(ctx context.Context, t *testing.T) { } } +// testAccPreCheckProxyGA skips tests that require the Network Firewall Proxy service +// to be Generally Available (GA). During the preview period, only one proxy can be +// created at a time. Set TF_AWS_NETWORKFIREWALL_PROXY_GA=1 to run these tests once +// the service is GA. +func testAccPreCheckProxyGA(t *testing.T) { + acctest.SkipIfEnvVarNotSet(t, "TF_AWS_NETWORKFIREWALL_PROXY_GA") +} + func testAccFirewallConfig_baseVPC(rName string) string { return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 1), fmt.Sprintf(` resource "aws_networkfirewall_firewall_policy" "test" { diff --git a/internal/service/networkfirewall/proxy.go b/internal/service/networkfirewall/proxy.go new file mode 100644 index 000000000000..6edaaac78d2b --- /dev/null +++ b/internal/service/networkfirewall/proxy.go @@ -0,0 +1,538 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/networkfirewall" + awstypes "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + sdkretry "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_networkfirewall_proxy", name="Proxy") +// @Tags(identifierAttribute="arn") +// @ArnIdentity(identityDuplicateAttributes="id") +// @Testing(hasNoPreExistingResource=true) +// @Testing(preIdentityVersion="v5.100.0") +func newResourceProxy(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceProxy{} + + r.SetDefaultCreateTimeout(60 * time.Minute) + r.SetDefaultUpdateTimeout(60 * time.Minute) + r.SetDefaultDeleteTimeout(60 * time.Minute) + + return r, nil +} + +type resourceProxy struct { + framework.ResourceWithModel[resourceProxyModel] + framework.WithTimeouts + framework.WithImportByIdentity +} + +func (r *resourceProxy) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: framework.ARNAttributeComputedOnly(), + names.AttrCreateTime: schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + names.AttrID: framework.IDAttributeDeprecatedWithAlternate(path.Root(names.AttrARN)), + "nat_gateway_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "private_dns_name": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "proxy_configuration_arn": schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf( + path.MatchRoot("proxy_configuration_arn"), + path.MatchRoot("proxy_configuration_name"), + ), + }, + }, + "proxy_configuration_name": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf( + path.MatchRoot("proxy_configuration_arn"), + path.MatchRoot("proxy_configuration_name"), + ), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + "update_time": schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, + }, + "update_token": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "vpc_endpoint_service_name": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "listener_properties": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[listenerPropertiesModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(2), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrPort: schema.Int32Attribute{ + Required: true, + }, + names.AttrType: schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.ListenerPropertyType](), + Required: true, + }, + }, + }, + }, + names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + "tls_intercept_properties": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[tlsInterceptPropertiesModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.IsRequired(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "pca_arn": schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Optional: true, + }, + "tls_intercept_mode": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.TlsInterceptMode](), + Computed: true, + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + }, + }, + }, + } +} + +func (r *resourceProxy) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var plan resourceProxyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var input networkfirewall.CreateProxyInput + resp.Diagnostics.Append(flex.Expand(ctx, plan, &input, flex.WithFieldNamePrefix("Proxy"))...) + if resp.Diagnostics.HasError() { + return + } + + input.Tags = getTagsIn(ctx) + + out, err := conn.CreateProxy(ctx, &input) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("creating NetworkFirewall Proxy (%s)", plan.ProxyName.ValueString()), err.Error()) + return + } + if out == nil || out.Proxy == nil { + resp.Diagnostics.AddError(fmt.Sprintf("creating NetworkFirewall Proxy (%s)", plan.ProxyName.ValueString()), errors.New("empty output").Error()) + return + } + + arn := aws.ToString(out.Proxy.ProxyArn) + + createTimeout := r.CreateTimeout(ctx, plan.Timeouts) + describeOut, err := waitProxyCreated(ctx, conn, arn, createTimeout) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("waiting for NetworkFirewall Proxy (%s) create", arn), err.Error()) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, describeOut.Proxy, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + plan.ID = plan.ProxyArn + plan.UpdateToken = flex.StringToFramework(ctx, describeOut.UpdateToken) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceProxy) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var state resourceProxyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := findProxyByARN(ctx, conn, state.ID.ValueString()) + if retry.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("reading NetworkFirewall Proxy (%s)", state.ID.ValueString()), err.Error()) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, out.Proxy, &state)...) + if resp.Diagnostics.HasError() { + return + } + + state.UpdateToken = flex.StringToFramework(ctx, out.UpdateToken) + + setTagsOut(ctx, out.Proxy.Tags) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceProxy) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var plan, state resourceProxyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + diff, d := flex.Diff(ctx, plan, state, flex.WithIgnoredField("Tags"), flex.WithIgnoredField("TagsAll")) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + + if diff.HasChanges() { + input := networkfirewall.UpdateProxyInput{ + ProxyArn: state.ProxyArn.ValueStringPointer(), + NatGatewayId: state.NatGatewayId.ValueStringPointer(), + UpdateToken: state.UpdateToken.ValueStringPointer(), + } + + // Handle TlsInterceptProperties + resp.Diagnostics.Append(flex.Expand(ctx, plan.TlsInterceptProperties, &input.TlsInterceptProperties)...) + if resp.Diagnostics.HasError() { + return + } + + // Handle ListenerProperties changes - determine what to add/remove + var planListeners, stateListeners []listenerPropertiesModel + resp.Diagnostics.Append(plan.ListenerProperties.ElementsAs(ctx, &planListeners, false)...) + resp.Diagnostics.Append(state.ListenerProperties.ElementsAs(ctx, &stateListeners, false)...) + if resp.Diagnostics.HasError() { + return + } + + // Build maps for comparison + stateListenerMap := make(map[string]listenerPropertiesModel) + for _, l := range stateListeners { + key := fmt.Sprintf("%d-%s", l.Port.ValueInt32(), l.Type.ValueString()) + stateListenerMap[key] = l + } + + planListenerMap := make(map[string]listenerPropertiesModel) + for _, l := range planListeners { + key := fmt.Sprintf("%d-%s", l.Port.ValueInt32(), l.Type.ValueString()) + planListenerMap[key] = l + } + + // Find listeners to add (in plan but not in state) + for key, l := range planListenerMap { + if _, exists := stateListenerMap[key]; !exists { + input.ListenerPropertiesToAdd = append(input.ListenerPropertiesToAdd, awstypes.ListenerPropertyRequest{ + Port: l.Port.ValueInt32Pointer(), + Type: awstypes.ListenerPropertyType(l.Type.ValueString()), + }) + } + } + + // Find listeners to remove (in state but not in plan) + for key, l := range stateListenerMap { + if _, exists := planListenerMap[key]; !exists { + input.ListenerPropertiesToRemove = append(input.ListenerPropertiesToRemove, awstypes.ListenerPropertyRequest{ + Port: l.Port.ValueInt32Pointer(), + Type: awstypes.ListenerPropertyType(l.Type.ValueString()), + }) + } + } + + out, err := conn.UpdateProxy(ctx, &input) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("updating NetworkFirewall Proxy (%s)", state.ID.ValueString()), err.Error()) + return + } + if out == nil || out.Proxy == nil { + resp.Diagnostics.AddError(fmt.Sprintf("updating NetworkFirewall Proxy (%s)", state.ID.ValueString()), errors.New("empty output").Error()) + return + } + + // Wait for modification to complete + updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + describeOut, err := waitProxyUpdated(ctx, conn, state.ID.ValueString(), updateTimeout) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("waiting for NetworkFirewall Proxy (%s) update", state.ID.ValueString()), err.Error()) + return + } + + resp.Diagnostics.Append(flex.Flatten(ctx, describeOut.Proxy, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + plan.UpdateToken = flex.StringToFramework(ctx, describeOut.UpdateToken) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceProxy) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var state resourceProxyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + input := networkfirewall.DeleteProxyInput{ + ProxyArn: state.ProxyArn.ValueStringPointer(), + NatGatewayId: state.NatGatewayId.ValueStringPointer(), + } + + _, err := conn.DeleteProxy(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + resp.Diagnostics.AddError(fmt.Sprintf("deleting NetworkFirewall Proxy (%s)", state.ID.ValueString()), err.Error()) + return + } + + deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + _, err = waitProxyDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("waiting for NetworkFirewall Proxy (%s) delete", state.ID.ValueString()), err.Error()) + return + } +} + +func findProxyByARN(ctx context.Context, conn *networkfirewall.Client, arn string) (*networkfirewall.DescribeProxyOutput, error) { + input := networkfirewall.DescribeProxyInput{ + ProxyArn: aws.String(arn), + } + + out, err := conn.DescribeProxy(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &sdkretry.NotFoundError{ + LastError: err, + LastRequest: &input, + } + } + + return nil, err + } + + if out == nil || out.Proxy == nil { + return nil, tfresource.NewEmptyResultError() + } + + if out.Proxy.DeleteTime != nil { + return nil, &sdkretry.NotFoundError{ + Message: "resource is deleted", + } + } + + return out, nil +} + +func statusProxy(ctx context.Context, conn *networkfirewall.Client, arn string) sdkretry.StateRefreshFunc { + return func() (any, string, error) { + out, err := findProxyByARN(ctx, conn, arn) + if retry.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, string(out.Proxy.ProxyState), nil + } +} + +func statusProxyModify(ctx context.Context, conn *networkfirewall.Client, arn string) sdkretry.StateRefreshFunc { + return func() (any, string, error) { + out, err := findProxyByARN(ctx, conn, arn) + if retry.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, string(out.Proxy.ProxyModifyState), nil + } +} + +func waitProxyCreated(ctx context.Context, conn *networkfirewall.Client, arn string, timeout time.Duration) (*networkfirewall.DescribeProxyOutput, error) { + stateConf := &sdkretry.StateChangeConf{ + Pending: enum.Slice(awstypes.ProxyStateAttaching), + Target: enum.Slice(awstypes.ProxyStateAttached), + Refresh: statusProxy(ctx, conn, arn), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*networkfirewall.DescribeProxyOutput); ok { + return out, err + } + + return nil, err +} + +func waitProxyUpdated(ctx context.Context, conn *networkfirewall.Client, arn string, timeout time.Duration) (*networkfirewall.DescribeProxyOutput, error) { + stateConf := &sdkretry.StateChangeConf{ + Pending: enum.Slice(awstypes.ProxyModifyStateModifying), + Target: enum.Slice(awstypes.ProxyModifyStateCompleted), + Refresh: statusProxyModify(ctx, conn, arn), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*networkfirewall.DescribeProxyOutput); ok { + return out, err + } + + return nil, err +} + +func waitProxyDeleted(ctx context.Context, conn *networkfirewall.Client, arn string, timeout time.Duration) (*networkfirewall.DescribeProxyOutput, error) { + stateConf := &sdkretry.StateChangeConf{ + Pending: enum.Slice(awstypes.ProxyStateAttached, awstypes.ProxyStateDetaching), + Target: []string{}, + Refresh: statusProxy(ctx, conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*networkfirewall.DescribeProxyOutput); ok { + return out, err + } + + return nil, err +} + +type resourceProxyModel struct { + framework.WithRegionModel + CreateTime timetypes.RFC3339 `tfsdk:"create_time"` + ID types.String `tfsdk:"id"` + ListenerProperties fwtypes.ListNestedObjectValueOf[listenerPropertiesModel] `tfsdk:"listener_properties"` + NatGatewayId types.String `tfsdk:"nat_gateway_id"` + PrivateDNSName types.String `tfsdk:"private_dns_name"` + ProxyArn types.String `tfsdk:"arn"` + ProxyConfigurationArn fwtypes.ARN `tfsdk:"proxy_configuration_arn"` + ProxyConfigurationName types.String `tfsdk:"proxy_configuration_name"` + ProxyName types.String `tfsdk:"name"` + Tags tftags.Map `tfsdk:"tags"` + TagsAll tftags.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + TlsInterceptProperties fwtypes.ListNestedObjectValueOf[tlsInterceptPropertiesModel] `tfsdk:"tls_intercept_properties"` + UpdateTime timetypes.RFC3339 `tfsdk:"update_time"` + UpdateToken types.String `tfsdk:"update_token"` + VpcEndpointServiceName types.String `tfsdk:"vpc_endpoint_service_name"` +} + +type listenerPropertiesModel struct { + Port types.Int32 `tfsdk:"port"` + Type fwtypes.StringEnum[awstypes.ListenerPropertyType] `tfsdk:"type"` +} + +type tlsInterceptPropertiesModel struct { + PcaArn fwtypes.ARN `tfsdk:"pca_arn"` + TlsInterceptMode fwtypes.StringEnum[awstypes.TlsInterceptMode] `tfsdk:"tls_intercept_mode"` +} diff --git a/internal/service/networkfirewall/proxy_configuration.go b/internal/service/networkfirewall/proxy_configuration.go new file mode 100644 index 000000000000..98d463caa5bc --- /dev/null +++ b/internal/service/networkfirewall/proxy_configuration.go @@ -0,0 +1,284 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall + +import ( + "context" + "errors" + + "github.com/YakDriver/smarterr" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/networkfirewall" + awstypes "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + "github.com/hashicorp/terraform-provider-aws/internal/smerr" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_networkfirewall_proxy_configuration", name="Proxy Configuration") +// @Tags(identifierAttribute="arn") +// @ArnIdentity(identityDuplicateAttributes="id") +// @ArnFormat("proxy-configuration/{name}") +// @Testing(hasNoPreExistingResource=true) +// @Testing(preIdentityVersion="v5.100.0") +func newResourceProxyConfiguration(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceProxyConfiguration{} + + return r, nil +} + +type resourceProxyConfiguration struct { + framework.ResourceWithModel[resourceProxyConfigurationModel] + framework.WithImportByIdentity +} + +func (r *resourceProxyConfiguration) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: framework.ARNAttributeComputedOnly(), + names.AttrDescription: schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrID: framework.IDAttributeDeprecatedWithAlternate(path.Root(names.AttrARN)), + names.AttrName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + "update_token": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "default_rule_phase_actions": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[defaultRulePhaseActionsModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.IsRequired(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "post_response": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.ProxyRulePhaseAction](), + Required: true, + }, + "pre_dns": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.ProxyRulePhaseAction](), + Required: true, + }, + "pre_request": schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.ProxyRulePhaseAction](), + Required: true, + }, + }, + }, + }, + }, + } +} + +func (r *resourceProxyConfiguration) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var data resourceProxyConfigurationModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &data)) + if resp.Diagnostics.HasError() { + return + } + + var input networkfirewall.CreateProxyConfigurationInput + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Expand(ctx, data, &input, flex.WithFieldNamePrefix("ProxyConfiguration"))) + if resp.Diagnostics.HasError() { + return + } + + input.Tags = getTagsIn(ctx) + + out, err := conn.CreateProxyConfiguration(ctx, &input) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, data.ProxyConfigurationName.String()) + return + } + if out == nil || out.ProxyConfiguration == nil { + smerr.AddError(ctx, &resp.Diagnostics, errors.New("empty output"), smerr.ID, data.ProxyConfigurationName.String()) + return + } + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Flatten(ctx, out.ProxyConfiguration, &data)) + if resp.Diagnostics.HasError() { + return + } + + data.ID = data.ProxyConfigurationArn + data.UpdateToken = flex.StringToFramework(ctx, out.UpdateToken) + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, data)) +} + +func (r *resourceProxyConfiguration) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var state resourceProxyConfigurationModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + out, err := findProxyConfigurationByARN(ctx, conn, state.ID.ValueString()) + if retry.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Flatten(ctx, out.ProxyConfiguration, &state)) + if resp.Diagnostics.HasError() { + return + } + + state.UpdateToken = flex.StringToFramework(ctx, out.UpdateToken) + + setTagsOut(ctx, out.ProxyConfiguration.Tags) + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, &state)) +} + +func (r *resourceProxyConfiguration) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var plan, state resourceProxyConfigurationModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + diff, d := flex.Diff(ctx, plan, state, flex.WithIgnoredField("Tags"), flex.WithIgnoredField("TagsAll")) + smerr.AddEnrich(ctx, &resp.Diagnostics, d) + if resp.Diagnostics.HasError() { + return + } + + if diff.HasChanges() { + var input networkfirewall.UpdateProxyConfigurationInput + input.UpdateToken = state.UpdateToken.ValueStringPointer() + + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Expand(ctx, plan.DefaultRulePhaseActions, &input.DefaultRulePhaseActions)) + if resp.Diagnostics.HasError() { + return + } + + out, err := conn.UpdateProxyConfiguration(ctx, &input) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + return + } + if out == nil || out.ProxyConfiguration == nil { + smerr.AddError(ctx, &resp.Diagnostics, errors.New("empty output"), smerr.ID, state.ID.String()) + return + } + + plan.UpdateToken = flex.StringToFramework(ctx, out.UpdateToken) + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, &plan)) +} + +func (r *resourceProxyConfiguration) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var state resourceProxyConfigurationModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + input := networkfirewall.DeleteProxyConfigurationInput{ + ProxyConfigurationArn: state.ProxyConfigurationArn.ValueStringPointer(), + } + + _, err := conn.DeleteProxyConfiguration(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + return + } +} + +type resourceProxyConfigurationModel struct { + framework.WithRegionModel + DefaultRulePhaseActions fwtypes.ListNestedObjectValueOf[defaultRulePhaseActionsModel] `tfsdk:"default_rule_phase_actions"` + Description types.String `tfsdk:"description"` + ID types.String `tfsdk:"id"` + ProxyConfigurationArn types.String `tfsdk:"arn"` + ProxyConfigurationName types.String `tfsdk:"name"` + Tags tftags.Map `tfsdk:"tags"` + TagsAll tftags.Map `tfsdk:"tags_all"` + UpdateToken types.String `tfsdk:"update_token"` +} + +type defaultRulePhaseActionsModel struct { + PostResponse fwtypes.StringEnum[awstypes.ProxyRulePhaseAction] `tfsdk:"post_response"` + PreDNS fwtypes.StringEnum[awstypes.ProxyRulePhaseAction] `tfsdk:"pre_dns"` + PreRequest fwtypes.StringEnum[awstypes.ProxyRulePhaseAction] `tfsdk:"pre_request"` +} + +func findProxyConfigurationByARN(ctx context.Context, conn *networkfirewall.Client, arn string) (*networkfirewall.DescribeProxyConfigurationOutput, error) { + input := networkfirewall.DescribeProxyConfigurationInput{ + ProxyConfigurationArn: aws.String(arn), + } + + out, err := conn.DescribeProxyConfiguration(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, smarterr.NewError(&retry.NotFoundError{ + LastError: err, + }) + } + + return nil, smarterr.NewError(err) + } + + if out == nil || out.ProxyConfiguration == nil { + return nil, smarterr.NewError(tfresource.NewEmptyResultError()) + } + + if out.ProxyConfiguration.DeleteTime != nil { + return nil, smarterr.NewError(&retry.NotFoundError{ + Message: "resource is deleted", + }) + } + + return out, nil +} diff --git a/internal/service/networkfirewall/proxy_configuration_identity_gen_test.go b/internal/service/networkfirewall/proxy_configuration_identity_gen_test.go new file mode 100644 index 000000000000..551e66cfd5cf --- /dev/null +++ b/internal/service/networkfirewall/proxy_configuration_identity_gen_test.go @@ -0,0 +1,353 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by internal/generate/identitytests/main.go; DO NOT EDIT. + +package networkfirewall_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfstatecheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/statecheck" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccNetworkFirewallProxyConfiguration_Identity_basic(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_configuration.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyConfigurationDestroy(ctx, t), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectRegionalARNFormat(resourceName, tfjsonpath.New(names.AttrARN), "network-firewall", "proxy-configuration/{name}"), + statecheck.CompareValuePairs(resourceName, tfjsonpath.New(names.AttrID), resourceName, tfjsonpath.New(names.AttrARN), compare.ValuesSame()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrARN)), + }, + }, + + // Step 2: Import command + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ImportStateKind: resource.ImportCommandWithID, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 3: Import block with Import ID + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + + // Step 4: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxyConfiguration_Identity_regionOverride(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_configuration.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: acctest.CheckDestroyNoop, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectRegionalARNAlternateRegionFormat(resourceName, tfjsonpath.New(names.AttrARN), "network-firewall", "proxy-configuration/{name}"), + statecheck.CompareValuePairs(resourceName, tfjsonpath.New(names.AttrID), resourceName, tfjsonpath.New(names.AttrARN), compare.ValuesSame()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrARN)), + }, + }, + + // Step 2: Import command with appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ImportStateKind: resource.ImportCommandWithID, + ImportStateIdFunc: acctest.CrossRegionImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 3: Import command without appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ImportStateKind: resource.ImportCommandWithID, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 4: Import block with Import ID and appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportStateIdFunc: acctest.CrossRegionImportStateIdFunc(resourceName), + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + + // Step 5: Import block with Import ID and no appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + + // Step 6: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxyConfiguration_Identity_ExistingResource_basic(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_configuration.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyConfigurationDestroy(ctx, t), + Steps: []resource.TestStep{ + // Step 1: Create pre-Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/basic_v5.100.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectNoIdentity(resourceName), + }, + }, + + // Step 2: v6.0 Identity set on refresh + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/basic_v6.0.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationExists(ctx, t, resourceName), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrARN)), + }, + }, + + // Step 3: Current version + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrARN)), + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxyConfiguration_Identity_ExistingResource_noRefreshNoChange(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_configuration.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyConfigurationDestroy(ctx, t), + AdditionalCLIOptions: &resource.AdditionalCLIOptions{ + Plan: resource.PlanOptions{ + NoRefresh: true, + }, + }, + Steps: []resource.TestStep{ + // Step 1: Create pre-Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/basic_v5.100.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectNoIdentity(resourceName), + }, + }, + + // Step 2: Current version + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + }, + }, + }) +} diff --git a/internal/service/networkfirewall/proxy_configuration_list.go b/internal/service/networkfirewall/proxy_configuration_list.go new file mode 100644 index 000000000000..e99e68e0b0a3 --- /dev/null +++ b/internal/service/networkfirewall/proxy_configuration_list.go @@ -0,0 +1,113 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall + +import ( + "context" + "fmt" + "iter" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/networkfirewall" + awstypes "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/retry" +) + +// Function annotations are used for list resource registration to the Provider. DO NOT EDIT. +// @FrameworkListResource("aws_networkfirewall_proxy_configuration") +func newProxyConfigurationResourceAsListResource() list.ListResourceWithConfigure { + return &proxyConfigurationListResource{} +} + +var _ list.ListResource = &proxyConfigurationListResource{} + +type proxyConfigurationListResource struct { + resourceProxyConfiguration + framework.WithList +} + +func (r *proxyConfigurationListResource) List(ctx context.Context, request list.ListRequest, stream *list.ListResultsStream) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var query listProxyConfigurationModel + if request.Config.Raw.IsKnown() && !request.Config.Raw.IsNull() { + if diags := request.Config.Get(ctx, &query); diags.HasError() { + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + } + + stream.Results = func(yield func(list.ListResult) bool) { + result := request.NewListResult(ctx) + var input networkfirewall.ListProxyConfigurationsInput + for summary, err := range listProxyConfigurations(ctx, conn, &input) { + if err != nil { + result = fwdiag.NewListResultErrorDiagnostic(err) + yield(result) + return + } + + arn := aws.ToString(summary.Arn) + out, err := findProxyConfigurationByARN(ctx, conn, arn) + if retry.NotFound(err) { + continue + } + if err != nil { + result = fwdiag.NewListResultErrorDiagnostic(err) + yield(result) + return + } + + var data resourceProxyConfigurationModel + + r.SetResult(ctx, r.Meta(), request.IncludeResource, &data, &result, func() { + if diags := fwflex.Flatten(ctx, out.ProxyConfiguration, &data); diags.HasError() { + result.Diagnostics.Append(diags...) + yield(result) + return + } + + data.UpdateToken = fwflex.StringToFramework(ctx, out.UpdateToken) + result.DisplayName = aws.ToString(summary.Name) + }) + + if result.Diagnostics.HasError() { + result = list.ListResult{Diagnostics: result.Diagnostics} + yield(result) + return + } + + if !yield(result) { + return + } + } + } +} + +type listProxyConfigurationModel struct { + framework.WithRegionModel +} + +func listProxyConfigurations(ctx context.Context, conn *networkfirewall.Client, input *networkfirewall.ListProxyConfigurationsInput) iter.Seq2[awstypes.ProxyConfigurationMetadata, error] { + return func(yield func(awstypes.ProxyConfigurationMetadata, error) bool) { + pages := networkfirewall.NewListProxyConfigurationsPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + yield(awstypes.ProxyConfigurationMetadata{}, fmt.Errorf("listing NetworkFirewall Proxy Configuration resources: %w", err)) + return + } + + for _, item := range page.ProxyConfigurations { + if !yield(item, nil) { + return + } + } + } + } +} diff --git a/internal/service/networkfirewall/proxy_configuration_list_test.go b/internal/service/networkfirewall/proxy_configuration_list_test.go new file mode 100644 index 000000000000..dfbf8c674493 --- /dev/null +++ b/internal/service/networkfirewall/proxy_configuration_list_test.go @@ -0,0 +1,178 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfquerycheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/querycheck" + tfqueryfilter "github.com/hashicorp/terraform-provider-aws/internal/acctest/queryfilter" + tfstatecheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/statecheck" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testAccNetworkFirewallProxyConfiguration_List_basic(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + + resourceName1 := "aws_networkfirewall_proxy_configuration.test[0]" + resourceName2 := "aws_networkfirewall_proxy_configuration.test[1]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.Test(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyConfigurationDestroy(ctx, t), + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/list_basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + statecheck.ExpectKnownValue(resourceName2, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/list_basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectIdentity("aws_networkfirewall_proxy_configuration.test", map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + querycheck.ExpectIdentity("aws_networkfirewall_proxy_configuration.test", map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + }, + }, + }, + }) +} + +func testAccNetworkFirewallProxyConfiguration_List_includeResource(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + + resourceName1 := "aws_networkfirewall_proxy_configuration.test[0]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + identity1 := tfstatecheck.Identity() + + acctest.Test(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyConfigurationDestroy(ctx, t), + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/list_include_resource/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(1), + }, + ConfigStateChecks: []statecheck.StateCheck{ + identity1.GetIdentity(resourceName1), + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/list_include_resource/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(1), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + tfquerycheck.ExpectIdentityFunc("aws_networkfirewall_proxy_configuration.test", identity1.Checks()), + querycheck.ExpectResourceKnownValues("aws_networkfirewall_proxy_configuration.test", tfqueryfilter.ByResourceIdentityFunc(identity1.Checks()), []querycheck.KnownValueCheck{ + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrName), knownvalue.StringExact(rName+"-0")), + }), + }, + }, + }, + }) +} + +func testAccNetworkFirewallProxyConfiguration_List_regionOverride(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + + resourceName1 := "aws_networkfirewall_proxy_configuration.test[0]" + resourceName2 := "aws_networkfirewall_proxy_configuration.test[1]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + identity1 := tfstatecheck.Identity() + identity2 := tfstatecheck.Identity() + + acctest.Test(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckMultipleRegion(t, 2); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/list_region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ConfigStateChecks: []statecheck.StateCheck{ + identity1.GetIdentity(resourceName1), + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + identity2.GetIdentity(resourceName2), + statecheck.ExpectKnownValue(resourceName2, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfiguration/list_region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + tfquerycheck.ExpectIdentityFunc("aws_networkfirewall_proxy_configuration.test", identity1.Checks()), + tfquerycheck.ExpectIdentityFunc("aws_networkfirewall_proxy_configuration.test", identity2.Checks()), + }, + }, + }, + }) +} diff --git a/internal/service/networkfirewall/proxy_configuration_rule_group_attachments_exclusive.go b/internal/service/networkfirewall/proxy_configuration_rule_group_attachments_exclusive.go new file mode 100644 index 000000000000..9aa9164de0a2 --- /dev/null +++ b/internal/service/networkfirewall/proxy_configuration_rule_group_attachments_exclusive.go @@ -0,0 +1,422 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall + +import ( + "cmp" + "context" + "errors" + "slices" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/networkfirewall" + awstypes "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + "github.com/hashicorp/terraform-provider-aws/internal/smerr" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive", name="Proxy Configuration Rule Group Attachments Exclusive") +// @ArnIdentity("proxy_configuration_arn",identityDuplicateAttributes="id") +// @Testing(hasNoPreExistingResource=true) +// @Testing(preIdentityVersion="v5.100.0") +func newResourceProxyConfigurationRuleGroupAttachmentsExclusive(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceProxyConfigurationRuleGroupAttachmentsExclusive{} + + return r, nil +} + +type resourceProxyConfigurationRuleGroupAttachmentsExclusive struct { + framework.ResourceWithModel[proxyConfigurationRuleGroupAttachmentModel] + framework.WithImportByIdentity +} + +func (r *resourceProxyConfigurationRuleGroupAttachmentsExclusive) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrID: framework.IDAttributeDeprecatedWithAlternate(path.Root(names.AttrARN)), + "proxy_configuration_arn": schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "update_token": schema.StringAttribute{ + Computed: true, + }, + }, + Blocks: map[string]schema.Block{ + "rule_group": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[RuleGroupAttachmentModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "proxy_rule_group_name": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + }, + } +} + +func (r *resourceProxyConfigurationRuleGroupAttachmentsExclusive) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var plan proxyConfigurationRuleGroupAttachmentModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) + if resp.Diagnostics.HasError() { + return + } + + proxyConfigArn := plan.ProxyConfigurationArn.ValueString() + + // First, get the current update token + out, err := findProxyConfigurationByARN(ctx, conn, proxyConfigArn) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, proxyConfigArn) + return + } + + // Build the list of rule groups to attach with InsertPosition based on order + var ruleGroups []awstypes.ProxyRuleGroupAttachment + planRuleGroups, d := plan.RuleGroups.ToSlice(ctx) + smerr.AddEnrich(ctx, &resp.Diagnostics, d) + if resp.Diagnostics.HasError() { + return + } + + for i, rg := range planRuleGroups { + ruleGroups = append(ruleGroups, awstypes.ProxyRuleGroupAttachment{ + ProxyRuleGroupName: rg.ProxyRuleGroupName.ValueStringPointer(), + InsertPosition: aws.Int32(int32(i)), + }) + } + + input := &networkfirewall.AttachRuleGroupsToProxyConfigurationInput{ + ProxyConfigurationArn: aws.String(proxyConfigArn), + RuleGroups: ruleGroups, + UpdateToken: out.UpdateToken, + } + + _, err = conn.AttachRuleGroupsToProxyConfiguration(ctx, input) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, proxyConfigArn) + return + } + + plan.setID() + + // Read back the update token from AWS + refreshedOut, err := findProxyConfigurationByARN(ctx, conn, proxyConfigArn) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, proxyConfigArn) + return + } + + // Set the update token from the refreshed state, but keep the plan's rule group order + // since we've just applied that order via the API + plan.UpdateToken = flex.StringToFramework(ctx, refreshedOut.UpdateToken) + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, plan)) +} + +func (r *resourceProxyConfigurationRuleGroupAttachmentsExclusive) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var state proxyConfigurationRuleGroupAttachmentModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + proxyConfigArn := state.ID.ValueString() + + out, err := findProxyConfigurationByARN(ctx, conn, proxyConfigArn) + if retry.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, proxyConfigArn) + return + } + + // If there are no rule groups attached, the attachment resource no longer exists + if out.ProxyConfiguration == nil || out.ProxyConfiguration.RuleGroups == nil || len(out.ProxyConfiguration.RuleGroups) == 0 { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(errors.New("no rule groups attached to proxy configuration"))) + resp.State.RemoveResource(ctx) + return + } + + setProxyConfigurationRuleGroupsState(ctx, out, &state) + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, &state)) +} + +func (r *resourceProxyConfigurationRuleGroupAttachmentsExclusive) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var plan, state proxyConfigurationRuleGroupAttachmentModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + proxyConfigArn := state.ID.ValueString() + + // Get current update token from AWS (required for attach/detach operations) + out, err := findProxyConfigurationByARN(ctx, conn, proxyConfigArn) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, proxyConfigArn) + return + } + + updateToken := out.UpdateToken + + // Get the current rule groups from Terraform state + stateRuleGroups, d := state.RuleGroups.ToSlice(ctx) + smerr.AddEnrich(ctx, &resp.Diagnostics, d) + if resp.Diagnostics.HasError() { + return + } + + // Get the planned rule groups + planRuleGroups, d := plan.RuleGroups.ToSlice(ctx) + smerr.AddEnrich(ctx, &resp.Diagnostics, d) + if resp.Diagnostics.HasError() { + return + } + + // Build maps for comparison + stateRuleGroupNames := make(map[string]bool) + for _, rg := range stateRuleGroups { + stateRuleGroupNames[rg.ProxyRuleGroupName.ValueString()] = true + } + + planRuleGroupNames := make(map[string]bool) + for _, rg := range planRuleGroups { + planRuleGroupNames[rg.ProxyRuleGroupName.ValueString()] = true + } + + // Detach rule groups that are in state but not in plan + var ruleGroupsToDetach []string + for name := range stateRuleGroupNames { + if !planRuleGroupNames[name] { + ruleGroupsToDetach = append(ruleGroupsToDetach, name) + } + } + + if len(ruleGroupsToDetach) > 0 { + detachInput := &networkfirewall.DetachRuleGroupsFromProxyConfigurationInput{ + ProxyConfigurationArn: aws.String(proxyConfigArn), + RuleGroupNames: ruleGroupsToDetach, + UpdateToken: updateToken, + } + + detachOut, err := conn.DetachRuleGroupsFromProxyConfiguration(ctx, detachInput) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, proxyConfigArn) + return + } + updateToken = detachOut.UpdateToken + } + + // Attach rule groups that are in plan but not in state + var ruleGroupsToAttach []awstypes.ProxyRuleGroupAttachment + for i, rg := range planRuleGroups { + if !stateRuleGroupNames[rg.ProxyRuleGroupName.ValueString()] { + ruleGroupsToAttach = append(ruleGroupsToAttach, awstypes.ProxyRuleGroupAttachment{ + ProxyRuleGroupName: rg.ProxyRuleGroupName.ValueStringPointer(), + InsertPosition: aws.Int32(int32(i)), + }) + } + } + + if len(ruleGroupsToAttach) > 0 { + attachInput := &networkfirewall.AttachRuleGroupsToProxyConfigurationInput{ + ProxyConfigurationArn: aws.String(proxyConfigArn), + RuleGroups: ruleGroupsToAttach, + UpdateToken: updateToken, + } + + attachOut, err := conn.AttachRuleGroupsToProxyConfiguration(ctx, attachInput) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, proxyConfigArn) + return + } + updateToken = attachOut.UpdateToken + } + + // Check if reordering is needed (same groups but different order) + needsReorder := false + if len(stateRuleGroups) == len(planRuleGroups) && len(ruleGroupsToAttach) == 0 && len(ruleGroupsToDetach) == 0 { + // Same groups exist, check if order has changed + for i := range planRuleGroups { + if planRuleGroups[i].ProxyRuleGroupName.ValueString() != stateRuleGroups[i].ProxyRuleGroupName.ValueString() { + needsReorder = true + break + } + } + } + + if needsReorder { + var ruleGroupPriorities []awstypes.ProxyRuleGroupPriority + for i, rg := range planRuleGroups { + ruleGroupPriorities = append(ruleGroupPriorities, awstypes.ProxyRuleGroupPriority{ + ProxyRuleGroupName: rg.ProxyRuleGroupName.ValueStringPointer(), + NewPosition: aws.Int32(int32(i)), + }) + } + + priorityInput := &networkfirewall.UpdateProxyRuleGroupPrioritiesInput{ + ProxyConfigurationArn: aws.String(proxyConfigArn), + RuleGroups: ruleGroupPriorities, + UpdateToken: updateToken, + } + + _, err = conn.UpdateProxyRuleGroupPriorities(ctx, priorityInput) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, proxyConfigArn) + return + } + } + + // Read back the update token from AWS + refreshedOut, err := findProxyConfigurationByARN(ctx, conn, proxyConfigArn) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, proxyConfigArn) + return + } + + // Set the update token from the refreshed state, but keep the plan's rule group order + // since we've just applied that order via the API + plan.UpdateToken = flex.StringToFramework(ctx, refreshedOut.UpdateToken) + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, &plan)) +} + +func (r *resourceProxyConfigurationRuleGroupAttachmentsExclusive) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var state proxyConfigurationRuleGroupAttachmentModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + proxyConfigArn := state.ID.ValueString() + + // Get current state to retrieve update token + out, err := findProxyConfigurationByARN(ctx, conn, proxyConfigArn) + if retry.NotFound(err) { + return + } + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, proxyConfigArn) + return + } + + // Get all rule groups to detach + stateRuleGroups, d := state.RuleGroups.ToSlice(ctx) + smerr.AddEnrich(ctx, &resp.Diagnostics, d) + if resp.Diagnostics.HasError() { + return + } + + if len(stateRuleGroups) == 0 { + return + } + + var ruleGroupNames []string + for _, rg := range stateRuleGroups { + ruleGroupNames = append(ruleGroupNames, rg.ProxyRuleGroupName.ValueString()) + } + + input := &networkfirewall.DetachRuleGroupsFromProxyConfigurationInput{ + ProxyConfigurationArn: aws.String(proxyConfigArn), + RuleGroupNames: ruleGroupNames, + UpdateToken: out.UpdateToken, + } + + _, err = conn.DetachRuleGroupsFromProxyConfiguration(ctx, input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + // Ignore error if rule groups are already detached (specific InvalidRequestException message) + var invalidRequestErr *awstypes.InvalidRequestException + if errors.As(err, &invalidRequestErr) && invalidRequestErr.Message != nil { + if strings.Contains(*invalidRequestErr.Message, "not currently attached") { + return + } + } + + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, proxyConfigArn) + return + } +} + +type proxyConfigurationRuleGroupAttachmentModel struct { + framework.WithRegionModel + ID types.String `tfsdk:"id"` + ProxyConfigurationArn fwtypes.ARN `tfsdk:"proxy_configuration_arn"` + RuleGroups fwtypes.ListNestedObjectValueOf[RuleGroupAttachmentModel] `tfsdk:"rule_group"` + UpdateToken types.String `tfsdk:"update_token"` +} + +// RuleGroupAttachmentModel is exported for use in tests +type RuleGroupAttachmentModel struct { + ProxyRuleGroupName types.String `tfsdk:"proxy_rule_group_name"` +} + +func (data *proxyConfigurationRuleGroupAttachmentModel) setID() { + data.ID = data.ProxyConfigurationArn.StringValue +} + +func setProxyConfigurationRuleGroupsState(ctx context.Context, out *networkfirewall.DescribeProxyConfigurationOutput, model *proxyConfigurationRuleGroupAttachmentModel) { + if out.ProxyConfiguration == nil || out.ProxyConfiguration.RuleGroups == nil { + model.RuleGroups = fwtypes.NewListNestedObjectValueOfValueSliceMust(ctx, []RuleGroupAttachmentModel{}) + model.UpdateToken = flex.StringToFramework(ctx, out.UpdateToken) + return + } + + // Sort by Priority to maintain order (lower priority number = higher priority = first in list) + sortedRuleGroups := make([]awstypes.ProxyConfigRuleGroup, len(out.ProxyConfiguration.RuleGroups)) + copy(sortedRuleGroups, out.ProxyConfiguration.RuleGroups) + slices.SortStableFunc(sortedRuleGroups, func(a, b awstypes.ProxyConfigRuleGroup) int { + return cmp.Compare(aws.ToInt32(a.Priority), aws.ToInt32(b.Priority)) + }) + + var ruleGroups []RuleGroupAttachmentModel + for _, rg := range sortedRuleGroups { + ruleGroups = append(ruleGroups, RuleGroupAttachmentModel{ + ProxyRuleGroupName: flex.StringToFramework(ctx, rg.ProxyRuleGroupName), + }) + } + + model.RuleGroups = fwtypes.NewListNestedObjectValueOfValueSliceMust(ctx, ruleGroups) + model.UpdateToken = flex.StringToFramework(ctx, out.UpdateToken) +} diff --git a/internal/service/networkfirewall/proxy_configuration_rule_group_attachments_exclusive_identity_gen_test.go b/internal/service/networkfirewall/proxy_configuration_rule_group_attachments_exclusive_identity_gen_test.go new file mode 100644 index 000000000000..4662e66c35ba --- /dev/null +++ b/internal/service/networkfirewall/proxy_configuration_rule_group_attachments_exclusive_identity_gen_test.go @@ -0,0 +1,351 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by internal/generate/identitytests/main.go; DO NOT EDIT. + +package networkfirewall_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfstatecheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/statecheck" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_Identity_basic(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveDestroy(ctx, t), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs(resourceName, tfjsonpath.New(names.AttrID), resourceName, tfjsonpath.New("proxy_configuration_arn"), compare.ValuesSame()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + "proxy_configuration_arn": knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New("proxy_configuration_arn")), + }, + }, + + // Step 2: Import command + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ImportStateKind: resource.ImportCommandWithID, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 3: Import block with Import ID + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("proxy_configuration_arn"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + + // Step 4: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("proxy_configuration_arn"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_Identity_regionOverride(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: acctest.CheckDestroyNoop, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs(resourceName, tfjsonpath.New(names.AttrID), resourceName, tfjsonpath.New("proxy_configuration_arn"), compare.ValuesSame()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + "proxy_configuration_arn": knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New("proxy_configuration_arn")), + }, + }, + + // Step 2: Import command with appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ImportStateKind: resource.ImportCommandWithID, + ImportStateIdFunc: acctest.CrossRegionImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 3: Import command without appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ImportStateKind: resource.ImportCommandWithID, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 4: Import block with Import ID and appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportStateIdFunc: acctest.CrossRegionImportStateIdFunc(resourceName), + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("proxy_configuration_arn"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + + // Step 5: Import block with Import ID and no appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("proxy_configuration_arn"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + + // Step 6: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("proxy_configuration_arn"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_Identity_ExistingResource_basic(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveDestroy(ctx, t), + Steps: []resource.TestStep{ + // Step 1: Create pre-Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/basic_v5.100.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectNoIdentity(resourceName), + }, + }, + + // Step 2: v6.0 Identity set on refresh + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/basic_v6.0.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveExists(ctx, t, resourceName), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + "proxy_configuration_arn": knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New("proxy_configuration_arn")), + }, + }, + + // Step 3: Current version + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + "proxy_configuration_arn": knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New("proxy_configuration_arn")), + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_Identity_ExistingResource_noRefreshNoChange(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveDestroy(ctx, t), + AdditionalCLIOptions: &resource.AdditionalCLIOptions{ + Plan: resource.PlanOptions{ + NoRefresh: true, + }, + }, + Steps: []resource.TestStep{ + // Step 1: Create pre-Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/basic_v5.100.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectNoIdentity(resourceName), + }, + }, + + // Step 2: Current version + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ConfigDirectory: config.StaticDirectory("testdata/ProxyConfigurationRuleGroupAttachmentsExclusive/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + }, + }, + }) +} diff --git a/internal/service/networkfirewall/proxy_configuration_rule_group_attachments_exclusive_test.go b/internal/service/networkfirewall/proxy_configuration_rule_group_attachments_exclusive_test.go new file mode 100644 index 000000000000..98bbf2f71258 --- /dev/null +++ b/internal/service/networkfirewall/proxy_configuration_rule_group_attachments_exclusive_test.go @@ -0,0 +1,387 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/networkfirewall" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + tfnetworkfirewall "github.com/hashicorp/terraform-provider-aws/internal/service/networkfirewall" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_basic(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyConfigurationOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive.test" + proxyConfigResourceName := "aws_networkfirewall_proxy_configuration.test" + ruleGroup1ResourceName := "aws_networkfirewall_proxy_rule_group.test1" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveExists(ctx, t, resourceName, &v), + resource.TestCheckResourceAttrPair(resourceName, names.AttrID, proxyConfigResourceName, names.AttrARN), + resource.TestCheckResourceAttrPair(resourceName, "proxy_configuration_arn", proxyConfigResourceName, names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "rule_group.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "rule_group.0.proxy_rule_group_name", ruleGroup1ResourceName, names.AttrName), + resource.TestCheckResourceAttrSet(resourceName, "update_token"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"update_token"}, + }, + }, + }) +} + +func testAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_disappears(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyConfigurationOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveExists(ctx, t, resourceName, &v), + acctest.CheckFrameworkResourceDisappearsWithStateFunc(ctx, t, tfnetworkfirewall.ResourceProxyConfigurationRuleGroupAttachmentsExclusive, resourceName, proxyConfigurationRuleGroupAttachmentsExclusiveDisappearsStateFunc), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_updateAdd(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v1, v2 networkfirewall.DescribeProxyConfigurationOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive.test" + ruleGroup3ResourceName := "aws_networkfirewall_proxy_rule_group.test3" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_twoRuleGroups(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveExists(ctx, t, resourceName, &v1), + resource.TestCheckResourceAttr(resourceName, "rule_group.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"update_token"}, + }, + { + Config: testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_threeRuleGroups(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveExists(ctx, t, resourceName, &v2), + resource.TestCheckResourceAttr(resourceName, "rule_group.#", "3"), + resource.TestCheckResourceAttrPair(resourceName, "rule_group.2.proxy_rule_group_name", ruleGroup3ResourceName, names.AttrName), + ), + }, + }, + }) +} + +func testAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_updateRemove(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v1, v2 networkfirewall.DescribeProxyConfigurationOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_twoRuleGroups(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveExists(ctx, t, resourceName, &v1), + resource.TestCheckResourceAttr(resourceName, "rule_group.#", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"update_token"}, + }, + { + Config: testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveExists(ctx, t, resourceName, &v2), + resource.TestCheckResourceAttr(resourceName, "rule_group.#", "1"), + ), + }, + }, + }) +} + +func testAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_updateReorder(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v1, v2 networkfirewall.DescribeProxyConfigurationOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive.test" + ruleGroup1ResourceName := "aws_networkfirewall_proxy_rule_group.test1" + ruleGroup2ResourceName := "aws_networkfirewall_proxy_rule_group.test2" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_twoRuleGroups(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveExists(ctx, t, resourceName, &v1), + resource.TestCheckResourceAttr(resourceName, "rule_group.#", "2"), + resource.TestCheckResourceAttrPair(resourceName, "rule_group.0.proxy_rule_group_name", ruleGroup1ResourceName, names.AttrName), + resource.TestCheckResourceAttrPair(resourceName, "rule_group.1.proxy_rule_group_name", ruleGroup2ResourceName, names.AttrName), + ), + }, + { + Config: testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_twoRuleGroupsReversed(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveExists(ctx, t, resourceName, &v2), + resource.TestCheckResourceAttr(resourceName, "rule_group.#", "2"), + resource.TestCheckResourceAttrPair(resourceName, "rule_group.0.proxy_rule_group_name", ruleGroup2ResourceName, names.AttrName), + resource.TestCheckResourceAttrPair(resourceName, "rule_group.1.proxy_rule_group_name", ruleGroup1ResourceName, names.AttrName), + ), + }, + }, + }) +} + +func testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveDestroy(ctx context.Context, t *testing.T) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.ProviderMeta(ctx, t).NetworkFirewallClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive" { + continue + } + + out, err := tfnetworkfirewall.FindProxyConfigurationByARN(ctx, conn, rs.Primary.ID) + + if retry.NotFound(err) { + continue + } + + if err != nil { + return err + } + + // Check if there are any rule groups attached + if out != nil && out.ProxyConfiguration != nil && out.ProxyConfiguration.RuleGroups != nil { + if len(out.ProxyConfiguration.RuleGroups) > 0 { + return fmt.Errorf("NetworkFirewall Proxy Configuration Rule Group Attachment still exists: %s has %d rule groups", rs.Primary.ID, len(out.ProxyConfiguration.RuleGroups)) + } + } + } + + return nil + } +} + +func testAccCheckProxyConfigurationRuleGroupAttachmentsExclusiveExists(ctx context.Context, t *testing.T, n string, v ...*networkfirewall.DescribeProxyConfigurationOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.ProviderMeta(ctx, t).NetworkFirewallClient(ctx) + + output, err := tfnetworkfirewall.FindProxyConfigurationByARN(ctx, conn, rs.Primary.Attributes["proxy_configuration_arn"]) + + if err != nil { + return err + } + + if len(v) > 0 { + *v[0] = *output + } + + return nil + } +} + +func proxyConfigurationRuleGroupAttachmentsExclusiveDisappearsStateFunc(ctx context.Context, state *tfsdk.State, is *terraform.InstanceState) error { + // Set the id attribute (needed for Delete to find the resource) + if v, ok := is.Attributes[names.AttrID]; ok { + if diags := state.SetAttribute(ctx, path.Root(names.AttrID), types.StringValue(v)); diags.HasError() { + return fmt.Errorf("setting id: %s", diags.Errors()[0].Detail()) + } + } + + // Set the rule_group nested block from the instance state + ruleGroupCount := 0 + if v, ok := is.Attributes["rule_group.#"]; ok { + _, _ = fmt.Sscanf(v, "%d", &ruleGroupCount) + } + + if ruleGroupCount > 0 { + var ruleGroups []tfnetworkfirewall.RuleGroupAttachmentModel + for i := 0; i < ruleGroupCount; i++ { + key := fmt.Sprintf("rule_group.%d.proxy_rule_group_name", i) + if v, ok := is.Attributes[key]; ok { + ruleGroups = append(ruleGroups, tfnetworkfirewall.RuleGroupAttachmentModel{ + ProxyRuleGroupName: types.StringValue(v), + }) + } + } + + ruleGroupsList := fwtypes.NewListNestedObjectValueOfValueSliceMust(ctx, ruleGroups) + if diags := state.SetAttribute(ctx, path.Root("rule_group"), ruleGroupsList); diags.HasError() { + return fmt.Errorf("setting rule_group: %s", diags.Errors()[0].Detail()) + } + } + + return nil +} + +func testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_base(rName string) string { + return fmt.Sprintf(` +resource "aws_networkfirewall_proxy_configuration" "test" { + name = %[1]q + + default_rule_phase_actions { + post_response = "ALLOW" + pre_dns = "ALLOW" + pre_request = "ALLOW" + } +} + +resource "aws_networkfirewall_proxy_rule_group" "test1" { + name = "%[1]s-1" +} + +resource "aws_networkfirewall_proxy_rule_group" "test2" { + name = "%[1]s-2" +} + +resource "aws_networkfirewall_proxy_rule_group" "test3" { + name = "%[1]s-3" +} +`, rName) +} + +func testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_basic(rName string) string { + return acctest.ConfigCompose( + testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_base(rName), + ` +resource "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive" "test" { + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.test.arn + + rule_group { + proxy_rule_group_name = aws_networkfirewall_proxy_rule_group.test1.name + } +} +`) +} + +func testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_twoRuleGroups(rName string) string { + return acctest.ConfigCompose( + testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_base(rName), + ` +resource "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive" "test" { + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.test.arn + + rule_group { + proxy_rule_group_name = aws_networkfirewall_proxy_rule_group.test1.name + } + + rule_group { + proxy_rule_group_name = aws_networkfirewall_proxy_rule_group.test2.name + } +} +`) +} + +func testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_twoRuleGroupsReversed(rName string) string { + return acctest.ConfigCompose( + testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_base(rName), + ` +resource "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive" "test" { + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.test.arn + + rule_group { + proxy_rule_group_name = aws_networkfirewall_proxy_rule_group.test2.name + } + + rule_group { + proxy_rule_group_name = aws_networkfirewall_proxy_rule_group.test1.name + } +} +`) +} + +func testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_threeRuleGroups(rName string) string { + return acctest.ConfigCompose( + testAccProxyConfigurationRuleGroupAttachmentsExclusiveConfig_base(rName), + ` +resource "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive" "test" { + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.test.arn + + rule_group { + proxy_rule_group_name = aws_networkfirewall_proxy_rule_group.test1.name + } + + rule_group { + proxy_rule_group_name = aws_networkfirewall_proxy_rule_group.test2.name + } + + rule_group { + proxy_rule_group_name = aws_networkfirewall_proxy_rule_group.test3.name + } +} +`) +} diff --git a/internal/service/networkfirewall/proxy_configuration_test.go b/internal/service/networkfirewall/proxy_configuration_test.go new file mode 100644 index 000000000000..1b1297f91319 --- /dev/null +++ b/internal/service/networkfirewall/proxy_configuration_test.go @@ -0,0 +1,242 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall_test + +import ( + "context" + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/service/networkfirewall" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + tfnetworkfirewall "github.com/hashicorp/terraform-provider-aws/internal/service/networkfirewall" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testAccNetworkFirewallProxyConfiguration_basic(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyConfigurationOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_configuration.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyConfigurationDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyConfigurationConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyConfigurationExists(ctx, t, resourceName, &v), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "network-firewall", regexache.MustCompile(`proxy-configuration/.+$`)), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, "default_rule_phase_actions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "default_rule_phase_actions.0.post_response", "ALLOW"), + resource.TestCheckResourceAttr(resourceName, "default_rule_phase_actions.0.pre_dns", "ALLOW"), + resource.TestCheckResourceAttr(resourceName, "default_rule_phase_actions.0.pre_request", "ALLOW"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "0"), + resource.TestCheckResourceAttrSet(resourceName, "update_token"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"update_token"}, + }, + }, + }) +} + +func testAccNetworkFirewallProxyConfiguration_disappears(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyConfigurationOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_configuration.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyConfigurationDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyConfigurationConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckProxyConfigurationExists(ctx, t, resourceName, &v), + acctest.CheckFrameworkResourceDisappears(ctx, t, tfnetworkfirewall.ResourceProxyConfiguration, resourceName), + ), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + }, + }) +} + +func testAccNetworkFirewallProxyConfiguration_tags(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyConfigurationOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_configuration.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyConfigurationDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyConfigurationConfig_tags1(rName, acctest.CtKey1, acctest.CtValue1), + Check: resource.ComposeTestCheckFunc( + testAccCheckProxyConfigurationExists(ctx, t, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey1, acctest.CtValue1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"update_token"}, + }, + { + Config: testAccProxyConfigurationConfig_tags2(rName, acctest.CtKey1, acctest.CtValue1Updated, acctest.CtKey2, acctest.CtValue2), + Check: resource.ComposeTestCheckFunc( + testAccCheckProxyConfigurationExists(ctx, t, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "2"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey1, acctest.CtValue1Updated), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey2, acctest.CtValue2), + ), + }, + { + Config: testAccProxyConfigurationConfig_tags1(rName, acctest.CtKey2, acctest.CtValue2), + Check: resource.ComposeTestCheckFunc( + testAccCheckProxyConfigurationExists(ctx, t, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey2, acctest.CtValue2), + ), + }, + }, + }) +} + +func testAccCheckProxyConfigurationDestroy(ctx context.Context, t *testing.T) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.ProviderMeta(ctx, t).NetworkFirewallClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_networkfirewall_proxy_configuration" { + continue + } + + out, err := tfnetworkfirewall.FindProxyConfigurationByARN(ctx, conn, rs.Primary.ID) + + if retry.NotFound(err) { + continue + } + + if out != nil && out.ProxyConfiguration != nil && out.ProxyConfiguration.DeleteTime != nil { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("NetworkFirewall Proxy Configuration %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckProxyConfigurationExists(ctx context.Context, t *testing.T, n string, v ...*networkfirewall.DescribeProxyConfigurationOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.ProviderMeta(ctx, t).NetworkFirewallClient(ctx) + + output, err := tfnetworkfirewall.FindProxyConfigurationByARN(ctx, conn, rs.Primary.Attributes[names.AttrARN]) + + if err != nil { + return err + } + + if len(v) > 0 { + *v[0] = *output + } + + return nil + } +} + +func testAccProxyConfigurationConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_networkfirewall_proxy_configuration" "test" { + name = %[1]q + + default_rule_phase_actions { + post_response = "ALLOW" + pre_dns = "ALLOW" + pre_request = "ALLOW" + } +} +`, rName) +} + +func testAccProxyConfigurationConfig_tags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_networkfirewall_proxy_configuration" "test" { + name = %[1]q + + default_rule_phase_actions { + post_response = "ALLOW" + pre_dns = "ALLOW" + pre_request = "ALLOW" + } + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccProxyConfigurationConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_networkfirewall_proxy_configuration" "test" { + name = %[1]q + + default_rule_phase_actions { + post_response = "ALLOW" + pre_dns = "ALLOW" + pre_request = "ALLOW" + } + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/internal/service/networkfirewall/proxy_identity_gen_test.go b/internal/service/networkfirewall/proxy_identity_gen_test.go new file mode 100644 index 000000000000..0ce2024aed2d --- /dev/null +++ b/internal/service/networkfirewall/proxy_identity_gen_test.go @@ -0,0 +1,351 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by internal/generate/identitytests/main.go; DO NOT EDIT. + +package networkfirewall_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfstatecheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/statecheck" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccNetworkFirewallProxy_Identity_basic(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyDestroy(ctx, t), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs(resourceName, tfjsonpath.New(names.AttrID), resourceName, tfjsonpath.New(names.AttrARN), compare.ValuesSame()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrARN)), + }, + }, + + // Step 2: Import command + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ImportStateKind: resource.ImportCommandWithID, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 3: Import block with Import ID + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + + // Step 4: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxy_Identity_regionOverride(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: acctest.CheckDestroyNoop, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs(resourceName, tfjsonpath.New(names.AttrID), resourceName, tfjsonpath.New(names.AttrARN), compare.ValuesSame()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrARN)), + }, + }, + + // Step 2: Import command with appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ImportStateKind: resource.ImportCommandWithID, + ImportStateIdFunc: acctest.CrossRegionImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 3: Import command without appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ImportStateKind: resource.ImportCommandWithID, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 4: Import block with Import ID and appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportStateIdFunc: acctest.CrossRegionImportStateIdFunc(resourceName), + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + + // Step 5: Import block with Import ID and no appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + + // Step 6: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxy_Identity_ExistingResource_basic(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyDestroy(ctx, t), + Steps: []resource.TestStep{ + // Step 1: Create pre-Identity + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/basic_v5.100.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectNoIdentity(resourceName), + }, + }, + + // Step 2: v6.0 Identity set on refresh + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/basic_v6.0.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyExists(ctx, t, resourceName), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrARN)), + }, + }, + + // Step 3: Current version + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ConfigDirectory: config.StaticDirectory("testdata/Proxy/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrARN)), + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxy_Identity_ExistingResource_noRefreshNoChange(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyDestroy(ctx, t), + AdditionalCLIOptions: &resource.AdditionalCLIOptions{ + Plan: resource.PlanOptions{ + NoRefresh: true, + }, + }, + Steps: []resource.TestStep{ + // Step 1: Create pre-Identity + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/basic_v5.100.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectNoIdentity(resourceName), + }, + }, + + // Step 2: Current version + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ConfigDirectory: config.StaticDirectory("testdata/Proxy/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + }, + }, + }) +} diff --git a/internal/service/networkfirewall/proxy_list.go b/internal/service/networkfirewall/proxy_list.go new file mode 100644 index 000000000000..432b55c921d1 --- /dev/null +++ b/internal/service/networkfirewall/proxy_list.go @@ -0,0 +1,113 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall + +import ( + "context" + "fmt" + "iter" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/networkfirewall" + awstypes "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/retry" +) + +// Function annotations are used for list resource registration to the Provider. DO NOT EDIT. +// @FrameworkListResource("aws_networkfirewall_proxy") +func newProxyResourceAsListResource() list.ListResourceWithConfigure { + return &proxyListResource{} +} + +var _ list.ListResource = &proxyListResource{} + +type proxyListResource struct { + resourceProxy + framework.WithList +} + +func (r *proxyListResource) List(ctx context.Context, request list.ListRequest, stream *list.ListResultsStream) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var query listProxyModel + if request.Config.Raw.IsKnown() && !request.Config.Raw.IsNull() { + if diags := request.Config.Get(ctx, &query); diags.HasError() { + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + } + + stream.Results = func(yield func(list.ListResult) bool) { + result := request.NewListResult(ctx) + var input networkfirewall.ListProxiesInput + for proxySummary, err := range listProxies(ctx, conn, &input) { + if err != nil { + result = fwdiag.NewListResultErrorDiagnostic(err) + yield(result) + return + } + + arn := aws.ToString(proxySummary.Arn) + out, err := findProxyByARN(ctx, conn, arn) + if retry.NotFound(err) { + continue + } + if err != nil { + result = fwdiag.NewListResultErrorDiagnostic(err) + yield(result) + return + } + + var data resourceProxyModel + + r.SetResult(ctx, r.Meta(), request.IncludeResource, &data, &result, func() { + if diags := fwflex.Flatten(ctx, out.Proxy, &data); diags.HasError() { + result.Diagnostics.Append(diags...) + yield(result) + return + } + + data.UpdateToken = fwflex.StringToFramework(ctx, out.UpdateToken) + result.DisplayName = aws.ToString(proxySummary.Name) + }) + + if result.Diagnostics.HasError() { + result = list.ListResult{Diagnostics: result.Diagnostics} + yield(result) + return + } + + if !yield(result) { + return + } + } + } +} + +type listProxyModel struct { + framework.WithRegionModel +} + +func listProxies(ctx context.Context, conn *networkfirewall.Client, input *networkfirewall.ListProxiesInput) iter.Seq2[awstypes.ProxyMetadata, error] { + return func(yield func(awstypes.ProxyMetadata, error) bool) { + pages := networkfirewall.NewListProxiesPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + yield(awstypes.ProxyMetadata{}, fmt.Errorf("listing NetworkFirewall Proxy resources: %w", err)) + return + } + + for _, item := range page.Proxies { + if !yield(item, nil) { + return + } + } + } + } +} diff --git a/internal/service/networkfirewall/proxy_list_test.go b/internal/service/networkfirewall/proxy_list_test.go new file mode 100644 index 000000000000..82902a731159 --- /dev/null +++ b/internal/service/networkfirewall/proxy_list_test.go @@ -0,0 +1,183 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfquerycheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/querycheck" + tfqueryfilter "github.com/hashicorp/terraform-provider-aws/internal/acctest/queryfilter" + tfstatecheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/statecheck" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testAccNetworkFirewallProxy_List_basic(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + + resourceName1 := "aws_networkfirewall_proxy.test[0]" + resourceName2 := "aws_networkfirewall_proxy.test[1]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.Test(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t); testAccPreCheckProxyGA(t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyDestroy(ctx, t), + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/list_basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + statecheck.ExpectKnownValue(resourceName2, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/Proxy/list_basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectIdentity("aws_networkfirewall_proxy.test", map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + querycheck.ExpectIdentity("aws_networkfirewall_proxy.test", map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + }, + }, + }, + }) +} + +func testAccNetworkFirewallProxy_List_includeResource(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + + resourceName1 := "aws_networkfirewall_proxy.test[0]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + identity1 := tfstatecheck.Identity() + + acctest.Test(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyDestroy(ctx, t), + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/list_include_resource/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(1), + }, + ConfigStateChecks: []statecheck.StateCheck{ + identity1.GetIdentity(resourceName1), + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/Proxy/list_include_resource/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(1), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + tfquerycheck.ExpectIdentityFunc("aws_networkfirewall_proxy.test", identity1.Checks()), + querycheck.ExpectResourceKnownValues("aws_networkfirewall_proxy.test", tfqueryfilter.ByResourceIdentityFunc(identity1.Checks()), []querycheck.KnownValueCheck{ + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrName), knownvalue.StringExact(rName+"-0")), + }), + }, + }, + }, + }) +} + +func testAccNetworkFirewallProxy_List_regionOverride(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + + resourceName1 := "aws_networkfirewall_proxy.test[0]" + resourceName2 := "aws_networkfirewall_proxy.test[1]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + identity1 := tfstatecheck.Identity() + identity2 := tfstatecheck.Identity() + + acctest.Test(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckMultipleRegion(t, 2) + testAccPreCheck(ctx, t) + testAccPreCheckProxyGA(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/Proxy/list_region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ConfigStateChecks: []statecheck.StateCheck{ + identity1.GetIdentity(resourceName1), + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + identity2.GetIdentity(resourceName2), + statecheck.ExpectKnownValue(resourceName2, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/Proxy/list_region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + tfquerycheck.ExpectIdentityFunc("aws_networkfirewall_proxy.test", identity1.Checks()), + tfquerycheck.ExpectIdentityFunc("aws_networkfirewall_proxy.test", identity2.Checks()), + }, + }, + }, + }) +} diff --git a/internal/service/networkfirewall/proxy_rule_group.go b/internal/service/networkfirewall/proxy_rule_group.go new file mode 100644 index 000000000000..458432152f30 --- /dev/null +++ b/internal/service/networkfirewall/proxy_rule_group.go @@ -0,0 +1,231 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/networkfirewall" + awstypes "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "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-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + "github.com/hashicorp/terraform-provider-aws/internal/smerr" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_networkfirewall_proxy_rule_group", name="Proxy Rule Group") +// @Tags(identifierAttribute="arn") +// @ArnIdentity(identityDuplicateAttributes="id") +// @ArnFormat("proxy-rule-group/{name}") +// @Testing(hasNoPreExistingResource=true) +// @Testing(preIdentityVersion="v5.100.0") +func newResourceProxyRuleGroup(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceProxyRuleGroup{} + + return r, nil +} + +type resourceProxyRuleGroup struct { + framework.ResourceWithModel[resourceProxyRuleGroupModel] + framework.WithImportByIdentity +} + +func (r *resourceProxyRuleGroup) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: framework.ARNAttributeComputedOnly(), + names.AttrDescription: schema.StringAttribute{ + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrID: framework.IDAttributeDeprecatedWithAlternate(path.Root(names.AttrARN)), + names.AttrName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + "update_token": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +func (r *resourceProxyRuleGroup) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var plan resourceProxyRuleGroupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + input := &networkfirewall.CreateProxyRuleGroupInput{ + ProxyRuleGroupName: plan.ProxyRuleGroupName.ValueStringPointer(), + Tags: getTagsIn(ctx), + } + + if !plan.Description.IsNull() { + input.Description = plan.Description.ValueStringPointer() + } + + out, err := conn.CreateProxyRuleGroup(ctx, input) + if err != nil { + resp.Diagnostics.AddError( + "creating Network Firewall Proxy Rule Group", + err.Error(), + ) + return + } + + if out == nil || out.ProxyRuleGroup == nil { + resp.Diagnostics.AddError( + "creating Network Firewall Proxy Rule Group", + "empty output", + ) + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Flatten(ctx, out.ProxyRuleGroup, &plan)) + if resp.Diagnostics.HasError() { + return + } + + plan.ID = plan.ProxyRuleGroupArn + plan.UpdateToken = flex.StringToFramework(ctx, out.UpdateToken) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceProxyRuleGroup) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var state resourceProxyRuleGroupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := findProxyRuleGroupByARN(ctx, conn, state.ID.ValueString()) + if retry.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "reading Network Firewall Proxy Rule Group", + err.Error(), + ) + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Flatten(ctx, out.ProxyRuleGroup, &state)) + if resp.Diagnostics.HasError() { + return + } + + state.UpdateToken = flex.StringToFramework(ctx, out.UpdateToken) + + setTagsOut(ctx, out.ProxyRuleGroup.Tags) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceProxyRuleGroup) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan resourceProxyRuleGroupModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // Tags are updated via the service package framework; only state sync is needed here. + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceProxyRuleGroup) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var state resourceProxyRuleGroupModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + input := &networkfirewall.DeleteProxyRuleGroupInput{ + ProxyRuleGroupArn: state.ID.ValueStringPointer(), + } + + _, err := conn.DeleteProxyRuleGroup(ctx, input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + resp.Diagnostics.AddError( + "deleting Network Firewall Proxy Rule Group", + err.Error(), + ) + return + } +} + +func findProxyRuleGroupByARN(ctx context.Context, conn *networkfirewall.Client, arn string) (*networkfirewall.DescribeProxyRuleGroupOutput, error) { + input := &networkfirewall.DescribeProxyRuleGroupInput{ + ProxyRuleGroupArn: aws.String(arn), + } + + output, err := conn.DescribeProxyRuleGroup(ctx, input) + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + } + } + if err != nil { + return nil, err + } + + if output == nil || output.ProxyRuleGroup == nil { + return nil, tfresource.NewEmptyResultError() + } + + if output.ProxyRuleGroup.DeleteTime != nil { + return nil, &retry.NotFoundError{ + Message: "resource is deleted", + } + } + + return output, nil +} + +type resourceProxyRuleGroupModel struct { + framework.WithRegionModel + Description types.String `tfsdk:"description"` + ID types.String `tfsdk:"id"` + ProxyRuleGroupArn types.String `tfsdk:"arn"` + ProxyRuleGroupName types.String `tfsdk:"name"` + Tags tftags.Map `tfsdk:"tags"` + TagsAll tftags.Map `tfsdk:"tags_all"` + UpdateToken types.String `tfsdk:"update_token"` +} diff --git a/internal/service/networkfirewall/proxy_rule_group_identity_gen_test.go b/internal/service/networkfirewall/proxy_rule_group_identity_gen_test.go new file mode 100644 index 000000000000..bdb446c448a3 --- /dev/null +++ b/internal/service/networkfirewall/proxy_rule_group_identity_gen_test.go @@ -0,0 +1,353 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by internal/generate/identitytests/main.go; DO NOT EDIT. + +package networkfirewall_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfstatecheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/statecheck" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccNetworkFirewallProxyRuleGroup_Identity_basic(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_rule_group.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyRuleGroupDestroy(ctx, t), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRuleGroupExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectRegionalARNFormat(resourceName, tfjsonpath.New(names.AttrARN), "network-firewall", "proxy-rule-group/{name}"), + statecheck.CompareValuePairs(resourceName, tfjsonpath.New(names.AttrID), resourceName, tfjsonpath.New(names.AttrARN), compare.ValuesSame()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrARN)), + }, + }, + + // Step 2: Import command + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ImportStateKind: resource.ImportCommandWithID, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 3: Import block with Import ID + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + + // Step 4: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxyRuleGroup_Identity_regionOverride(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_rule_group.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: acctest.CheckDestroyNoop, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectRegionalARNAlternateRegionFormat(resourceName, tfjsonpath.New(names.AttrARN), "network-firewall", "proxy-rule-group/{name}"), + statecheck.CompareValuePairs(resourceName, tfjsonpath.New(names.AttrID), resourceName, tfjsonpath.New(names.AttrARN), compare.ValuesSame()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrARN)), + }, + }, + + // Step 2: Import command with appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ImportStateKind: resource.ImportCommandWithID, + ImportStateIdFunc: acctest.CrossRegionImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 3: Import command without appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ImportStateKind: resource.ImportCommandWithID, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 4: Import block with Import ID and appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportStateIdFunc: acctest.CrossRegionImportStateIdFunc(resourceName), + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + + // Step 5: Import block with Import ID and no appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + + // Step 6: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxyRuleGroup_Identity_ExistingResource_basic(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_rule_group.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyRuleGroupDestroy(ctx, t), + Steps: []resource.TestStep{ + // Step 1: Create pre-Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/basic_v5.100.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRuleGroupExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectNoIdentity(resourceName), + }, + }, + + // Step 2: v6.0 Identity set on refresh + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/basic_v6.0.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRuleGroupExists(ctx, t, resourceName), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrARN)), + }, + }, + + // Step 3: Current version + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New(names.AttrARN)), + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxyRuleGroup_Identity_ExistingResource_noRefreshNoChange(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_rule_group.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyRuleGroupDestroy(ctx, t), + AdditionalCLIOptions: &resource.AdditionalCLIOptions{ + Plan: resource.PlanOptions{ + NoRefresh: true, + }, + }, + Steps: []resource.TestStep{ + // Step 1: Create pre-Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/basic_v5.100.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRuleGroupExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectNoIdentity(resourceName), + }, + }, + + // Step 2: Current version + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + }, + }, + }) +} diff --git a/internal/service/networkfirewall/proxy_rule_group_list.go b/internal/service/networkfirewall/proxy_rule_group_list.go new file mode 100644 index 000000000000..f9431f6386f9 --- /dev/null +++ b/internal/service/networkfirewall/proxy_rule_group_list.go @@ -0,0 +1,113 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall + +import ( + "context" + "fmt" + "iter" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/networkfirewall" + awstypes "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/retry" +) + +// Function annotations are used for list resource registration to the Provider. DO NOT EDIT. +// @FrameworkListResource("aws_networkfirewall_proxy_rule_group") +func newProxyRuleGroupResourceAsListResource() list.ListResourceWithConfigure { + return &proxyRuleGroupListResource{} +} + +var _ list.ListResource = &proxyRuleGroupListResource{} + +type proxyRuleGroupListResource struct { + resourceProxyRuleGroup + framework.WithList +} + +func (r *proxyRuleGroupListResource) List(ctx context.Context, request list.ListRequest, stream *list.ListResultsStream) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var query listProxyRuleGroupModel + if request.Config.Raw.IsKnown() && !request.Config.Raw.IsNull() { + if diags := request.Config.Get(ctx, &query); diags.HasError() { + stream.Results = list.ListResultsStreamDiagnostics(diags) + return + } + } + + stream.Results = func(yield func(list.ListResult) bool) { + result := request.NewListResult(ctx) + var input networkfirewall.ListProxyRuleGroupsInput + for summary, err := range listProxyRuleGroups(ctx, conn, &input) { + if err != nil { + result = fwdiag.NewListResultErrorDiagnostic(err) + yield(result) + return + } + + arn := aws.ToString(summary.Arn) + out, err := findProxyRuleGroupByARN(ctx, conn, arn) + if retry.NotFound(err) { + continue + } + if err != nil { + result = fwdiag.NewListResultErrorDiagnostic(err) + yield(result) + return + } + + var data resourceProxyRuleGroupModel + + r.SetResult(ctx, r.Meta(), request.IncludeResource, &data, &result, func() { + if diags := fwflex.Flatten(ctx, out.ProxyRuleGroup, &data); diags.HasError() { + result.Diagnostics.Append(diags...) + yield(result) + return + } + + data.UpdateToken = fwflex.StringToFramework(ctx, out.UpdateToken) + result.DisplayName = aws.ToString(summary.Name) + }) + + if result.Diagnostics.HasError() { + result = list.ListResult{Diagnostics: result.Diagnostics} + yield(result) + return + } + + if !yield(result) { + return + } + } + } +} + +type listProxyRuleGroupModel struct { + framework.WithRegionModel +} + +func listProxyRuleGroups(ctx context.Context, conn *networkfirewall.Client, input *networkfirewall.ListProxyRuleGroupsInput) iter.Seq2[awstypes.ProxyRuleGroupMetadata, error] { + return func(yield func(awstypes.ProxyRuleGroupMetadata, error) bool) { + pages := networkfirewall.NewListProxyRuleGroupsPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + yield(awstypes.ProxyRuleGroupMetadata{}, fmt.Errorf("listing NetworkFirewall Proxy Rule Group resources: %w", err)) + return + } + + for _, item := range page.ProxyRuleGroups { + if !yield(item, nil) { + return + } + } + } + } +} diff --git a/internal/service/networkfirewall/proxy_rule_group_list_test.go b/internal/service/networkfirewall/proxy_rule_group_list_test.go new file mode 100644 index 000000000000..ba180ae597fd --- /dev/null +++ b/internal/service/networkfirewall/proxy_rule_group_list_test.go @@ -0,0 +1,178 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/querycheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfquerycheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/querycheck" + tfqueryfilter "github.com/hashicorp/terraform-provider-aws/internal/acctest/queryfilter" + tfstatecheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/statecheck" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testAccNetworkFirewallProxyRuleGroup_List_basic(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + + resourceName1 := "aws_networkfirewall_proxy_rule_group.test[0]" + resourceName2 := "aws_networkfirewall_proxy_rule_group.test[1]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.Test(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyRuleGroupDestroy(ctx, t), + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/list_basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + statecheck.ExpectKnownValue(resourceName2, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/list_basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + querycheck.ExpectIdentity("aws_networkfirewall_proxy_rule_group.test", map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + querycheck.ExpectIdentity("aws_networkfirewall_proxy_rule_group.test", map[string]knownvalue.Check{ + names.AttrARN: knownvalue.NotNull(), + }), + }, + }, + }, + }) +} + +func testAccNetworkFirewallProxyRuleGroup_List_includeResource(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + + resourceName1 := "aws_networkfirewall_proxy_rule_group.test[0]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + identity1 := tfstatecheck.Identity() + + acctest.Test(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyRuleGroupDestroy(ctx, t), + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/list_include_resource/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(1), + }, + ConfigStateChecks: []statecheck.StateCheck{ + identity1.GetIdentity(resourceName1), + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/list_include_resource/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(1), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + tfquerycheck.ExpectIdentityFunc("aws_networkfirewall_proxy_rule_group.test", identity1.Checks()), + querycheck.ExpectResourceKnownValues("aws_networkfirewall_proxy_rule_group.test", tfqueryfilter.ByResourceIdentityFunc(identity1.Checks()), []querycheck.KnownValueCheck{ + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + tfquerycheck.KnownValueCheck(tfjsonpath.New(names.AttrName), knownvalue.StringExact(rName+"-0")), + }), + }, + }, + }, + }) +} + +func testAccNetworkFirewallProxyRuleGroup_List_regionOverride(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + + resourceName1 := "aws_networkfirewall_proxy_rule_group.test[0]" + resourceName2 := "aws_networkfirewall_proxy_rule_group.test[1]" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + identity1 := tfstatecheck.Identity() + identity2 := tfstatecheck.Identity() + + acctest.Test(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_14_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t); acctest.PreCheckMultipleRegion(t, 2); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/list_region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ConfigStateChecks: []statecheck.StateCheck{ + identity1.GetIdentity(resourceName1), + statecheck.ExpectKnownValue(resourceName1, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + identity2.GetIdentity(resourceName2), + statecheck.ExpectKnownValue(resourceName2, tfjsonpath.New(names.AttrARN), knownvalue.NotNull()), + }, + }, + + // Step 2: Query + { + Query: true, + ConfigDirectory: config.StaticDirectory("testdata/ProxyRuleGroup/list_region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "resource_count": config.IntegerVariable(2), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + QueryResultChecks: []querycheck.QueryResultCheck{ + tfquerycheck.ExpectIdentityFunc("aws_networkfirewall_proxy_rule_group.test", identity1.Checks()), + tfquerycheck.ExpectIdentityFunc("aws_networkfirewall_proxy_rule_group.test", identity2.Checks()), + }, + }, + }, + }) +} diff --git a/internal/service/networkfirewall/proxy_rule_group_test.go b/internal/service/networkfirewall/proxy_rule_group_test.go new file mode 100644 index 000000000000..1ba71b0ca5a1 --- /dev/null +++ b/internal/service/networkfirewall/proxy_rule_group_test.go @@ -0,0 +1,220 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall_test + +import ( + "context" + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/service/networkfirewall" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + tfnetworkfirewall "github.com/hashicorp/terraform-provider-aws/internal/service/networkfirewall" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testAccNetworkFirewallProxyRuleGroup_basic(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyRuleGroupOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_rule_group.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyRuleGroupDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyRuleGroupConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRuleGroupExists(ctx, t, resourceName, &v), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "network-firewall", regexache.MustCompile(`proxy-rule-group/.+$`)), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "0"), + resource.TestCheckResourceAttrSet(resourceName, "update_token"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"update_token"}, + }, + }, + }) +} + +func testAccNetworkFirewallProxyRuleGroup_disappears(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyRuleGroupOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_rule_group.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyRuleGroupDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyRuleGroupConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckProxyRuleGroupExists(ctx, t, resourceName, &v), + acctest.CheckFrameworkResourceDisappears(ctx, t, tfnetworkfirewall.ResourceProxyRuleGroup, resourceName), + ), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + }, + }) +} + +func testAccNetworkFirewallProxyRuleGroup_tags(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyRuleGroupOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_rule_group.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyRuleGroupDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyRuleGroupConfig_tags1(rName, acctest.CtKey1, acctest.CtValue1), + Check: resource.ComposeTestCheckFunc( + testAccCheckProxyRuleGroupExists(ctx, t, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey1, acctest.CtValue1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"update_token"}, + }, + { + Config: testAccProxyRuleGroupConfig_tags2(rName, acctest.CtKey1, acctest.CtValue1Updated, acctest.CtKey2, acctest.CtValue2), + Check: resource.ComposeTestCheckFunc( + testAccCheckProxyRuleGroupExists(ctx, t, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "2"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey1, acctest.CtValue1Updated), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey2, acctest.CtValue2), + ), + }, + { + Config: testAccProxyRuleGroupConfig_tags1(rName, acctest.CtKey2, acctest.CtValue2), + Check: resource.ComposeTestCheckFunc( + testAccCheckProxyRuleGroupExists(ctx, t, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "1"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsKey2, acctest.CtValue2), + ), + }, + }, + }) +} + +func testAccCheckProxyRuleGroupDestroy(ctx context.Context, t *testing.T) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.ProviderMeta(ctx, t).NetworkFirewallClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_networkfirewall_proxy_rule_group" { + continue + } + + out, err := tfnetworkfirewall.FindProxyRuleGroupByARN(ctx, conn, rs.Primary.ID) + + if retry.NotFound(err) { + continue + } + + if out != nil && out.ProxyRuleGroup != nil && out.ProxyRuleGroup.DeleteTime != nil { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("NetworkFirewall Proxy Rule Group %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckProxyRuleGroupExists(ctx context.Context, t *testing.T, n string, v ...*networkfirewall.DescribeProxyRuleGroupOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.ProviderMeta(ctx, t).NetworkFirewallClient(ctx) + + output, err := tfnetworkfirewall.FindProxyRuleGroupByARN(ctx, conn, rs.Primary.Attributes[names.AttrARN]) + + if err != nil { + return err + } + + if len(v) > 0 { + *v[0] = *output + } + + return nil + } +} + +func testAccProxyRuleGroupConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_networkfirewall_proxy_rule_group" "test" { + name = %[1]q +} +`, rName) +} + +func testAccProxyRuleGroupConfig_tags1(rName, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_networkfirewall_proxy_rule_group" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + } +} +`, rName, tagKey1, tagValue1) +} + +func testAccProxyRuleGroupConfig_tags2(rName, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return fmt.Sprintf(` +resource "aws_networkfirewall_proxy_rule_group" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } +} +`, rName, tagKey1, tagValue1, tagKey2, tagValue2) +} diff --git a/internal/service/networkfirewall/proxy_rules_exclusive.go b/internal/service/networkfirewall/proxy_rules_exclusive.go new file mode 100644 index 000000000000..5bb78df7d9cf --- /dev/null +++ b/internal/service/networkfirewall/proxy_rules_exclusive.go @@ -0,0 +1,757 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall + +import ( + "context" + "errors" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/networkfirewall" + awstypes "github.com/aws/aws-sdk-go-v2/service/networkfirewall/types" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + "github.com/hashicorp/terraform-provider-aws/internal/smerr" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_networkfirewall_proxy_rules_exclusive", name="Proxy Rules Exclusive") +// @ArnIdentity("proxy_rule_group_arn",identityDuplicateAttributes="id") +// @ArnFormat("proxy-rule-group/{name}") +// @Testing(hasNoPreExistingResource=true) +// @Testing(preIdentityVersion="v5.100.0") +func newResourceProxyRulesExclusive(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceProxyRulesExclusive{} + + return r, nil +} + +type resourceProxyRulesExclusive struct { + framework.ResourceWithModel[resourceProxyRulesExclusiveModel] + framework.WithImportByIdentity +} + +func (r *resourceProxyRulesExclusive) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrID: framework.IDAttributeDeprecatedWithAlternate(path.Root("proxy_rule_group_arn")), + "proxy_rule_group_arn": schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "post_response": proxyRuleSchemaBlock(ctx), + "pre_dns": proxyRuleSchemaBlock(ctx), + "pre_request": proxyRuleSchemaBlock(ctx), + }, + } +} + +func proxyRuleSchemaBlock(ctx context.Context) schema.Block { + return schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[proxyRuleModel](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrAction: schema.StringAttribute{ + CustomType: fwtypes.StringEnumType[awstypes.ProxyRulePhaseAction](), + Required: true, + }, + names.AttrDescription: schema.StringAttribute{ + Optional: true, + }, + "proxy_rule_name": schema.StringAttribute{ + Required: true, + }, + }, + Blocks: map[string]schema.Block{ + "conditions": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[proxyRuleConditionModel](ctx), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "condition_key": schema.StringAttribute{ + Required: true, + }, + "condition_operator": schema.StringAttribute{ + Required: true, + }, + "condition_values": schema.ListAttribute{ + CustomType: fwtypes.ListOfStringType, + ElementType: types.StringType, + Required: true, + }, + }, + }, + }, + }, + }, + } +} + +func (r *resourceProxyRulesExclusive) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var plan resourceProxyRulesExclusiveModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) + if resp.Diagnostics.HasError() { + return + } + + input := networkfirewall.CreateProxyRulesInput{ + ProxyRuleGroupArn: plan.ProxyRuleGroupArn.ValueStringPointer(), + } + + var rulesByPhase awstypes.CreateProxyRulesByRequestPhase + + rulesByPhase.PostRESPONSE = proxyExpandRulesForPhase(ctx, plan.PostRESPONSE, &resp.Diagnostics) + rulesByPhase.PreDNS = proxyExpandRulesForPhase(ctx, plan.PreDNS, &resp.Diagnostics) + rulesByPhase.PreREQUEST = proxyExpandRulesForPhase(ctx, plan.PreREQUEST, &resp.Diagnostics) + + if resp.Diagnostics.HasError() { + return + } + + input.Rules = &rulesByPhase + + out, err := conn.CreateProxyRules(ctx, &input) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ProxyRuleGroupArn.String()) + return + } + if out == nil || out.ProxyRuleGroup == nil { + smerr.AddError(ctx, &resp.Diagnostics, errors.New("empty output"), smerr.ID, plan.ProxyRuleGroupArn.String()) + return + } + + plan.setID() + + readOut, err := findProxyRulesByGroupARN(ctx, conn, plan.ProxyRuleGroupArn.ValueString()) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ProxyRuleGroupArn.String()) + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, setProxyRulesState(ctx, readOut, &plan)) + if resp.Diagnostics.HasError() { + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, plan)) +} + +func (r *resourceProxyRulesExclusive) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var state resourceProxyRulesExclusiveModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + out, err := findProxyRulesByGroupARN(ctx, conn, state.ProxyRuleGroupArn.ValueString()) + if retry.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ProxyRuleGroupArn.String()) + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, setProxyRulesState(ctx, out, &state)) + if resp.Diagnostics.HasError() { + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, &state)) +} + +func (r *resourceProxyRulesExclusive) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var plan, state resourceProxyRulesExclusiveModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + // Get current state to obtain update token and existing rules from AWS + currentRules, err := findProxyRulesByGroupARN(ctx, conn, state.ProxyRuleGroupArn.ValueString()) + if err != nil && !retry.NotFound(err) { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + return + } + + var updateToken *string + + // Extract rules from AWS for each phase + var statePostRESPONSE, statePreDNS, statePreREQUEST []proxyRuleModel + if currentRules != nil && currentRules.ProxyRuleGroup != nil && currentRules.ProxyRuleGroup.Rules != nil { + rules := currentRules.ProxyRuleGroup.Rules + statePostRESPONSE = proxyExtractRulesFromPhase(ctx, rules.PostRESPONSE, &resp.Diagnostics) + statePreDNS = proxyExtractRulesFromPhase(ctx, rules.PreDNS, &resp.Diagnostics) + statePreREQUEST = proxyExtractRulesFromPhase(ctx, rules.PreREQUEST, &resp.Diagnostics) + } + + // Extract plan rules for each phase + planPostRESPONSE := proxyExtractPlanRules(ctx, plan.PostRESPONSE, &resp.Diagnostics) + planPreDNS := proxyExtractPlanRules(ctx, plan.PreDNS, &resp.Diagnostics) + planPreREQUEST := proxyExtractPlanRules(ctx, plan.PreREQUEST, &resp.Diagnostics) + + if resp.Diagnostics.HasError() { + return + } + + // Track rules to delete, update, and create + // Using map to avoid duplicates since we track by name + rulesToDelete := make(map[string]bool) + rulesToUpdate := make(map[string]proxyRuleModel) + rulesToRecreate := make(map[string]proxyRuleRecreateInfo) + + // Process each phase + processProxyRulesPhaseChanges(ctx, statePostRESPONSE, planPostRESPONSE, "PostRESPONSE", rulesToDelete, rulesToUpdate, rulesToRecreate) + processProxyRulesPhaseChanges(ctx, statePreDNS, planPreDNS, "PreDNS", rulesToDelete, rulesToUpdate, rulesToRecreate) + processProxyRulesPhaseChanges(ctx, statePreREQUEST, planPreREQUEST, "PreREQUEST", rulesToDelete, rulesToUpdate, rulesToRecreate) + + // Remove any rules from the update list that are being recreated + // This ensures we don't try to update a rule that's being deleted + for name := range rulesToRecreate { + delete(rulesToUpdate, name) + } + + // Step 1: Delete rules (including those being recreated) + // This must happen before Step 3 to satisfy proxy_rule_name uniqueness constraint + var deleteList []string + for name := range rulesToDelete { + deleteList = append(deleteList, name) + } + + if len(deleteList) > 0 { + deleteInput := networkfirewall.DeleteProxyRulesInput{ + ProxyRuleGroupArn: plan.ProxyRuleGroupArn.ValueStringPointer(), + Rules: deleteList, + } + + _, err = conn.DeleteProxyRules(ctx, &deleteInput) + if err != nil && !errs.IsA[*awstypes.ResourceNotFoundException](err) { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ID.String()) + return + } + + // Refresh update token after deletion and verify rules were deleted + currentRules, err = findProxyRulesByGroupARN(ctx, conn, plan.ProxyRuleGroupArn.ValueString()) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ID.String()) + return + } + // Verify that the deleted rules are actually gone + if currentRules.ProxyRuleGroup != nil && currentRules.ProxyRuleGroup.Rules != nil { + existingRuleNames := proxyCollectRuleNames(currentRules.ProxyRuleGroup.Rules) + + for _, deletedName := range deleteList { + if existingRuleNames[deletedName] { + smerr.AddError(ctx, &resp.Diagnostics, errors.New("rule deletion not yet complete"), smerr.ID, plan.ID.String()) + return + } + } + } + } + + // Step 2: Update rules (action, description, and conditions) + for _, ruleModel := range rulesToUpdate { + // Refresh the update token before each update + currentRules, err = findProxyRulesByGroupARN(ctx, conn, plan.ProxyRuleGroupArn.ValueString()) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ID.String()) + return + } + updateToken = currentRules.UpdateToken + + // Build a map of current rules by name to get old conditions + currentRulesByName := proxyBuildRuleMapByName(ctx, currentRules.ProxyRuleGroup, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + updateInput := networkfirewall.UpdateProxyRuleInput{ + ProxyRuleGroupArn: plan.ProxyRuleGroupArn.ValueStringPointer(), + ProxyRuleName: ruleModel.ProxyRuleName.ValueStringPointer(), + UpdateToken: updateToken, + } + + // Update action if specified + if !ruleModel.Action.IsNull() && !ruleModel.Action.IsUnknown() { + updateInput.Action = ruleModel.Action.ValueEnum() + } + + // Update description if specified + if !ruleModel.Description.IsNull() && !ruleModel.Description.IsUnknown() { + updateInput.Description = ruleModel.Description.ValueStringPointer() + } + + // Remove old conditions + if currentRule, exists := currentRulesByName[ruleModel.ProxyRuleName.ValueString()]; exists { + if !currentRule.Conditions.IsNull() && !currentRule.Conditions.IsUnknown() { + var oldConditions []proxyRuleConditionModel + smerr.AddEnrich(ctx, &resp.Diagnostics, currentRule.Conditions.ElementsAs(ctx, &oldConditions, false)) + for _, cond := range oldConditions { + var removeCondition awstypes.ProxyRuleCondition + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Expand(ctx, cond, &removeCondition)) + updateInput.RemoveConditions = append(updateInput.RemoveConditions, removeCondition) + } + } + } + + // Add new conditions + if !ruleModel.Conditions.IsNull() && !ruleModel.Conditions.IsUnknown() { + var newConditions []proxyRuleConditionModel + smerr.AddEnrich(ctx, &resp.Diagnostics, ruleModel.Conditions.ElementsAs(ctx, &newConditions, false)) + for _, cond := range newConditions { + var addCondition awstypes.ProxyRuleCondition + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Expand(ctx, cond, &addCondition)) + updateInput.AddConditions = append(updateInput.AddConditions, addCondition) + } + } + + if resp.Diagnostics.HasError() { + return + } + + _, err = conn.UpdateProxyRule(ctx, &updateInput) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ID.String()) + return + } + } + + // Step 3: Create/recreate rules + // Safe to create rules now - all deletions completed in Step 1, ensuring proxy_rule_name uniqueness + if len(rulesToRecreate) > 0 { + var rulesByPhase awstypes.CreateProxyRulesByRequestPhase + + // Organize rules by phase + for _, ruleData := range rulesToRecreate { + var rule awstypes.CreateProxyRule + smerr.AddEnrich(ctx, &resp.Diagnostics, flex.Expand(ctx, ruleData.rule, &rule)) + if resp.Diagnostics.HasError() { + return + } + rule.InsertPosition = &ruleData.position + + switch ruleData.phase { + case "PostRESPONSE": + rulesByPhase.PostRESPONSE = append(rulesByPhase.PostRESPONSE, rule) + case "PreDNS": + rulesByPhase.PreDNS = append(rulesByPhase.PreDNS, rule) + case "PreREQUEST": + rulesByPhase.PreREQUEST = append(rulesByPhase.PreREQUEST, rule) + } + } + + createInput := networkfirewall.CreateProxyRulesInput{ + ProxyRuleGroupArn: plan.ProxyRuleGroupArn.ValueStringPointer(), + Rules: &rulesByPhase, + } + + _, err = conn.CreateProxyRules(ctx, &createInput) + + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ID.String()) + return + } + } + + // Read back to get full state + readOut, err := findProxyRulesByGroupARN(ctx, conn, plan.ProxyRuleGroupArn.ValueString()) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ProxyRuleGroupArn.String()) + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, setProxyRulesState(ctx, readOut, &plan)) + if resp.Diagnostics.HasError() { + return + } + + smerr.AddEnrich(ctx, &resp.Diagnostics, resp.State.Set(ctx, &plan)) +} + +// ruleNeedsRecreate determines if a rule needs to be deleted and recreated + +func (r *resourceProxyRulesExclusive) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().NetworkFirewallClient(ctx) + + var state resourceProxyRulesExclusiveModel + smerr.AddEnrich(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + // Get all rule names for this group + out, err := findProxyRulesByGroupARN(ctx, conn, state.ID.ValueString()) + if err != nil && !retry.NotFound(err) { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + return + } + + if out != nil && out.ProxyRuleGroup != nil && out.ProxyRuleGroup.Rules != nil { + ruleNamesMap := proxyCollectRuleNames(out.ProxyRuleGroup.Rules) + var ruleNames []string + for name := range ruleNamesMap { + ruleNames = append(ruleNames, name) + } + + if len(ruleNames) > 0 { + input := networkfirewall.DeleteProxyRulesInput{ + ProxyRuleGroupArn: state.ProxyRuleGroupArn.ValueStringPointer(), + Rules: ruleNames, + } + + _, err = conn.DeleteProxyRules(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + return + } + } + } +} + +func findProxyRulesByGroupARN(ctx context.Context, conn *networkfirewall.Client, groupARN string) (*networkfirewall.DescribeProxyRuleGroupOutput, error) { + input := networkfirewall.DescribeProxyRuleGroupInput{ + ProxyRuleGroupArn: aws.String(groupARN), + } + + out, err := conn.DescribeProxyRuleGroup(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + } + } + + return nil, err + } + + if out == nil || out.ProxyRuleGroup == nil { + return nil, &retry.NotFoundError{ + Message: "proxy rule group not found", + } + } + + return out, nil +} + +func setProxyRulesState(ctx context.Context, out *networkfirewall.DescribeProxyRuleGroupOutput, model *resourceProxyRulesExclusiveModel) diag.Diagnostics { + var diags diag.Diagnostics + + if out.ProxyRuleGroup == nil || out.ProxyRuleGroup.Rules == nil { + return diags + } + + rules := out.ProxyRuleGroup.Rules + + model.PostRESPONSE = proxyFlattenRulesForPhase(ctx, rules.PostRESPONSE, &diags) + model.PreDNS = proxyFlattenRulesForPhase(ctx, rules.PreDNS, &diags) + model.PreREQUEST = proxyFlattenRulesForPhase(ctx, rules.PreREQUEST, &diags) + + if out.ProxyRuleGroup.ProxyRuleGroupArn != nil { + model.ProxyRuleGroupArn = fwtypes.ARNValue(aws.ToString(out.ProxyRuleGroup.ProxyRuleGroupArn)) + } + + return diags +} + +// conditionsEqual compares only the conditions of two rules +func proxyRuleConditionsEqual(ctx context.Context, a, b proxyRuleModel) bool { + // Compare conditions count + if a.Conditions.IsNull() != b.Conditions.IsNull() { + return false + } + + if a.Conditions.IsNull() { + return true + } + + var aConditions, bConditions []proxyRuleConditionModel + a.Conditions.ElementsAs(ctx, &aConditions, false) + b.Conditions.ElementsAs(ctx, &bConditions, false) + + if len(aConditions) != len(bConditions) { + return false + } + + // Compare each condition + for i := range aConditions { + if aConditions[i].ConditionKey.ValueString() != bConditions[i].ConditionKey.ValueString() { + return false + } + if aConditions[i].ConditionOperator.ValueString() != bConditions[i].ConditionOperator.ValueString() { + return false + } + + var aValues, bValues []types.String + aConditions[i].ConditionValues.ElementsAs(ctx, &aValues, false) + bConditions[i].ConditionValues.ElementsAs(ctx, &bValues, false) + + if len(aValues) != len(bValues) { + return false + } + + for j := range aValues { + if aValues[j].ValueString() != bValues[j].ValueString() { + return false + } + } + } + + return true +} + +// processProxyRulesPhaseChanges compares state and plan rules for a given phase and populates +// the maps tracking which rules need to be deleted, updated, or recreated +func processProxyRulesPhaseChanges( + ctx context.Context, + stateRules, planRules []proxyRuleModel, + phaseName string, + rulesToDelete map[string]bool, + rulesToUpdate map[string]proxyRuleModel, + rulesToRecreate map[string]proxyRuleRecreateInfo, +) { + // Compare position by position + for i, planRule := range planRules { + planName := planRule.ProxyRuleName.ValueString() + + // Check if a rule exists at this position in state + if i < len(stateRules) { + stateRule := stateRules[i] + stateName := stateRule.ProxyRuleName.ValueString() + + // Check what kind of change this is + if planName == stateName { + // Same name at same position - check if attributes changed + if !proxyRuleConditionsEqual(ctx, stateRule, planRule) || + stateRule.Action.ValueEnum() != planRule.Action.ValueEnum() || + stateRule.Description.ValueString() != planRule.Description.ValueString() { + // Action, description, or conditions changed - can use UpdateProxyRule + rulesToUpdate[planName] = planRule + } + // If no changes, do nothing + } else { + // Different names at same position + // The old rule at this position needs to be deleted + rulesToDelete[stateName] = true + // The new rule needs to be created at this position + rulesToRecreate[planName] = proxyRuleRecreateInfo{ + rule: planRule, + position: int32(i), + phase: phaseName, + } + } + } else { + // Plan has more rules than state - this is a new rule + rulesToRecreate[planName] = proxyRuleRecreateInfo{ + rule: planRule, + position: int32(i), + phase: phaseName, + } + } + } + + // If state has more rules than plan, mark extras for deletion + for i := len(planRules); i < len(stateRules); i++ { + rulesToDelete[stateRules[i].ProxyRuleName.ValueString()] = true + } +} + +// proxyExpandRulesForPhase converts plan rules to AWS CreateProxyRule format with positions +func proxyExpandRulesForPhase(ctx context.Context, rulesList fwtypes.ListNestedObjectValueOf[proxyRuleModel], diags *diag.Diagnostics) []awstypes.CreateProxyRule { + if rulesList.IsNull() || rulesList.IsUnknown() { + return nil + } + + var ruleModels []proxyRuleModel + smerr.AddEnrich(ctx, diags, rulesList.ElementsAs(ctx, &ruleModels, false)) + if diags.HasError() { + return nil + } + + var rules []awstypes.CreateProxyRule + for i, ruleModel := range ruleModels { + var rule awstypes.CreateProxyRule + smerr.AddEnrich(ctx, diags, flex.Expand(ctx, ruleModel, &rule)) + if diags.HasError() { + return nil + } + insertPos := int32(i) + rule.InsertPosition = &insertPos + rules = append(rules, rule) + } + + return rules +} + +// proxyExtractRulesFromPhase converts AWS ProxyRule format to proxyRuleModel +func proxyExtractRulesFromPhase(ctx context.Context, awsRules []awstypes.ProxyRule, diags *diag.Diagnostics) []proxyRuleModel { + var ruleModels []proxyRuleModel + for _, rule := range awsRules { + var ruleModel proxyRuleModel + smerr.AddEnrich(ctx, diags, flex.Flatten(ctx, &rule, &ruleModel)) + if diags.HasError() { + return nil + } + ruleModels = append(ruleModels, ruleModel) + } + return ruleModels +} + +// proxyExtractPlanRules extracts rules from plan's ListNestedObjectValue +func proxyExtractPlanRules(ctx context.Context, rulesList fwtypes.ListNestedObjectValueOf[proxyRuleModel], diags *diag.Diagnostics) []proxyRuleModel { + if rulesList.IsNull() || rulesList.IsUnknown() { + return nil + } + + var ruleModels []proxyRuleModel + smerr.AddEnrich(ctx, diags, rulesList.ElementsAs(ctx, &ruleModels, false)) + return ruleModels +} + +// proxyFlattenRulesForPhase converts AWS rules to framework list format +func proxyFlattenRulesForPhase(ctx context.Context, awsRules []awstypes.ProxyRule, diags *diag.Diagnostics) fwtypes.ListNestedObjectValueOf[proxyRuleModel] { + if len(awsRules) == 0 { + return fwtypes.NewListNestedObjectValueOfNull[proxyRuleModel](ctx) + } + + var ruleModels []proxyRuleModel + for _, rule := range awsRules { + var ruleModel proxyRuleModel + diags.Append(flex.Flatten(ctx, &rule, &ruleModel)...) + if diags.HasError() { + return fwtypes.NewListNestedObjectValueOfNull[proxyRuleModel](ctx) + } + ruleModels = append(ruleModels, ruleModel) + } + + list, d := fwtypes.NewListNestedObjectValueOfValueSlice(ctx, ruleModels) + diags.Append(d...) + return list +} + +// proxyCollectRuleNames collects all rule names from all phases into a map +func proxyCollectRuleNames(rules *awstypes.ProxyRulesByRequestPhase) map[string]bool { + ruleNames := make(map[string]bool) + + for _, rule := range rules.PostRESPONSE { + if rule.ProxyRuleName != nil { + ruleNames[*rule.ProxyRuleName] = true + } + } + for _, rule := range rules.PreDNS { + if rule.ProxyRuleName != nil { + ruleNames[*rule.ProxyRuleName] = true + } + } + for _, rule := range rules.PreREQUEST { + if rule.ProxyRuleName != nil { + ruleNames[*rule.ProxyRuleName] = true + } + } + + return ruleNames +} + +// proxyBuildRuleMapByName builds a map of rules by name from current AWS state +func proxyBuildRuleMapByName(ctx context.Context, proxyRuleGroup *awstypes.ProxyRuleGroup, diags *diag.Diagnostics) map[string]proxyRuleModel { + rulesByName := make(map[string]proxyRuleModel) + + if proxyRuleGroup == nil || proxyRuleGroup.Rules == nil { + return rulesByName + } + + rules := proxyRuleGroup.Rules + + for _, rule := range rules.PostRESPONSE { + var rm proxyRuleModel + smerr.AddEnrich(ctx, diags, flex.Flatten(ctx, &rule, &rm)) + if diags.HasError() { + return nil + } + rulesByName[rm.ProxyRuleName.ValueString()] = rm + } + for _, rule := range rules.PreDNS { + var rm proxyRuleModel + smerr.AddEnrich(ctx, diags, flex.Flatten(ctx, &rule, &rm)) + if diags.HasError() { + return nil + } + rulesByName[rm.ProxyRuleName.ValueString()] = rm + } + for _, rule := range rules.PreREQUEST { + var rm proxyRuleModel + smerr.AddEnrich(ctx, diags, flex.Flatten(ctx, &rule, &rm)) + if diags.HasError() { + return nil + } + rulesByName[rm.ProxyRuleName.ValueString()] = rm + } + + return rulesByName +} + +type resourceProxyRulesExclusiveModel struct { + framework.WithRegionModel + ID types.String `tfsdk:"id"` + PostRESPONSE fwtypes.ListNestedObjectValueOf[proxyRuleModel] `tfsdk:"post_response"` + PreDNS fwtypes.ListNestedObjectValueOf[proxyRuleModel] `tfsdk:"pre_dns"` + PreREQUEST fwtypes.ListNestedObjectValueOf[proxyRuleModel] `tfsdk:"pre_request"` + ProxyRuleGroupArn fwtypes.ARN `tfsdk:"proxy_rule_group_arn"` +} + +type proxyRuleModel struct { + Action fwtypes.StringEnum[awstypes.ProxyRulePhaseAction] `tfsdk:"action"` + Conditions fwtypes.ListNestedObjectValueOf[proxyRuleConditionModel] `tfsdk:"conditions"` + Description types.String `tfsdk:"description"` + ProxyRuleName types.String `tfsdk:"proxy_rule_name"` +} + +type proxyRuleConditionModel struct { + ConditionKey types.String `tfsdk:"condition_key"` + ConditionOperator types.String `tfsdk:"condition_operator"` + ConditionValues fwtypes.ListValueOf[types.String] `tfsdk:"condition_values"` +} + +// proxyRuleRecreateInfo holds information about a rule that needs to be recreated +type proxyRuleRecreateInfo struct { + rule proxyRuleModel + position int32 + phase string +} + +func (data *resourceProxyRulesExclusiveModel) setID() { + data.ID = data.ProxyRuleGroupArn.StringValue +} diff --git a/internal/service/networkfirewall/proxy_rules_exclusive_identity_gen_test.go b/internal/service/networkfirewall/proxy_rules_exclusive_identity_gen_test.go new file mode 100644 index 000000000000..0748d5d0fd52 --- /dev/null +++ b/internal/service/networkfirewall/proxy_rules_exclusive_identity_gen_test.go @@ -0,0 +1,353 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +// Code generated by internal/generate/identitytests/main.go; DO NOT EDIT. + +package networkfirewall_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + tfstatecheck "github.com/hashicorp/terraform-provider-aws/internal/acctest/statecheck" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccNetworkFirewallProxyRulesExclusive_Identity_basic(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_rules_exclusive.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyRulesExclusiveDestroy(ctx, t), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRulesExclusiveExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectRegionalARNFormat(resourceName, tfjsonpath.New("proxy_rule_group_arn"), "network-firewall", "proxy-rule-group/{name}"), + statecheck.CompareValuePairs(resourceName, tfjsonpath.New(names.AttrID), resourceName, tfjsonpath.New("proxy_rule_group_arn"), compare.ValuesSame()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + "proxy_rule_group_arn": knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New("proxy_rule_group_arn")), + }, + }, + + // Step 2: Import command + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ImportStateKind: resource.ImportCommandWithID, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 3: Import block with Import ID + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("proxy_rule_group_arn"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + + // Step 4: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("proxy_rule_group_arn"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.Region())), + }, + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxyRulesExclusive_Identity_regionOverride(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_rules_exclusive.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: acctest.CheckDestroyNoop, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + // Step 1: Setup + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectRegionalARNAlternateRegionFormat(resourceName, tfjsonpath.New("proxy_rule_group_arn"), "network-firewall", "proxy-rule-group/{name}"), + statecheck.CompareValuePairs(resourceName, tfjsonpath.New(names.AttrID), resourceName, tfjsonpath.New("proxy_rule_group_arn"), compare.ValuesSame()), + statecheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + "proxy_rule_group_arn": knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New("proxy_rule_group_arn")), + }, + }, + + // Step 2: Import command with appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ImportStateKind: resource.ImportCommandWithID, + ImportStateIdFunc: acctest.CrossRegionImportStateIdFunc(resourceName), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 3: Import command without appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ImportStateKind: resource.ImportCommandWithID, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + + // Step 4: Import block with Import ID and appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportStateIdFunc: acctest.CrossRegionImportStateIdFunc(resourceName), + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("proxy_rule_group_arn"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + + // Step 5: Import block with Import ID and no appended "@" + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithID, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("proxy_rule_group_arn"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + + // Step 6: Import block with Resource Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/region_override/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + "region": config.StringVariable(acctest.AlternateRegion()), + }, + ResourceName: resourceName, + ImportState: true, + ImportStateKind: resource.ImportBlockWithResourceIdentity, + ImportPlanChecks: resource.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New("proxy_rule_group_arn"), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrID), knownvalue.NotNull()), + plancheck.ExpectKnownValue(resourceName, tfjsonpath.New(names.AttrRegion), knownvalue.StringExact(acctest.AlternateRegion())), + }, + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxyRulesExclusive_Identity_ExistingResource_basic(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_rules_exclusive.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyRulesExclusiveDestroy(ctx, t), + Steps: []resource.TestStep{ + // Step 1: Create pre-Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/basic_v5.100.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRulesExclusiveExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectNoIdentity(resourceName), + }, + }, + + // Step 2: v6.0 Identity set on refresh + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/basic_v6.0.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRulesExclusiveExists(ctx, t, resourceName), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + "proxy_rule_group_arn": knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New("proxy_rule_group_arn")), + }, + }, + + // Step 3: Current version + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionNoop), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectIdentity(resourceName, map[string]knownvalue.Check{ + "proxy_rule_group_arn": knownvalue.NotNull(), + }), + statecheck.ExpectIdentityValueMatchesState(resourceName, tfjsonpath.New("proxy_rule_group_arn")), + }, + }, + }, + }) +} + +func TestAccNetworkFirewallProxyRulesExclusive_Identity_ExistingResource_noRefreshNoChange(t *testing.T) { + ctx := acctest.Context(t) + + resourceName := "aws_networkfirewall_proxy_rules_exclusive.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_12_0), + }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + CheckDestroy: testAccCheckProxyRulesExclusiveDestroy(ctx, t), + AdditionalCLIOptions: &resource.AdditionalCLIOptions{ + Plan: resource.PlanOptions{ + NoRefresh: true, + }, + }, + Steps: []resource.TestStep{ + // Step 1: Create pre-Identity + { + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/basic_v5.100.0/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRulesExclusiveExists(ctx, t, resourceName), + ), + ConfigStateChecks: []statecheck.StateCheck{ + tfstatecheck.ExpectNoIdentity(resourceName), + }, + }, + + // Step 2: Current version + { + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ConfigDirectory: config.StaticDirectory("testdata/ProxyRulesExclusive/basic/"), + ConfigVariables: config.Variables{ + acctest.CtRName: config.StringVariable(rName), + }, + }, + }, + }) +} diff --git a/internal/service/networkfirewall/proxy_rules_exclusive_test.go b/internal/service/networkfirewall/proxy_rules_exclusive_test.go new file mode 100644 index 000000000000..6c6a9500fa92 --- /dev/null +++ b/internal/service/networkfirewall/proxy_rules_exclusive_test.go @@ -0,0 +1,585 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/networkfirewall" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + tfnetworkfirewall "github.com/hashicorp/terraform-provider-aws/internal/service/networkfirewall" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testAccNetworkFirewallProxyRulesExclusive_basic(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyRuleGroupOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_rules_exclusive.test" + ruleGroupResourceName := "aws_networkfirewall_proxy_rule_group.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyRulesExclusiveDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyRulesExclusiveConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRulesExclusiveExists(ctx, t, resourceName, &v), + resource.TestCheckResourceAttrPair(resourceName, "proxy_rule_group_arn", ruleGroupResourceName, names.AttrARN), + // Pre-DNS phase + resource.TestCheckResourceAttr(resourceName, "pre_dns.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.proxy_rule_name", fmt.Sprintf("%s-predns", rName)), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.action", "ALLOW"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.conditions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.conditions.0.condition_key", "request:DestinationDomain"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.conditions.0.condition_operator", "StringEquals"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.conditions.0.condition_values.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.conditions.0.condition_values.0", "amazonaws.com"), + // Pre-REQUEST phase + resource.TestCheckResourceAttr(resourceName, "pre_request.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_request.0.proxy_rule_name", fmt.Sprintf("%s-prerequest", rName)), + resource.TestCheckResourceAttr(resourceName, "pre_request.0.action", "DENY"), + resource.TestCheckResourceAttr(resourceName, "pre_request.0.conditions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_request.0.conditions.0.condition_key", "request:Http:Method"), + resource.TestCheckResourceAttr(resourceName, "pre_request.0.conditions.0.condition_operator", "StringEquals"), + resource.TestCheckResourceAttr(resourceName, "pre_request.0.conditions.0.condition_values.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_request.0.conditions.0.condition_values.0", "DELETE"), + // Post-RESPONSE phase + resource.TestCheckResourceAttr(resourceName, "post_response.#", "1"), + resource.TestCheckResourceAttr(resourceName, "post_response.0.proxy_rule_name", fmt.Sprintf("%s-postresponse", rName)), + resource.TestCheckResourceAttr(resourceName, "post_response.0.action", "ALERT"), + resource.TestCheckResourceAttr(resourceName, "post_response.0.conditions.#", "1"), + resource.TestCheckResourceAttr(resourceName, "post_response.0.conditions.0.condition_key", "response:Http:StatusCode"), + resource.TestCheckResourceAttr(resourceName, "post_response.0.conditions.0.condition_operator", "NumericGreaterThanEquals"), + resource.TestCheckResourceAttr(resourceName, "post_response.0.conditions.0.condition_values.#", "1"), + resource.TestCheckResourceAttr(resourceName, "post_response.0.conditions.0.condition_values.0", "500"), + ), + }, + { + Config: testAccProxyRulesExclusiveConfig_single(rName), + ResourceName: resourceName, + ImportState: true, + }, + }, + }) +} + +func testAccNetworkFirewallProxyRulesExclusive_disappears(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyRuleGroupOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_rules_exclusive.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyRulesExclusiveDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyRulesExclusiveConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckProxyRulesExclusiveExists(ctx, t, resourceName, &v), + acctest.CheckFrameworkResourceDisappears(ctx, t, tfnetworkfirewall.ResourceProxyRulesExclusive, resourceName), + ), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + }, + }, + }) +} + +func testAccNetworkFirewallProxyRulesExclusive_updateAdd(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v1, v2 networkfirewall.DescribeProxyRuleGroupOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_rules_exclusive.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyRulesExclusiveDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyRulesExclusiveConfig_single(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRulesExclusiveExists(ctx, t, resourceName, &v1), + resource.TestCheckResourceAttr(resourceName, "pre_dns.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_request.#", "0"), + resource.TestCheckResourceAttr(resourceName, "post_response.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccProxyRulesExclusiveConfig_add(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRulesExclusiveExists(ctx, t, resourceName, &v2), + resource.TestCheckResourceAttr(resourceName, "pre_dns.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_request.#", "1"), + resource.TestCheckResourceAttr(resourceName, "post_response.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_request.0.proxy_rule_name", fmt.Sprintf("%s-prerequest-new", rName)), + resource.TestCheckResourceAttr(resourceName, "post_response.0.proxy_rule_name", fmt.Sprintf("%s-postresponse-new", rName)), + ), + }, + }, + }) +} + +func testAccNetworkFirewallProxyRulesExclusive_updateModify(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v1, v2 networkfirewall.DescribeProxyRuleGroupOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_rules_exclusive.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyRulesExclusiveDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyRulesExclusiveConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRulesExclusiveExists(ctx, t, resourceName, &v1), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.action", "ALLOW"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.conditions.0.condition_values.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.conditions.0.condition_values.0", "amazonaws.com"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccProxyRulesExclusiveConfig_modified(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRulesExclusiveExists(ctx, t, resourceName, &v2), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.action", "DENY"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.conditions.0.condition_values.#", "2"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.conditions.0.condition_values.0", "example.com"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.conditions.0.condition_values.1", "test.com"), + ), + }, + }, + }) +} + +func testAccNetworkFirewallProxyRulesExclusive_updateRemove(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v1, v2 networkfirewall.DescribeProxyRuleGroupOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_rules_exclusive.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyRulesExclusiveDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyRulesExclusiveConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRulesExclusiveExists(ctx, t, resourceName, &v1), + resource.TestCheckResourceAttr(resourceName, "pre_dns.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_request.#", "1"), + resource.TestCheckResourceAttr(resourceName, "post_response.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccProxyRulesExclusiveConfig_single(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRulesExclusiveExists(ctx, t, resourceName, &v2), + resource.TestCheckResourceAttr(resourceName, "pre_dns.#", "1"), + resource.TestCheckResourceAttr(resourceName, "pre_request.#", "0"), + resource.TestCheckResourceAttr(resourceName, "post_response.#", "0"), + ), + }, + }, + }) +} + +func testAccNetworkFirewallProxyRulesExclusive_multipleRulesPerPhase(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyRuleGroupOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy_rules_exclusive.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewall), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyRulesExclusiveDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyRulesExclusiveConfig_multiplePerPhase(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyRulesExclusiveExists(ctx, t, resourceName, &v), + // Pre-DNS phase - 2 rules + resource.TestCheckResourceAttr(resourceName, "pre_dns.#", "2"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.proxy_rule_name", fmt.Sprintf("%s-predns-1", rName)), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.action", "ALLOW"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.0.conditions.0.condition_values.0", "amazonaws.com"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.1.proxy_rule_name", fmt.Sprintf("%s-predns-2", rName)), + resource.TestCheckResourceAttr(resourceName, "pre_dns.1.action", "DENY"), + resource.TestCheckResourceAttr(resourceName, "pre_dns.1.conditions.0.condition_values.0", "malicious.com"), + // Pre-REQUEST phase - 2 rules + resource.TestCheckResourceAttr(resourceName, "pre_request.#", "2"), + resource.TestCheckResourceAttr(resourceName, "pre_request.0.proxy_rule_name", fmt.Sprintf("%s-prerequest-1", rName)), + resource.TestCheckResourceAttr(resourceName, "pre_request.0.action", "DENY"), + resource.TestCheckResourceAttr(resourceName, "pre_request.0.conditions.0.condition_values.0", "DELETE"), + resource.TestCheckResourceAttr(resourceName, "pre_request.1.proxy_rule_name", fmt.Sprintf("%s-prerequest-2", rName)), + resource.TestCheckResourceAttr(resourceName, "pre_request.1.action", "DENY"), + resource.TestCheckResourceAttr(resourceName, "pre_request.1.conditions.0.condition_values.0", "PUT"), + // Post-RESPONSE phase - 2 rules + resource.TestCheckResourceAttr(resourceName, "post_response.#", "2"), + resource.TestCheckResourceAttr(resourceName, "post_response.0.proxy_rule_name", fmt.Sprintf("%s-postresponse-1", rName)), + resource.TestCheckResourceAttr(resourceName, "post_response.0.action", "ALERT"), + resource.TestCheckResourceAttr(resourceName, "post_response.0.conditions.0.condition_values.0", "500"), + resource.TestCheckResourceAttr(resourceName, "post_response.1.proxy_rule_name", fmt.Sprintf("%s-postresponse-2", rName)), + resource.TestCheckResourceAttr(resourceName, "post_response.1.action", "ALERT"), + resource.TestCheckResourceAttr(resourceName, "post_response.1.conditions.0.condition_values.0", "404"), + ), + }, + { + + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckProxyRulesExclusiveDestroy(ctx context.Context, t *testing.T) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.ProviderMeta(ctx, t).NetworkFirewallClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_networkfirewall_proxy_rules_exclusive" { + continue + } + + // The resource ID is the proxy rule group ARN + out, err := tfnetworkfirewall.FindProxyRuleGroupByARN(ctx, conn, rs.Primary.ID) + + if retry.NotFound(err) { + continue + } + + if err != nil { + return err + } + + // Check if there are any rules in the group + if out != nil && out.ProxyRuleGroup != nil && out.ProxyRuleGroup.Rules != nil { + rules := out.ProxyRuleGroup.Rules + if len(rules.PreDNS) > 0 || len(rules.PreREQUEST) > 0 || len(rules.PostRESPONSE) > 0 { + return fmt.Errorf("NetworkFirewall Proxy Rules still exist in group %s", rs.Primary.ID) + } + } + } + + return nil + } +} + +func testAccCheckProxyRulesExclusiveExists(ctx context.Context, t *testing.T, n string, v ...*networkfirewall.DescribeProxyRuleGroupOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.ProviderMeta(ctx, t).NetworkFirewallClient(ctx) + + output, err := tfnetworkfirewall.FindProxyRuleGroupByARN(ctx, conn, rs.Primary.Attributes["proxy_rule_group_arn"]) + + if err != nil { + return err + } + + if len(v) > 0 { + *v[0] = *output + } + + return nil + } +} + +func testAccProxyRulesExclusiveConfig_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_networkfirewall_proxy_rule_group" "test" { + name = %[1]q +} + +resource "aws_networkfirewall_proxy_rules_exclusive" "test" { + proxy_rule_group_arn = aws_networkfirewall_proxy_rule_group.test.arn + + pre_dns { + proxy_rule_name = "%[1]s-predns" + action = "ALLOW" + description = "%[1]s-predns-description" + + conditions { + condition_key = "request:DestinationDomain" + condition_operator = "StringEquals" + condition_values = ["amazonaws.com"] + } + } + + pre_request { + proxy_rule_name = "%[1]s-prerequest" + action = "DENY" + description = "%[1]s-prerequest-description" + + conditions { + condition_key = "request:Http:Method" + condition_operator = "StringEquals" + condition_values = ["DELETE"] + } + } + + post_response { + proxy_rule_name = "%[1]s-postresponse" + action = "ALERT" + description = "%[1]s-postresponse-description" + + conditions { + condition_key = "response:Http:StatusCode" + condition_operator = "NumericGreaterThanEquals" + condition_values = ["500"] + } + } +} +`, rName) +} + +func testAccProxyRulesExclusiveConfig_single(rName string) string { + return fmt.Sprintf(` +resource "aws_networkfirewall_proxy_rule_group" "test" { + name = %[1]q +} + +resource "aws_networkfirewall_proxy_rules_exclusive" "test" { + proxy_rule_group_arn = aws_networkfirewall_proxy_rule_group.test.arn + + pre_dns { + proxy_rule_name = "%[1]s-predns" + action = "ALLOW" + + conditions { + condition_key = "request:DestinationDomain" + condition_operator = "StringEquals" + condition_values = ["amazonaws.com"] + } + } +} +`, rName) +} + +func testAccProxyRulesExclusiveConfig_modified(rName string) string { + return fmt.Sprintf(` +resource "aws_networkfirewall_proxy_rule_group" "test" { + name = %[1]q +} + +resource "aws_networkfirewall_proxy_rules_exclusive" "test" { + proxy_rule_group_arn = aws_networkfirewall_proxy_rule_group.test.arn + + pre_dns { + proxy_rule_name = "%[1]s-predns" + action = "ALLOW" + description = "%[1]s-predns-description" + + conditions { + condition_key = "request:DestinationDomain" + condition_operator = "StringEquals" + condition_values = ["example.com", "test.com"] + } + } + + pre_request { + proxy_rule_name = "%[1]s-prerequest" + action = "DENY" + + conditions { + condition_key = "request:Http:Method" + condition_operator = "StringEquals" + condition_values = ["DELETE"] + } + } + + post_response { + proxy_rule_name = "%[1]s-postresponse" + action = "ALERT" + description = "%[1]s-postresponse-description" + + conditions { + condition_key = "response:Http:StatusCode" + condition_operator = "NumericGreaterThanEquals" + condition_values = ["500"] + } + } +} +`, rName) +} + +func testAccProxyRulesExclusiveConfig_add(rName string) string { + return fmt.Sprintf(` +resource "aws_networkfirewall_proxy_rule_group" "test" { + name = %[1]q +} + +resource "aws_networkfirewall_proxy_rules_exclusive" "test" { + proxy_rule_group_arn = aws_networkfirewall_proxy_rule_group.test.arn + + pre_dns { + proxy_rule_name = "%[1]s-predns" + action = "ALLOW" + + conditions { + condition_key = "request:DestinationDomain" + condition_operator = "StringEquals" + condition_values = ["amazonaws.com"] + } + } + + pre_request { + proxy_rule_name = "%[1]s-prerequest-new" + action = "DENY" + + conditions { + condition_key = "request:Http:Method" + condition_operator = "StringEquals" + condition_values = ["POST"] + } + } + + post_response { + proxy_rule_name = "%[1]s-postresponse-new" + action = "ALERT" + + conditions { + condition_key = "response:Http:StatusCode" + condition_operator = "NumericGreaterThanEquals" + condition_values = ["400"] + } + } +} +`, rName) +} + +func testAccProxyRulesExclusiveConfig_multiplePerPhase(rName string) string { + return fmt.Sprintf(` +resource "aws_networkfirewall_proxy_rule_group" "test" { + name = %[1]q +} + +resource "aws_networkfirewall_proxy_rules_exclusive" "test" { + proxy_rule_group_arn = aws_networkfirewall_proxy_rule_group.test.arn + + pre_dns { + proxy_rule_name = "%[1]s-predns-1" + action = "ALLOW" + + conditions { + condition_key = "request:DestinationDomain" + condition_operator = "StringEquals" + condition_values = ["amazonaws.com"] + } + } + + pre_dns { + proxy_rule_name = "%[1]s-predns-2" + action = "DENY" + + conditions { + condition_key = "request:DestinationDomain" + condition_operator = "StringEquals" + condition_values = ["malicious.com"] + } + } + + pre_request { + proxy_rule_name = "%[1]s-prerequest-1" + action = "DENY" + + conditions { + condition_key = "request:Http:Method" + condition_operator = "StringEquals" + condition_values = ["DELETE"] + } + } + + pre_request { + proxy_rule_name = "%[1]s-prerequest-2" + action = "DENY" + + conditions { + condition_key = "request:Http:Method" + condition_operator = "StringEquals" + condition_values = ["PUT"] + } + } + + post_response { + proxy_rule_name = "%[1]s-postresponse-1" + action = "ALERT" + + conditions { + condition_key = "response:Http:StatusCode" + condition_operator = "NumericGreaterThanEquals" + condition_values = ["500"] + } + } + + post_response { + proxy_rule_name = "%[1]s-postresponse-2" + action = "ALERT" + + conditions { + condition_key = "response:Http:StatusCode" + condition_operator = "NumericEquals" + condition_values = ["404"] + } + } +} +`, rName) +} diff --git a/internal/service/networkfirewall/proxy_serial_test.go b/internal/service/networkfirewall/proxy_serial_test.go new file mode 100644 index 000000000000..e60f78f0ede7 --- /dev/null +++ b/internal/service/networkfirewall/proxy_serial_test.go @@ -0,0 +1,67 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall_test + +import ( + "testing" + + "github.com/hashicorp/terraform-provider-aws/internal/acctest" +) + +// Network Firewall Proxy resources are in preview and have strict concurrency limits, +// so all proxy acceptance tests must run serially. +func TestAccNetworkFirewallProxy_serial(t *testing.T) { + t.Parallel() + + testCases := map[string]map[string]func(t *testing.T){ + "Proxy": { + acctest.CtBasic: testAccNetworkFirewallProxy_basic, + acctest.CtDisappears: testAccNetworkFirewallProxy_disappears, + "tlsInterceptEnabled": testAccNetworkFirewallProxy_tlsInterceptEnabled, + "logging": testAccNetworkFirewallProxy_logging, + }, + "ProxyList": { + acctest.CtBasic: testAccNetworkFirewallProxy_List_basic, + "includeResource": testAccNetworkFirewallProxy_List_includeResource, + "regionOverride": testAccNetworkFirewallProxy_List_regionOverride, + }, + "ProxyConfiguration": { + acctest.CtBasic: testAccNetworkFirewallProxyConfiguration_basic, + acctest.CtDisappears: testAccNetworkFirewallProxyConfiguration_disappears, + "tags": testAccNetworkFirewallProxyConfiguration_tags, + }, + "ProxyConfigurationList": { + acctest.CtBasic: testAccNetworkFirewallProxyConfiguration_List_basic, + "includeResource": testAccNetworkFirewallProxyConfiguration_List_includeResource, + "regionOverride": testAccNetworkFirewallProxyConfiguration_List_regionOverride, + }, + "ProxyRuleGroup": { + acctest.CtBasic: testAccNetworkFirewallProxyRuleGroup_basic, + acctest.CtDisappears: testAccNetworkFirewallProxyRuleGroup_disappears, + "tags": testAccNetworkFirewallProxyRuleGroup_tags, + }, + "ProxyRuleGroupList": { + acctest.CtBasic: testAccNetworkFirewallProxyRuleGroup_List_basic, + "includeResource": testAccNetworkFirewallProxyRuleGroup_List_includeResource, + "regionOverride": testAccNetworkFirewallProxyRuleGroup_List_regionOverride, + }, + "ProxyRulesExclusive": { + acctest.CtBasic: testAccNetworkFirewallProxyRulesExclusive_basic, + acctest.CtDisappears: testAccNetworkFirewallProxyRulesExclusive_disappears, + "updateAdd": testAccNetworkFirewallProxyRulesExclusive_updateAdd, + "updateModify": testAccNetworkFirewallProxyRulesExclusive_updateModify, + "updateRemove": testAccNetworkFirewallProxyRulesExclusive_updateRemove, + "multipleRulesPerPhase": testAccNetworkFirewallProxyRulesExclusive_multipleRulesPerPhase, + }, + "ProxyConfigurationRuleGroupAttachmentsExclusive": { + acctest.CtBasic: testAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_basic, + acctest.CtDisappears: testAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_disappears, + "updateAdd": testAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_updateAdd, + "updateRemove": testAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_updateRemove, + "updateReorder": testAccNetworkFirewallProxyConfigurationRuleGroupAttachmentsExclusive_updateReorder, + }, + } + + acctest.RunSerialTests2Levels(t, testCases, 0) +} diff --git a/internal/service/networkfirewall/proxy_test.go b/internal/service/networkfirewall/proxy_test.go new file mode 100644 index 000000000000..fe3211290308 --- /dev/null +++ b/internal/service/networkfirewall/proxy_test.go @@ -0,0 +1,612 @@ +// Copyright IBM Corp. 2014, 2026 +// SPDX-License-Identifier: MPL-2.0 + +package networkfirewall_test + +import ( + "context" + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/service/networkfirewall" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/retry" + tfnetworkfirewall "github.com/hashicorp/terraform-provider-aws/internal/service/networkfirewall" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func testAccNetworkFirewallProxy_basic(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyExists(ctx, t, resourceName, &v), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "network-firewall", regexache.MustCompile(`proxy/.+$`)), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttrSet(resourceName, "nat_gateway_id"), + resource.TestCheckResourceAttr(resourceName, "listener_properties.#", "2"), + resource.TestCheckResourceAttr(resourceName, "listener_properties.0.port", "8080"), + resource.TestCheckResourceAttr(resourceName, "listener_properties.0.type", "HTTP"), + resource.TestCheckResourceAttr(resourceName, "listener_properties.1.port", "443"), + resource.TestCheckResourceAttr(resourceName, "listener_properties.1.type", "HTTPS"), + resource.TestCheckResourceAttr(resourceName, "tls_intercept_properties.0.tls_intercept_mode", "DISABLED"), + resource.TestCheckResourceAttrSet(resourceName, names.AttrCreateTime), + resource.TestCheckResourceAttrSet(resourceName, "update_token"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"update_token"}, + }, + }, + }) +} + +func testAccNetworkFirewallProxy_disappears(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckProxyExists(ctx, t, resourceName, &v), + acctest.CheckFrameworkResourceDisappears(ctx, t, tfnetworkfirewall.ResourceProxy, resourceName), + ), + ExpectNonEmptyPlan: true, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + }, + }) +} + +func testAccNetworkFirewallProxy_tlsInterceptEnabled(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + var v networkfirewall.DescribeProxyOutput + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_networkfirewall_proxy.test" + pcaResourceName := "aws_acmpca_certificate_authority.test" + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyConfig_tlsInterceptEnabled(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckProxyExists(ctx, t, resourceName, &v), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "network-firewall", regexache.MustCompile(`proxy/.+$`)), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttrSet(resourceName, "nat_gateway_id"), + resource.TestCheckResourceAttr(resourceName, "listener_properties.#", "2"), + resource.TestCheckResourceAttr(resourceName, "listener_properties.0.port", "8080"), + resource.TestCheckResourceAttr(resourceName, "listener_properties.0.type", "HTTP"), + resource.TestCheckResourceAttr(resourceName, "listener_properties.1.port", "443"), + resource.TestCheckResourceAttr(resourceName, "listener_properties.1.type", "HTTPS"), + resource.TestCheckResourceAttr(resourceName, "tls_intercept_properties.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tls_intercept_properties.0.tls_intercept_mode", "ENABLED"), + resource.TestCheckResourceAttrPair(resourceName, "tls_intercept_properties.0.pca_arn", pcaResourceName, names.AttrARN), + resource.TestCheckResourceAttrSet(resourceName, names.AttrCreateTime), + resource.TestCheckResourceAttrSet(resourceName, "update_token"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"update_token"}, + }, + }, + }) +} + +func testAccNetworkFirewallProxy_logging(t *testing.T) { + t.Helper() + + ctx := acctest.Context(t) + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.Test(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.NetworkFirewallServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckProxyDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccProxyConfig_logging(rName), + Check: resource.ComposeAggregateTestCheckFunc( + // CloudWatch Logs delivery source + resource.TestCheckResourceAttr("aws_cloudwatch_log_delivery_source.cwl", "log_type", "ALERT_LOGS"), + resource.TestCheckResourceAttrPair("aws_cloudwatch_log_delivery_source.cwl", names.AttrResourceARN, "aws_networkfirewall_proxy.test", names.AttrARN), + // CloudWatch Logs delivery destination + resource.TestCheckResourceAttr("aws_cloudwatch_log_delivery_destination.cwl", "delivery_destination_type", "CWL"), + // CloudWatch Logs delivery + resource.TestCheckResourceAttrSet("aws_cloudwatch_log_delivery.cwl", names.AttrID), + resource.TestCheckResourceAttrPair("aws_cloudwatch_log_delivery.cwl", "delivery_source_name", "aws_cloudwatch_log_delivery_source.cwl", names.AttrName), + resource.TestCheckResourceAttrPair("aws_cloudwatch_log_delivery.cwl", "delivery_destination_arn", "aws_cloudwatch_log_delivery_destination.cwl", names.AttrARN), + // S3 delivery source + resource.TestCheckResourceAttr("aws_cloudwatch_log_delivery_source.s3", "log_type", "ALLOW_LOGS"), + resource.TestCheckResourceAttrPair("aws_cloudwatch_log_delivery_source.s3", names.AttrResourceARN, "aws_networkfirewall_proxy.test", names.AttrARN), + // S3 delivery destination + resource.TestCheckResourceAttr("aws_cloudwatch_log_delivery_destination.s3", "delivery_destination_type", "S3"), + // S3 delivery + resource.TestCheckResourceAttrSet("aws_cloudwatch_log_delivery.s3", names.AttrID), + resource.TestCheckResourceAttrPair("aws_cloudwatch_log_delivery.s3", "delivery_source_name", "aws_cloudwatch_log_delivery_source.s3", names.AttrName), + resource.TestCheckResourceAttrPair("aws_cloudwatch_log_delivery.s3", "delivery_destination_arn", "aws_cloudwatch_log_delivery_destination.s3", names.AttrARN), + ), + }, + }, + }) +} + +func testAccCheckProxyDestroy(ctx context.Context, t *testing.T) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.ProviderMeta(ctx, t).NetworkFirewallClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_networkfirewall_proxy" { + continue + } + + out, err := tfnetworkfirewall.FindProxyByARN(ctx, conn, rs.Primary.ID) + + if retry.NotFound(err) { + continue + } + + if out != nil && out.Proxy != nil && out.Proxy.DeleteTime != nil { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("NetworkFirewall Proxy %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckProxyExists(ctx context.Context, t *testing.T, n string, v ...*networkfirewall.DescribeProxyOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.ProviderMeta(ctx, t).NetworkFirewallClient(ctx) + + output, err := tfnetworkfirewall.FindProxyByARN(ctx, conn, rs.Primary.Attributes[names.AttrARN]) + + if err != nil { + return err + } + + if len(v) > 0 { + *v[0] = *output + } + + return nil + } +} + +// testAccProxyConfig_baseVPC creates a reusable VPC configuration for proxy tests. +// It includes: +// - VPC with CIDR 10.0.0.0/16 +// - Public subnet (10.0.1.0/24) with Internet Gateway +// - Private subnet (10.0.2.0/24) +// - Internet Gateway +// - NAT Gateway in the public subnet +// - Route tables for public and private subnets +func testAccProxyConfig_baseVPC(rName string) string { + return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = %[1]q + } +} + +resource "aws_subnet" "public" { + vpc_id = aws_vpc.test.id + cidr_block = "10.0.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = true + + tags = { + Name = "%[1]s-public" + } +} + +resource "aws_subnet" "private" { + vpc_id = aws_vpc.test.id + cidr_block = "10.0.2.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + + tags = { + Name = "%[1]s-private" + } +} + +resource "aws_internet_gateway" "test" { + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} + +resource "aws_eip" "test" { + domain = "vpc" + + tags = { + Name = %[1]q + } + + depends_on = [aws_internet_gateway.test] +} + +resource "aws_nat_gateway" "test" { + allocation_id = aws_eip.test.id + subnet_id = aws_subnet.public.id + + tags = { + Name = %[1]q + } + + depends_on = [aws_internet_gateway.test] +} + +resource "aws_route_table" "public" { + vpc_id = aws_vpc.test.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.test.id + } + + tags = { + Name = "%[1]s-public" + } +} + +resource "aws_route_table" "private" { + vpc_id = aws_vpc.test.id + + route { + cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.test.id + } + + tags = { + Name = "%[1]s-private" + } +} + +resource "aws_route_table_association" "public" { + subnet_id = aws_subnet.public.id + route_table_id = aws_route_table.public.id +} + +resource "aws_route_table_association" "private" { + subnet_id = aws_subnet.private.id + route_table_id = aws_route_table.private.id +} +`, rName)) +} + +// testAccProxyConfig_baseProxyConfiguration creates a basic proxy configuration +// that can be reused across proxy tests. +func testAccProxyConfig_baseProxyConfiguration(rName string) string { + return fmt.Sprintf(` +resource "aws_networkfirewall_proxy_configuration" "test" { + name = %[1]q + + default_rule_phase_actions { + post_response = "ALLOW" + pre_dns = "ALLOW" + pre_request = "ALLOW" + } +} +`, rName) +} + +func testAccProxyConfig_basic(rName string) string { + return acctest.ConfigCompose( + testAccProxyConfig_baseVPC(rName), + testAccProxyConfig_baseProxyConfiguration(rName), + fmt.Sprintf(` +resource "aws_networkfirewall_proxy" "test" { + name = %[1]q + nat_gateway_id = aws_nat_gateway.test.id + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.test.arn + + tls_intercept_properties { + tls_intercept_mode = "DISABLED" + } + + listener_properties { + port = 8080 + type = "HTTP" + } + + listener_properties { + port = 443 + type = "HTTPS" + } +} +`, rName)) +} + +func testAccProxyConfig_logging(rName string) string { + return acctest.ConfigCompose( + testAccProxyConfig_baseVPC(rName), + testAccProxyConfig_baseProxyConfiguration(rName), + fmt.Sprintf(` +resource "aws_networkfirewall_proxy" "test" { + name = %[1]q + nat_gateway_id = aws_nat_gateway.test.id + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.test.arn + + tls_intercept_properties { + tls_intercept_mode = "DISABLED" + } + + listener_properties { + port = 8080 + type = "HTTP" + } + + listener_properties { + port = 443 + type = "HTTPS" + } +} + +# CloudWatch Logs delivery + +resource "aws_cloudwatch_log_group" "test" { + name = %[1]q + retention_in_days = 7 +} + +resource "aws_cloudwatch_log_delivery_source" "cwl" { + name = "%[1]s-cwl" + log_type = "ALERT_LOGS" + resource_arn = aws_networkfirewall_proxy.test.arn +} + +resource "aws_cloudwatch_log_delivery_destination" "cwl" { + name = "%[1]s-cwl" + + delivery_destination_configuration { + destination_resource_arn = aws_cloudwatch_log_group.test.arn + } +} + +resource "aws_cloudwatch_log_delivery" "cwl" { + delivery_source_name = aws_cloudwatch_log_delivery_source.cwl.name + delivery_destination_arn = aws_cloudwatch_log_delivery_destination.cwl.arn +} + +# S3 delivery + +resource "aws_s3_bucket" "test" { + bucket = %[1]q + force_destroy = true +} + +resource "aws_cloudwatch_log_delivery_source" "s3" { + name = "%[1]s-s3" + log_type = "ALLOW_LOGS" + resource_arn = aws_networkfirewall_proxy.test.arn +} + +resource "aws_cloudwatch_log_delivery_destination" "s3" { + name = "%[1]s-s3" + + delivery_destination_configuration { + destination_resource_arn = aws_s3_bucket.test.arn + } +} + +resource "aws_cloudwatch_log_delivery" "s3" { + delivery_source_name = aws_cloudwatch_log_delivery_source.s3.name + delivery_destination_arn = aws_cloudwatch_log_delivery_destination.s3.arn +} +`, rName)) +} + +func testAccProxyConfig_tlsInterceptEnabled(rName string) string { + return acctest.ConfigCompose( + testAccProxyConfig_baseVPC(rName), + testAccProxyConfig_baseProxyConfiguration(rName), + fmt.Sprintf(` +data "aws_caller_identity" "current" {} + +data "aws_partition" "current" {} + +data "aws_region" "current" {} + +# Create a root CA for TLS interception +resource "aws_acmpca_certificate_authority" "test" { + type = "ROOT" + + certificate_authority_configuration { + key_algorithm = "RSA_2048" + signing_algorithm = "SHA256WITHRSA" + + subject { + common_name = "%[1]s Terraform Test CA" + } + } + + permanent_deletion_time_in_days = 7 + + tags = { + Name = %[1]q + } +} + +resource "aws_acmpca_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + certificate_signing_request = aws_acmpca_certificate_authority.test.certificate_signing_request + signing_algorithm = "SHA512WITHRSA" + + template_arn = "arn:${data.aws_partition.current.partition}:acm-pca:::template/RootCACertificate/V1" + + validity { + type = "YEARS" + value = 1 + } +} + +resource "aws_acmpca_certificate_authority_certificate" "test" { + certificate_authority_arn = aws_acmpca_certificate_authority.test.arn + + certificate = aws_acmpca_certificate.test.certificate + certificate_chain = aws_acmpca_certificate.test.certificate_chain +} + +# Grant Network Firewall proxy permission to use the PCA +resource "aws_acmpca_policy" "test" { + resource_arn = aws_acmpca_certificate_authority.test.arn + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "NetworkFirewallProxyReadAccess" + Effect = "Allow" + Principal = { + Service = "proxy.network-firewall.amazonaws.com" + } + Action = [ + "acm-pca:GetCertificate", + "acm-pca:DescribeCertificateAuthority", + "acm-pca:GetCertificateAuthorityCertificate", + "acm-pca:ListTags", + "acm-pca:ListPermissions", + ] + Resource = aws_acmpca_certificate_authority.test.arn + Condition = { + ArnEquals = { + "aws:SourceArn" = aws_networkfirewall_proxy.test.arn + } + } + }, + { + Sid = "NetworkFirewallProxyIssueCertificate" + Effect = "Allow" + Principal = { + Service = "proxy.network-firewall.amazonaws.com" + } + Action = [ + "acm-pca:IssueCertificate", + ] + Resource = aws_acmpca_certificate_authority.test.arn + Condition = { + StringEquals = { + "acm-pca:TemplateArn" = "arn:${data.aws_partition.current.partition}:acm-pca:::template/SubordinateCACertificate_PathLen0/V1" + } + ArnEquals = { + "aws:SourceArn" = aws_networkfirewall_proxy.test.arn + } + } + } + ] + }) +} + +# Create RAM resource share for the PCA +resource "aws_ram_resource_share" "test" { + name = %[1]q + allow_external_principals = true + permission_arns = ["arn:${data.aws_partition.current.partition}:ram::aws:permission/AWSRAMSubordinateCACertificatePathLen0IssuanceCertificateAuthority"] + + tags = { + Name = %[1]q + } +} + +# Associate the PCA with the RAM share +resource "aws_ram_resource_share_associations_exclusive" "test" { + principals = ["proxy.network-firewall.amazonaws.com"] + resource_arns = [aws_acmpca_certificate_authority.test.arn] + resource_share_arn = aws_ram_resource_share.test.arn + sources = [data.aws_caller_identity.current.account_id] + + lifecycle { + ignore_changes = [ + resource_arns + ] + replace_triggered_by = [ + aws_acmpca_certificate_authority.test + ] + } +} + +resource "aws_networkfirewall_proxy" "test" { + name = %[1]q + nat_gateway_id = aws_nat_gateway.test.id + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.test.arn + + tls_intercept_properties { + tls_intercept_mode = "ENABLED" + pca_arn = aws_acmpca_certificate_authority.test.arn + } + + listener_properties { + port = 8080 + type = "HTTP" + } + + listener_properties { + port = 443 + type = "HTTPS" + } +} +`, rName)) +} diff --git a/internal/service/networkfirewall/service_package_gen.go b/internal/service/networkfirewall/service_package_gen.go index f759b1b518cd..d5b679c642d6 100644 --- a/internal/service/networkfirewall/service_package_gen.go +++ b/internal/service/networkfirewall/service_package_gen.go @@ -7,6 +7,8 @@ package networkfirewall import ( "context" + "iter" + "slices" "unique" "github.com/aws/aws-sdk-go-v2/aws" @@ -32,6 +34,65 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*inttypes.Ser Name: "Firewall Transit Gateway Attachment Accepter", Region: unique.Make(inttypes.ResourceRegionDefault()), }, + { + Factory: newResourceProxy, + TypeName: "aws_networkfirewall_proxy", + Name: "Proxy", + Tags: unique.Make(inttypes.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }), + Region: unique.Make(inttypes.ResourceRegionDefault()), + Identity: inttypes.RegionalARNIdentity(inttypes.WithIdentityDuplicateAttrs(names.AttrID)), + Import: inttypes.FrameworkImport{ + WrappedImport: true, + }, + }, + { + Factory: newResourceProxyConfiguration, + TypeName: "aws_networkfirewall_proxy_configuration", + Name: "Proxy Configuration", + Tags: unique.Make(inttypes.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }), + Region: unique.Make(inttypes.ResourceRegionDefault()), + Identity: inttypes.RegionalARNIdentity(inttypes.WithIdentityDuplicateAttrs(names.AttrID)), + Import: inttypes.FrameworkImport{ + WrappedImport: true, + }, + }, + { + Factory: newResourceProxyConfigurationRuleGroupAttachmentsExclusive, + TypeName: "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive", + Name: "Proxy Configuration Rule Group Attachments Exclusive", + Region: unique.Make(inttypes.ResourceRegionDefault()), + Identity: inttypes.RegionalARNIdentityNamed("proxy_configuration_arn", inttypes.WithIdentityDuplicateAttrs(names.AttrID)), + Import: inttypes.FrameworkImport{ + WrappedImport: true, + }, + }, + { + Factory: newResourceProxyRuleGroup, + TypeName: "aws_networkfirewall_proxy_rule_group", + Name: "Proxy Rule Group", + Tags: unique.Make(inttypes.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }), + Region: unique.Make(inttypes.ResourceRegionDefault()), + Identity: inttypes.RegionalARNIdentity(inttypes.WithIdentityDuplicateAttrs(names.AttrID)), + Import: inttypes.FrameworkImport{ + WrappedImport: true, + }, + }, + { + Factory: newResourceProxyRulesExclusive, + TypeName: "aws_networkfirewall_proxy_rules_exclusive", + Name: "Proxy Rules Exclusive", + Region: unique.Make(inttypes.ResourceRegionDefault()), + Identity: inttypes.RegionalARNIdentityNamed("proxy_rule_group_arn", inttypes.WithIdentityDuplicateAttrs(names.AttrID)), + Import: inttypes.FrameworkImport{ + WrappedImport: true, + }, + }, { Factory: newTLSInspectionConfigurationResource, TypeName: "aws_networkfirewall_tls_inspection_configuration", @@ -57,6 +118,41 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*inttypes.Ser } } +func (p *servicePackage) FrameworkListResources(ctx context.Context) iter.Seq[*inttypes.ServicePackageFrameworkListResource] { + return slices.Values([]*inttypes.ServicePackageFrameworkListResource{ + { + Factory: newProxyResourceAsListResource, + TypeName: "aws_networkfirewall_proxy", + Name: "Proxy", + Tags: unique.Make(inttypes.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }), + Region: unique.Make(inttypes.ResourceRegionDefault()), + Identity: inttypes.RegionalARNIdentity(inttypes.WithIdentityDuplicateAttrs(names.AttrID)), + }, + { + Factory: newProxyConfigurationResourceAsListResource, + TypeName: "aws_networkfirewall_proxy_configuration", + Name: "Proxy Configuration", + Tags: unique.Make(inttypes.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }), + Region: unique.Make(inttypes.ResourceRegionDefault()), + Identity: inttypes.RegionalARNIdentity(inttypes.WithIdentityDuplicateAttrs(names.AttrID)), + }, + { + Factory: newProxyRuleGroupResourceAsListResource, + TypeName: "aws_networkfirewall_proxy_rule_group", + Name: "Proxy Rule Group", + Tags: unique.Make(inttypes.ServicePackageResourceTags{ + IdentifierAttribute: names.AttrARN, + }), + Region: unique.Make(inttypes.ResourceRegionDefault()), + Identity: inttypes.RegionalARNIdentity(inttypes.WithIdentityDuplicateAttrs(names.AttrID)), + }, + }) +} + func (p *servicePackage) SDKDataSources(ctx context.Context) []*inttypes.ServicePackageSDKDataSource { return []*inttypes.ServicePackageSDKDataSource{ { diff --git a/internal/service/networkfirewall/sweep.go b/internal/service/networkfirewall/sweep.go index 03fd9aaaa17a..229fa761ccae 100644 --- a/internal/service/networkfirewall/sweep.go +++ b/internal/service/networkfirewall/sweep.go @@ -12,9 +12,32 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-provider-aws/internal/sweep" "github.com/hashicorp/terraform-provider-aws/internal/sweep/awsv2" + sweepfw "github.com/hashicorp/terraform-provider-aws/internal/sweep/framework" + "github.com/hashicorp/terraform-provider-aws/names" ) func RegisterSweepers() { + resource.AddTestSweepers("aws_networkfirewall_proxy", &resource.Sweeper{ + Name: "aws_networkfirewall_proxy", + F: sweepProxies, + Dependencies: []string{ + "aws_networkfirewall_proxy_configuration", + }, + }) + + resource.AddTestSweepers("aws_networkfirewall_proxy_configuration", &resource.Sweeper{ + Name: "aws_networkfirewall_proxy_configuration", + F: sweepProxyConfigurations, + Dependencies: []string{ + "aws_networkfirewall_proxy_rule_group", + }, + }) + + resource.AddTestSweepers("aws_networkfirewall_proxy_rule_group", &resource.Sweeper{ + Name: "aws_networkfirewall_proxy_rule_group", + F: sweepProxyRuleGroups, + }) + resource.AddTestSweepers("aws_networkfirewall_firewall_policy", &resource.Sweeper{ Name: "aws_networkfirewall_firewall_policy", F: sweepFirewallPolicies, @@ -45,6 +68,129 @@ func RegisterSweepers() { }) } +func sweepProxies(region string) error { + ctx := sweep.Context(region) + client, err := sweep.SharedRegionalSweepClient(ctx, region) + if err != nil { + return fmt.Errorf("getting client: %w", err) + } + conn := client.NetworkFirewallClient(ctx) + input := &networkfirewall.ListProxiesInput{} + sweepResources := make([]sweep.Sweepable, 0) + + pages := networkfirewall.NewListProxiesPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if awsv2.SkipSweepError(err) { + log.Printf("[WARN] Skipping NetworkFirewall Proxy sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error listing NetworkFirewall Proxies (%s): %w", region, err) + } + + for _, v := range page.Proxies { + id := aws.ToString(v.Arn) + log.Printf("[INFO] Deleting NetworkFirewall Proxy: %s", id) + sweepResources = append(sweepResources, sweepfw.NewSweepResource(newResourceProxy, client, + sweepfw.NewAttribute(names.AttrID, id), + )) + } + } + + err = sweep.SweepOrchestrator(ctx, sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping NetworkFirewall Proxies (%s): %w", region, err) + } + + return nil +} + +func sweepProxyConfigurations(region string) error { + ctx := sweep.Context(region) + client, err := sweep.SharedRegionalSweepClient(ctx, region) + if err != nil { + return fmt.Errorf("getting client: %w", err) + } + conn := client.NetworkFirewallClient(ctx) + input := &networkfirewall.ListProxyConfigurationsInput{} + sweepResources := make([]sweep.Sweepable, 0) + + pages := networkfirewall.NewListProxyConfigurationsPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if awsv2.SkipSweepError(err) { + log.Printf("[WARN] Skipping NetworkFirewall Proxy Configuration sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error listing NetworkFirewall Proxy Configurations (%s): %w", region, err) + } + + for _, v := range page.ProxyConfigurations { + id := aws.ToString(v.Arn) + log.Printf("[INFO] Deleting NetworkFirewall Proxy Configuration: %s", id) + sweepResources = append(sweepResources, sweepfw.NewSweepResource(newResourceProxyConfiguration, client, + sweepfw.NewAttribute(names.AttrID, id), + )) + } + } + + err = sweep.SweepOrchestrator(ctx, sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping NetworkFirewall Proxy Configurations (%s): %w", region, err) + } + + return nil +} + +func sweepProxyRuleGroups(region string) error { + ctx := sweep.Context(region) + client, err := sweep.SharedRegionalSweepClient(ctx, region) + if err != nil { + return fmt.Errorf("getting client: %w", err) + } + conn := client.NetworkFirewallClient(ctx) + input := &networkfirewall.ListProxyRuleGroupsInput{} + sweepResources := make([]sweep.Sweepable, 0) + + pages := networkfirewall.NewListProxyRuleGroupsPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if awsv2.SkipSweepError(err) { + log.Printf("[WARN] Skipping NetworkFirewall Proxy Rule Group sweep for %s: %s", region, err) + return nil + } + + if err != nil { + return fmt.Errorf("error listing NetworkFirewall Proxy Rule Groups (%s): %w", region, err) + } + + for _, v := range page.ProxyRuleGroups { + id := aws.ToString(v.Arn) + log.Printf("[INFO] Deleting NetworkFirewall Proxy Rule Group: %s", id) + sweepResources = append(sweepResources, sweepfw.NewSweepResource(newResourceProxyRuleGroup, client, + sweepfw.NewAttribute(names.AttrID, id), + )) + } + } + + err = sweep.SweepOrchestrator(ctx, sweepResources) + + if err != nil { + return fmt.Errorf("error sweeping NetworkFirewall Proxy Rule Groups (%s): %w", region, err) + } + + return nil +} + func sweepFirewallPolicies(region string) error { ctx := sweep.Context(region) client, err := sweep.SharedRegionalSweepClient(ctx, region) diff --git a/internal/service/networkfirewall/testdata/Proxy/list_basic/main.tf b/internal/service/networkfirewall/testdata/Proxy/list_basic/main.tf new file mode 100644 index 000000000000..df36f29123f4 --- /dev/null +++ b/internal/service/networkfirewall/testdata/Proxy/list_basic/main.tf @@ -0,0 +1,109 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +data "aws_availability_zones" "available" { + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = var.rName + } +} + +resource "aws_subnet" "public" { + vpc_id = aws_vpc.test.id + cidr_block = "10.0.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = true + + tags = { + Name = "${var.rName}-public" + } +} + +resource "aws_internet_gateway" "test" { + vpc_id = aws_vpc.test.id + + tags = { + Name = var.rName + } +} + +resource "aws_eip" "test" { + count = var.resource_count + + domain = "vpc" + + tags = { + Name = "${var.rName}-${count.index}" + } + + depends_on = [aws_internet_gateway.test] +} + +resource "aws_nat_gateway" "test" { + count = var.resource_count + + allocation_id = aws_eip.test[count.index].id + subnet_id = aws_subnet.public.id + + tags = { + Name = "${var.rName}-${count.index}" + } + + depends_on = [aws_internet_gateway.test] +} + +resource "aws_networkfirewall_proxy_configuration" "test" { + name = var.rName + + default_rule_phase_actions { + post_response = "ALLOW" + pre_dns = "ALLOW" + pre_request = "ALLOW" + } +} + +resource "aws_networkfirewall_proxy" "test" { + count = var.resource_count + + name = "${var.rName}-${count.index}" + nat_gateway_id = aws_nat_gateway.test[count.index].id + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.test.arn + + tls_intercept_properties { + tls_intercept_mode = "DISABLED" + } + + listener_properties { + port = 8080 + type = "HTTP" + } + + listener_properties { + port = 443 + type = "HTTPS" + } +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} diff --git a/internal/service/networkfirewall/testdata/Proxy/list_basic/query.tfquery.hcl b/internal/service/networkfirewall/testdata/Proxy/list_basic/query.tfquery.hcl new file mode 100644 index 000000000000..207e9741c725 --- /dev/null +++ b/internal/service/networkfirewall/testdata/Proxy/list_basic/query.tfquery.hcl @@ -0,0 +1,6 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_networkfirewall_proxy" "test" { + provider = aws +} diff --git a/internal/service/networkfirewall/testdata/Proxy/list_include_resource/main.tf b/internal/service/networkfirewall/testdata/Proxy/list_include_resource/main.tf new file mode 100644 index 000000000000..df36f29123f4 --- /dev/null +++ b/internal/service/networkfirewall/testdata/Proxy/list_include_resource/main.tf @@ -0,0 +1,109 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +data "aws_availability_zones" "available" { + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = var.rName + } +} + +resource "aws_subnet" "public" { + vpc_id = aws_vpc.test.id + cidr_block = "10.0.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = true + + tags = { + Name = "${var.rName}-public" + } +} + +resource "aws_internet_gateway" "test" { + vpc_id = aws_vpc.test.id + + tags = { + Name = var.rName + } +} + +resource "aws_eip" "test" { + count = var.resource_count + + domain = "vpc" + + tags = { + Name = "${var.rName}-${count.index}" + } + + depends_on = [aws_internet_gateway.test] +} + +resource "aws_nat_gateway" "test" { + count = var.resource_count + + allocation_id = aws_eip.test[count.index].id + subnet_id = aws_subnet.public.id + + tags = { + Name = "${var.rName}-${count.index}" + } + + depends_on = [aws_internet_gateway.test] +} + +resource "aws_networkfirewall_proxy_configuration" "test" { + name = var.rName + + default_rule_phase_actions { + post_response = "ALLOW" + pre_dns = "ALLOW" + pre_request = "ALLOW" + } +} + +resource "aws_networkfirewall_proxy" "test" { + count = var.resource_count + + name = "${var.rName}-${count.index}" + nat_gateway_id = aws_nat_gateway.test[count.index].id + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.test.arn + + tls_intercept_properties { + tls_intercept_mode = "DISABLED" + } + + listener_properties { + port = 8080 + type = "HTTP" + } + + listener_properties { + port = 443 + type = "HTTPS" + } +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} diff --git a/internal/service/networkfirewall/testdata/Proxy/list_include_resource/main.tfquery.hcl b/internal/service/networkfirewall/testdata/Proxy/list_include_resource/main.tfquery.hcl new file mode 100644 index 000000000000..f71313cc9857 --- /dev/null +++ b/internal/service/networkfirewall/testdata/Proxy/list_include_resource/main.tfquery.hcl @@ -0,0 +1,8 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_networkfirewall_proxy" "test" { + provider = aws + + include_resource = true +} diff --git a/internal/service/networkfirewall/testdata/Proxy/list_region_override/main.tf b/internal/service/networkfirewall/testdata/Proxy/list_region_override/main.tf new file mode 100644 index 000000000000..7c2455fd0801 --- /dev/null +++ b/internal/service/networkfirewall/testdata/Proxy/list_region_override/main.tf @@ -0,0 +1,127 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +data "aws_availability_zones" "available" { + region = var.region + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +resource "aws_vpc" "test" { + region = var.region + + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = var.rName + } +} + +resource "aws_subnet" "public" { + region = var.region + + vpc_id = aws_vpc.test.id + cidr_block = "10.0.1.0/24" + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = true + + tags = { + Name = "${var.rName}-public" + } +} + +resource "aws_internet_gateway" "test" { + region = var.region + + vpc_id = aws_vpc.test.id + + tags = { + Name = var.rName + } +} + +resource "aws_eip" "test" { + count = var.resource_count + region = var.region + + domain = "vpc" + + tags = { + Name = "${var.rName}-${count.index}" + } + + depends_on = [aws_internet_gateway.test] +} + +resource "aws_nat_gateway" "test" { + count = var.resource_count + region = var.region + + allocation_id = aws_eip.test[count.index].id + subnet_id = aws_subnet.public.id + + tags = { + Name = "${var.rName}-${count.index}" + } + + depends_on = [aws_internet_gateway.test] +} + +resource "aws_networkfirewall_proxy_configuration" "test" { + region = var.region + + name = var.rName + + default_rule_phase_actions { + post_response = "ALLOW" + pre_dns = "ALLOW" + pre_request = "ALLOW" + } +} + +resource "aws_networkfirewall_proxy" "test" { + count = var.resource_count + region = var.region + + name = "${var.rName}-${count.index}" + nat_gateway_id = aws_nat_gateway.test[count.index].id + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.test.arn + + tls_intercept_properties { + tls_intercept_mode = "DISABLED" + } + + listener_properties { + port = 8080 + type = "HTTP" + } + + listener_properties { + port = 443 + type = "HTTPS" + } +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} + +variable "region" { + description = "Region to deploy resource in" + type = string + nullable = false +} diff --git a/internal/service/networkfirewall/testdata/Proxy/list_region_override/main.tfquery.hcl b/internal/service/networkfirewall/testdata/Proxy/list_region_override/main.tfquery.hcl new file mode 100644 index 000000000000..2928e18044e7 --- /dev/null +++ b/internal/service/networkfirewall/testdata/Proxy/list_region_override/main.tfquery.hcl @@ -0,0 +1,10 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_networkfirewall_proxy" "test" { + provider = aws + + config { + region = var.region + } +} diff --git a/internal/service/networkfirewall/testdata/ProxyConfiguration/list_basic/main.tf b/internal/service/networkfirewall/testdata/ProxyConfiguration/list_basic/main.tf new file mode 100644 index 000000000000..7843b92714d5 --- /dev/null +++ b/internal/service/networkfirewall/testdata/ProxyConfiguration/list_basic/main.tf @@ -0,0 +1,26 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_networkfirewall_proxy_configuration" "test" { + count = var.resource_count + + name = "${var.rName}-${count.index}" + + default_rule_phase_actions { + post_response = "ALLOW" + pre_dns = "ALLOW" + pre_request = "ALLOW" + } +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} diff --git a/internal/service/networkfirewall/testdata/ProxyConfiguration/list_basic/query.tfquery.hcl b/internal/service/networkfirewall/testdata/ProxyConfiguration/list_basic/query.tfquery.hcl new file mode 100644 index 000000000000..b79d94160903 --- /dev/null +++ b/internal/service/networkfirewall/testdata/ProxyConfiguration/list_basic/query.tfquery.hcl @@ -0,0 +1,6 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_networkfirewall_proxy_configuration" "test" { + provider = aws +} diff --git a/internal/service/networkfirewall/testdata/ProxyConfiguration/list_include_resource/main.tf b/internal/service/networkfirewall/testdata/ProxyConfiguration/list_include_resource/main.tf new file mode 100644 index 000000000000..7843b92714d5 --- /dev/null +++ b/internal/service/networkfirewall/testdata/ProxyConfiguration/list_include_resource/main.tf @@ -0,0 +1,26 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_networkfirewall_proxy_configuration" "test" { + count = var.resource_count + + name = "${var.rName}-${count.index}" + + default_rule_phase_actions { + post_response = "ALLOW" + pre_dns = "ALLOW" + pre_request = "ALLOW" + } +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} diff --git a/internal/service/networkfirewall/testdata/ProxyConfiguration/list_include_resource/main.tfquery.hcl b/internal/service/networkfirewall/testdata/ProxyConfiguration/list_include_resource/main.tfquery.hcl new file mode 100644 index 000000000000..bfc6d60e3d5a --- /dev/null +++ b/internal/service/networkfirewall/testdata/ProxyConfiguration/list_include_resource/main.tfquery.hcl @@ -0,0 +1,8 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_networkfirewall_proxy_configuration" "test" { + provider = aws + + include_resource = true +} diff --git a/internal/service/networkfirewall/testdata/ProxyConfiguration/list_region_override/main.tf b/internal/service/networkfirewall/testdata/ProxyConfiguration/list_region_override/main.tf new file mode 100644 index 000000000000..4f5fbb0abdd8 --- /dev/null +++ b/internal/service/networkfirewall/testdata/ProxyConfiguration/list_region_override/main.tf @@ -0,0 +1,33 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_networkfirewall_proxy_configuration" "test" { + region = var.region + count = var.resource_count + + name = "${var.rName}-${count.index}" + + default_rule_phase_actions { + post_response = "ALLOW" + pre_dns = "ALLOW" + pre_request = "ALLOW" + } +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} + +variable "region" { + description = "Region to deploy resource in" + type = string + nullable = false +} diff --git a/internal/service/networkfirewall/testdata/ProxyConfiguration/list_region_override/main.tfquery.hcl b/internal/service/networkfirewall/testdata/ProxyConfiguration/list_region_override/main.tfquery.hcl new file mode 100644 index 000000000000..968af900f58d --- /dev/null +++ b/internal/service/networkfirewall/testdata/ProxyConfiguration/list_region_override/main.tfquery.hcl @@ -0,0 +1,10 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_networkfirewall_proxy_configuration" "test" { + provider = aws + + config { + region = var.region + } +} diff --git a/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_basic/main.tf b/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_basic/main.tf new file mode 100644 index 000000000000..22e61dcd85fe --- /dev/null +++ b/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_basic/main.tf @@ -0,0 +1,20 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_networkfirewall_proxy_rule_group" "test" { + count = var.resource_count + + name = "${var.rName}-${count.index}" +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} diff --git a/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_basic/query.tfquery.hcl b/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_basic/query.tfquery.hcl new file mode 100644 index 000000000000..6ff80916e705 --- /dev/null +++ b/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_basic/query.tfquery.hcl @@ -0,0 +1,6 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_networkfirewall_proxy_rule_group" "test" { + provider = aws +} diff --git a/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_include_resource/main.tf b/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_include_resource/main.tf new file mode 100644 index 000000000000..22e61dcd85fe --- /dev/null +++ b/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_include_resource/main.tf @@ -0,0 +1,20 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_networkfirewall_proxy_rule_group" "test" { + count = var.resource_count + + name = "${var.rName}-${count.index}" +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} diff --git a/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_include_resource/main.tfquery.hcl b/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_include_resource/main.tfquery.hcl new file mode 100644 index 000000000000..f63c0b143f68 --- /dev/null +++ b/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_include_resource/main.tfquery.hcl @@ -0,0 +1,8 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_networkfirewall_proxy_rule_group" "test" { + provider = aws + + include_resource = true +} diff --git a/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_region_override/main.tf b/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_region_override/main.tf new file mode 100644 index 000000000000..63f9688e044e --- /dev/null +++ b/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_region_override/main.tf @@ -0,0 +1,27 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +resource "aws_networkfirewall_proxy_rule_group" "test" { + region = var.region + count = var.resource_count + + name = "${var.rName}-${count.index}" +} + +variable "rName" { + description = "Name for resource" + type = string + nullable = false +} + +variable "resource_count" { + description = "Number of resources to create" + type = number + nullable = false +} + +variable "region" { + description = "Region to deploy resource in" + type = string + nullable = false +} diff --git a/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_region_override/main.tfquery.hcl b/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_region_override/main.tfquery.hcl new file mode 100644 index 000000000000..b48864ca17b7 --- /dev/null +++ b/internal/service/networkfirewall/testdata/ProxyRuleGroup/list_region_override/main.tfquery.hcl @@ -0,0 +1,10 @@ +# Copyright IBM Corp. 2014, 2026 +# SPDX-License-Identifier: MPL-2.0 + +list "aws_networkfirewall_proxy_rule_group" "test" { + provider = aws + + config { + region = var.region + } +} diff --git a/website/docs/r/networkfirewall_proxy.html.markdown b/website/docs/r/networkfirewall_proxy.html.markdown new file mode 100644 index 000000000000..b2882164c785 --- /dev/null +++ b/website/docs/r/networkfirewall_proxy.html.markdown @@ -0,0 +1,214 @@ +--- +subcategory: "Network Firewall" +layout: "aws" +page_title: "AWS: aws_networkfirewall_proxy" +description: |- + Manages an AWS Network Firewall Proxy. +--- + +# Resource: aws_networkfirewall_proxy + +Manages an AWS Network Firewall Proxy. + +~> **NOTE:** This resource is in preview and may change before general availability. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_networkfirewall_proxy" "example" { + name = "example" + nat_gateway_id = aws_nat_gateway.example.id + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.example.arn + + tls_intercept_properties { + tls_intercept_mode = "DISABLED" + } + + listener_properties { + port = 8080 + type = "HTTP" + } + + listener_properties { + port = 443 + type = "HTTPS" + } +} +``` + +### With TLS Interception Enabled + +```terraform +resource "aws_networkfirewall_proxy" "example" { + name = "example" + nat_gateway_id = aws_nat_gateway.example.id + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.example.arn + + tls_intercept_properties { + tls_intercept_mode = "ENABLED" + pca_arn = aws_acmpca_certificate_authority.example.arn + } + + listener_properties { + port = 8080 + type = "HTTP" + } + + listener_properties { + port = 443 + type = "HTTPS" + } + + tags = { + Name = "example" + } +} +``` + +### With Logging + +```terraform +resource "aws_networkfirewall_proxy" "example" { + name = "example" + nat_gateway_id = aws_nat_gateway.example.id + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.example.arn + + tls_intercept_properties { + tls_intercept_mode = "DISABLED" + } + + listener_properties { + port = 8080 + type = "HTTP" + } + + listener_properties { + port = 443 + type = "HTTPS" + } +} + +# CloudWatch Logs delivery + +resource "aws_cloudwatch_log_group" "example" { + name = "example" + retention_in_days = 7 +} + +resource "aws_cloudwatch_log_delivery_source" "cwl" { + name = "example-cwl" + log_type = "ALERT_LOGS" + resource_arn = aws_networkfirewall_proxy.example.arn +} + +resource "aws_cloudwatch_log_delivery_destination" "cwl" { + name = "example-cwl" + + delivery_destination_configuration { + destination_resource_arn = aws_cloudwatch_log_group.example.arn + } +} + +resource "aws_cloudwatch_log_delivery" "cwl" { + delivery_source_name = aws_cloudwatch_log_delivery_source.cwl.name + delivery_destination_arn = aws_cloudwatch_log_delivery_destination.cwl.arn +} + +# S3 delivery + +resource "aws_s3_bucket" "example" { + bucket = "example" + force_destroy = true +} + +resource "aws_cloudwatch_log_delivery_source" "s3" { + name = "example-s3" + log_type = "ALLOW_LOGS" + resource_arn = aws_networkfirewall_proxy.example.arn +} + +resource "aws_cloudwatch_log_delivery_destination" "s3" { + name = "example-s3" + + delivery_destination_configuration { + destination_resource_arn = aws_s3_bucket.example.arn + } +} + +resource "aws_cloudwatch_log_delivery" "s3" { + delivery_source_name = aws_cloudwatch_log_delivery_source.s3.name + delivery_destination_arn = aws_cloudwatch_log_delivery_destination.s3.arn +} +``` + +## Argument Reference + +The following arguments are required: + +* `name` - (Required, Forces new resource) Descriptive name of the proxy. +* `nat_gateway_id` - (Required, Forces new resource) ID of the NAT gateway to associate with the proxy. +* `tls_intercept_properties` - (Required) TLS interception properties block. See [TLS Intercept Properties](#tls-intercept-properties) below. + +The following arguments are optional: + +* `listener_properties` - (Optional) One or more listener properties blocks defining the ports and protocols the proxy listens on. See [Listener Properties](#listener-properties) below. +* `proxy_configuration_arn` - (Optional, Forces new resource) ARN of the proxy configuration to use. Exactly one of `proxy_configuration_arn` or `proxy_configuration_name` must be provided. +* `proxy_configuration_name` - (Optional, Forces new resource) Name of the proxy configuration to use. Exactly one of `proxy_configuration_arn` or `proxy_configuration_name` must be provided. +* `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). +* `tags` - (Optional) Map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +### TLS Intercept Properties + +The `tls_intercept_properties` block supports the following: + +* `tls_intercept_mode` - (Optional) TLS interception mode. Valid values: `ENABLED`, `DISABLED`. +* `pca_arn` - (Optional) ARN of the AWS Private Certificate Authority (PCA) used for TLS interception. Required when `tls_intercept_mode` is `ENABLED`. + +### Listener Properties + +Each `listener_properties` block supports the following: + +* `port` - (Required) Port number the proxy listens on. +* `type` - (Required) Protocol type. Valid values: `HTTP`, `HTTPS`. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn` - ARN of the Proxy. +* `create_time` - Creation timestamp of the proxy. +* `id` - ARN of the Proxy (deprecated, use `arn`). +* `private_dns_name` - Private DNS name assigned to the proxy. +* `proxy_configuration_arn` - ARN of the proxy configuration (populated when `proxy_configuration_name` is used). +* `proxy_configuration_name` - Name of the proxy configuration (populated when `proxy_configuration_arn` is used). +* `tags_all` - 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). +* `update_time` - Last update timestamp of the proxy. +* `update_token` - Token for optimistic locking, required for update operations. +* `vpc_endpoint_service_name` - VPC endpoint service name for the proxy. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `60m`) +* `update` - (Default `60m`) +* `delete` - (Default `60m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Network Firewall Proxy using the `arn`. For example: + +```terraform +import { + to = aws_networkfirewall_proxy.example + id = "arn:aws:network-firewall:us-west-2:123456789012:proxy/example" +} +``` + +Using `terraform import`, import Network Firewall Proxy using the `arn`. For example: + +```console +% terraform import aws_networkfirewall_proxy.example arn:aws:network-firewall:us-west-2:123456789012:proxy/example +``` diff --git a/website/docs/r/networkfirewall_proxy_configuration.html.markdown b/website/docs/r/networkfirewall_proxy_configuration.html.markdown new file mode 100644 index 000000000000..4356a8914c03 --- /dev/null +++ b/website/docs/r/networkfirewall_proxy_configuration.html.markdown @@ -0,0 +1,94 @@ +--- +subcategory: "Network Firewall" +layout: "aws" +page_title: "AWS: aws_networkfirewall_proxy_configuration" +description: |- + Manages an AWS Network Firewall Proxy Configuration. +--- + +# Resource: aws_networkfirewall_proxy_configuration + +Manages an AWS Network Firewall Proxy Configuration. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_networkfirewall_proxy_configuration" "example" { + name = "example" + + default_rule_phase_actions { + pre_dns = "ALLOW" + pre_request = "ALLOW" + post_response = "ALLOW" + } +} +``` + +### With Description and Tags + +```terraform +resource "aws_networkfirewall_proxy_configuration" "example" { + name = "example" + description = "Example proxy configuration" + + default_rule_phase_actions { + pre_dns = "DROP" + pre_request = "ALLOW" + post_response = "ALLOW" + } + + tags = { + Name = "example" + Environment = "production" + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `default_rule_phase_actions` - (Required) Default actions to take on proxy traffic. See [Default Rule Phase Actions](#default-rule-phase-actions) below. +* `name` - (Required) Descriptive name of the proxy configuration. + +The following arguments are optional: + +* `description` - (Optional) Description of the proxy configuration. +* `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). +* `tags` - (Optional) Map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +### Default Rule Phase Actions + +The `default_rule_phase_actions` block supports the following: + +* `post_response` - (Required) Default action for the POST_RESPONSE phase. Valid values: `ALLOW`, `DROP`. +* `pre_dns` - (Required) Default action for the PRE_DNS phase. Valid values: `ALLOW`, `DROP`. +* `pre_request` - (Required) Default action for the PRE_REQUEST phase. Valid values: `ALLOW`, `DROP`. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn` - ARN of the Proxy Configuration. +* `id` - ARN of the Proxy Configuration. +* `tags_all` - 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). +* `update_token` - Token used for optimistic locking. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Network Firewall Proxy Configuration using the `arn`. For example: + +```terraform +import { + to = aws_networkfirewall_proxy_configuration.example + id = "arn:aws:network-firewall:us-west-2:123456789012:proxy-configuration/example" +} +``` + +Using `terraform import`, import Network Firewall Proxy Configuration using the `arn`. For example: + +```console +% terraform import aws_networkfirewall_proxy_configuration.example arn:aws:network-firewall:us-west-2:123456789012:proxy-configuration/example +``` diff --git a/website/docs/r/networkfirewall_proxy_configuration_rule_group_attachments_exclusive.html.markdown b/website/docs/r/networkfirewall_proxy_configuration_rule_group_attachments_exclusive.html.markdown new file mode 100644 index 000000000000..aa724717e52a --- /dev/null +++ b/website/docs/r/networkfirewall_proxy_configuration_rule_group_attachments_exclusive.html.markdown @@ -0,0 +1,116 @@ +--- +subcategory: "Network Firewall" +layout: "aws" +page_title: "AWS: aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive" +description: |- + Manages an AWS Network Firewall Proxy Configuration Rule Group Attachments Exclusive resource. +--- + +# Resource: aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive + +Manages an AWS Network Firewall Proxy Configuration Rule Group Attachments Exclusive resource. This resource attaches proxy rule groups to a proxy configuration. + +~> **NOTE:** This resource requires an existing [`aws_networkfirewall_proxy_configuration`](networkfirewall_proxy_configuration.html) and [`aws_networkfirewall_proxy_rule_group`](networkfirewall_proxy_rule_group.html). + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_networkfirewall_proxy_configuration" "example" { + name = "example" + + default_rule_phase_actions { + post_response = "ALLOW" + pre_dns = "ALLOW" + pre_request = "ALLOW" + } +} + +resource "aws_networkfirewall_proxy_rule_group" "example" { + name = "example" +} + +resource "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive" "example" { + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.example.arn + + rule_group { + proxy_rule_group_name = aws_networkfirewall_proxy_rule_group.example.name + } +} +``` + +### Multiple Rule Groups + +```terraform +resource "aws_networkfirewall_proxy_configuration" "example" { + name = "example" + + default_rule_phase_actions { + post_response = "ALLOW" + pre_dns = "ALLOW" + pre_request = "ALLOW" + } +} + +resource "aws_networkfirewall_proxy_rule_group" "first" { + name = "first" +} + +resource "aws_networkfirewall_proxy_rule_group" "second" { + name = "second" +} + +resource "aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive" "example" { + proxy_configuration_arn = aws_networkfirewall_proxy_configuration.example.arn + + rule_group { + proxy_rule_group_name = aws_networkfirewall_proxy_rule_group.first.name + } + + rule_group { + proxy_rule_group_name = aws_networkfirewall_proxy_rule_group.second.name + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `proxy_configuration_arn` - (Required) ARN of the proxy configuration to attach rule groups to. + +The following arguments are optional: + +* `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). +* `rule_group` - (Optional) One or more rule group blocks. See [Rule Group](#rule-group) below. + +### Rule Group + +Each `rule_group` block supports the following: + +* `proxy_rule_group_name` - (Required) Name of the proxy rule group to attach. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `id` - ARN of the Proxy Configuration. +* `update_token` - Token used for optimistic locking. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Network Firewall Proxy Configuration Rule Group Attachments Exclusive using the `proxy_configuration_arn`. For example: + +```terraform +import { + to = aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive.example + id = "arn:aws:network-firewall:us-west-2:123456789012:proxy-configuration/example" +} +``` + +Using `terraform import`, import Network Firewall Proxy Configuration Rule Group Attachments Exclusive using the `proxy_configuration_arn`. For example: + +```console +% terraform import aws_networkfirewall_proxy_configuration_rule_group_attachments_exclusive.example arn:aws:network-firewall:us-west-2:123456789012:proxy-configuration/example +``` diff --git a/website/docs/r/networkfirewall_proxy_rule_group.html.markdown b/website/docs/r/networkfirewall_proxy_rule_group.html.markdown new file mode 100644 index 000000000000..194f87fcc8db --- /dev/null +++ b/website/docs/r/networkfirewall_proxy_rule_group.html.markdown @@ -0,0 +1,75 @@ +--- +subcategory: "Network Firewall" +layout: "aws" +page_title: "AWS: aws_networkfirewall_proxy_rule_group" +description: |- + Manages an AWS Network Firewall Proxy Rule Group resource. +--- + +# Resource: aws_networkfirewall_proxy_rule_group + +Manages an AWS Network Firewall Proxy Rule Group resource. A proxy rule group is a container for proxy rules that can be referenced by a proxy configuration. + +~> **NOTE:** This resource creates an empty proxy rule group. Use the [`aws_networkfirewall_proxy_rules_exclusive`](networkfirewall_proxy_rules_exclusive.html) resource to add rules to the group. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_networkfirewall_proxy_rule_group" "example" { + name = "example" +} +``` + +### With Description and Tags + +```terraform +resource "aws_networkfirewall_proxy_rule_group" "example" { + name = "example" + description = "Example proxy rule group for HTTP traffic" + + tags = { + Name = "example" + Environment = "production" + } +} +``` + +## Argument Reference + +The following arguments are required: + +* `name` - (Required) Descriptive name of the proxy rule group. + +The following arguments are optional: + +* `description` - (Optional) Description of the proxy rule group. +* `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). +* `tags` - (Optional) Map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn` - ARN of the Proxy Rule Group. +* `id` - ARN of the Proxy Rule Group. +* `tags_all` - 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). +* `update_token` - Token used for optimistic locking. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Network Firewall Proxy Rule Group using the `arn`. For example: + +```terraform +import { + to = aws_networkfirewall_proxy_rule_group.example + id = "arn:aws:network-firewall:us-west-2:123456789012:proxy-rule-group/example" +} +``` + +Using `terraform import`, import Network Firewall Proxy Rule Group using the `arn`. For example: + +```console +% terraform import aws_networkfirewall_proxy_rule_group.example arn:aws:network-firewall:us-west-2:123456789012:proxy-rule-group/example +``` diff --git a/website/docs/r/networkfirewall_proxy_rules_exclusive.html.markdown b/website/docs/r/networkfirewall_proxy_rules_exclusive.html.markdown new file mode 100644 index 000000000000..5ec59b64ede0 --- /dev/null +++ b/website/docs/r/networkfirewall_proxy_rules_exclusive.html.markdown @@ -0,0 +1,176 @@ +--- +subcategory: "Network Firewall" +layout: "aws" +page_title: "AWS: aws_networkfirewall_proxy_rules_exclusive" +description: |- + Manages AWS Network Firewall Proxy Rules Exclusive within a Proxy Rule Group. +--- + +# Resource: aws_networkfirewall_proxy_rules_exclusive + +Manages AWS Network Firewall Proxy Rules Exclusive within a Proxy Rule Group. Proxy rules define conditions and actions for HTTP/HTTPS traffic inspection across three request/response phases: PRE_DNS, PRE_REQUEST, and POST_RESPONSE. + +~> **NOTE:** This resource requires an existing [`aws_networkfirewall_proxy_rule_group`](networkfirewall_proxy_rule_group.html). + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_networkfirewall_proxy_rule_group" "example" { + name = "example" +} + +resource "aws_networkfirewall_proxy_rules_exclusive" "example" { + proxy_rule_group_arn = aws_networkfirewall_proxy_rule_group.example.arn + + pre_dns { + proxy_rule_name = "allow-example-com" + action = "ALLOW" + + conditions { + condition_key = "request:DestinationDomain" + condition_operator = "StringEquals" + condition_values = ["example.com"] + } + } +} +``` + +### Multiple Rules Across Phases + +```terraform +resource "aws_networkfirewall_proxy_rule_group" "example" { + name = "example" +} + +resource "aws_networkfirewall_proxy_rules_exclusive" "example" { + proxy_rule_group_arn = aws_networkfirewall_proxy_rule_group.example.arn + + # DNS phase rules + pre_dns { + proxy_rule_name = "block-malicious-domains" + action = "DROP" + description = "Block known malicious domains" + + conditions { + condition_key = "request:DestinationDomain" + condition_operator = "StringEquals" + condition_values = ["malicious.com", "badactor.net"] + } + } + + # Request phase rules + pre_request { + proxy_rule_name = "allow-api-requests" + action = "ALLOW" + description = "Allow API endpoint access" + + conditions { + condition_key = "request:Http:Uri" + condition_operator = "StringEquals" + condition_values = ["/api/v1", "/api/v2"] + } + + conditions { + condition_key = "request:Http:Method" + condition_operator = "StringEquals" + condition_values = ["GET", "POST"] + } + } + + # Response phase rules + post_response { + proxy_rule_name = "block-large-responses" + action = "DROP" + description = "Block responses with status code >= 500" + + conditions { + condition_key = "response:Http:StatusCode" + condition_operator = "NumericGreaterThanEquals" + condition_values = ["500"] + } + } +} +``` + +### Using Proxy Rule Group Name + +```terraform +resource "aws_networkfirewall_proxy_rule_group" "example" { + name = "example" +} + +resource "aws_networkfirewall_proxy_rules_exclusive" "example" { + proxy_rule_group_name = aws_networkfirewall_proxy_rule_group.example.name + + pre_dns { + proxy_rule_name = "allow-corporate-domains" + action = "ALLOW" + + conditions { + condition_key = "request:DestinationDomain" + condition_operator = "StringEquals" + condition_values = ["example.com", "example.org"] + } + } +} +``` + +## Argument Reference + +The following arguments are optional: + +* `post_response` - (Optional) Rules to apply during the POST_RESPONSE phase. See [Rule Configuration](#rule-configuration) below. +* `pre_dns` - (Optional) Rules to apply during the PRE_DNS phase. See [Rule Configuration](#rule-configuration) below. +* `pre_request` - (Optional) Rules to apply during the PRE_REQUEST phase. See [Rule Configuration](#rule-configuration) below. +* `proxy_rule_group_arn` - (Optional) ARN of the proxy rule group. Conflicts with `proxy_rule_group_name`. Required if `proxy_rule_group_name` is not specified. +* `proxy_rule_group_name` - (Optional) Name of the proxy rule group. Conflicts with `proxy_rule_group_arn`. Required if `proxy_rule_group_arn` is not specified. +* `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). + +### Rule Configuration + +Each rule block (`post_response`, `pre_dns`, `pre_request`) supports the following: + +* `action` - (Required) Action to take when conditions match. Valid values: `ALLOW`, `DROP`. +* `conditions` - (Required) One or more condition blocks. See [Conditions](#conditions) below. +* `proxy_rule_name` - (Required) Unique name for the proxy rule within the rule group. +* `description` - (Optional) Description of the rule. +* `insert_position` - (Optional) Position to insert the rule. Rules are evaluated in order. + +### Conditions + +Each `conditions` block supports the following: + +* `condition_key` - (Required) Attribute to evaluate. Valid values include: + - Request-based: `request:SourceAccount`, `request:SourceVpc`, `request:SourceVpce`, `request:Time`, `request:SourceIp`, `request:DestinationIp`, `request:SourcePort`, `request:DestinationPort`, `request:Protocol`, `request:DestinationDomain`, `request:Http:Uri`, `request:Http:Method`, `request:Http:UserAgent`, `request:Http:ContentType`, `request:Http:Header/` + - Response-based: `response:Http:StatusCode`, `response:Http:ContentType`, `response:Http:Header/` + + ~> **NOTE:** HTTP field matching for HTTPS requests requires TLS decryption to be enabled. Without TLS decryption, only IP-based filtering is available in the pre-request phase. +* `condition_operator` - (Required) Comparison operator. Valid values: `StringEquals`, `NumericGreaterThan`, `NumericGreaterThanEquals`. +* `condition_values` - (Required) List of values to compare against. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `id` - ARN of the Proxy Rule Group. +* `proxy_rule_group_arn` - ARN of the Proxy Rule Group (computed if `proxy_rule_group_name` was provided). +* `proxy_rule_group_name` - Name of the Proxy Rule Group (computed if `proxy_rule_group_arn` was provided). + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Network Firewall Proxy Rules Exclusive using the `proxy_rule_group_arn`. For example: + +```terraform +import { + to = aws_networkfirewall_proxy_rules_exclusive.example + id = "arn:aws:network-firewall:us-west-2:123456789012:proxy-rule-group/example" +} +``` + +Using `terraform import`, import Network Firewall Proxy Rules Exclusive using the `proxy_rule_group_arn`. For example: + +```console +% terraform import aws_networkfirewall_proxy_rules_exclusive.example arn:aws:network-firewall:us-west-2:123456789012:proxy-rule-group/example +```