Repository: cycloidio/terracost
Pull request #110 (2023-08-23 11:39:01)

aws: support of nat_gateway


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 098c2e6..7769909 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
 
 ### Added
 
+- AWS support for `aws_nat_gateway`
+  ([Pull #110](https://github.com/cycloidio/terracost/pull/110))
 - AWS support for `aws_eks_cluster`, `aws_eks_node_group`, `aws_efs_file_system`
   ([Pull #97](https://github.com/cycloidio/terracost/pull/97))
 - Support for remote module references
diff --git a/aws/filter.go b/aws/filter.go
index 083cd35..0b34218 100644
--- a/aws/filter.go
+++ b/aws/filter.go
@@ -51,7 +51,7 @@ func minimalFilterEC2(pp *price.WithProduct) bool {
 			}
 		}
 		return true
-	case "Storage", "System Operation":
+	case "Storage", "System Operation", "NAT Gateway":
 		return true
 	default:
 		return false
diff --git a/aws/terraform/nat_gateway.go b/aws/terraform/nat_gateway.go
new file mode 100644
index 0000000..b26c53f
--- /dev/null
+++ b/aws/terraform/nat_gateway.go
@@ -0,0 +1,112 @@
+package terraform
+
+import (
+	"github.com/mitchellh/mapstructure"
+	"github.com/shopspring/decimal"
+
+	"github.com/cycloidio/terracost/aws/region"
+	"github.com/cycloidio/terracost/price"
+	"github.com/cycloidio/terracost/product"
+	"github.com/cycloidio/terracost/query"
+	"github.com/cycloidio/terracost/util"
+)
+
+// NatGateway represents an NatGateway instance definition that can be cost-estimated.
+type NatGateway struct {
+	providerKey string
+	region      region.Code
+
+	// Usage
+	monthlyDataProcessedGB decimal.Decimal
+}
+
+type natGatewayValues struct {
+	Usage struct {
+		MonthlyDataProcessedGB float64 `mapstructure:"monthly_data_processed_gb"`
+	} `mapstructure:"tc_usage"`
+}
+
+func decodeNatGatewayValues(tfVals map[string]interface{}) (natGatewayValues, error) {
+	var v natGatewayValues
+	config := &mapstructure.DecoderConfig{
+		WeaklyTypedInput: true,
+		Result:           &v,
+	}
+
+	decoder, err := mapstructure.NewDecoder(config)
+	if err != nil {
+		return v, err
+	}
+
+	if err := decoder.Decode(tfVals); err != nil {
+		return v, err
+	}
+	return v, nil
+}
+
+// NewInstance creates a new Instance from Terraform values.
+func (p *Provider) newNatGateway(vals natGatewayValues) *NatGateway {
+
+	inst := &NatGateway{
+		providerKey:            p.key,
+		region:                 p.region,
+		monthlyDataProcessedGB: decimal.NewFromFloat(vals.Usage.MonthlyDataProcessedGB),
+	}
+
+	return inst
+}
+
+// Components returns the price component queries that make up this Instance.
+func (inst *NatGateway) Components() []query.Component {
+	components := []query.Component{inst.natGatewayInstanceComponent()}
+	components = append(components, inst.natGatewayDataProcessedComponent())
+
+	return components
+}
+
+func (inst *NatGateway) natGatewayInstanceComponent() query.Component {
+	return query.Component{
+		Name:           "NAT gateway",
+		Details:        []string{"NatGateway"},
+		HourlyQuantity: decimal.NewFromInt(1),
+		ProductFilter: &product.Filter{
+			Provider: util.StringPtr(inst.providerKey),
+			Service:  util.StringPtr("AmazonEC2"),
+			Family:   util.StringPtr("NAT Gateway"),
+			Location: util.StringPtr(inst.region.String()),
+			AttributeFilters: []*product.AttributeFilter{
+				{Key: "UsageType", ValueRegex: util.StringPtr(".*NatGateway-Hours")},
+			},
+		},
+		PriceFilter: &price.Filter{
+			Unit: util.StringPtr("Hrs"),
+			AttributeFilters: []*price.AttributeFilter{
+				{Key: "TermType", Value: util.StringPtr("OnDemand")},
+			},
+		},
+	}
+}
+
+func (inst *NatGateway) natGatewayDataProcessedComponent() query.Component {
+	return query.Component{
+		Name:            "NAT Data processed",
+		Details:         []string{"NatGateway Data processed"},
+		Usage:           true,
+		MonthlyQuantity: inst.monthlyDataProcessedGB,
+		ProductFilter: &product.Filter{
+			Provider: util.StringPtr(inst.providerKey),
+			Service:  util.StringPtr("AmazonEC2"),
+			Family:   util.StringPtr("NAT Gateway"),
+			Location: util.StringPtr(inst.region.String()),
+			AttributeFilters: []*product.AttributeFilter{
+				{Key: "UsageType", ValueRegex: util.StringPtr(".*NatGateway-Bytes")},
+			},
+		},
+		PriceFilter: &price.Filter{
+			Unit: util.StringPtr("GB"),
+			AttributeFilters: []*price.AttributeFilter{
+				{Key: "TermType", Value: util.StringPtr("OnDemand")},
+			},
+		},
+	}
+}
diff --git a/aws/terraform/provider.go b/aws/terraform/provider.go
index 7ad4ad5..eff553e 100644
--- a/aws/terraform/provider.go
+++ b/aws/terraform/provider.go
@@ -77,12 +77,6 @@ func (p *Provider) ResourceComponents(rss map[string]terraform.Resource, tfRes t
 			return nil
 		}
 		return p.newDBInstance(vals).Components()
-	case "aws_lb", "aws_alb":
-		vals, err := decodeLBValues(tfRes.Values)
-		if err != nil {
-			return nil
-		}
-		return p.newLB(vals).Components()
 	case "aws_elb":
 		// ELB Classic does not have any special configuration.
 		vals := lbValues{LoadBalancerType: "classic"}
@@ -99,6 +93,18 @@ func (p *Provider) ResourceComponents(rss map[string]terraform.Resource, tfRes t
 			return nil
 		}
 		return p.newEKSNodeGroup(rss, vals).Components()
+	case "aws_lb", "aws_alb":
+		vals, err := decodeLBValues(tfRes.Values)
+		if err != nil {
+			return nil
+		}
+		return p.newLB(vals).Components()
+	case "aws_nat_gateway":
+		vals, err := decodeNatGatewayValues(tfRes.Values)
+		if err != nil {
+			return nil
+		}
+		return p.newNatGateway(vals).Components()
 	default:
 		return nil
 	}
diff --git a/usage/usage.go b/usage/usage.go
index 0ce1499..11a709d 100644
--- a/usage/usage.go
+++ b/usage/usage.go
@@ -19,10 +19,13 @@ var Default = Usage{
 			"vcpu_count":                       2,
 		},
 		"aws_efs_file_system": map[string]interface{}{
-			"storage_gb":                         230,
-			"infrequent_access_storage_gb":       100,
-			"monthly_infrequent_access_read_gb":  50,
-			"monthly_infrequent_access_write_gb": 100,
+			"storage_gb":                         180,
+			"infrequent_access_storage_gb":       10,
+			"monthly_infrequent_access_read_gb":  20,
+			"monthly_infrequent_access_write_gb": 30,
+		},
+		"aws_nat_gateway": map[string]interface{}{
+			"monthly_data_processed_gb": 10,
 		},
 	},
 }


