mirror of https://github.com/hashicorp/terraform
parent
dee62db2f0
commit
ea9d8cc873
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
Loading…
Reference in new issue