Repository: cycloidio/terracost
Pull request #32 (2021-03-29 13:36:56)

aws: add support for aws_lb/aws_alb/aws_elb resources
This PR adds support for the following resources from the `AWSELB` service:
- `aws_elb` - Classic Load Balancer
- `aws_lb`/`aws_alb` - Load Balancer v2

In order to estimate these properly, the `AWSELB` service pricing must be ingested first.


xescugc: And the CI is failing
patrislav: Rebased

Patch:
diff --git a/aws/filter.go b/aws/filter.go
index e66baae..cab7c35 100644
--- a/aws/filter.go
+++ b/aws/filter.go
@@ -20,6 +20,8 @@ func MinimalFilter(pp *price.WithProduct) bool {
 		return minimalFilterEC2(pp)
 	case "AmazonRDS":
 		return minimalFilterRDS(pp)
+	case "AWSELB":
+		return true // the ELB service is minimal already
 	default:
 		return false
 	}
diff --git a/aws/terraform/lb.go b/aws/terraform/lb.go
new file mode 100644
index 0000000..893a398
--- /dev/null
+++ b/aws/terraform/lb.go
@@ -0,0 +1,86 @@
+package terraform
+
+import (
+	"github.com/cycloidio/terracost/price"
+	"github.com/cycloidio/terracost/product"
+	"github.com/cycloidio/terracost/query"
+	"github.com/cycloidio/terracost/util"
+	"github.com/mitchellh/mapstructure"
+	"github.com/shopspring/decimal"
+
+	"github.com/cycloidio/terracost/aws/region"
+)
+
+// LB represents a Load Balancer definition that can be cost-estimated.
+type LB struct {
+	provider *Provider
+	region   region.Code
+
+	// lbType describes the type of the Load Balancer.
+	// Valid values: "application", "gateway", "network".
+	// A special value of "classic" is allowed to represent a Classic Load Balancer.
+	lbType string
+}
+
+// lbValues represents the structure of Terraform values for aws_lb/aws_alb resource.
+type lbValues struct {
+	LoadBalancerType string `mapstructure:"load_balancer_type"`
+}
+
+// decodeLBValues decodes and returns lbValues from a Terraform values map.
+func decodeLBValues(tfVals map[string]interface{}) (lbValues, error) {
+	var v lbValues
+	if err := mapstructure.Decode(tfVals, &v); err != nil {
+		return v, err
+	}
+	return v, nil
+}
+
+// newLB created a new LB from lbValues.
+func (p *Provider) newLB(vals lbValues) *LB {
+	return &LB{
+		provider: p,
+		region:   p.region,
+		lbType:   vals.LoadBalancerType,
+	}
+}
+
+// Components returns the price component queries that make up this LB.
+func (lb *LB) Components() []query.Component {
+	return []query.Component{lb.loadBalancerComponent()}
+}
+
+func (lb *LB) loadBalancerComponent() query.Component {
+	var family, name string
+	switch lb.lbType {
+	case "network":
+		name = "Network Load Balancer"
+		family = "Load Balancer-Network"
+	case "gateway":
+		name = "Gateway Load Balancer"
+		family = "Load Balancer-Gateway"
+	case "classic":
+		name = "Classic Load Balancer"
+		family = "Load Balancer"
+	default:
+		name = "Application Load Balancer"
+		family = "Load Balancer-Application"
+	}
+
+	return query.Component{
+		Name:           name,
+		HourlyQuantity: decimal.NewFromInt(1),
+		ProductFilter: &product.Filter{
+			Provider: util.StringPtr(lb.provider.key),
+			Service:  util.StringPtr("AWSELB"),
+			Family:   util.StringPtr(family),
+			Location: util.StringPtr(lb.region.String()),
+			AttributeFilters: []*product.AttributeFilter{
+				{Key: "usagetype", ValueRegex: util.StringPtr("LoadBalancerUsage")},
+			},
+		},
+		PriceFilter: &price.Filter{
+			Unit: util.StringPtr("Hrs"),
+		},
+	}
+}
diff --git a/aws/terraform/provider.go b/aws/terraform/provider.go
index 7cbd790..fae713d 100644
--- a/aws/terraform/provider.go
+++ b/aws/terraform/provider.go
@@ -44,6 +44,16 @@ func (p *Provider) ResourceComponents(tfRes terraform.Resource) []query.Componen
 			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"}
+		return p.newLB(vals).Components()
 	default:
 		return nil
 	}