Test patch:
diff --git a/aws/terraform/efs_file_system_test.go b/aws/terraform/efs_file_system_test.go
index 201f732..6507482 100644
--- a/aws/terraform/efs_file_system_test.go
+++ b/aws/terraform/efs_file_system_test.go
@@ -32,7 +32,7 @@ func TestEFSFileSystem_Components(t *testing.T) {
 		expected := []query.Component{
 			{
 				Name:            "Storage .*-TimedStorage-ByteHrs",
-				MonthlyQuantity: decimal.NewFromFloat(230),
+				MonthlyQuantity: decimal.NewFromFloat(180),
 				Unit:            "GB",
 				Details:         []string{"EFS storage", ".*-TimedStorage-ByteHrs"},
 				Usage:           true,
@@ -74,7 +74,7 @@ func TestEFSFileSystem_Components(t *testing.T) {
 		expected := []query.Component{
 			{
 				Name:            "Storage .*-TimedStorage-ByteHrs",
-				MonthlyQuantity: decimal.NewFromFloat(230),
+				MonthlyQuantity: decimal.NewFromFloat(180),
 				Unit:            "GB",
 				Details:         []string{"EFS storage", ".*-TimedStorage-ByteHrs"},
 				Usage:           true,
@@ -90,7 +90,7 @@ func TestEFSFileSystem_Components(t *testing.T) {
 			},
 			{
 				Name:            "Provisioned throughput",
-				MonthlyQuantity: decimal.NewFromFloat(8.5),
+				MonthlyQuantity: decimal.NewFromFloat(11),
 				Unit:            "MBps",
 				Details:         []string{"Througput"},
 				Usage:           true,
@@ -106,7 +106,7 @@ func TestEFSFileSystem_Components(t *testing.T) {
 			},
 			{
 				Name:            "Storage .*-IATimedStorage-ByteHrs",
-				MonthlyQuantity: decimal.NewFromFloat(100),
+				MonthlyQuantity: decimal.NewFromFloat(10),
 				Unit:            "GB", Details: []string{"EFS storage", ".*-IATimedStorage-ByteHrs"},
 				Usage: true,
 				ProductFilter: &product.Filter{
@@ -121,7 +121,7 @@ func TestEFSFileSystem_Components(t *testing.T) {
 			},
 			{
 				Name:            "Requests Read",
-				MonthlyQuantity: decimal.NewFromFloat(50),
+				MonthlyQuantity: decimal.NewFromFloat(20),
 				Unit:            "GB",
 				Details:         []string{"Requests", "Infrequent Access", "Read"},
 				Usage:           true,
@@ -138,7 +138,7 @@ func TestEFSFileSystem_Components(t *testing.T) {
 			},
 			{
 				Name:            "Requests Write",
-				MonthlyQuantity: decimal.NewFromFloat(100),
+				MonthlyQuantity: decimal.NewFromFloat(30),
 				Unit:            "GB",
 				Details:         []string{"Requests", "Infrequent Access", "Write"},
 				Usage:           true,
diff --git a/aws/terraform/nat_gateway_test.go b/aws/terraform/nat_gateway_test.go
new file mode 100644
index 0000000..863738c
--- /dev/null
+++ b/aws/terraform/nat_gateway_test.go
@@ -0,0 +1,84 @@
+package terraform
+
+import (
+	"testing"
+
+	"github.com/shopspring/decimal"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/cycloidio/terracost/price"
+	"github.com/cycloidio/terracost/product"
+	"github.com/cycloidio/terracost/query"
+	"github.com/cycloidio/terracost/terraform"
+	"github.com/cycloidio/terracost/usage"
+	"github.com/cycloidio/terracost/util"
+)
+
+func TestNatGateway_Components(t *testing.T) {
+	p, err := NewProvider("aws", "us-east-1")
+	require.NoError(t, err)
+
+	t.Run("NAT", func(t *testing.T) {
+		tfres := terraform.Resource{
+			Address:      "aws_nat_gateway.test",
+			Type:         "aws_nat_gateway",
+			Name:         "test",
+			ProviderName: "aws",
+			Values: map[string]interface{}{
+				"allocation_id": "id",
+			},
+		}
+		rss := map[string]terraform.Resource{}
+
+		expected := []query.Component{
+			{
+				Name:           "NAT gateway",
+				Details:        []string{"NatGateway"},
+				HourlyQuantity: decimal.NewFromInt(1),
+				ProductFilter: &product.Filter{
+					Provider: util.StringPtr("aws"),
+					Service:  util.StringPtr("AmazonEC2"),
+					Family:   util.StringPtr("NAT Gateway"),
+					Location: util.StringPtr("us-east-1"),
+					AttributeFilters: []*product.AttributeFilter{
+						{Key: "UsageType", ValueRegex: util.StringPtr(".*NatGateway-Hours")},
+					},
+				},
+				PriceFilter: &price.Filter{
+					Unit: util.StringPtr("Hrs"),
+					AttributeFilters: []*price.AttributeFilter{
+						{Key: "TermType", Value: util.StringPtr("OnDemand")},
+					},
+				},
+			},
+
+			{
+				Name:            "NAT Data processed",
+				Details:         []string{"NatGateway Data processed"},
+				Usage:           true,
+				MonthlyQuantity: decimal.NewFromFloat(10),
+				ProductFilter: &product.Filter{
+					Provider: util.StringPtr("aws"),
+					Service:  util.StringPtr("AmazonEC2"),
+					Family:   util.StringPtr("NAT Gateway"),
+					Location: util.StringPtr("us-east-1"),
+					AttributeFilters: []*product.AttributeFilter{
+						{Key: "UsageType", ValueRegex: util.StringPtr(".*NatGateway-Bytes")},
+					},
+				},
+				PriceFilter: &price.Filter{
+					Unit: util.StringPtr("GB"),
+					AttributeFilters: []*price.AttributeFilter{
+						{Key: "TermType", Value: util.StringPtr("OnDemand")},
+					},
+				},
+			},
+		}
+
+		us := usage.Default.GetUsage("aws_nat_gateway")
+		tfres.Values[usage.Key] = us
+		actual := p.ResourceComponents(rss, tfres)
+		assert.Equal(t, expected, actual)
+	})
+}