terraform: add new init graph workflow and builder

Daniel Banck 2 weeks ago
parent dee62db2f0
commit ea9d8cc873
No known key found for this signature in database

@ -0,0 +1,60 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package terraform
import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type InitOpts struct {
Walker configs.ModuleWalker
// SetVariables are the raw values for root module variables as provided
// by the user who is requesting the run, prior to any normalization or
// substitution of defaults. See the documentation for the InputValue
// type for more information on how to correctly populate this.
SetVariables InputValues
}
func (c *Context) Init(rootMod *configs.Module, initOpts InitOpts) (*configs.Config, tfdiags.Diagnostics) {
return c.init(rootMod, initOpts)
}
func (c *Context) init(rootMod *configs.Module, initOpts InitOpts) (*configs.Config, tfdiags.Diagnostics) {
defer c.acquireRun("init")()
var diags tfdiags.Diagnostics
config := &configs.Config{
Module: rootMod,
Path: addrs.RootModule,
Children: map[string]*configs.Config{},
}
config.Root = config
graph, moreDiags := c.initGraph(config, initOpts)
diags = diags.Append(moreDiags)
if diags.HasErrors() {
return nil, diags
}
walker, walkDiags := c.walk(graph, walkInit, &graphWalkOpts{
Config: config,
})
diags = diags.Append(walker.NonFatalDiagnostics)
diags = diags.Append(walkDiags)
return config, diags
}
func (c *Context) initGraph(config *configs.Config, initOpts InitOpts) (*Graph, tfdiags.Diagnostics) {
graph, diags := (&InitGraphBuilder{
Config: config,
RootVariableValues: initOpts.SetVariables,
Walker: initOpts.Walker,
}).Build(addrs.RootModuleInstance)
return graph, diags
}

