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

Fix providers that depend on output of module #2925

Merged
merged 2 commits into from
Mar 5, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions cmd/infracost/breakdown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,20 @@ func TestBreakdownWithMultipleProviders(t *testing.T) {
)
}

func TestBreakdownWithProvidersDependingOnData(t *testing.T) {
// This test doesn't pass for the non-graph evaluator
GoldenFileCommandTest(
t,
testutil.CalcGoldenFileTestdataDirName(),
[]string{
"breakdown",
"--path",
path.Join("./testdata", testutil.CalcGoldenFileTestdataDirName()),
},
&GoldenFileOptions{IgnoreNonGraph: true},
)
}

func TestBreakdownMultiProjectWithError(t *testing.T) {
testName := testutil.CalcGoldenFileTestdataDirName()
dir := path.Join("./testdata", testName)
Expand Down
9 changes: 6 additions & 3 deletions cmd/infracost/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type GoldenFileOptions = struct {
Env map[string]string
// RunTerraformCLI sets the cmd test to also run the cmd with --terraform-force-cli set
RunTerraformCLI bool
IgnoreNonGraph bool
}

func DefaultOptions() *GoldenFileOptions {
Expand All @@ -50,9 +51,11 @@ func DefaultOptions() *GoldenFileOptions {
}

func GoldenFileCommandTest(t *testing.T, testName string, args []string, testOptions *GoldenFileOptions, ctxOptions ...func(ctx *config.RunContext)) {
t.Run("HCL", func(t *testing.T) {
goldenFileCommandTest(t, testName, args, testOptions, true, ctxOptions...)
})
if testOptions == nil || !testOptions.IgnoreNonGraph {
t.Run("HCL", func(t *testing.T) {
goldenFileCommandTest(t, testName, args, testOptions, true, ctxOptions...)
})
}

t.Run("HCL Graph", func(t *testing.T) {
ctxOptions = append(ctxOptions, func(ctx *config.RunContext) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Project: infracost/infracost/cmd/infracost/testdata/breakdown_with_providers_depending_on_data

Name Monthly Qty Unit Monthly Cost

module.mod_eu1.aws_instance.instance_eu1[0]
├─ Instance usage (Linux/UNIX, on-demand, t2.micro) 730 hours $9.20
└─ root_block_device
└─ Storage (general purpose SSD, gp2) 8 GB $0.88

module.mod_us2.aws_instance.instance_us2[0]
├─ Instance usage (Linux/UNIX, on-demand, t2.micro) 730 hours $8.47
└─ root_block_device
└─ Storage (general purpose SSD, gp2) 8 GB $0.80

OVERALL TOTAL $19.35
──────────────────────────────────
2 cloud resources were detected:
∙ 2 were estimated, all of which include usage-based costs, see https://infracost.io/usage-file

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Project ┃ Monthly cost ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╋━━━━━━━━━━━━━━┫
┃ infracost/infracost/cmd/infraco...th_providers_depending_on_data ┃ $19 ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┻━━━━━━━━━━━━━━┛

Err:

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
provider "aws" {
skip_credentials_validation = true
skip_requesting_account_id = true
access_key = "mock_access_key"
secret_key = "mock_secret_key"
region = module.mod_us2.region_us2
}

provider "aws" {
alias = "eu1"
skip_credentials_validation = true
skip_requesting_account_id = true
access_key = "mock_access_key"
secret_key = "mock_secret_key"
region = module.mod_eu1.region_eu1

}

module "mod_us2" {
source = "./mod"
}

module "mod_eu1" {
source = "./mod"

providers = {
aws = aws.eu1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
output "region_us2" {
value = "us-east-2"
}

output "region_eu1" {
value = "eu-west-1"
}

data "aws_region" "current" {}

resource "aws_instance" "instance_us2" {
count = data.aws_region.current.name == "us-east-2" ? 1 : 0
ami = "ami-674cbc1e"
instance_type = "t2.micro"
}

resource "aws_instance" "instance_eu1" {
count = data.aws_region.current.name == "eu-west-1" ? 1 : 0
ami = "ami-674cbc1e"
instance_type = "t2.micro"
}
5 changes: 2 additions & 3 deletions internal/hcl/attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -992,8 +992,7 @@ func (attr *Attribute) VerticesReferenced(b *Block) []VertexReference {
continue
}

isProviderReference := (usesProviderConfiguration(b) && attr.Name() == "provider") || (b.Type() == "module" && attr.Name() == "providers")

isProviderReference := usesProviderConfiguration(b) && attr.Name() == "provider"
if isProviderReference {
key = fmt.Sprintf("provider.%s", strings.TrimSuffix(key, "."))
}
Expand Down Expand Up @@ -1350,7 +1349,7 @@ func shouldSkipRef(block *Block, attr *Attribute, key string) bool {
}

// Provider references can come through as `aws.`
isProviderReference := (usesProviderConfiguration(block) && attr.Name() == "provider") || (block.Type() == "module" && attr.Name() == "providers")
isProviderReference := usesProviderConfiguration(block) && attr.Name() == "provider"
if !isProviderReference && strings.HasSuffix(key, ".") {
return true
}
Expand Down
100 changes: 49 additions & 51 deletions internal/hcl/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"path/filepath"
"reflect"
"regexp"
"slices"
"strings"

"github.com/hashicorp/hcl/v2"
Expand Down Expand Up @@ -119,8 +118,26 @@ func NewEvaluator(
Functions: ExpFunctions(module.RootPath, logger),
}, nil, logger)

for key, provider := range module.Providers {
ctx.Set(provider, key)
// Add any provider references from blocks in this module.
// We do this here instead of loadModuleWithProviders to make sure
// this also works with the root module
if module.ProviderReferences == nil {
module.ProviderReferences = make(map[string]*Block)
}

providerBlocks := module.Blocks.OfType("provider")
for _, block := range providerBlocks {
k := block.Label()

alias := block.GetAttribute("alias")
if alias != nil {
k = k + "." + alias.AsString()
}
module.ProviderReferences[k] = block
}

for key, provider := range module.ProviderReferences {
ctx.Set(provider.Values(), key)
}

if visitedModules == nil {
Expand Down Expand Up @@ -250,6 +267,14 @@ func (e *Evaluator) collectModules() *Module {
root.Modules = append(root.Modules, definition.Module)
}

// Reload the provider references for this module instance
// We need to do this so when we call ProviderConfigKey() we get the fully
// resolved provider. We might be able to improve this by only evaluating the
// provider block when we need it.
for name, providerBlock := range root.ProviderReferences {
e.ctx.Set(providerBlock.Values(), name)
}

if v := e.MissingVars(); len(v) > 0 {
root.Warnings = append(root.Warnings, schema.NewDiagMissingVars(v...))
}
Expand Down Expand Up @@ -330,16 +355,16 @@ func (e *Evaluator) evaluateModules() {

moduleEvaluator := NewEvaluator(
Module{
Name: fullName,
Source: moduleCall.Module.Source,
Providers: moduleCall.Module.Providers,
Blocks: moduleCall.Module.RawBlocks,
RawBlocks: moduleCall.Module.RawBlocks,
RootPath: e.module.RootPath,
ModulePath: moduleCall.Path,
Modules: nil,
Parent: &e.module,
SourceURL: moduleCall.Module.SourceURL,
Name: fullName,
Source: moduleCall.Module.Source,
Blocks: moduleCall.Module.RawBlocks,
RawBlocks: moduleCall.Module.RawBlocks,
RootPath: e.module.RootPath,
ModulePath: moduleCall.Path,
Modules: nil,
Parent: &e.module,
SourceURL: moduleCall.Module.SourceURL,
ProviderReferences: moduleCall.Module.ProviderReferences,
},
e.workingDir,
vars,
Expand Down Expand Up @@ -995,52 +1020,25 @@ func (e *Evaluator) loadModuleWithProviders(b *Block) (*ModuleCall, error) {
return modCall, err
}

// now load the providers
providers := map[string]cty.Value{}
// inherit any providers from the providers from the parent module
for key, provider := range e.module.Providers {
providers[key] = provider
}
// Pass any provider references that should be inherited by the module.
// This includes any implicit providers that are inherited from the parent
// module, as well as any explicit provider references that are passed in
// via the "providers" attribute.
providerRefs := map[string]*Block{}

providerBlocks := e.getValuesByBlockType("provider")
for key, provider := range providerBlocks.AsValueMap() {
providers[key] = provider
for key, block := range e.module.ProviderReferences {
providerRefs[key] = block
}

// adjust the providers based on the module.providers mapping
providerAttr := b.GetAttribute("providers")
if providerAttr != nil {
mappedProviders := providerAttr.ProvidersValue().AsValueMap()

// sort the map keys to ensure that we always populate root providers (e.g. "aws") before
// aliased ones ("aws.my_alias")
keys := make([]string, 0, len(mappedProviders))
for k := range mappedProviders {
keys = append(keys, k)
}
slices.Sort(keys)

for _, k := range keys {
split := strings.SplitN(k, ".", 2)
if len(split) == 2 {
parentMap := map[string]cty.Value{}
if parent, ok := providers[split[0]]; ok {
// In some cases parent will be a mock so we can just ignore it, unfortunately
// sometimes ignoring it causes the non-graph evaluator to get stuck in evaluation
// loops, so we only ignore mocks if we are graph-evaluating.
if !e.isGraph || (e.isGraph && parent.Type().IsObjectType()) {
parentMap = parent.AsValueMap()
}
}
parentMap[split[1]] = mappedProviders[k]
providers[split[0]] = cty.ObjectVal(parentMap)
} else {
providers[k] = mappedProviders[k]
}
decodedProviders := providerAttr.DecodeProviders()
for key, val := range decodedProviders {
providerRefs[key] = providerRefs[val]
}
}

modCall.Module.Providers = providers
modCall.Module.ProviderReferences = providerRefs

return modCall, nil
}
Expand Down
9 changes: 9 additions & 0 deletions internal/hcl/graph_vertex_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ func (v *VertexData) Visit(mutex *sync.Mutex) error {
return fmt.Errorf("could not find block %q in module %q", v.block.FullName(), moduleInstance.name)
}

// Reload the provider references for this module instance
// We need to do this because we might be evaluating a data block that
// needs to get data from a provider block, e.g. aws_default_tags.
// We might be able to improve this by only evaluating the
// provider block when we need it.
for name, providerBlock := range e.module.ProviderReferences {
e.ctx.Set(providerBlock.Values(), name)
}

err := v.evaluate(e, blockInstance)
if err != nil {
return fmt.Errorf("could not evaluate data block %q", v.ID())
Expand Down
12 changes: 7 additions & 5 deletions internal/hcl/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package hcl
import (
"strconv"

"github.com/zclconf/go-cty/cty"

"github.com/infracost/infracost/internal/schema"
)

Expand All @@ -22,9 +20,8 @@ type ModuleCall struct {

// Module encapsulates all the Blocks that are part of a Module in a Terraform project.
type Module struct {
Name string
Source string
Providers map[string]cty.Value
Name string
Source string

Blocks Blocks
// RawBlocks are the Blocks that were built when the module was loaded from the filesystem.
Expand All @@ -47,6 +44,11 @@ type Module struct {
// SourceURL is the discovered remote url for the module. This will only be
// filled if the module is a remote module.
SourceURL string

// ProviderReferences is a map of provider names (relative to the module) to
// the provider block that defines that provider. We keep track of this so we
// can re-evaluate the provider blocks when we need to.
ProviderReferences map[string]*Block
}

// Index returns the count index of the Module using the name.
Expand Down