Skip to content

Commit 5861dbf

Browse files
terraform: ugly huge change to weave in new HCL2-oriented types
Due to how deeply the configuration types go into Terraform Core, there isn't a great way to switch out to HCL2 gradually. As a consequence, this huge commit gets us from the old state to a _compilable_ new state, but does not yet attempt to fix any tests and has a number of known missing parts and bugs. We will continue to iterate on this in forthcoming commits, heading back towards passing tests and making Terraform fully-functional again. The three main goals here are: - Use the configuration models from the "configs" package instead of the older models in the "config" package, which is now deprecated and preserved only to help us write our migration tool. - Do expression inspection and evaluation using the functionality of the new "lang" package, instead of the Interpolator type and related functionality in the main "terraform" package. - Represent addresses of various objects using types in the addrs package, rather than hand-constructed strings. This is not critical to support the above, but was a big help during the implementation of these other points since it made it much more explicit what kind of address is expected in each context. Since our new packages are built to accommodate some future planned features that are not yet implemented (e.g. the "for_each" argument on resources, "count"/"for_each" on modules), and since there's still a fair amount of functionality still using old-style APIs, there is a moderate amount of shimming here to connect new assumptions with old, hopefully in a way that makes it easier to find and eliminate these shims later. I apologize in advance to the person who inevitably just found this huge commit while spelunking through the commit history.
1 parent 9625d62 commit 5861dbf

File tree

130 files changed

+5264
-4198
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

130 files changed

+5264
-4198
lines changed