@ -0,0 +1,710 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package terraform
import (
"path/filepath"
"strings"
"testing"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/getmodules/moduleaddrs"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
var _ configs.ModuleWalker = (*MockModuleWalker)(nil)
type MockModuleWalker struct {
Calls []*configs.ModuleRequest
DefaultModule *configs.Module
// the string key refers to ModuleSource.String()
MockedCalls map[string]*configs.Module
}
func (m *MockModuleWalker) LoadModule(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) {
m.Calls = append(m.Calls, req)
if mod, ok := m.MockedCalls[req.SourceAddr.String()]; ok {
return mod, nil, nil
}
return m.DefaultModule, nil, nil
}
func (m *MockModuleWalker) MockModuleCalls(t *testing.T, calls map[string]*configs.Module) {
t.Helper()
if m.MockedCalls == nil {
m.MockedCalls = make(map[string]*configs.Module)
}
for k, v := range calls {
// Make sure we can parse the module source
ms := mustModuleSource(t, k)
m.MockedCalls[ms.String()] = v
}
}
func TestInit(t *testing.T) {
for name, tc := range map[string]struct {
module map[string]string
vars InputValues
mockedLoadModuleCalls map[string]map[string]string
// m -> root module
// mc -> module calls
expectDiags func(m *configs.Module, mc map[string]*configs.Module) tfdiags.Diagnostics
expectLoadModuleCalls []*configs.ModuleRequest
}{
"empty config": {
module: map[string]string{"main.tf": ``},
},
"local - no variables": {
module: map[string]string{
"main.tf": `
module "example" {
source = "./modules/example"
}
`,
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "./modules/example"),
}},
},
"remote - no variables": {
module: map[string]string{
"main.tf": `
module "example" {
source = "terraform-aws-modules/vpc/aws"
version = "6.6.0"
}
module "example2" {
source = "terraform-iaac/cert-manager/kubernetes"
}
`,
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "terraform-iaac/cert-manager/kubernetes"),
}, {
SourceAddr: mustModuleSource(t, "terraform-aws-modules/vpc/aws"),
VersionConstraint: mustVersionContraint(t, "= 6.6.0"),
}},
},
"local - with variables": {
module: map[string]string{
"main.tf": `
variable "name" {
type = string
const = true
}
module "example" {
source = "./modules/${var.name}"
}
`,
},
vars: InputValues{
"name": &InputValue{Value: cty.StringVal("example"), SourceType: ValueFromCLIArg},
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "./modules/example"),
}},
},
"local with non-const variables": {
module: map[string]string{
"main.tf": `
variable "name" {
type = string
}
module "example" {
source = "./modules/${var.name}"
}
`,
},
vars: InputValues{
"name": &InputValue{Value: cty.StringVal("example"), SourceType: ValueFromCLIArg},
},
expectDiags: func(m *configs.Module, mc map[string]*configs.Module) tfdiags.Diagnostics {
// TODO: We should try to somehow add an "extra" into the diagnostics to indicate
// that this may be caused by a non-const variable used during init.
return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid module source`,
Detail: `The value of a reference in the module source is unknown.`,
Subject: &hcl.Range{
Filename: filepath.Join(m.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 6, Column: 27, Byte: 82},
End: hcl.Pos{Line: 6, Column: 35, Byte: 90},
},
})
},
},
"remote - with variable in source": {
module: map[string]string{
"main.tf": `
variable "name" {
type = string
const = true
}
module "example2" {
source = "terraform-iaac/${var.name}/kubernetes"
}
`,
},
vars: InputValues{
"name": &InputValue{Value: cty.StringVal("cert-manager"), SourceType: ValueFromCLIArg},
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "terraform-iaac/cert-manager/kubernetes"),
}},
},
"remote - with variable in constraint": {
module: map[string]string{
"main.tf": `
variable "name" {
type = string
const = true
}
module "example2" {
source = "terraform-iaac/cert-manager/kubernetes"
version = ">= ${var.name}"
}
`,
},
vars: InputValues{
"name": &InputValue{Value: cty.StringVal("1.2.3"), SourceType: ValueFromCLIArg},
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "terraform-iaac/cert-manager/kubernetes"),
VersionConstraint: mustVersionContraint(t, ">= 1.2.3"),
}},
},
"locals in module sources": {
module: map[string]string{
"main.tf": `
variable "name" {
type = string
const = true
}
locals {
org_and_repo = "terraform-iaac/${var.name}"
}
module "example2" {
source = "${local.org_and_repo}/kubernetes"
}
`,
},
vars: InputValues{
"name": &InputValue{Value: cty.StringVal("cert-manager"), SourceType: ValueFromCLIArg},
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "terraform-iaac/cert-manager/kubernetes"),
VersionConstraint: mustVersionContraint(t, ">= 1.2.3"),
}},
},
"each in module sources": {
module: map[string]string{
"main.tf": `
module "example" {
for_each = toset(["cert-manager", "helm"])
source = "terraform-iaac/${each.key}/kubernetes"
}
`,
},
vars: InputValues{
"name": &InputValue{Value: cty.StringVal("cert-manager"), SourceType: ValueFromCLIArg},
},
expectDiags: func(m *configs.Module, mc map[string]*configs.Module) tfdiags.Diagnostics {
return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid module source`,
Detail: `The module source can only reference input variables and local values.`,
Subject: &hcl.Range{
Filename: filepath.Join(m.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 4, Column: 31, Byte: 95},
End: hcl.Pos{Line: 4, Column: 39, Byte: 103},
},
})
},
},
"module variables in source": {
module: map[string]string{
"main.tf": `
module "mod" {
source = "./mod"
name = "cert-manager"
}
`,
},
vars: InputValues{
"name": &InputValue{Value: cty.StringVal("cert-manager"), SourceType: ValueFromCLIArg},
},
mockedLoadModuleCalls: map[string]map[string]string{
"./mod": {
"main.tf": `
variable "name" {
type = string
const = true
}
module "example" {
source = "terraform-iaac/${var.name}/kubernetes"
}
`,
},
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "./mod"),
}, {
SourceAddr: mustModuleSource(t, "terraform-iaac/cert-manager/kubernetes"),
}},
},
"undefined variable in module source": {
module: map[string]string{
"main.tf": `
variable "name" {
type = string
const = true
}
module "example2" {
source = "terraform-iaac/${var.name}/kubernetes"
}
`,
},
expectDiags: func(m *configs.Module, mc map[string]*configs.Module) tfdiags.Diagnostics {
return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Required variable not set",
Detail: `The variable "name" is required, but is not set.`,
Subject: &hcl.Range{
Filename: filepath.Join(m.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 2, Column: 1, Byte: 1},
End: hcl.Pos{Line: 2, Column: 16, Byte: 16},
},
})
},
},
"resource reference in module source": {
module: map[string]string{
"main.tf": `
resource "null_resource" "example" {}
module "example" {
source = "terraform-iaac/${null_resource.example.id}/kubernetes"
}
`,
},
expectDiags: func(m *configs.Module, mc map[string]*configs.Module) tfdiags.Diagnostics {
return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module source",
Detail: "The module source can only reference input variables and local values.",
Subject: &hcl.Range{
Filename: filepath.Join(m.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 5, Column: 33, Byte: 91},
End: hcl.Pos{Line: 5, Column: 54, Byte: 112},
},
})
},
},
"resource reference in module call": {
module: map[string]string{
"main.tf": `
variable "name" {
type = string
default = "aws"
const = true
}
resource "null_resource" "example" {}
module "example" {
source = "./${var.name}"
name = var.name
this_should_be_unknown_and_not_cause_error = null_resource.example.id
}
`,
},
mockedLoadModuleCalls: map[string]map[string]string{
"./aws": {
"main.tf": `
variable "name" {
type = string
const = true
}
variable "this_should_be_unknown_and_not_cause_error" {
type = string
}
module "example" {
source = "terraform-iaac/${var.name}/kubernetes"
}
output "foo" {
value = var.this_should_be_unknown_and_not_cause_error
}
`,
},
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "terraform-iaac/aws/kubernetes"),
}, {
SourceAddr: mustModuleSource(t, "./aws"),
}},
},
"module output reference in module source": {
module: map[string]string{
"main.tf": `
module "example" {
source = "./module/example"
}
module "example2" {
source = "terraform-iaac/${module.example.id}/kubernetes"
}
`,
},
mockedLoadModuleCalls: map[string]map[string]string{
"./module/example": {
"main.tf": `
output "id" {
value = "example-id"
}
`,
}},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "./module/example"),
}},
expectDiags: func(m *configs.Module, mc map[string]*configs.Module) tfdiags.Diagnostics {
return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module source",
Detail: "The module source can only reference input variables and local values.",
Subject: &hcl.Range{
Filename: filepath.Join(m.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 7, Column: 33, Byte: 107},
End: hcl.Pos{Line: 7, Column: 50, Byte: 124},
},
})
},
},
"nested module loading - no variables": {
module: map[string]string{
"main.tf": `
module "parent" {
source = "hashicorp/parent/aws"
}
`,
},
mockedLoadModuleCalls: map[string]map[string]string{
"hashicorp/parent/aws": {
"main.tf": `
module "child" {
source = "hashicorp/child/aws"
}
`,
},
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "hashicorp/parent/aws"),
}, {
SourceAddr: mustModuleSource(t, "hashicorp/child/aws"),
}},
},
"nested module loading - with variables": {
module: map[string]string{
"main.tf": `
module "parent" {
source = "hashicorp/parent/aws"
name = "child"
}
`,
},
mockedLoadModuleCalls: map[string]map[string]string{
"hashicorp/parent/aws": {
"main.tf": `
variable "name" {
type = string
const = true
}
module "child" {
source = "hashicorp/${var.name}/aws"
name = "grand${var.name}"
}
`,
},
"hashicorp/child/aws": {
"main.tf": `
variable "name" {
type = string
const = true
}
module "grandchild" {
source = "hashicorp/${var.name}/aws"
}
`,
},
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "hashicorp/parent/aws"),
}, {
SourceAddr: mustModuleSource(t, "hashicorp/child/aws"),
}, {
SourceAddr: mustModuleSource(t, "hashicorp/grandchild/aws"),
}},
},
"module nested expansion": {
module: map[string]string{
"main.tf": `
module "fromdisk" {
source = "./mod"
namespace = "terraform-iaac"
}
`,
},
mockedLoadModuleCalls: map[string]map[string]string{
"./mod": {
"main.tf": `
locals {
source = var.namespace
}
variable "namespace" {
type = string
const = true
}
module "terraform" {
source = "${var.namespace}/helm/kubernetes"
}
output "name" {
value = "fooo"
}
`,
},
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "./mod"),
}, {
SourceAddr: mustModuleSource(t, "terraform-iaac/helm/kubernetes"),
}},
},
"const variable with no value and no default": {
module: map[string]string{"main.tf": `
variable "name" {
type = string
const = true
}
module "example" {
source = "./modules/${var.name}"
}
`,
},
expectDiags: func(m *configs.Module, mc map[string]*configs.Module) tfdiags.Diagnostics {
return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Required variable not set`,
Detail: `The variable "name" is required, but is not set.`,
Subject: &hcl.Range{
Filename: filepath.Join(m.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 2, Column: 1, Byte: 1},
End: hcl.Pos{Line: 2, Column: 16, Byte: 16},
},
})
},
},
"const variable with default": {
module: map[string]string{"main.tf": `
variable "name" {
type = string
const = true
default = "example"
}
module "example" {
source = "./modules/${var.name}"
}
`,
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "./modules/example"),
}},
},
"non-const variable passed into const module variable": {
module: map[string]string{"main.tf": `
variable "name" {
type = string
default = "example"
}
module "example" {
source = "./modules/example"
name = "./modules/${var.name}2"
}
`,
},
mockedLoadModuleCalls: map[string]map[string]string{
"./modules/example": {
"main.tf": `
variable "name" {
type = string
const = true
}
`,
},
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "./modules/example"),
}},
expectDiags: func(m *configs.Module, mc map[string]*configs.Module) tfdiags.Diagnostics {
return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Constant variables must be known`,
Detail: `Only a constant value can be passed into a constant module variable.`,
Subject: &hcl.Range{
Filename: filepath.Join(m.SourceDir, "main.tf"),
Start: hcl.Pos{Line: 8, Column: 10, Byte: 118},
End: hcl.Pos{Line: 8, Column: 34, Byte: 142},
},
})
},
},
"non-const module variable used as const": {
module: map[string]string{"main.tf": `
module "example" {
source = "./modules/example"
name = "foo"
}
`,
},
mockedLoadModuleCalls: map[string]map[string]string{
"./modules/example": {
"main.tf": `
variable "name" {
type = string
}
module "nested" {
source = "./modules/${var.name}"
}
`,
},
},
expectLoadModuleCalls: []*configs.ModuleRequest{{
SourceAddr: mustModuleSource(t, "./modules/example"),
}},
expectDiags: func(m *configs.Module, mc map[string]*configs.Module) tfdiags.Diagnostics {
// TODO: We should try to somehow add an "extra" into the diagnostics to indicate
// that this may be caused by a non-constant variable used during init.
return tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid module source`,
Detail: `The value of a reference in the module source is unknown.`,
Subject: &hcl.Range{
Filename: filepath.Join(mc["./modules/example"].SourceDir, "main.tf"),
Start: hcl.Pos{Line: 7, Column: 27, Byte: 82},
End: hcl.Pos{Line: 7, Column: 35, Byte: 90},
},
})
},
},
} {
t.Run(name, func(t *testing.T) {
m := testRootModuleInline(t, tc.module)
ctx := testContext2(t, &ContextOpts{})
moduleWalker := MockModuleWalker{
DefaultModule: testRootModuleInline(t, map[string]string{"main.tf": `// empty`}),
}
mockedModules := make(map[string]*configs.Module)
if tc.mockedLoadModuleCalls != nil {
for k, v := range tc.mockedLoadModuleCalls {
mockedModules[k] = testRootModuleInline(t, v)
}
moduleWalker.MockModuleCalls(t, mockedModules)
}
_, diags := ctx.Init(m, InitOpts{
SetVariables: tc.vars,
Walker: &moduleWalker,
})
if tc.expectDiags != nil {
tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectDiags(m, mockedModules))
} else {
tfdiags.AssertNoDiagnostics(t, diags)
}
if len(moduleWalker.Calls) != len(tc.expectLoadModuleCalls) {
t.Fatalf("expected %d LoadModule calls, got %d", len(tc.expectLoadModuleCalls), len(moduleWalker.Calls))
}
// Create a map of expected sources for easier comparison
expectedSources := make(map[string]bool)
foundSources := []string{}
for _, expected := range tc.expectLoadModuleCalls {
expectedSources[expected.SourceAddr.String()] = false
}
// Mark sources as found
for _, call := range moduleWalker.Calls {
source := call.SourceAddr.String()
foundSources = append(foundSources, source)
if _, exists := expectedSources[source]; !exists {
t.Errorf("unexpected LoadModule call for source %q", source)
} else {
expectedSources[source] = true
}
}
// Check all expected sources were called
for source, found := range expectedSources {
if !found {
t.Errorf("expected LoadModule call for source %q but it was not called. Calls that were made: \n %s", source, strings.Join(foundSources, ", "))
}
}
})
}
}
func mustModuleSource(t *testing.T, rawStr string) addrs.ModuleSource {
src, err := moduleaddrs.ParseModuleSource(rawStr)
if err != nil {
t.Fatalf("failed to parse module source %q: %s", rawStr, err)
}
return src
}
func mustVersionContraint(t *testing.T, rawStr string) configs.VersionConstraint {
constraints, err := version.NewConstraint(rawStr)
if err != nil {
t.Fatalf("failed to parse version constraint %q: %s", rawStr, err)
}
return configs.VersionConstraint{
Required: constraints,
}
}

