mirror of
https://github.com/hashicorp/terraform.git
synced 2026-06-09 08:58:34 -04:00
Introduce a new init graph
The new init graph builder creates a small graph that can be used to install or load module configuration. It reuses different walkers to either install modules or validate the manifest during configuration loading. The new module install node dynamically expands the graph after module installation with a subgraph for the installed module.
This commit is contained in:
parent
7127c76b04
commit
28b76c1105
13 changed files with 1840 additions and 20 deletions
42
internal/terraform/config_graph_build.go
Normal file
42
internal/terraform/config_graph_build.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
// BuildConfigWithGraph builds a configuration tree using the init graph so
|
||||
// that module sources and versions can be resolved with full expression
|
||||
// evaluation before loading descendant modules.
|
||||
func BuildConfigWithGraph(rootMod *configs.Module, walker configs.ModuleWalker, vars InputValues, loader configs.MockDataLoader) (*configs.Config, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
ctx, ctxDiags := NewContext(&ContextOpts{
|
||||
Parallelism: 1,
|
||||
})
|
||||
diags = diags.Append(ctxDiags)
|
||||
if ctxDiags.HasErrors() {
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
cfg, initDiags := ctx.Init(rootMod, InitOpts{
|
||||
Walker: walker,
|
||||
SetVariables: vars,
|
||||
})
|
||||
diags = diags.Append(initDiags)
|
||||
if diags.HasErrors() {
|
||||
if cfg == nil && rootMod != nil {
|
||||
cfg = &configs.Config{Module: rootMod}
|
||||
cfg.Root = cfg
|
||||
}
|
||||
return cfg, diags
|
||||
}
|
||||
|
||||
finalDiags := configs.FinalizeConfig(cfg, walker, loader)
|
||||
diags = diags.Append(finalDiags)
|
||||
|
||||
return cfg, diags
|
||||
}
|
||||
60
internal/terraform/context_init.go
Normal file
60
internal/terraform/context_init.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// Copyright IBM Corp. 2014, 2026
|
||||
// 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
|
||||
}
|
||||
712
internal/terraform/context_init_test.go
Normal file
712
internal/terraform/context_init_test.go
Normal file
|
|
@ -0,0 +1,712 @@
|
|||
// Copyright IBM Corp. 2014, 2026
|
||||
// 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-static 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-static 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"),
|
||||
}},
|
||||
},
|
||||
|
||||
"static 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},
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
"static 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-static variable passed into static 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: `Static variables must be known`,
|
||||
Detail: `Only a static value can be passed into a static 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-static module variable used as static": {
|
||||
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-static 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{
|
||||
Parallelism: 1,
|
||||
})
|
||||
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,
|
||||
}
|
||||
}
|
||||
83
internal/terraform/graph_builder_init.go
Normal file
83
internal/terraform/graph_builder_init.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/addrs"
|
||||
"github.com/hashicorp/terraform/internal/configs"
|
||||
"github.com/hashicorp/terraform/internal/dag"
|
||||
"github.com/hashicorp/terraform/internal/tfdiags"
|
||||
)
|
||||
|
||||
type InitGraphBuilder struct {
|
||||
// A config derived from the root module
|
||||
Config *configs.Config
|
||||
|
||||
RootVariableValues InputValues
|
||||
|
||||
Walker configs.ModuleWalker
|
||||
}
|
||||
|
||||
// See GraphBuilder
|
||||
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)
|
||||
}
|
||||
|
||||
// See GraphBuilder
|
||||
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,
|
||||
Walker: b.Walker,
|
||||
},
|
||||
|
||||
&LocalTransformer{
|
||||
Config: b.Config,
|
||||
},
|
||||
|
||||
&ReferenceTransformer{},
|
||||
|
||||
// Filters out any vertices that aren't relevant to the init graph
|
||||
&TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
switch n := v.(type) {
|
||||
case *nodeInstallModule:
|
||||
return true
|
||||
case *NodeRootVariable:
|
||||
return n.Config.Const
|
||||
case *nodeExpandModuleVariable:
|
||||
return n.Config.Const
|
||||
default:
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
&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
|
||||
)
|
||||
|
|
|
|||
392
internal/terraform/node_module_install.go
Normal file
392
internal/terraform/node_module_install.go
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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
|
||||
Walker 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.Walker.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,
|
||||
VersionConstraint: version,
|
||||
}
|
||||
|
||||
// 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.Walker,
|
||||
}).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 {
|
||||
// NOTE: We leave add as nil for any situation where the
|
||||
// source attribute is invalid, so any code which tries to carefully
|
||||
// use the partial result of a failed config decode must be
|
||||
// resilient to that.
|
||||
addr = nil
|
||||
|
||||
// NOTE: In practice it's actually very unlikely to end up here,
|
||||
// because our source address parser can turn just about any string
|
||||
// into some sort of remote package address, and so for most errors
|
||||
// we'll detect them only during module installation. There are
|
||||
// still a _few_ purely-syntax errors we can catch at parsing time,
|
||||
// though, mostly related to remote package sub-paths and local
|
||||
// paths.
|
||||
switch err := err.(type) {
|
||||
case *moduleaddrs.MaybeRelativePathErr:
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid module source address",
|
||||
Detail: fmt.Sprintf(
|
||||
"Terraform failed to determine your intended installation method for remote module package %q.\n\nIf you intended this as a path relative to the current module, use \"./%s\" instead. The \"./\" prefix indicates that the address is a relative filesystem path.",
|
||||
err.Addr, err.Addr,
|
||||
),
|
||||
Subject: sourceExpr.Range().Ptr(),
|
||||
})
|
||||
default:
|
||||
if hasVersion {
|
||||
// In this case we'll include some extra context that
|
||||
// we assumed a registry source address due to the
|
||||
// version argument.
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid registry module source address",
|
||||
Detail: fmt.Sprintf("Failed to parse module registry address: %s.\n\nTerraform assumed that you intended a module registry source address because you also set the argument \"version\", which applies only to registry modules.", err),
|
||||
Subject: sourceExpr.Range().Ptr(),
|
||||
})
|
||||
} else {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid module source address",
|
||||
Detail: fmt.Sprintf("Failed to parse module source address: %s.", err),
|
||||
Subject: sourceExpr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return addr, diags
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -217,14 +217,26 @@ func (n *nodeModuleVariable) Execute(ctx EvalContext, op walkOperation) (diags t
|
|||
log.Printf("[TRACE] nodeModuleVariable: evaluating %s", n.Addr)
|
||||
|
||||
var val cty.Value
|
||||
var errSourceRange tfdiags.SourceRange
|
||||
var err error
|
||||
|
||||
switch op {
|
||||
case walkValidate:
|
||||
val, err = n.evalModuleVariable(ctx, true)
|
||||
val, errSourceRange, err = n.evalModuleVariable(ctx, true)
|
||||
diags = diags.Append(err)
|
||||
case walkInit:
|
||||
// During init we only want to record the value if it's static;
|
||||
// otherwise we record it as dynamic to prevent its use in
|
||||
// static contexts.
|
||||
// We still evaluate it fully here to catch any errors early.
|
||||
if n.Config.Const {
|
||||
val, errSourceRange, err = n.evalModuleVariable(ctx, false)
|
||||
diags = diags.Append(err)
|
||||
} else {
|
||||
val = cty.DynamicVal
|
||||
}
|
||||
default:
|
||||
val, err = n.evalModuleVariable(ctx, false)
|
||||
val, errSourceRange, err = n.evalModuleVariable(ctx, false)
|
||||
diags = diags.Append(err)
|
||||
}
|
||||
if diags.HasErrors() {
|
||||
|
|
@ -236,6 +248,15 @@ func (n *nodeModuleVariable) Execute(ctx EvalContext, op walkOperation) (diags t
|
|||
diags = diags.Append(deprecationDiags)
|
||||
}
|
||||
|
||||
if op == walkInit && n.Config.Const && !val.IsWhollyKnown() {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Static variables must be known",
|
||||
Detail: "Only a static value can be passed into a static module variable.",
|
||||
Subject: errSourceRange.ToHCL().Ptr(),
|
||||
})
|
||||
}
|
||||
|
||||
// Set values for arguments of a child module call, for later retrieval
|
||||
// during expression evaluation.
|
||||
ctx.NamedValues().SetInputVariableValue(n.Addr, val)
|
||||
|
|
@ -263,7 +284,7 @@ func (n *nodeModuleVariable) DotNode(name string, opts *dag.DotOpts) *dag.DotNod
|
|||
// validateOnly indicates that this evaluation is only for config
|
||||
// validation, and we will not have any expansion module instance
|
||||
// repetition data.
|
||||
func (n *nodeModuleVariable) evalModuleVariable(ctx EvalContext, validateOnly bool) (cty.Value, error) {
|
||||
func (n *nodeModuleVariable) evalModuleVariable(ctx EvalContext, validateOnly bool) (cty.Value, tfdiags.SourceRange, error) {
|
||||
var diags tfdiags.Diagnostics
|
||||
var givenVal cty.Value
|
||||
var errSourceRange tfdiags.SourceRange
|
||||
|
|
@ -289,7 +310,7 @@ func (n *nodeModuleVariable) evalModuleVariable(ctx EvalContext, validateOnly bo
|
|||
val, moreDiags := scope.EvalExpr(expr, cty.DynamicPseudoType)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
return cty.DynamicVal, diags.ErrWithWarnings()
|
||||
return cty.DynamicVal, errSourceRange, diags.ErrWithWarnings()
|
||||
}
|
||||
givenVal = val
|
||||
errSourceRange = tfdiags.SourceRangeFromHCL(expr.Range())
|
||||
|
|
@ -320,7 +341,7 @@ func (n *nodeModuleVariable) evalModuleVariable(ctx EvalContext, validateOnly bo
|
|||
})
|
||||
}
|
||||
|
||||
return finalVal, diags.ErrWithWarnings()
|
||||
return finalVal, errSourceRange, diags.ErrWithWarnings()
|
||||
}
|
||||
|
||||
// nodeModuleVariableInPartialModule represents an infinite set of possible
|
||||
|
|
|
|||
|
|
@ -110,19 +110,43 @@ func (n *NodeRootVariable) Execute(ctx EvalContext, op walkOperation) tfdiags.Di
|
|||
}
|
||||
}
|
||||
|
||||
finalVal, moreDiags := PrepareFinalInputVariableValue(
|
||||
addr,
|
||||
givenVal,
|
||||
n.Config,
|
||||
)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
// No point in proceeding to validations then, because they'll
|
||||
// probably fail trying to work with a value of the wrong type.
|
||||
return diags
|
||||
}
|
||||
// During init we only want to prepare the final value for static variables.
|
||||
if op == walkInit {
|
||||
var finalVal cty.Value
|
||||
if n.Config.Const {
|
||||
var moreDiags tfdiags.Diagnostics
|
||||
finalVal, moreDiags = PrepareFinalInputVariableValue(
|
||||
addr,
|
||||
givenVal,
|
||||
n.Config,
|
||||
)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
// No point in proceeding to validations then, because they'll
|
||||
// probably fail trying to work with a value of the wrong type.
|
||||
return diags
|
||||
}
|
||||
} else {
|
||||
// All non-static variables are unknown during init.
|
||||
finalVal = cty.UnknownVal(n.Config.Type)
|
||||
}
|
||||
ctx.NamedValues().SetInputVariableValue(addr, finalVal)
|
||||
|
||||
ctx.NamedValues().SetInputVariableValue(addr, finalVal)
|
||||
} else {
|
||||
finalVal, moreDiags := PrepareFinalInputVariableValue(
|
||||
addr,
|
||||
givenVal,
|
||||
n.Config,
|
||||
)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
// No point in proceeding to validations then, because they'll
|
||||
// probably fail trying to work with a value of the wrong type.
|
||||
return diags
|
||||
}
|
||||
|
||||
ctx.NamedValues().SetInputVariableValue(addr, finalVal)
|
||||
}
|
||||
|
||||
// Custom validation rules are handled by a separate graph node of type
|
||||
// nodeVariableValidation, added by variableValidationTransformer.
|
||||
|
|
|
|||
42
internal/terraform/transform_filter.go
Normal file
42
internal/terraform/transform_filter.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform/internal/dag"
|
||||
)
|
||||
|
||||
// TransformFilter is a GraphTransformer that filters out nodes from the graph based on a provided function. The Keep function should return true for nodes that should be kept in the graph, and false for nodes that should be removed. The transformer will mark all nodes that the node to keep depends on as well, ensuring that the resulting graph is still valid.
|
||||
type TransformFilter struct {
|
||||
Keep func(node dag.Vertex) bool
|
||||
}
|
||||
|
||||
var _ GraphTransformer = (*TransformFilter)(nil)
|
||||
|
||||
func (t *TransformFilter) Transform(g *Graph) error {
|
||||
// Partition vertices into kept and candidates for removal.
|
||||
var kept []dag.Vertex
|
||||
var removalCandidates []dag.Vertex
|
||||
for _, v := range g.Vertices() {
|
||||
if t.Keep(v) {
|
||||
kept = append(kept, v)
|
||||
} else {
|
||||
removalCandidates = append(removalCandidates, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Also keep all ancestors (transitive dependencies) of the kept
|
||||
// nodes so the resulting graph stays valid.
|
||||
ancestors := g.Ancestors(kept...)
|
||||
|
||||
// Remove every vertex that isn't explicitly kept and isn't an
|
||||
// ancestor of a kept node.
|
||||
for _, v := range removalCandidates {
|
||||
if !ancestors.Include(v) {
|
||||
g.Remove(v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
386
internal/terraform/transform_filter_test.go
Normal file
386
internal/terraform/transform_filter_test.go
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/dag"
|
||||
)
|
||||
|
||||
func TestTransformFilter(t *testing.T) {
|
||||
t.Run("empty graph", func(t *testing.T) {
|
||||
var g Graph
|
||||
tf := &TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
if actual != "" {
|
||||
t.Fatalf("expected empty graph, got:\n%s", actual)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("keep all", func(t *testing.T) {
|
||||
var g Graph
|
||||
g.Add("a")
|
||||
g.Add("b")
|
||||
g.Add("c")
|
||||
g.Connect(dag.BasicEdge("a", "b"))
|
||||
g.Connect(dag.BasicEdge("b", "c"))
|
||||
|
||||
tf := &TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := strings.TrimSpace(`
|
||||
a
|
||||
b
|
||||
b
|
||||
c
|
||||
c
|
||||
`)
|
||||
if actual != expected {
|
||||
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("remove all", func(t *testing.T) {
|
||||
var g Graph
|
||||
g.Add("a")
|
||||
g.Add("b")
|
||||
g.Add("c")
|
||||
g.Connect(dag.BasicEdge("a", "b"))
|
||||
g.Connect(dag.BasicEdge("b", "c"))
|
||||
|
||||
tf := &TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
return false
|
||||
},
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
if actual != "" {
|
||||
t.Fatalf("expected empty graph, got:\n%s", actual)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("keep node preserves its dependencies", func(t *testing.T) {
|
||||
// a -> b -> c
|
||||
// Keep only "a"; "b" and "c" should be preserved as ancestors.
|
||||
var g Graph
|
||||
g.Add("a")
|
||||
g.Add("b")
|
||||
g.Add("c")
|
||||
g.Connect(dag.BasicEdge("a", "b"))
|
||||
g.Connect(dag.BasicEdge("b", "c"))
|
||||
|
||||
tf := &TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
return v.(string) == "a"
|
||||
},
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := strings.TrimSpace(`
|
||||
a
|
||||
b
|
||||
b
|
||||
c
|
||||
c
|
||||
`)
|
||||
if actual != expected {
|
||||
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("keep leaf removes dependents", func(t *testing.T) {
|
||||
// a -> b -> c
|
||||
// Keep only "c"; "a" and "b" are not ancestors of "c" so they
|
||||
// should be removed.
|
||||
var g Graph
|
||||
g.Add("a")
|
||||
g.Add("b")
|
||||
g.Add("c")
|
||||
g.Connect(dag.BasicEdge("a", "b"))
|
||||
g.Connect(dag.BasicEdge("b", "c"))
|
||||
|
||||
tf := &TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
return v.(string) == "c"
|
||||
},
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := "c"
|
||||
if actual != expected {
|
||||
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("keep middle preserves dependencies and removes dependents", func(t *testing.T) {
|
||||
// a -> b -> c
|
||||
// Keep "b"; "c" is an ancestor and stays, "a" depends on "b"
|
||||
// but is not an ancestor so it is removed.
|
||||
var g Graph
|
||||
g.Add("a")
|
||||
g.Add("b")
|
||||
g.Add("c")
|
||||
g.Connect(dag.BasicEdge("a", "b"))
|
||||
g.Connect(dag.BasicEdge("b", "c"))
|
||||
|
||||
tf := &TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
return v.(string) == "b"
|
||||
},
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := strings.TrimSpace(`
|
||||
b
|
||||
c
|
||||
c
|
||||
`)
|
||||
if actual != expected {
|
||||
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("diamond keep root preserves all", func(t *testing.T) {
|
||||
// a -> b -> d
|
||||
// a -> c -> d
|
||||
// Keep "a"; everything is an ancestor of "a" so nothing is removed.
|
||||
var g Graph
|
||||
g.Add("a")
|
||||
g.Add("b")
|
||||
g.Add("c")
|
||||
g.Add("d")
|
||||
g.Connect(dag.BasicEdge("a", "b"))
|
||||
g.Connect(dag.BasicEdge("a", "c"))
|
||||
g.Connect(dag.BasicEdge("b", "d"))
|
||||
g.Connect(dag.BasicEdge("c", "d"))
|
||||
|
||||
tf := &TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
return v.(string) == "a"
|
||||
},
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := strings.TrimSpace(`
|
||||
a
|
||||
b
|
||||
c
|
||||
b
|
||||
d
|
||||
c
|
||||
d
|
||||
d
|
||||
`)
|
||||
if actual != expected {
|
||||
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("diamond keep one branch", func(t *testing.T) {
|
||||
// a -> b -> d
|
||||
// a -> c -> d
|
||||
// Keep "b"; "d" is an ancestor of "b" so it stays. "a" and "c"
|
||||
// are not ancestors of "b" so they are removed.
|
||||
var g Graph
|
||||
g.Add("a")
|
||||
g.Add("b")
|
||||
g.Add("c")
|
||||
g.Add("d")
|
||||
g.Connect(dag.BasicEdge("a", "b"))
|
||||
g.Connect(dag.BasicEdge("a", "c"))
|
||||
g.Connect(dag.BasicEdge("b", "d"))
|
||||
g.Connect(dag.BasicEdge("c", "d"))
|
||||
|
||||
tf := &TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
return v.(string) == "b"
|
||||
},
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := strings.TrimSpace(`
|
||||
b
|
||||
d
|
||||
d
|
||||
`)
|
||||
if actual != expected {
|
||||
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("disconnected nodes are removed", func(t *testing.T) {
|
||||
// a -> b, c (standalone)
|
||||
// Keep "a"; "b" is preserved as ancestor, "c" has no connection
|
||||
// and is removed.
|
||||
var g Graph
|
||||
g.Add("a")
|
||||
g.Add("b")
|
||||
g.Add("c")
|
||||
g.Connect(dag.BasicEdge("a", "b"))
|
||||
|
||||
tf := &TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
return v.(string) == "a"
|
||||
},
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := strings.TrimSpace(`
|
||||
a
|
||||
b
|
||||
b
|
||||
`)
|
||||
if actual != expected {
|
||||
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple kept nodes merge their ancestors", func(t *testing.T) {
|
||||
// a -> b -> d
|
||||
// c -> d
|
||||
// Keep "a" and "c"; their combined ancestors are "b" and "d",
|
||||
// so the entire graph is preserved.
|
||||
var g Graph
|
||||
g.Add("a")
|
||||
g.Add("b")
|
||||
g.Add("c")
|
||||
g.Add("d")
|
||||
g.Connect(dag.BasicEdge("a", "b"))
|
||||
g.Connect(dag.BasicEdge("b", "d"))
|
||||
g.Connect(dag.BasicEdge("c", "d"))
|
||||
|
||||
tf := &TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
s := v.(string)
|
||||
return s == "a" || s == "c"
|
||||
},
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := strings.TrimSpace(`
|
||||
a
|
||||
b
|
||||
b
|
||||
d
|
||||
c
|
||||
d
|
||||
d
|
||||
`)
|
||||
if actual != expected {
|
||||
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("shared dependency kept through one branch", func(t *testing.T) {
|
||||
// a -> c
|
||||
// b -> c
|
||||
// Keep "a"; "c" is an ancestor and stays, "b" is removed.
|
||||
var g Graph
|
||||
g.Add("a")
|
||||
g.Add("b")
|
||||
g.Add("c")
|
||||
g.Connect(dag.BasicEdge("a", "c"))
|
||||
g.Connect(dag.BasicEdge("b", "c"))
|
||||
|
||||
tf := &TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
return v.(string) == "a"
|
||||
},
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := strings.TrimSpace(`
|
||||
a
|
||||
c
|
||||
c
|
||||
`)
|
||||
if actual != expected {
|
||||
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single node kept", func(t *testing.T) {
|
||||
var g Graph
|
||||
g.Add("a")
|
||||
|
||||
tf := &TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
expected := "a"
|
||||
if actual != expected {
|
||||
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single node removed", func(t *testing.T) {
|
||||
var g Graph
|
||||
g.Add("a")
|
||||
|
||||
tf := &TransformFilter{
|
||||
Keep: func(v dag.Vertex) bool {
|
||||
return false
|
||||
},
|
||||
}
|
||||
if err := tf.Transform(&g); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
actual := strings.TrimSpace(g.String())
|
||||
if actual != "" {
|
||||
t.Fatalf("expected empty graph, got:\n%s", actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
48
internal/terraform/transform_module_install.go
Normal file
48
internal/terraform/transform_module_install.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright IBM Corp. 2014, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
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
|
||||
Walker 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)
|
||||
|
||||
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,
|
||||
Walker: t.Walker,
|
||||
}
|
||||
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…
Reference in a new issue