backend/backend.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import (
99
"errors"
1010
"time"
1111

12-
"github.com/hashicorp/terraform/configs"
13-
1412
"github.com/zclconf/go-cty/cty"
1513

14+
"github.com/hashicorp/terraform/addrs"
1615
"github.com/hashicorp/terraform/command/clistate"
1716
"github.com/hashicorp/terraform/config/configschema"
17+
"github.com/hashicorp/terraform/configs"
1818
"github.com/hashicorp/terraform/configs/configload"
1919
"github.com/hashicorp/terraform/state"
2020
"github.com/hashicorp/terraform/terraform"
@@ -168,8 +168,8 @@ type Operation struct {
168168
// The options below are more self-explanatory and affect the runtime
169169
// behavior of the operation.
170170
Destroy bool
171-
Targets []string
172-
Variables map[string]interface{}
171+
Targets []addrs.Targetable
172+
Variables map[string]UnparsedVariableValue
173173
AutoApprove bool
174174
DestroyForce bool
175175

backend/local/backend_apply.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,11 @@ func (b *Local) opApply(
137137

138138
// Start the apply in a goroutine so that we can be interrupted.
139139
var applyState *terraform.State
140-
var applyErr error
140+
var applyDiags tfdiags.Diagnostics
141141
doneCh := make(chan struct{})
142142
go func() {
143143
defer close(doneCh)
144-
_, applyErr = tfCtx.Apply()
144+
_, applyDiags = tfCtx.Apply()
145145
// we always want the state, even if apply failed
146146
applyState = tfCtx.State()
147147
}()
@@ -165,12 +165,8 @@ func (b *Local) opApply(
165165
return
166166
}
167167

168-
if applyErr != nil {
169-
diags = diags.Append(tfdiags.Sourceless(
170-
tfdiags.Error,
171-
applyErr.Error(),
172-
"Terraform does not automatically rollback in the face of errors. Instead, your Terraform state file has been partially updated with any resources that successfully completed. Please address the error above and apply again to incrementally change your infrastructure.",
173-
))
168+
diags = diags.Append(applyDiags)
169+
if applyDiags.HasErrors() {
174170
b.ReportResult(runningOp, diags)
175171
return
176172
}

backend/local/backend_local.go

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package local
22

33
import (
44
"context"
5-
"errors"
65

76
"github.com/hashicorp/terraform/command/clistate"
87

@@ -59,15 +58,23 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State,
5958
opts.Destroy = op.Destroy
6059
opts.Targets = op.Targets
6160
opts.UIInput = op.UIIn
62-
if op.Variables != nil {
63-
opts.Variables = op.Variables
61+
62+
// Load the configuration using the caller-provided configuration loader.
63+
config, configDiags := op.ConfigLoader.LoadConfig(op.ConfigDir)
64+
diags = diags.Append(configDiags)
65+
if configDiags.HasErrors() {
66+
return nil, nil, diags
6467
}
68+
opts.Config = config
6569

66-
// FIXME: Configuration is temporarily stubbed out here to artificially
67-
// create a stopping point in our work to switch to the new config loader.
68-
// This means no backend-provided Terraform operations will actually work.
69-
// This will be addressed in a subsequent commit.
70-
opts.Module = nil
70+
variables, varDiags := backend.ParseVariableValues(op.Variables, config.Module.Variables)
71+
diags = diags.Append(varDiags)
72+
if diags.HasErrors() {
73+
return nil, nil, diags
74+
}
75+
if op.Variables != nil {
76+
opts.Variables = variables
77+
}
7178

7279
// Load our state
7380
// By the time we get here, the backend creation code in "command" took
@@ -78,23 +85,14 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State,
7885

7986
// Build the context
8087
var tfCtx *terraform.Context
88+
var ctxDiags tfdiags.Diagnostics
8189
if op.Plan != nil {
82-
tfCtx, err = op.Plan.Context(&opts)
90+
tfCtx, ctxDiags = op.Plan.Context(&opts)
8391
} else {
84-
tfCtx, err = terraform.NewContext(&opts)
85-
}
86-
87-
// any errors resolving plugins returns this
88-
if rpe, ok := err.(*terraform.ResourceProviderError); ok {
89-
b.pluginInitRequired(rpe)
90-
// we wrote the full UI error here, so return a generic error for flow
91-
// control in the command.
92-
diags = diags.Append(errors.New("Can't satisfy plugin requirements"))
93-
return nil, nil, diags
92+
tfCtx, ctxDiags = terraform.NewContext(&opts)
9493
}
95-
96-
if err != nil {
97-
diags = diags.Append(err)
94+
diags = diags.Append(ctxDiags)
95+
if ctxDiags.HasErrors() {
9896
return nil, nil, diags
9997
}
10098

@@ -107,8 +105,9 @@ func (b *Local) context(op *backend.Operation) (*terraform.Context, state.State,
107105
mode |= terraform.InputModeVar
108106
mode |= terraform.InputModeVarUnset
109107

110-
if err := tfCtx.Input(mode); err != nil {
111-
diags = diags.Append(errwrap.Wrapf("Error asking for user input: {{err}}", err))
108+
inputDiags := tfCtx.Input(mode)
109+
diags = diags.Append(inputDiags)
110+
if inputDiags.HasErrors() {
112111
return nil, nil, diags
113112
}
114113
}

backend/local/backend_plan.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,20 +87,20 @@ func (b *Local) opPlan(
8787

8888
// Perform the plan in a goroutine so we can be interrupted
8989
var plan *terraform.Plan
90-
var planErr error
90+
var planDiags tfdiags.Diagnostics
9191
doneCh := make(chan struct{})
9292
go func() {
9393
defer close(doneCh)
9494
log.Printf("[INFO] backend/local: plan calling Plan")
95-
plan, planErr = tfCtx.Plan()
95+
plan, planDiags = tfCtx.Plan()
9696
}()
9797

9898
if b.opWait(doneCh, stopCtx, cancelCtx, tfCtx, opState) {
9999
return
100100
}
101101

102-
if planErr != nil {
103-
diags = diags.Append(planErr)
102+
diags = diags.Append(planDiags)
103+
if planDiags.HasErrors() {
104104
b.ReportResult(runningOp, diags)
105105
return
106106
}

backend/local/backend_refresh.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ func (b *Local) opRefresh(
6060

6161
// Perform the refresh in a goroutine so we can be interrupted
6262
var newState *terraform.State
63-
var refreshErr error
63+
var refreshDiags tfdiags.Diagnostics
6464
doneCh := make(chan struct{})
6565
go func() {
6666
defer close(doneCh)
67-
newState, refreshErr = tfCtx.Refresh()
67+
newState, refreshDiags = tfCtx.Refresh()
6868
log.Printf("[INFO] backend/local: refresh calling Refresh")
6969
}()
7070

@@ -74,8 +74,8 @@ func (b *Local) opRefresh(
7474

7575
// write the resulting state to the running op
7676
runningOp.State = newState
77-
if refreshErr != nil {
78-
diags = diags.Append(refreshErr)
77+
diags = diags.Append(refreshDiags)
78+
if refreshDiags.HasErrors() {
7979
b.ReportResult(runningOp, diags)
8080
return
8181
}

backend/unparsed_value.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package backend
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/hcl2/hcl"
7+
"github.com/hashicorp/terraform/configs"
8+
"github.com/hashicorp/terraform/terraform"
9+
"github.com/hashicorp/terraform/tfdiags"
10+
)
11+
12+
// UnparsedVariableValue represents a variable value provided by the caller
13+
// whose parsing must be deferred until configuration is available.
14+
//
15+
// This exists to allow processing of variable-setting arguments (e.g. in the
16+
// command package) to be separated from parsing (in the backend package).
17+
type UnparsedVariableValue interface {
18+
// ParseVariableValue information in the provided variable configuration
19+
// to parse (if necessary) and return the variable value encapsulated in
20+
// the receiver.
21+
//
22+
// If error diagnostics are returned, the resulting value may be invalid
23+
// or incomplete.
24+
ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics)
25+
}
26+
27+
func ParseVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*configs.Variable) (terraform.InputValues, tfdiags.Diagnostics) {
28+
var diags tfdiags.Diagnostics
29+
ret := make(terraform.InputValues, len(vv))
30+
31+
for name, rv := range vv {
32+
var mode configs.VariableParsingMode
33+
config, declared := decls[name]
34+
if declared {
35+
mode = config.ParsingMode
36+
} else {
37+
mode = configs.VariableParseLiteral
38+
}
39+
40+
val, valDiags := rv.ParseVariableValue(mode)
41+
diags = diags.Append(valDiags)
42+
if valDiags.HasErrors() {
43+
continue
44+
}
45+
46+
if !declared {
47+
switch val.SourceType {
48+
case terraform.ValueFromConfig, terraform.ValueFromFile:
49+
// These source types have source ranges, so we can produce
50+
// a nice error message with good context.
51+
diags = diags.Append(&hcl.Diagnostic{
52+
Severity: hcl.DiagError,
53+
Summary: "Value for undeclared variable",
54+
Detail: fmt.Sprintf("The root module does not declare a variable named %q. To use this value, add a \"variable\" block to the configuration.", name),
55+
Subject: val.SourceRange.ToHCL().Ptr(),
56+
})
57+
case terraform.ValueFromEnvVar:
58+
// We allow and ignore undeclared names for environment
59+
// variables, because users will often set these globally
60+
// when they are used across many (but not necessarily all)
61+
// configurations.
62+
case terraform.ValueFromCLIArg:
63+
diags = diags.Append(tfdiags.Sourceless(
64+
tfdiags.Error,
65+
"Value for undeclared variable",
66+
fmt.Sprintf("A variable named %q was assigned on the command line, but the root module does not declare a variable of that name. To use this value, add a \"variable\" block to the configuration.", name),
67+
))
68+
default:
69+
// For all other source types we are more vague, but other situations
70+
// don't generally crop up at this layer in practice.
71+
diags = diags.Append(tfdiags.Sourceless(
72+
tfdiags.Error,
73+
"Value for undeclared variable",
74+
fmt.Sprintf("A variable named %q was assigned a value, but the root module does not declare a variable of that name. To use this value, add a \"variable\" block to the configuration.", name),
75+
))
76+
}
77+
continue
78+
}
79+
80+
ret[name] = val
81+
}
82+
83+
return ret, diags
84+
}

command/apply.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import (
77
"sort"
88
"strings"
99

10-
"github.com/hashicorp/terraform/configs"
11-
12-
"github.com/hashicorp/terraform/tfdiags"
13-
1410
"github.com/hashicorp/go-getter"
11+
12+
"github.com/hashicorp/terraform/addrs"
1513
"github.com/hashicorp/terraform/backend"
1614
"github.com/hashicorp/terraform/config"
15+
"github.com/hashicorp/terraform/configs"
1716
"github.com/hashicorp/terraform/terraform"
17+
"github.com/hashicorp/terraform/tfdiags"
1818
)
1919

2020
// ApplyCommand is a Command implementation that applies a Terraform
@@ -313,7 +313,7 @@ Options:
313313
return strings.TrimSpace(helpText)
314314
}
315315

316-
func outputsAsString(state *terraform.State, modPath []string, schema []*config.Output, includeHeader bool) string {
316+
func outputsAsString(state *terraform.State, modPath addrs.ModuleInstance, schema []*config.Output, includeHeader bool) string {
317317
if state == nil {
318318
return ""
319319
}

command/flag_kv.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ package command
33
import (
44
"fmt"
55
"strings"
6+
7+
"github.com/hashicorp/hcl2/hcl"
8+
"github.com/hashicorp/hcl2/hcl/hclsyntax"
9+
"github.com/hashicorp/terraform/addrs"
10+
"github.com/hashicorp/terraform/tfdiags"
611
)
712

813
// FlagStringKV is a flag.Value implementation for parsing user variables
@@ -41,3 +46,34 @@ func (v *FlagStringSlice) Set(raw string) error {
4146

4247
return nil
4348
}
49+
50+
// FlagTargetSlice is a flag.Value implementation for parsing target addresses
51+
// from the command line, such as -target=aws_instance.foo -target=aws_vpc.bar .
52+
type FlagTargetSlice []addrs.Targetable
53+
54+
func (v *FlagTargetSlice) String() string {
55+
return ""
56+
}
57+
58+
func (v *FlagTargetSlice) Set(raw string) error {
59+
// FIXME: This is not an ideal way to deal with this because it requires
60+
// us to do parsing in a context where we can't nicely return errors
61+
// to the user.
62+
63+
var diags tfdiags.Diagnostics
64+
synthFilename := fmt.Sprintf("-target=%q", raw)
65+
traversal, syntaxDiags := hclsyntax.ParseTraversalAbs([]byte(raw), synthFilename, hcl.Pos{Line: 1, Column: 1})
66+
diags = diags.Append(syntaxDiags)
67+
if syntaxDiags.HasErrors() {
68+
return diags.Err()
69+
}
70+
71+
target, targetDiags := addrs.ParseTarget(traversal)
72+
diags = diags.Append(targetDiags)
73+
if targetDiags.HasErrors() {
74+
return diags.Err()
75+
}
76+
77+
*v = append(*v, target.Subject)
78+
return nil
79+
}

command/graph.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,13 @@ func (c *GraphCommand) Run(args []string) int {
127127

128128
// Skip validation during graph generation - we want to see the graph even if
129129
// it is invalid for some reason.
130-
g, err := ctx.Graph(graphType, &terraform.ContextGraphOpts{
130+
g, graphDiags := ctx.Graph(graphType, &terraform.ContextGraphOpts{
131131
Verbose: verbose,
132132
Validate: false,
133133
})
134-
if err != nil {
135-
c.Ui.Error(fmt.Sprintf("Error creating graph: %s", err))
134+
diags = diags.Append(graphDiags)
135+
if graphDiags.HasErrors() {
136+
c.showDiagnostics(diags)
136137
return 1
137138
}
138139

0 commit comments

Comments
 (0)