@ -0,0 +1,60 @@
package terraform
import (
"log"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type InitGraphBuilder struct {
Config *configs.Config
RootVariableValues InputValues
Walker configs.ModuleWalker
}
func (b *InitGraphBuilder) Build(path addrs.ModuleInstance) (*Graph, tfdiags.Diagnostics) {
log.Printf("[TRACE] building graph for terraform dependencies")
return (&BasicGraphBuilder{
Steps: b.Steps(),
Name: "InitGraphBuilder",
}).Build(path)
}
func (b *InitGraphBuilder) Steps() []GraphTransformer {
steps := []GraphTransformer{}
if b.Config.Parent == nil {
steps = append(steps, &RootVariableTransformer{
Config: b.Config,
RawValues: b.RootVariableValues,
})
} else {
steps = append(steps, &ModuleVariableTransformer{
Config: b.Config,
ModuleOnly: true,
})
}
steps = append(steps, []GraphTransformer{
&ModuleTransformer{
Config: b.Config,
Installer: b.Walker,
},
&LocalTransformer{
Config: b.Config,
},
&ReferenceTransformer{},
&RootTransformer{},
&TransitiveReductionTransformer{},
}...)
return steps
}

@ -17,4 +17,5 @@ const (
walkDestroy
walkImport
walkEval // used just to prepare EvalContext for expression evaluation, with no other actions
walkInit
)

@ -0,0 +1,343 @@
package terraform
import (
"github.com/hashicorp/go-version"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/getmodules/moduleaddrs"
"github.com/hashicorp/terraform/internal/lang/langrefs"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type nodeInstallModule struct {
// We're using a ModuleInstance here,
// because the downstream graph builder requires it.
// But it was constructed with addrs.NoKey
Addr addrs.ModuleInstance
ModuleCall *configs.ModuleCall
Parent *configs.Config
Installer configs.ModuleWalker
// Stores the configuration of the installed module
Config *configs.Config
// Stores the version of the installed module
Version *version.Version
}
var (
_ GraphNodeExecutable = (*nodeInstallModule)(nil)
_ GraphNodeReferencer = (*nodeInstallModule)(nil)
_ GraphNodeDynamicExpandable = (*nodeInstallModule)(nil)
_ GraphNodeModuleInstance = (*nodeInstallModule)(nil)
)
func (n *nodeInstallModule) Path() addrs.ModuleInstance {
return n.Addr.Parent()
}
func (n *nodeInstallModule) Name() string {
return n.Addr.String()
}
func (n *nodeInstallModule) ModulePath() addrs.Module {
return n.Addr.Module().Parent()
}
func (n *nodeInstallModule) References() []*addrs.Reference {
var refs []*addrs.Reference
sourceRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, n.ModuleCall.SourceExpr)
refs = append(refs, sourceRefs...)
versionRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, n.ModuleCall.VersionExpr)
refs = append(refs, versionRefs...)
// We need to resolve all module inputs as well, because some might be used
// in the module as a constant variable to build a nested module source
attrs, _ := n.ModuleCall.Config.JustAttributes()
for _, attr := range attrs {
inputRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, attr.Expr)
refs = append(refs, inputRefs...)
}
return refs
}
func (n *nodeInstallModule) Execute(ctx EvalContext, walkOp walkOperation) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
var version configs.VersionConstraint
if n.ModuleCall.VersionExpr != nil {
var versionDiags tfdiags.Diagnostics
version, versionDiags = decodeVersionConstraint(n.ModuleCall.VersionExpr, ctx)
diags = diags.Append(versionDiags)
if diags.HasErrors() {
return diags
}
}
hasVersion := n.ModuleCall.VersionExpr != nil
source, sourceDiags := decodeSource(n.ModuleCall.SourceExpr, hasVersion, ctx)
diags = diags.Append(sourceDiags)
if diags.HasErrors() {
return diags
}
req := &configs.ModuleRequest{
Name: n.ModuleCall.Name,
Path: n.Addr.Module(),
SourceAddr: source,
SourceAddrRange: n.ModuleCall.SourceExpr.Range(),
VersionConstraint: version,
Parent: n.Parent,
CallRange: n.ModuleCall.DeclRange,
}
cfg, v, modDiags := n.Installer.LoadModule(req)
diags = diags.Append(modDiags)
if diags.HasErrors() {
return diags
}
config := &configs.Config{
Module: cfg,
Parent: n.Parent,
Path: n.Addr.Module(),
Root: n.Parent.Root,
Children: map[string]*configs.Config{},
CallRange: n.ModuleCall.DeclRange,
SourceAddr: source,
SourceAddrRange: n.ModuleCall.SourceExpr.Range(),
Version: v,
}
// Insert the installed module into the children of the current module
currentModuleKey := n.Addr[len(n.Addr)-1].Name
n.Parent.Children[currentModuleKey] = config
n.Config = config
n.Version = v
return nil
}
func (n *nodeInstallModule) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) {
var g Graph
var diags tfdiags.Diagnostics
expander := ctx.InstanceExpander()
_, call := n.Addr.Call()
expander.SetModuleSingle(n.Path(), call)
graph, graphDiags := (&InitGraphBuilder{
Config: n.Config,
Walker: n.Installer,
}).Build(n.Addr)
diags = diags.Append(graphDiags)
if graphDiags.HasErrors() {
return nil, diags
}
g.Subsume(&graph.AcyclicGraph.Graph)
addRootNodeToGraph(&g)
return &g, nil
}
func decodeSource(sourceExpr hcl.Expression, hasVersion bool, ctx EvalContext) (addrs.ModuleSource, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var addr addrs.ModuleSource
var err error
refs, refsDiags := langrefs.ReferencesInExpr(addrs.ParseRef, sourceExpr)
diags = diags.Append(refsDiags)
if diags.HasErrors() {
return nil, diags
}
for _, ref := range refs {
switch ref.Subject.(type) {
case addrs.InputVariable, addrs.LocalValue:
// These are allowed
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module source",
Detail: "The module source can only reference input variables and local values.",
Subject: ref.SourceRange.ToHCL().Ptr(),
})
return nil, diags
}
}
value, valueDiags := ctx.EvaluateExpr(sourceExpr, cty.String, nil)
diags = diags.Append(valueDiags)
if diags.HasErrors() {
return nil, diags
}
if !value.IsWhollyKnown() {
tExpr, ok := sourceExpr.(*hclsyntax.TemplateExpr)
if !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module source",
Detail: "The module source contains a reference that is unknown during init.",
Subject: sourceExpr.Range().Ptr(),
})
return nil, diags
}
for _, part := range tExpr.Parts {
partVal, partDiags := ctx.EvaluateExpr(part, cty.DynamicPseudoType, nil)
diags = diags.Append(partDiags)
if diags.HasErrors() {
return nil, diags
}
scope := ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey)
hclCtx, evalDiags := scope.EvalContext(refs)
diags = diags.Append(evalDiags)
if diags.HasErrors() {
return nil, diags
}
if !partVal.IsKnown() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module source",
Detail: "The value of a reference in the module source is unknown.",
Subject: part.Range().Ptr(),
Expression: part,
EvalContext: hclCtx,
Extra: diagnosticCausedByUnknown(true),
})
return nil, diags
}
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module source",
Detail: "The module source contains a reference that is unknown.",
Subject: sourceExpr.Range().Ptr(),
})
return nil, diags
}
if hasVersion {
addr, err = moduleaddrs.ParseModuleSourceRegistry(value.AsString())
} else {
addr, err = moduleaddrs.ParseModuleSource(value.AsString())
}
if err != nil {
diags = diags.Append(diags, err)
}
return addr, nil
}
func decodeVersionConstraint(versionExpr hcl.Expression, ctx EvalContext) (configs.VersionConstraint, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
rng := versionExpr.Range()
ret := configs.VersionConstraint{
DeclRange: rng,
}
refs, refsDiags := langrefs.ReferencesInExpr(addrs.ParseRef, versionExpr)
diags = diags.Append(refsDiags)
if diags.HasErrors() {
return ret, diags
}
for _, ref := range refs {
switch ref.Subject.(type) {
case addrs.InputVariable, addrs.LocalValue:
// These are allowed
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module version",
Detail: "The module version can only reference input variables and local values.",
Subject: ref.SourceRange.ToHCL().Ptr(),
})
return ret, diags
}
}
value, valueDiags := ctx.EvaluateExpr(versionExpr, cty.String, nil)
diags = diags.Append(valueDiags)
if diags.HasErrors() {
return ret, diags
}
if value.IsNull() {
// A null version constraint is strange, but we'll just treat it
// like an empty constraint set.
return ret, diags
}
if !value.IsWhollyKnown() {
tExpr, ok := versionExpr.(*hclsyntax.TemplateExpr)
if !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module version",
Detail: "The module version contains a reference that is unknown during init.",
Subject: versionExpr.Range().Ptr(),
})
return ret, diags
}
for _, part := range tExpr.Parts {
partVal, partDiags := ctx.EvaluateExpr(part, cty.DynamicPseudoType, nil)
diags = diags.Append(partDiags)
if diags.HasErrors() {
return ret, diags
}
scope := ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey)
hclCtx, evalDiags := scope.EvalContext(refs)
diags = diags.Append(evalDiags)
if diags.HasErrors() {
return ret, diags
}
if !partVal.IsKnown() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module version",
Detail: "The value of a reference in the module version is unknown.",
Subject: part.Range().Ptr(),
Expression: part,
EvalContext: hclCtx,
Extra: diagnosticCausedByUnknown(true),
})
return ret, diags
}
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid module version",
Detail: "The module version contains a reference that is unknown.",
Subject: versionExpr.Range().Ptr(),
})
return ret, diags
}
constraintStr := value.AsString()
constraints, err := version.NewConstraint(constraintStr)
if err != nil {
// NewConstraint doesn't return user-friendly errors, so we'll just
// ignore the provided error and produce our own generic one.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid version constraint",
Detail: "This string does not use correct version constraint syntax.", // Not very actionable :(
Subject: rng.Ptr(),
})
return ret, diags
}
ret.Required = constraints
return ret, diags
}

