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

testing: add handling of unknown providers and empty terraform #55

Merged
merged 3 commits into from
Aug 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,26 @@ GO_RUN_CMD := $(GO_CMD) run
GOLINT_CMD := $(GO_RUN_CMD) golang.org/x/lint/golint
GOIMPORTS_CMD := $(GO_RUN_CMD) golang.org/x/tools/cmd/goimports

MYSQL_USER := root
MYSQL_PASS := terracost
MYSQL_DB := terracost_test
MYSQL_DUMP ?= mysql/testdata/2021-08-12-pricing.sql.gz

# The ci rule is voluntarily simple because
# the CI requires to run the command separately
# and not all at once, otherwise it wouldn't work
.PHONY: ci
ci: lint test
ci: lint
@$(GO_TEST_CMD) ./...

.PHONY: test
test: db-up
test: lint down db-up db-migrate db-inject
@$(GO_TEST_CMD) ./...

.PHONY: db-inject
db-inject:
@zcat $(MYSQL_DUMP) | $(DOCKER_COMPOSE_CMD) exec -T database mysql -u$(MYSQL_USER) -p$(MYSQL_PASS) $(MYSQL_DB)

.PHONY: lint
lint:
@$(GOLINT_CMD) -set_exit_status ./... && test -z "`$(GO_CMD) list -f {{.Dir}} ./... | xargs $(GOIMPORTS_CMD) -l | tee /dev/stderr`"
Expand All @@ -22,6 +35,7 @@ lint:
db-up: # Start the DB server
ifeq ($(IS_CI), 0)
@$(DOCKER_COMPOSE_CMD) up -d database
@$(DOCKER_COMPOSE_CMD) run wait -c database:3306
endif

.PHONY: down
Expand All @@ -32,6 +46,10 @@ down:
db-migrate: db-up
@$(GO_RUN_CMD) scripts/migrate.go

.PHONY: db-cli
db-cli:
@$(DOCKER_COMPOSE_CMD) exec database mysql -u$(MYSQL_USER) -p$(MYSQL_PASS) $(MYSQL_DB)

.PHONY: generate
generate:
@rm -rf ./mock/
Expand Down
16 changes: 14 additions & 2 deletions cost/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,13 @@ func TestResourceDiff_Cost(t *testing.T) {

_, err := rd.PriorCost()
require.Error(t, err)
assert.Equal(t, "failed calculating prior cost: currency mismatch: expected USD, got EUR", err.Error())
// Because of the iterration of the map the order of currency can be reversed.
// We want simply to ensure that an error is returned, the order doesn't matter
errs := []string{
"failed calculating prior cost: currency mismatch: expected USD, got EUR",
"failed calculating prior cost: currency mismatch: expected EUR, got USD",
}
assert.Contains(t, errs, err.Error())
})
t.Run("PlannedCurrencyMismatch", func(t *testing.T) {
rd := &cost.ResourceDiff{
Expand All @@ -230,6 +236,12 @@ func TestResourceDiff_Cost(t *testing.T) {

_, err := rd.PlannedCost()
require.Error(t, err)
assert.Equal(t, "failed calculating planned cost: currency mismatch: expected USD, got EUR", err.Error())
// Because of the iterration of the map the order of currency can be reversed.
// We want simply to ensure that an error is returned, the order doesn't matter
errs := []string{
"failed calculating planned cost: currency mismatch: expected USD, got EUR",
"failed calculating planned cost: currency mismatch: expected EUR, got USD",
}
assert.Contains(t, errs, err.Error())
})
}
4 changes: 4 additions & 0 deletions cost/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/cycloidio/terracost/price"
"github.com/cycloidio/terracost/product"
"github.com/cycloidio/terracost/query"
"github.com/cycloidio/terracost/terraform"
)