Test patch:
diff --git a/aws/terraform/lb_test.go b/aws/terraform/lb_test.go
new file mode 100644
index 0000000..f5d8ca7
--- /dev/null
+++ b/aws/terraform/lb_test.go
@@ -0,0 +1,149 @@
+package terraform_test
+
+import (
+	"testing"
+
+	"github.com/cycloidio/terracost/price"
+	"github.com/cycloidio/terracost/product"
+	"github.com/cycloidio/terracost/query"
+	"github.com/cycloidio/terracost/terraform"
+	"github.com/cycloidio/terracost/util"
+	"github.com/shopspring/decimal"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	awstf "github.com/cycloidio/terracost/aws/terraform"
+)
+
+func TestLB_Components(t *testing.T) {
+	p, err := awstf.NewProvider("aws", "eu-west-3")
+	require.NoError(t, err)
+
+	t.Run("DefaultValues", func(t *testing.T) {
+		tfres := terraform.Resource{
+			Address:      "aws_lb.test",
+			Type:         "aws_lb",
+			Name:         "test",
+			ProviderName: "aws",
+			Values:       map[string]interface{}{},
+		}
+		expected := []query.Component{
+			{
+				Name:           "Application Load Balancer",
+				HourlyQuantity: decimal.NewFromInt(1),
+				ProductFilter: &product.Filter{
+					Provider: util.StringPtr("aws"),
+					Service:  util.StringPtr("AWSELB"),
+					Family:   util.StringPtr("Load Balancer-Application"),
+					Location: util.StringPtr("eu-west-3"),
+					AttributeFilters: []*product.AttributeFilter{
+						{Key: "usagetype", ValueRegex: util.StringPtr("LoadBalancerUsage")},
+					},
+				},
+				PriceFilter: &price.Filter{
+					Unit: util.StringPtr("Hrs"),
+				},
+			},
+		}
+
+		actual := p.ResourceComponents(tfres)
+		assert.Equal(t, expected, actual)
+	})
+
+	t.Run("NetworkLoadBalancer", func(t *testing.T) {
+		tfres := terraform.Resource{
+			Address:      "aws_lb.test",
+			Type:         "aws_lb",
+			Name:         "test",
+			ProviderName: "aws",
+			Values: map[string]interface{}{
+				"load_balancer_type": "network",
+			},
+		}
+		expected := []query.Component{
+			{
+				Name:           "Network Load Balancer",
+				HourlyQuantity: decimal.NewFromInt(1),
+				ProductFilter: &product.Filter{
+					Provider: util.StringPtr("aws"),
+					Service:  util.StringPtr("AWSELB"),
+					Family:   util.StringPtr("Load Balancer-Network"),
+					Location: util.StringPtr("eu-west-3"),
+					AttributeFilters: []*product.AttributeFilter{
+						{Key: "usagetype", ValueRegex: util.StringPtr("LoadBalancerUsage")},
+					},
+				},
+				PriceFilter: &price.Filter{
+					Unit: util.StringPtr("Hrs"),
+				},
+			},
+		}
+
+		actual := p.ResourceComponents(tfres)
+		assert.Equal(t, expected, actual)
+	})
+
+	t.Run("GatewayLoadBalancer", func(t *testing.T) {
+		tfres := terraform.Resource{
+			Address:      "aws_lb.test",
+			Type:         "aws_lb",
+			Name:         "test",
+			ProviderName: "aws",
+			Values: map[string]interface{}{
+				"load_balancer_type": "gateway",
+			},
+		}
+		expected := []query.Component{
+			{
+				Name:           "Gateway Load Balancer",
+				HourlyQuantity: decimal.NewFromInt(1),
+				ProductFilter: &product.Filter{
+					Provider: util.StringPtr("aws"),
+					Service:  util.StringPtr("AWSELB"),
+					Family:   util.StringPtr("Load Balancer-Gateway"),
+					Location: util.StringPtr("eu-west-3"),
+					AttributeFilters: []*product.AttributeFilter{
+						{Key: "usagetype", ValueRegex: util.StringPtr("LoadBalancerUsage")},
+					},
+				},
+				PriceFilter: &price.Filter{
+					Unit: util.StringPtr("Hrs"),
+				},
+			},
+		}
+
+		actual := p.ResourceComponents(tfres)
+		assert.Equal(t, expected, actual)
+	})
+
+	t.Run("ClassicLoadBalancer", func(t *testing.T) {
+		tfres := terraform.Resource{
+			Address:      "aws_elb.test",
+			Type:         "aws_elb",
+			Name:         "test",
+			ProviderName: "aws",
+			Values:       map[string]interface{}{},
+		}
+		expected := []query.Component{
+			{
+				Name:           "Classic Load Balancer",
+				HourlyQuantity: decimal.NewFromInt(1),
+				ProductFilter: &product.Filter{
+					Provider: util.StringPtr("aws"),
+					Service:  util.StringPtr("AWSELB"),
+					Family:   util.StringPtr("Load Balancer"),
+					Location: util.StringPtr("eu-west-3"),
+					AttributeFilters: []*product.AttributeFilter{
+						{Key: "usagetype", ValueRegex: util.StringPtr("LoadBalancerUsage")},
+					},
+				},
+				PriceFilter: &price.Filter{
+					Unit: util.StringPtr("Hrs"),
+				},
+			},
+		}
+
+		actual := p.ResourceComponents(tfres)
+		assert.Equal(t, expected, actual)
+	})
+}