Skip to content

Commit a47b026

Browse files
authored
terraform: Add support for Terraform v1.2 syntax (#1403)
* Bump Terraform version to v1.2.1 * Fix build error due to remove DefaultVariableValues terraform.DefaultVariableValues is removed in this commit: hashicorp/terraform@36c4d4c This removes the redundant code, but the implementation itself is not wrong, so move it to the tflint package.
1 parent 667a88c commit a47b026

27 files changed

+999
-510
lines changed

terraform/addrs/module_source.go

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,36 @@ var moduleSourceLocalPrefixes = []string{
4646
"..\\",
4747
}
4848

49+
// ParseModuleSource parses a module source address as given in the "source"
50+
// argument inside a "module" block in the configuration.
51+
//
52+
// For historical reasons this syntax is a bit overloaded, supporting three
53+
// different address types:
54+
// - Local paths starting with either ./ or ../, which are special because
55+
// Terraform considers them to belong to the same "package" as the caller.
56+
// - Module registry addresses, given as either NAMESPACE/NAME/SYSTEM or
57+
// HOST/NAMESPACE/NAME/SYSTEM, in which case the remote registry serves
58+
// as an indirection over the third address type that follows.
59+
// - Various URL-like and other heuristically-recognized strings which
60+
// we currently delegate to the external library go-getter.
61+
//
62+
// There is some ambiguity between the module registry addresses and go-getter's
63+
// very liberal heuristics and so this particular function will typically treat
64+
// an invalid registry address as some other sort of remote source address
65+
// rather than returning an error. If you know that you're expecting a
66+
// registry address in particular, use ParseModuleSourceRegistry instead, which
67+
// can therefore expose more detailed error messages about registry address
68+
// parsing in particular.
4969
func ParseModuleSource(raw string) (ModuleSource, error) {
50-
for _, prefix := range moduleSourceLocalPrefixes {
51-
if strings.HasPrefix(raw, prefix) {
52-
localAddr, err := parseModuleSourceLocal(raw)
53-
if err != nil {
54-
// This is to make sure we really return a nil ModuleSource in
55-
// this case, rather than an interface containing the zero
56-
// value of ModuleSourceLocal.
57-
return nil, err
58-
}
59-
return localAddr, nil
70+
if isModuleSourceLocal(raw) {
71+
localAddr, err := parseModuleSourceLocal(raw)
72+
if err != nil {
73+
// This is to make sure we really return a nil ModuleSource in
74+
// this case, rather than an interface containing the zero
75+
// value of ModuleSourceLocal.
76+
return nil, err
6077
}
78+
return localAddr, nil
6179
}
6280

6381
// For historical reasons, whether an address is a registry
@@ -71,7 +89,7 @@ func ParseModuleSource(raw string) (ModuleSource, error) {
7189
// the registry source parse error gets returned to the caller,
7290
// which is annoying but has been true for many releases
7391
// without it posing a serious problem in practice.)
74-
if ret, err := parseModuleSourceRegistry(raw); err == nil {
92+
if ret, err := ParseModuleSourceRegistry(raw); err == nil {
7593
return ret, nil
7694
}
7795

@@ -150,6 +168,15 @@ func parseModuleSourceLocal(raw string) (ModuleSourceLocal, error) {
150168
return ModuleSourceLocal(clean), nil
151169
}
152170

171+
func isModuleSourceLocal(raw string) bool {
172+
for _, prefix := range moduleSourceLocalPrefixes {
173+
if strings.HasPrefix(raw, prefix) {
174+
return true
175+
}
176+
}
177+
return false
178+
}
179+
153180
func (s ModuleSourceLocal) moduleSource() {}
154181

155182
func (s ModuleSourceLocal) String() string {
@@ -195,6 +222,30 @@ const DefaultModuleRegistryHost = svchost.Hostname("registry.terraform.io")
195222
var moduleRegistryNamePattern = regexp.MustCompile("^[0-9A-Za-z](?:[0-9A-Za-z-_]{0,62}[0-9A-Za-z])?$")
196223
var moduleRegistryTargetSystemPattern = regexp.MustCompile("^[0-9a-z]{1,64}$")
197224

225+
// ParseModuleSourceRegistry is a variant of ParseModuleSource which only
226+
// accepts module registry addresses, and will reject any other address type.
227+
//
228+
// Use this instead of ParseModuleSource if you know from some other surrounding
229+
// context that an address is intended to be a registry address rather than
230+
// some other address type, which will then allow for better error reporting
231+
// due to the additional information about user intent.
232+
func ParseModuleSourceRegistry(raw string) (ModuleSource, error) {
233+
// Before we delegate to the "real" function we'll just make sure this
234+
// doesn't look like a local source address, so we can return a better
235+
// error message for that situation.
236+
if isModuleSourceLocal(raw) {
237+
return ModuleSourceRegistry{}, fmt.Errorf("can't use local directory %q as a module registry address", raw)
238+
}
239+
240+
ret, err := parseModuleSourceRegistry(raw)
241+
if err != nil {
242+
// This is to make sure we return a nil ModuleSource, rather than
243+
// a non-nil ModuleSource containing a zero-value ModuleSourceRegistry.
244+
return nil, err
245+
}
246+
return ret, nil
247+
}
248+
198249
func parseModuleSourceRegistry(raw string) (ModuleSourceRegistry, error) {
199250
var err error
200251

@@ -298,11 +349,10 @@ func parseModuleRegistryTargetSystem(given string) (string, error) {
298349
// Similar to the names in provider source addresses, we defined these
299350
// to be compatible with what filesystems and typical remote systems
300351
// like GitHub allow in names. Unfortunately we didn't end up defining
301-
// these exactly equivalently: provider names can only use dashes as
302-
// punctuation, whereas module names can use underscores. So here we're
303-
// using some regular expressions from the original module source
304-
// implementation, rather than using the IDNA rules as we do in
305-
// ParseProviderPart.
352+
// these exactly equivalently: provider names can't use dashes or
353+
// underscores. So here we're using some regular expressions from the
354+
// original module source implementation, rather than using the IDNA rules
355+
// as we do in ParseProviderPart.
306356

307357
if !moduleRegistryTargetSystemPattern.MatchString(given) {
308358
return "", fmt.Errorf("must be between one and 64 ASCII letters or digits")

terraform/configs/check.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package configs
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/hashicorp/hcl/v2"
7+
"github.com/terraform-linters/tflint/terraform/addrs"
8+
"github.com/terraform-linters/tflint/terraform/lang"
9+
)
10+
11+
// CheckRule represents a configuration-defined validation rule, precondition,
12+
// or postcondition. Blocks of this sort can appear in a few different places
13+
// in configuration, including "validation" blocks for variables,
14+
// and "precondition" and "postcondition" blocks for resources.
15+
type CheckRule struct {
16+
// Condition is an expression that must evaluate to true if the condition
17+
// holds or false if it does not. If the expression produces an error then
18+
// that's considered to be a bug in the module defining the check.
19+
//
20+
// The available variables in a condition expression vary depending on what
21+
// a check is attached to. For example, validation rules attached to
22+
// input variables can only refer to the variable that is being validated.
23+
Condition hcl.Expression
24+
25+
// ErrorMessage should be one or more full sentences, which should be in
26+
// English for consistency with the rest of the error message output but
27+
// can in practice be in any language. The message should describe what is
28+
// required for the condition to return true in a way that would make sense
29+
// to a caller of the module.
30+
//
31+
// The error message expression has the same variables available for
32+
// interpolation as the corresponding condition.
33+
ErrorMessage hcl.Expression
34+
35+
DeclRange hcl.Range
36+
}
37+
38+
// validateSelfReferences looks for references in the check rule matching the
39+
// specified resource address, returning error diagnostics if such a reference
40+
// is found.
41+
func (cr *CheckRule) validateSelfReferences(checkType string, addr addrs.Resource) hcl.Diagnostics {
42+
var diags hcl.Diagnostics
43+
refs, _ := lang.References(cr.Condition.Variables())
44+
for _, ref := range refs {
45+
var refAddr addrs.Resource
46+
47+
switch rs := ref.Subject.(type) {
48+
case addrs.Resource:
49+
refAddr = rs
50+
case addrs.ResourceInstance:
51+
refAddr = rs.Resource
52+
default:
53+
continue
54+
}
55+
56+
if refAddr.Equal(addr) {
57+
diags = diags.Append(&hcl.Diagnostic{
58+
Severity: hcl.DiagError,
59+
Summary: fmt.Sprintf("Invalid reference in %s", checkType),
60+
Detail: fmt.Sprintf("Configuration for %s may not refer to itself.", addr.String()),
61+
Subject: cr.Condition.Range().Ptr(),
62+
})
63+
break
64+
}
65+
}
66+
return diags
67+
}
68+
69+
// decodeCheckRuleBlock decodes the contents of the given block as a check rule.
70+
//
71+
// Unlike most of our "decode..." functions, this one can be applied to blocks
72+
// of various types as long as their body structures are "check-shaped". The
73+
// function takes the containing block only because some error messages will
74+
// refer to its location, and the returned object's DeclRange will be the
75+
// block's header.
76+
func decodeCheckRuleBlock(block *hcl.Block, override bool) (*CheckRule, hcl.Diagnostics) {
77+
var diags hcl.Diagnostics
78+
cr := &CheckRule{
79+
DeclRange: block.DefRange,
80+
}
81+
82+
if override {
83+
// For now we'll just forbid overriding check blocks, to simplify
84+
// the initial design. If we can find a clear use-case for overriding
85+
// checks in override files and there's a way to define it that
86+
// isn't confusing then we could relax this.
87+
diags = diags.Append(&hcl.Diagnostic{
88+
Severity: hcl.DiagError,
89+
Summary: fmt.Sprintf("Can't override %s blocks", block.Type),
90+
Detail: fmt.Sprintf("Override files cannot override %q blocks.", block.Type),
91+
Subject: cr.DeclRange.Ptr(),
92+
})
93+
return cr, diags
94+
}
95+
96+
content, moreDiags := block.Body.Content(checkRuleBlockSchema)
97+
diags = append(diags, moreDiags...)
98+
99+
if attr, exists := content.Attributes["condition"]; exists {
100+
cr.Condition = attr.Expr
101+
102+
if len(cr.Condition.Variables()) == 0 {
103+
// A condition expression that doesn't refer to any variable is
104+
// pointless, because its result would always be a constant.
105+
diags = diags.Append(&hcl.Diagnostic{
106+
Severity: hcl.DiagError,
107+
Summary: fmt.Sprintf("Invalid %s expression", block.Type),
108+
Detail: "The condition expression must refer to at least one object from elsewhere in the configuration, or else its result would not be checking anything.",
109+
Subject: cr.Condition.Range().Ptr(),
110+
})
111+
}
112+
}
113+
114+
if attr, exists := content.Attributes["error_message"]; exists {
115+
cr.ErrorMessage = attr.Expr
116+
}
117+
118+
return cr, diags
119+
}
120+
121+
var checkRuleBlockSchema = &hcl.BodySchema{
122+
Attributes: []hcl.AttributeSchema{
123+
{
124+
Name: "condition",
125+
Required: true,
126+
},
127+
{
128+
Name: "error_message",
129+
Required: true,
130+
},
131+
},
132+
}

terraform/configs/config_build.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,15 @@ func BuildConfig(root *Module, walker ModuleWalker) (*Config, hcl.Diagnostics) {
2323
cfg.Root = cfg // Root module is self-referential.
2424
cfg.Children, diags = buildChildModules(cfg, walker)
2525

26-
// Now that the config is built, we can connect the provider names to all
27-
// the known types for validation.
28-
cfg.resolveProviderTypes()
26+
// Skip provider resolution if there are any errors, since the provider
27+
// configurations themselves may not be valid.
28+
if !diags.HasErrors() {
29+
// Now that the config is built, we can connect the provider names to all
30+
// the known types for validation.
31+
cfg.resolveProviderTypes()
32+
}
2933

30-
diags = append(diags, validateProviderConfigs(nil, cfg, false)...)
34+
diags = append(diags, validateProviderConfigs(nil, cfg, nil)...)
3135

3236
return cfg, diags
3337
}

terraform/configs/configschema/implied_type.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ func (b *Block) ContainsSensitive() bool {
4747
if attrS.NestedType != nil && attrS.NestedType.ContainsSensitive() {
4848
return true
4949
}
50+
if attrS.NestedType != nil && attrS.NestedType.ContainsSensitive() {
51+
return true
52+
}
5053
}
5154
for _, blockS := range b.BlockTypes {
5255
if blockS.ContainsSensitive() {

terraform/configs/module_call.go

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,35 @@ func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagno
5959
})
6060
}
6161

62+
haveVersionArg := false
63+
if attr, exists := content.Attributes["version"]; exists {
64+
var versionDiags hcl.Diagnostics
65+
mc.Version, versionDiags = decodeVersionConstraint(attr)
66+
diags = append(diags, versionDiags...)
67+
haveVersionArg = true
68+
}
69+
6270
if attr, exists := content.Attributes["source"]; exists {
6371
mc.SourceSet = true
6472
mc.SourceAddrRange = attr.Expr.Range()
6573
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &mc.SourceAddrRaw)
6674
diags = append(diags, valDiags...)
6775
if !valDiags.HasErrors() {
68-
addr, err := addrs.ParseModuleSource(mc.SourceAddrRaw)
76+
var addr addrs.ModuleSource
77+
var err error
78+
if haveVersionArg {
79+
addr, err = addrs.ParseModuleSourceRegistry(mc.SourceAddrRaw)
80+
} else {
81+
addr, err = addrs.ParseModuleSource(mc.SourceAddrRaw)
82+
}
6983
mc.SourceAddr = addr
7084
if err != nil {
85+
// NOTE: We leave mc.SourceAddr as nil for any situation where the
86+
// source attribute is invalid, so any code which tries to carefully
87+
// use the partial result of a failed config decode must be
88+
// resilient to that.
89+
mc.SourceAddr = nil
90+
7191
// NOTE: In practice it's actually very unlikely to end up here,
7292
// because our source address parser can turn just about any string
7393
// into some sort of remote package address, and so for most errors
@@ -87,25 +107,27 @@ func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagno
87107
Subject: mc.SourceAddrRange.Ptr(),
88108
})
89109
default:
90-
diags = append(diags, &hcl.Diagnostic{
91-
Severity: hcl.DiagError,
92-
Summary: "Invalid module source address",
93-
Detail: fmt.Sprintf("Failed to parse module source address: %s.", err),
94-
Subject: mc.SourceAddrRange.Ptr(),
95-
})
110+
if haveVersionArg {
111+
// In this case we'll include some extra context that
112+
// we assumed a registry source address due to the
113+
// version argument.
114+
diags = append(diags, &hcl.Diagnostic{
115+
Severity: hcl.DiagError,
116+
Summary: "Invalid registry module source address",
117+
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),
118+
Subject: mc.SourceAddrRange.Ptr(),
119+
})
120+
} else {
121+
diags = append(diags, &hcl.Diagnostic{
122+
Severity: hcl.DiagError,
123+
Summary: "Invalid module source address",
124+
Detail: fmt.Sprintf("Failed to parse module source address: %s.", err),
125+
Subject: mc.SourceAddrRange.Ptr(),
126+
})
127+
}
96128
}
97129
}
98130
}
99-
// NOTE: We leave mc.SourceAddr as nil for any situation where the
100-
// source attribute is invalid, so any code which tries to carefully
101-
// use the partial result of a failed config decode must be
102-
// resilient to that.
103-
}
104-
105-
if attr, exists := content.Attributes["version"]; exists {
106-
var versionDiags hcl.Diagnostics
107-
mc.Version, versionDiags = decodeVersionConstraint(attr)
108-
diags = append(diags, versionDiags...)
109131
}
110132

111133
if attr, exists := content.Attributes["count"]; exists {

0 commit comments

Comments
 (0)