// Backend represents a storage method used to query pricing data. It must include concrete implementations
Expand All @@ -33,6 +34,9 @@ var (
func NewState(ctx context.Context, backend Backend, queries []query.Resource) (*State, error) {
state := &State{Resources: make(map[string]Resource)}

if len(queries) == 0 {
return nil, terraform.ErrNoQueries
}
for _, res := range queries {
// Mark the Resource as skipped if there are no valid Components.
state.ensureResource(res.Address, res.Provider, res.Type, len(res.Components) == 0)
Expand Down
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,21 @@ services:
ports:
- '33060:3306'
environment:
- MYSQL_USER=root
- MYSQL_ROOT_PASSWORD=terracost
- MYSQL_DATABASE=terracost_test
networks:
terracost-subnet:
ipv4_address: 172.44.0.2

# This service is used to wait for service to be ready
# to accept connections, see dev-env-up target in Makefile.
wait:
image: dokku/wait:0.4.3
networks:
terracost-subnet:
ipv4_address: 172.44.0.42

networks:
terracost-subnet:
driver: bridge
Expand Down
181 changes: 107 additions & 74 deletions e2e/aws_estimation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,32 @@ import (
"github.com/cycloidio/terracost/terraform"
)

var terraformProviderInitializer = terraform.ProviderInitializer{
// terraformAWSTestProviderInitializer is a testing ProviderInitializer
// pricing are directly inserted inside the database, which allows us to
// test the processing with smaller subset of data, as well as the functioning
// of MatchNames for a given provider - as data are injected using 'aws-test'
// which is also used in the tfplan & co.
var terraformAWSTestProviderInitializer = terraform.ProviderInitializer{
MatchNames: []string{"aws", "aws-test"},
Provider: func(config map[string]string) (terraform.Provider, error) {
regCode := region.Code(config["region"])
return awstf.NewProvider("aws-test", regCode)
},
}

// terraformAWSProviderInitializer is a proper AWS provider.
// We do not want to reuse the testing terraformAWSTestProviderInitializer
// because the HCL contains actual valid AWS resources and provider
// meaning 'aws' is used an not 'aws-test'. On top of that pricing data
// from a real dump are injected to ensure better testing scenarios
var terraformAWSProviderInitializer = terraform.ProviderInitializer{
xescugc marked this conversation as resolved.
Show resolved Hide resolved
MatchNames: []string{"aws"},
Provider: func(config map[string]string) (terraform.Provider, error) {
regCode := region.Code(config["region"])
return awstf.NewProvider("aws", regCode)
},
}

func TestAWSEstimation(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode")
Expand Down Expand Up @@ -127,86 +145,101 @@ func TestAWSEstimation(t *testing.T) {
require.NoError(t, err)
}

t.Run("Success", func(t *testing.T) {
f, err := os.Open("../testdata/terraform-plan.json")
require.NoError(t, err)
defer f.Close()
t.Run("TFPlan", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {
f, err := os.Open("../testdata/aws/terraform-plan.json")
require.NoError(t, err)
defer f.Close()

plan, err := costestimation.EstimateTerraformPlan(ctx, backend, f, terraformAWSTestProviderInitializer)
require.NoError(t, err)

pcost, err := plan.PriorCost()
assert.NoError(t, err)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(91.2), "USD"), pcost)

pcost, err = plan.PlannedCost()
assert.NoError(t, err)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(901.5), "USD"), pcost)

diffs := plan.ResourceDifferences()
require.Len(t, diffs, 2)

for _, diff := range diffs {
switch diff.Address {
case "aws_instance.example":
compute := diff.ComponentDiffs["Compute"]
require.NotNil(t, compute)
assert.Equal(t, []string{"Linux", "on-demand", "t2.micro"}, compute.Prior.Details)
assert.Equal(t, []string{"Linux", "on-demand", "t2.xlarge"}, compute.Planned.Details)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(87.6), "USD"), compute.PriorCost())
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(897.9), "USD"), compute.PlannedCost())

rootVol := diff.ComponentDiffs["Root volume: Storage"]
require.NotNil(t, rootVol)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), compute.PriorCost())
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), compute.PlannedCost())

priorCost, err := diff.PriorCost()
require.NoError(t, err)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(91.2), "USD"), priorCost)

plannedCost, err := diff.PlannedCost()
require.NoError(t, err)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(901.5), "USD"), plannedCost)

case "aws_lb.example":
lb := diff.ComponentDiffs["Application Load Balancer"]
require.NotNil(t, lb)
assert.False(t, diff.Valid())
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(0), ""), lb.Planned.Cost())

priorCost, err := diff.PriorCost()
require.NoError(t, err)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(0), ""), priorCost)

plannedCost, err := diff.PlannedCost()
require.NoError(t, err)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(0), ""), plannedCost)
}
}
})

plan, err := costestimation.EstimateTerraformPlan(ctx, backend, f, terraformProviderInitializer)
require.NoError(t, err)
t.Run("ProductNotFound", func(t *testing.T) {
f, err := os.Open("../testdata/aws/terraform-plan-invalid.json")
require.NoError(t, err)
defer f.Close()

pcost, err := plan.PriorCost()
assert.NoError(t, err)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(91.2), "USD"), pcost)

pcost, err = plan.PlannedCost()
assert.NoError(t, err)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(901.5), "USD"), pcost)

diffs := plan.ResourceDifferences()
require.Len(t, diffs, 2)

for _, diff := range diffs {
switch diff.Address {
case "aws_instance.example":
compute := diff.ComponentDiffs["Compute"]
require.NotNil(t, compute)
assert.Equal(t, []string{"Linux", "on-demand", "t2.micro"}, compute.Prior.Details)
assert.Equal(t, []string{"Linux", "on-demand", "t2.xlarge"}, compute.Planned.Details)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(87.6), "USD"), compute.PriorCost())
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(897.9), "USD"), compute.PlannedCost())

