Skip to content

Commit 8cc7960

Browse files
committed
Adding support for dynamic providers
Signed-off-by: Diogenes Fernandes <[email protected]>
1 parent 6fc5ee0 commit 8cc7960

File tree

3 files changed

+204
-15
lines changed

3 files changed

+204
-15
lines changed

opentofu/opentofu.go

Lines changed: 197 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package opentofu
22

33
import (
4+
"fmt"
5+
46
"github.com/hashicorp/go-version"
57
"github.com/hashicorp/hcl/v2"
68
"github.com/hashicorp/hcl/v2/gohcl"
79
"github.com/hashicorp/hcl/v2/hclsyntax"
10+
hcljson "github.com/hashicorp/hcl/v2/json"
11+
regaddr "github.com/opentofu/registry-address"
812
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
13+
"github.com/zclconf/go-cty/cty"
914
)
1015

1116
// ModuleCall represents a "module" block.
@@ -66,26 +71,208 @@ type Local struct {
6671

6772
// ProviderRef represents a reference to a provider like `provider = google.europe` in a resource or module.
6873
type ProviderRef struct {
69-
Name string
70-
DefRange hcl.Range
74+
Name string
75+
Alias string
76+
AliasRange *hcl.Range
77+
KeyExpression hcl.Expression
78+
DefRange hcl.Range
7179
}
7280

73-
// @see https://github.com/hashicorp/terraform/blob/v1.2.7/internal/configs/resource.go#L624-L695
74-
func decodeProviderRef(expr hcl.Expression, defRange hcl.Range) (*ProviderRef, hcl.Diagnostics) {
75-
expr, diags := shimTraversalInString(expr)
81+
func ExprIsNativeQuotedString(expr hcl.Expression) bool {
82+
_, ok := expr.(*hclsyntax.TemplateExpr)
83+
return ok
84+
}
85+
86+
func IsProviderPartNormalized(str string) (bool, error) {
87+
normalized, err := regaddr.ParseProviderPart(str)
88+
if err != nil {
89+
return false, err
90+
}
91+
if str == normalized {
92+
return true, nil
93+
}
94+
return false, nil
95+
}
96+
97+
func checkProviderNameNormalized(name string, declrange hcl.Range) hcl.Diagnostics {
98+
var diags hcl.Diagnostics
99+
// verify that the provider local name is normalized
100+
normalized, err := IsProviderPartNormalized(name)
101+
if err != nil {
102+
diags = append(diags, &hcl.Diagnostic{
103+
Severity: hcl.DiagError,
104+
Summary: "Invalid provider local name",
105+
Detail: fmt.Sprintf("%s is an invalid provider local name: %s", name, err),
106+
Subject: &declrange,
107+
})
108+
return diags
109+
}
110+
if !normalized {
111+
// we would have returned this error already
112+
normalizedProvider, _ := regaddr.ParseProviderPart(name)
113+
diags = append(diags, &hcl.Diagnostic{
114+
Severity: hcl.DiagError,
115+
Summary: "Invalid provider local name",
116+
Detail: fmt.Sprintf("Provider names must be normalized. Replace %q with %q to fix this error.", name, normalizedProvider),
117+
Subject: &declrange,
118+
})
119+
}
120+
return diags
121+
}
122+
123+
func ConvertJSONExpressionToHCL(expr hcl.Expression) (hcl.Expression, hcl.Diagnostics) {
124+
var diags hcl.Diagnostics
125+
// We can abuse the hcl json api and rely on the fact that calling
126+
// Value on a json expression with no EvalContext will return the
127+
// raw string. We can then parse that as normal hcl syntax, and
128+
// continue with the decoding.
129+
value, ds := expr.Value(nil)
130+
diags = append(diags, ds...)
76131
if diags.HasErrors() {
77132
return nil, diags
78133
}
79134

80-
traversal, diags := hcl.AbsTraversalForExpr(expr)
135+
if value.Type() != cty.String || value.IsNull() {
136+
diags = append(diags, &hcl.Diagnostic{
137+
Severity: hcl.DiagError,
138+
Summary: "Expected string expression",
139+
Detail: fmt.Sprintf("This value must be a string, but got %s.", value.Type().FriendlyName()),
140+
Subject: expr.Range().Ptr(),
141+
})
142+
return nil, diags
143+
}
144+
145+
expr, ds = hclsyntax.ParseExpression([]byte(value.AsString()), expr.Range().Filename, expr.Range().Start)
146+
diags = append(diags, ds...)
147+
if diags.HasErrors() {
148+
return nil, diags
149+
}
150+
151+
return expr, diags
152+
}
153+
154+
// @see https://github.com/opentofu/opentofu/blob/3258c673194ecba26d856fb825d4eb4a7e36ab34/internal/configs/resource.go#L903
155+
func decodeProviderRef(expr hcl.Expression, defRange hcl.Range) (*ProviderRef, hcl.Diagnostics) {
156+
var diags hcl.Diagnostics
157+
var keyExpr hcl.Expression
158+
const (
159+
// name.alias[const_key]
160+
nameIndex = 0
161+
aliasIndex = 1
162+
keyIndex = 2
163+
)
164+
165+
var maxTraversalLength = keyIndex + 1
166+
167+
if ok := hcljson.IsJSONExpression(expr); ok {
168+
expr, diags = ConvertJSONExpressionToHCL(expr)
169+
if diags.HasErrors() {
170+
return nil, diags
171+
}
172+
}
173+
174+
// name.alias[expr_key]
175+
if iex, ok := expr.(*hclsyntax.IndexExpr); ok {
176+
maxTraversalLength = aliasIndex + 1 // expr key found, no const key allowed
177+
178+
keyExpr = iex.Key
179+
expr = iex.Collection
180+
}
181+
182+
var shimDiags hcl.Diagnostics
183+
expr, shimDiags = shimTraversalInString(expr)
184+
diags = append(diags, shimDiags...)
185+
186+
traversal, travDiags := hcl.AbsTraversalForExpr(expr)
187+
188+
// AbsTraversalForExpr produces only generic errors, so we'll discard
189+
// the errors given and produce our own with extra context. If we didn't
190+
// get any errors then we might still have warnings, though.
191+
if !travDiags.HasErrors() {
192+
diags = append(diags, travDiags...)
193+
}
194+
195+
if len(traversal) == 0 || len(traversal) > maxTraversalLength {
196+
// A provider reference was given as a string literal in the legacy
197+
// configuration language and there are lots of examples out there
198+
// showing that usage, so we'll sniff for that situation here and
199+
// produce a specialized error message for it to help users find
200+
// the new correct form.
201+
if ExprIsNativeQuotedString(expr) {
202+
diags = append(diags, &hcl.Diagnostic{
203+
Severity: hcl.DiagError,
204+
Summary: "Invalid provider configuration reference",
205+
Detail: "A provider configuration reference must not be given in quotes.",
206+
Subject: expr.Range().Ptr(),
207+
})
208+
return nil, diags
209+
}
210+
211+
diags = append(diags, &hcl.Diagnostic{
212+
Severity: hcl.DiagError,
213+
Summary: "Invalid provider configuration reference",
214+
Detail: fmt.Sprintf("The provider argument requires a provider type name, optionally followed by a period and then a configuration alias and optional instance key."),
215+
Subject: expr.Range().Ptr(),
216+
})
217+
return nil, diags
218+
}
219+
220+
// verify that the provider local name is normalized
221+
name := traversal.RootName()
222+
nameDiags := checkProviderNameNormalized(name, traversal[nameIndex].SourceRange())
223+
diags = append(diags, nameDiags...)
81224
if diags.HasErrors() {
82225
return nil, diags
83226
}
84227

85-
return &ProviderRef{
86-
Name: traversal.RootName(),
87-
DefRange: defRange,
88-
}, nil
228+
ret := &ProviderRef{
229+
Name: traversal.RootName(),
230+
DefRange: defRange,
231+
KeyExpression: keyExpr,
232+
AliasRange: nil,
233+
}
234+
235+
if len(traversal) > aliasIndex {
236+
aliasStep, ok := traversal[aliasIndex].(hcl.TraverseAttr)
237+
if !ok {
238+
diags = append(diags, &hcl.Diagnostic{
239+
Severity: hcl.DiagError,
240+
Summary: "Invalid provider configuration reference",
241+
Detail: "Provider name must either stand alone or be followed by a period and then a configuration alias.",
242+
Subject: traversal[aliasIndex].SourceRange().Ptr(),
243+
})
244+
return ret, diags
245+
}
246+
247+
ret.Alias = aliasStep.Name
248+
ret.AliasRange = aliasStep.SourceRange().Ptr()
249+
}
250+
251+
if len(traversal) > keyIndex {
252+
indexStep, ok := traversal[keyIndex].(hcl.TraverseIndex)
253+
if !ok {
254+
diags = append(diags, &hcl.Diagnostic{
255+
Severity: hcl.DiagError,
256+
Summary: "Invalid provider configuration reference",
257+
Detail: "Provider name must either stand alone or be followed by a period and then a configuration alias.",
258+
Subject: traversal[keyIndex].SourceRange().Ptr(),
259+
})
260+
return ret, diags
261+
}
262+
263+
ret.KeyExpression = hcl.StaticExpr(indexStep.Key, traversal.SourceRange())
264+
}
265+
266+
if len(ret.Alias) == 0 && ret.KeyExpression != nil {
267+
diags = append(diags, &hcl.Diagnostic{
268+
Severity: hcl.DiagError,
269+
Summary: "Invalid provider configuration reference",
270+
Detail: "Provider assignment requires an alias when specifying an instance key, in the form of provider.name[instance_key]",
271+
Subject: traversal.SourceRange().Ptr(),
272+
})
273+
}
274+
275+
return ret, nil
89276
}
90277

91278
// @see https://github.com/hashicorp/terraform/blob/v1.2.5/internal/configs/compat_shim.go#L34

opentofu/runner.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ func (r *Runner) GetProviderRefs() (map[string]*ProviderRef, hcl.Diagnostics) {
230230
for _, data := range block.Body.Blocks {
231231
if attr, exists := data.Body.Attributes["provider"]; exists {
232232
ref, decodeDiags := decodeProviderRef(attr.Expr, data.DefRange)
233+
233234
diags = diags.Extend(decodeDiags)
234235
if decodeDiags.HasErrors() {
235236
continue

rules/opentofu_required_providers.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type OpentofuRequiredProvidersRule struct {
1818
tflint.DefaultRule
1919
}
2020

21-
type terraformRequiredProvidersRuleConfig struct {
21+
type opentofuRequiredProvidersRuleConfig struct {
2222
// Source specifies whether the rule should assert the presence of a `source` attribute
2323
Source *bool `hclext:"source,optional"`
2424
// Version specifies whether the rule should assert the presence of a `version` attribute
@@ -51,8 +51,8 @@ func (r *OpentofuRequiredProvidersRule) Link() string {
5151
}
5252

5353
// config returns the rule config, with defaults
54-
func (r *OpentofuRequiredProvidersRule) config(runner tflint.Runner) (*terraformRequiredProvidersRuleConfig, error) {
55-
config := &terraformRequiredProvidersRuleConfig{}
54+
func (r *OpentofuRequiredProvidersRule) config(runner tflint.Runner) (*opentofuRequiredProvidersRuleConfig, error) {
55+
config := &opentofuRequiredProvidersRuleConfig{}
5656

5757
if err := runner.DecodeRuleConfig(r.Name(), config); err != nil {
5858
return nil, err
@@ -107,12 +107,12 @@ func (r *OpentofuRequiredProvidersRule) Check(rr tflint.Runner) error {
107107

108108
for _, provider := range body.Blocks {
109109
if _, exists := provider.Body.Attributes["version"]; exists {
110-
if err := runner.EmitIssue(
110+
if runErr := runner.EmitIssue(
111111
r,
112112
"provider version constraint should be specified via `required_providers`",
113113
provider.DefRange,
114114
); err != nil {
115-
return err
115+
return runErr
116116
}
117117
}
118118
}
@@ -214,6 +214,7 @@ func (r *OpentofuRequiredProvidersRule) Check(rr tflint.Runner) error {
214214

215215
if source, exists := vm["source"]; exists {
216216
p, err := tfaddr.ParseProviderSource(source.AsString())
217+
217218
if err != nil {
218219
return err
219220
}

0 commit comments

Comments
 (0)