Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#38917 #39197 Fixing the expand function and the remove functi… #40092

3 changes: 3 additions & 0 deletions .changelog/40092.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/aws_networkfirewall_logging_configuration: Correctly manage all configured `logging_configuration.log_destination_config`s
```
240 changes: 101 additions & 139 deletions internal/service/networkfirewall/logging_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ package networkfirewall

import (
"context"
"errors"
"fmt"
"log"
"slices"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/networkfirewall"
Expand Down Expand Up @@ -51,11 +51,9 @@ func resourceLoggingConfiguration() *schema.Resource {
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"log_destination_config": {
// At most 3 configurations can exist,
// with 1 destination for FLOW logs and 1 for ALERT logs and 1 for TLS Logs
Type: schema.TypeSet,
Required: true,
MaxItems: 3,
MaxItems: len(enum.Values[awstypes.LogType]()),
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"log_destination": {
Expand All @@ -80,6 +78,33 @@ func resourceLoggingConfiguration() *schema.Resource {
},
},
},

CustomizeDiff: func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error {
// Ensure distinct logging_configuration.log_destination_config.log_type values.
if v, ok := d.GetOk(names.AttrLoggingConfiguration); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
tfMap := v.([]interface{})[0].(map[string]interface{})

if v, ok := tfMap["log_destination_config"].(*schema.Set); ok && v.Len() > 0 {
logTypes := make(map[string]struct{})

for _, tfMapRaw := range v.List() {
tfMap, ok := tfMapRaw.(map[string]interface{})
if !ok {
continue
}

if v, ok := tfMap["log_type"].(string); ok && v != "" {
if _, ok := logTypes[v]; ok {
return fmt.Errorf("duplicate logging_configuration.log_destination_config.log_type value: %s", v)
}
logTypes[v] = struct{}{}
}
}
}
}

return nil
},
}
}

Expand All @@ -88,9 +113,15 @@ func resourceLoggingConfigurationCreate(ctx context.Context, d *schema.ResourceD
conn := meta.(*conns.AWSClient).NetworkFirewallClient(ctx)

firewallARN := d.Get("firewall_arn").(string)
loggingConfigs := expandLoggingConfigurations(d.Get(names.AttrLoggingConfiguration).([]interface{}))
if err := addLoggingConfigurations(ctx, conn, firewallARN, loggingConfigs); err != nil {
return sdkdiag.AppendFromErr(diags, err)

if v, ok := d.GetOk(names.AttrLoggingConfiguration); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
tfMap := v.([]interface{})[0].(map[string]interface{})

if v, ok := tfMap["log_destination_config"].(*schema.Set); ok && v.Len() > 0 {
if err := addLogDestinationConfigs(ctx, conn, firewallARN, &awstypes.LoggingConfiguration{}, expandLogDestinationConfigs(v.List())); err != nil {
return sdkdiag.AppendFromErr(diags, err)
}
}
}

d.SetId(firewallARN)
Expand Down Expand Up @@ -126,23 +157,22 @@ func resourceLoggingConfigurationUpdate(ctx context.Context, d *schema.ResourceD
var diags diag.Diagnostics
conn := meta.(*conns.AWSClient).NetworkFirewallClient(ctx)

o, n := d.GetChange(names.AttrLoggingConfiguration)
output, err := findLoggingConfigurationByARN(ctx, conn, d.Id())

// Remove destination configs one by one, if any.
if oldConfig := o.([]interface{}); len(oldConfig) != 0 && oldConfig[0] != nil {
if loggingConfig := expandLoggingConfigurationOnUpdate(oldConfig); loggingConfig != nil {
if err := removeLoggingConfiguration(ctx, conn, d.Id(), loggingConfig); err != nil {
return sdkdiag.AppendFromErr(diags, err)
}
}
if err != nil {
return sdkdiag.AppendErrorf(diags, "reading NetworkFirewall Logging Configuration (%s): %s", d.Id(), err)
}

// Only send new LoggingConfiguration with content.
if newConfig := n.([]interface{}); len(newConfig) != 0 && newConfig[0] != nil {
loggingConfigs := expandLoggingConfigurations(d.Get(names.AttrLoggingConfiguration).([]interface{}))
if err := addLoggingConfigurations(ctx, conn, d.Id(), loggingConfigs); err != nil {
return sdkdiag.AppendFromErr(diags, err)
}
o, n := d.GetChange("logging_configuration.0.log_destination_config")
os, ns := o.(*schema.Set), n.(*schema.Set)
add, del := ns.Difference(os), os.Difference(ns)

if err := deleteLogDestinationConfigs(ctx, conn, d.Id(), output.LoggingConfiguration, expandLogDestinationConfigs(del.List())); err != nil {
return sdkdiag.AppendFromErr(diags, err)
}

if err := addLogDestinationConfigs(ctx, conn, d.Id(), output.LoggingConfiguration, expandLogDestinationConfigs(add.List())); err != nil {
return sdkdiag.AppendFromErr(diags, err)
}

return append(diags, resourceLoggingConfigurationRead(ctx, d, meta)...)
Expand All @@ -162,64 +192,61 @@ func resourceLoggingConfigurationDelete(ctx context.Context, d *schema.ResourceD
return sdkdiag.AppendErrorf(diags, "reading NetworkFirewall Logging Configuration (%s): %s", d.Id(), err)
}

if output != nil && output.LoggingConfiguration != nil {
log.Printf("[DEBUG] Deleting NetworkFirewall Logging Configuration: %s", d.Id())
err := removeLoggingConfiguration(ctx, conn, d.Id(), output.LoggingConfiguration)
if err != nil {
return sdkdiag.AppendFromErr(diags, err)
log.Printf("[DEBUG] Deleting NetworkFirewall Logging Configuration: %s", d.Id())
if v, ok := d.GetOk(names.AttrLoggingConfiguration); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil {
tfMap := v.([]interface{})[0].(map[string]interface{})

if v, ok := tfMap["log_destination_config"].(*schema.Set); ok && v.Len() > 0 {
if err := deleteLogDestinationConfigs(ctx, conn, d.Id(), output.LoggingConfiguration, expandLogDestinationConfigs(v.List())); err != nil {
return sdkdiag.AppendFromErr(diags, err)
}
}
}

return diags
}

func addLoggingConfigurations(ctx context.Context, conn *networkfirewall.Client, arn string, loggingConfigs []*awstypes.LoggingConfiguration) error {
var errs []error
// See https://docs.aws.amazon.com/network-firewall/latest/APIReference/API_UpdateLoggingConfiguration.html.
// The logging configuration is changed one LogDestinationConfig at a time.

func addLogDestinationConfigs(ctx context.Context, conn *networkfirewall.Client, firewallARN string, loggingConfiguration *awstypes.LoggingConfiguration, logDestinationConfigs []awstypes.LogDestinationConfig) error {
for _, logDestinationConfig := range logDestinationConfigs {
loggingConfiguration.LogDestinationConfigs = append(loggingConfiguration.LogDestinationConfigs, logDestinationConfig)

for _, loggingConfig := range loggingConfigs {
input := &networkfirewall.UpdateLoggingConfigurationInput{
FirewallArn: aws.String(arn),
LoggingConfiguration: loggingConfig,
FirewallArn: aws.String(firewallARN),
LoggingConfiguration: loggingConfiguration,
}

_, err := conn.UpdateLoggingConfiguration(ctx, input)

if err != nil {
errs = append(errs, fmt.Errorf("adding NetworkFirewall Logging Configuration (%s): %w", arn, err))
return fmt.Errorf("adding NetworkFirewall Logging Configuration (%s): %w", firewallARN, err)
}
}

return errors.Join(errs...)
return nil
}

func removeLoggingConfiguration(ctx context.Context, conn *networkfirewall.Client, arn string, loggingConfig *awstypes.LoggingConfiguration) error {
if loggingConfig == nil {
return nil
}

var errs []error
func deleteLogDestinationConfigs(ctx context.Context, conn *networkfirewall.Client, firewallARN string, loggingConfiguration *awstypes.LoggingConfiguration, logDestinationConfigs []awstypes.LogDestinationConfig) error {
for _, logDestinationConfig := range logDestinationConfigs {
loggingConfiguration.LogDestinationConfigs = slices.DeleteFunc(loggingConfiguration.LogDestinationConfigs, func(v awstypes.LogDestinationConfig) bool {
return v.LogType == logDestinationConfig.LogType
})

// Must delete destination configs one at a time.
for i, logDestinationConfig := range loggingConfig.LogDestinationConfigs {
input := &networkfirewall.UpdateLoggingConfigurationInput{
FirewallArn: aws.String(arn),
}

if i == 0 && len(loggingConfig.LogDestinationConfigs) == 2 {
loggingConfig := &awstypes.LoggingConfiguration{
LogDestinationConfigs: []awstypes.LogDestinationConfig{logDestinationConfig},
}
input.LoggingConfiguration = loggingConfig
FirewallArn: aws.String(firewallARN),
LoggingConfiguration: loggingConfiguration,
}

_, err := conn.UpdateLoggingConfiguration(ctx, input)

if err != nil {
errs = append(errs, fmt.Errorf("removing NetworkFirewall Logging Configuration (%s): %w", arn, err))
return fmt.Errorf("deleting NetworkFirewall Logging Configuration (%s): %w", firewallARN, err)
}
}

return errors.Join(errs...)
return nil
}

func findLoggingConfigurationByARN(ctx context.Context, conn *networkfirewall.Client, arn string) (*networkfirewall.DescribeLoggingConfigurationOutput, error) {
Expand All @@ -240,111 +267,46 @@ func findLoggingConfigurationByARN(ctx context.Context, conn *networkfirewall.Cl
return nil, err
}

if output == nil || output.LoggingConfiguration == nil {
if output == nil || output.LoggingConfiguration == nil || len(output.LoggingConfiguration.LogDestinationConfigs) == 0 {
return nil, tfresource.NewEmptyResultError(input)
}

return output, nil
}

func expandLoggingConfigurations(tfList []interface{}) []*awstypes.LoggingConfiguration {
if len(tfList) == 0 || tfList[0] == nil {
return nil
}

tfMap, ok := tfList[0].(map[string]interface{})
if !ok {
func expandLogDestinationConfigs(tfList []interface{}) []awstypes.LogDestinationConfig {
if len(tfList) == 0 {
return nil
}

apiObjects := make([]*awstypes.LoggingConfiguration, 0)

if v, ok := tfMap["log_destination_config"].(*schema.Set); ok && v.Len() > 0 {
for _, tfMapRaw := range v.List() {
tfMap, ok := tfMapRaw.(map[string]interface{})
if !ok {
continue
}

logDestinationConfig := awstypes.LogDestinationConfig{}

if v, ok := tfMap["log_destination"].(map[string]interface{}); ok && len(v) > 0 {
logDestinationConfig.LogDestination = flex.ExpandStringValueMap(v)
}
if v, ok := tfMap["log_destination_type"].(string); ok && v != "" {
logDestinationConfig.LogDestinationType = awstypes.LogDestinationType(v)
}
if v, ok := tfMap["log_type"].(string); ok && v != "" {
logDestinationConfig.LogType = awstypes.LogType(v)
}
var apiObjects []awstypes.LogDestinationConfig

// Exclude empty LogDestinationConfig due to TypeMap in TypeSet behavior.
// Related: https://github.com/hashicorp/terraform-plugin-sdk/issues/588.
if logDestinationConfig.LogDestination == nil && logDestinationConfig.LogDestinationType == "" && logDestinationConfig.LogType == "" {
continue
}

apiObject := &awstypes.LoggingConfiguration{}
// Include all (max 2) "log_destination_config" i.e. prepend the already-expanded loggingConfig.
if len(apiObjects) == 1 && len(apiObjects[0].LogDestinationConfigs) == 1 {
apiObject.LogDestinationConfigs = append(apiObject.LogDestinationConfigs, apiObjects[0].LogDestinationConfigs[0])
}
apiObject.LogDestinationConfigs = append(apiObject.LogDestinationConfigs, logDestinationConfig)

apiObjects = append(apiObjects, apiObject)
for _, tfMapRaw := range tfList {
tfMap, ok := tfMapRaw.(map[string]interface{})
if !ok {
continue
}
}

return apiObjects
}

func expandLoggingConfigurationOnUpdate(tfList []interface{}) *awstypes.LoggingConfiguration {
if len(tfList) == 0 || tfList[0] == nil {
return nil
}

tfMap, ok := tfList[0].(map[string]interface{})
if !ok {
return nil
}

apiObject := &awstypes.LoggingConfiguration{}

if v, ok := tfMap["log_destination_config"].(*schema.Set); ok && v.Len() > 0 {
tfList := v.List()
logDestinationConfigs := make([]awstypes.LogDestinationConfig, 0, len(tfList))

for _, tfMapRaw := range tfList {
tfMap, ok := tfMapRaw.(map[string]interface{})
if !ok {
continue
}
apiObject := awstypes.LogDestinationConfig{}

logDestinationConfig := awstypes.LogDestinationConfig{}

if v, ok := tfMap["log_destination"].(map[string]interface{}); ok && len(v) > 0 {
logDestinationConfig.LogDestination = flex.ExpandStringValueMap(v)
}
if v, ok := tfMap["log_destination_type"].(string); ok && v != "" {
logDestinationConfig.LogDestinationType = awstypes.LogDestinationType(v)
}
if v, ok := tfMap["log_type"].(string); ok && v != "" {
logDestinationConfig.LogType = awstypes.LogType(v)
}
if v, ok := tfMap["log_destination"].(map[string]interface{}); ok && len(v) > 0 {
apiObject.LogDestination = flex.ExpandStringValueMap(v)
}

// Exclude empty LogDestinationConfig due to TypeMap in TypeSet behavior.
// Related: https://github.com/hashicorp/terraform-plugin-sdk/issues/588.
if logDestinationConfig.LogDestination == nil && logDestinationConfig.LogDestinationType == "" && logDestinationConfig.LogType == "" {
continue
}
if v, ok := tfMap["log_destination_type"].(string); ok && v != "" {
apiObject.LogDestinationType = awstypes.LogDestinationType(v)
}

logDestinationConfigs = append(logDestinationConfigs, logDestinationConfig)
if v, ok := tfMap["log_type"].(string); ok && v != "" {
apiObject.LogType = awstypes.LogType(v)
} else {
continue
}

apiObject.LogDestinationConfigs = logDestinationConfigs
apiObjects = append(apiObjects, apiObject)
}

return apiObject
return apiObjects
}

func flattenLoggingConfiguration(apiObject *awstypes.LoggingConfiguration) []interface{} {
Expand All @@ -353,13 +315,13 @@ func flattenLoggingConfiguration(apiObject *awstypes.LoggingConfiguration) []int
}

tfMap := map[string]interface{}{
"log_destination_config": flattenLoggingConfigurationLogDestinationConfigs(apiObject.LogDestinationConfigs),
"log_destination_config": flattenLogDestinationConfigs(apiObject.LogDestinationConfigs),
}

return []interface{}{tfMap}
}

func flattenLoggingConfigurationLogDestinationConfigs(apiObjects []awstypes.LogDestinationConfig) []interface{} {
func flattenLogDestinationConfigs(apiObjects []awstypes.LogDestinationConfig) []interface{} {
tfList := make([]interface{}, 0, len(apiObjects))

for _, apiObject := range apiObjects {
Expand Down
Loading
Loading