rootVol := diff.ComponentDiffs["Root volume: Storage"]
require.NotNil(t, rootVol)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), compute.PriorCost())
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), compute.PlannedCost())

priorCost, err := diff.PriorCost()
require.NoError(t, err)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(91.2), "USD"), priorCost)

plannedCost, err := diff.PlannedCost()
require.NoError(t, err)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(901.5), "USD"), plannedCost)

case "aws_lb.example":
lb := diff.ComponentDiffs["Application Load Balancer"]
require.NotNil(t, lb)
assert.False(t, diff.Valid())
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(0), ""), lb.Planned.Cost())

priorCost, err := diff.PriorCost()
require.NoError(t, err)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(0), ""), priorCost)

plannedCost, err := diff.PlannedCost()
require.NoError(t, err)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(0), ""), plannedCost)
}
}
})
plan, err := costestimation.EstimateTerraformPlan(ctx, backend, f, terraformAWSTestProviderInitializer)
require.NoError(t, err)

t.Run("ProductNotFound", func(t *testing.T) {
f, err := os.Open("../testdata/terraform-plan-invalid.json")
require.NoError(t, err)
defer f.Close()
diffs := plan.ResourceDifferences()
require.Len(t, diffs, 1)
rd := diffs[0]

plan, err := costestimation.EstimateTerraformPlan(ctx, backend, f, terraformProviderInitializer)
require.NoError(t, err)
rootVol := diffs[0].ComponentDiffs["Root volume: Storage"]
require.NotNil(t, rootVol)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), rootVol.PriorCost())
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), rootVol.PlannedCost())

expected := map[string]error{
"Compute": cost.ErrProductNotFound,
}
assert.Equal(t, expected, rd.Errors())
})
})
t.Run("HCL", func(t *testing.T) {
t.Run("Success", func(t *testing.T) {

diffs := plan.ResourceDifferences()
require.Len(t, diffs, 1)
rd := diffs[0]
plan, err := costestimation.EstimateHCL(ctx, backend, nil, "../testdata/aws/stack-aws", terraformAWSProviderInitializer)
require.NoError(t, err)

rootVol := diffs[0].ComponentDiffs["Root volume: Storage"]
require.NotNil(t, rootVol)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), rootVol.PriorCost())
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(3.6), "USD"), rootVol.PlannedCost())
assert.Nil(t, plan.Prior)

expected := map[string]error{
"Compute": cost.ErrProductNotFound,
}
assert.Equal(t, expected, rd.Errors())
pcost, err := plan.PlannedCost()
assert.NoError(t, err)
assertCostEqual(t, cost.NewMonthly(decimal.NewFromFloat(32.374), "USD"), pcost)
})
})
}

Expand Down
63 changes: 63 additions & 0 deletions e2e/invalid_estimation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package e2e

import (
"context"
"database/sql"
"os"
"testing"

costestimation "github.com/cycloidio/terracost"
"github.com/cycloidio/terracost/mysql"
"github.com/cycloidio/terracost/terraform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// This provide is currently UNSUPPORTED, it aims to test an UNSUPPORTED
// provider errors for both terraform and HCL files

func TestVMWareEstimation(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode")
}

ctx := context.Background()
db, err := sql.Open("mysql", "root:terracost@tcp(172.44.0.2:3306)/terracost_test?multiStatements=true")
require.NoError(t, err)

backend := mysql.NewBackend(db)

t.Run("TFPlan", func(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
f, err := os.Open("../testdata/invalid/terraform-empty-plan.json")
require.NoError(t, err)
defer f.Close()

plan, err := costestimation.EstimateTerraformPlan(ctx, backend, f, terraformAWSTestProviderInitializer)
require.Error(t, err, terraform.ErrNoQueries)
assert.Nil(t, plan)
})
t.Run("UnsupportedProvider", func(t *testing.T) {
f, err := os.Open("../testdata/invalid/terraform-unsupported-plan.json")
require.NoError(t, err)
defer f.Close()

plan, err := costestimation.EstimateTerraformPlan(ctx, backend, f, terraformAWSTestProviderInitializer)
require.Error(t, err, terraform.ErrNoKnownProvider)
assert.Nil(t, plan)
})
})

t.Run("HCL", func(t *testing.T) {
t.Run("UnsupportedProvider", func(t *testing.T) {
plan, err := costestimation.EstimateHCL(ctx, backend, nil, "../testdata/invalid/stack-vmware", terraformAWSProviderInitializer)
assert.Nil(t, plan)
assert.Error(t, err, terraform.ErrNoKnownProvider)
})
t.Run("EmptyTerraform", func(t *testing.T) {
plan, err := costestimation.EstimateHCL(ctx, backend, nil, "../testdata/invalid/stack-empty", terraformAWSProviderInitializer)
assert.Nil(t, plan)
assert.Error(t, err, terraform.ErrNoQueries)
})
})
}
Loading