@ -147,6 +147,49 @@ func testModuleInline(t testing.TB, sources map[string]string, parserOpts ...con
return config
}
func testRootModuleInline(t testing.TB, sources map[string]string) *configs.Module {
t.Helper()
cfgPath, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal(err)
}
for path, configStr := range sources {
dir := filepath.Dir(path)
if dir != "." {
err := os.MkdirAll(filepath.Join(cfgPath, dir), os.FileMode(0777))
if err != nil {
t.Fatalf("Error creating subdir: %s", err)
}
}
// Write the configuration
cfgF, err := os.Create(filepath.Join(cfgPath, path))
if err != nil {
t.Fatalf("Error creating temporary file for config: %s", err)
}
_, err = io.Copy(cfgF, strings.NewReader(configStr))
cfgF.Close()
if err != nil {
t.Fatalf("Error creating temporary file for config: %s", err)
}
}
loader, cleanup := configload.NewLoaderForTests(t)
defer cleanup()
// We need to be able to exercise experimental features in our integration tests.
loader.AllowLanguageExperiments(true)
mod, diags := loader.Parser().LoadConfigDir(cfgPath)
if diags.HasErrors() {
t.Fatal(diags.Error())
}
return mod
}
// testSetResourceInstanceCurrent is a helper function for tests that sets a Current,
// Ready resource instance for the given module.
func testSetResourceInstanceCurrent(module *states.Module, resource, attrsJson, provider string) {

@ -0,0 +1,46 @@
package terraform
import (
"log"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/dag"
)
type ModuleTransformer struct {
Config *configs.Config
Installer configs.ModuleWalker
}
func (t *ModuleTransformer) Transform(graph *Graph) error {
if t.Config == nil {
return nil
}
for _, call := range t.Config.Module.ModuleCalls {
instancePath := graph.Path.Child(call.Name, addrs.NoKey)
// instancePath := graph.Path.Module().Child(call.Name)
err := t.transform(graph, t.Config, instancePath, call)
if err != nil {
return err
}
}
return nil
}
func (t *ModuleTransformer) transform(graph *Graph, cfg *configs.Config, path addrs.ModuleInstance, modCall *configs.ModuleCall) error {
n := &nodeInstallModule{
Addr: path,
ModuleCall: modCall,
Parent: cfg,
Installer: t.Installer,
}
var installNode dag.Vertex = n
graph.Add(installNode)
log.Printf("[TRACE] ModuleTransformer: Added %s as %T", path, installNode)
return nil
}

@ -29,6 +29,10 @@ import (
type ModuleVariableTransformer struct {
Config *configs.Config
// ModuleOnly, if true, makes the transformer only process the
// variables in the current module, skipping any child modules.
ModuleOnly bool
// Planning must be set to true when building a planning graph, and must be
// false when building an apply graph.
Planning bool
@ -39,7 +43,11 @@ type ModuleVariableTransformer struct {
}
func (t *ModuleVariableTransformer) Transform(g *Graph) error {
return t.transform(g, nil, t.Config)
if t.ModuleOnly && t.Config.Parent != nil {
return t.transformSingle(g, t.Config.Parent, t.Config)
} else {
return t.transform(g, nil, t.Config)
}
}
func (t *ModuleVariableTransformer) transform(g *Graph, parent, c *configs.Config) error {

@ -16,11 +16,12 @@ func _() {
_ = x[walkDestroy-5]
_ = x[walkImport-6]
_ = x[walkEval-7]
_ = x[walkInit-8]
}
const _walkOperation_name = "walkInvalidwalkApplywalkPlanwalkPlanDestroywalkValidatewalkDestroywalkImportwalkEval"
const _walkOperation_name = "walkInvalidwalkApplywalkPlanwalkPlanDestroywalkValidatewalkDestroywalkImportwalkEvalwalkInit"
var _walkOperation_index = [...]uint8{0, 11, 20, 28, 43, 55, 66, 76, 84}
var _walkOperation_index = [...]uint8{0, 11, 20, 28, 43, 55, 66, 76, 84, 92}
func (i walkOperation) String() string {
idx := int(i) - 0

Loading…
Cancel
Save