Skip to content

Commit d1ec9c7

Browse files
feat: support provider aliases (#342)
* feat: update rules/api/rule.go.tmpl * go generate ./... * feat: change runner.AWSClient to runner.AWSClients * test: fix tests * fix: fix awsClient * fix: get alias name from resource's provider attribute Based on https://github.com/hashicorp/terraform/blob/3fbedf25430ead97eb42575d344427db3c32d524/internal/configs/resource.go#L498-L569 * style: add links to original codes * fix: get provider attribute explicitly * fix: allow deep check without provider block * fix: allow deep check without provider block
1 parent 63c0933 commit d1ec9c7

36 files changed

+559
-96
lines changed

aws/decode.go

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
package aws
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/hashicorp/hcl/v2"
8+
"github.com/hashicorp/hcl/v2/hclsyntax"
9+
"golang.org/x/net/idna"
10+
)
11+
12+
// original code: https://github.com/hashicorp/terraform/blob/3fbedf25430ead97eb42575d344427db3c32d524/internal/configs/resource.go#L484-L496
13+
type ProviderConfigRef struct {
14+
Name string
15+
NameRange hcl.Range
16+
Alias string
17+
AliasRange *hcl.Range // nil if alias not set
18+
19+
// TODO: this may not be set in some cases, so it is not yet suitable for
20+
// use outside of this package. We currently only use it for internal
21+
// validation, but once we verify that this can be set in all cases, we can
22+
// export this so providers don't need to be re-resolved.
23+
// This same field is also added to the Provider struct.
24+
// providerType addrs.Provider
25+
}
26+
27+
// original code: https://github.com/hashicorp/terraform/blob/3fbedf25430ead97eb42575d344427db3c32d524/internal/configs/resource.go#L498-L569
28+
func decodeProviderConfigRef(expr hcl.Expression, argName string) (*ProviderConfigRef, hcl.Diagnostics) {
29+
var diags hcl.Diagnostics
30+
31+
var shimDiags hcl.Diagnostics
32+
expr, shimDiags = shimTraversalInString(expr, false)
33+
diags = append(diags, shimDiags...)
34+
35+
traversal, travDiags := hcl.AbsTraversalForExpr(expr)
36+
37+
// AbsTraversalForExpr produces only generic errors, so we'll discard
38+
// the errors given and produce our own with extra context. If we didn't
39+
// get any errors then we might still have warnings, though.
40+
if !travDiags.HasErrors() {
41+
diags = append(diags, travDiags...)
42+
}
43+
44+
if len(traversal) < 1 || len(traversal) > 2 {
45+
// A provider reference was given as a string literal in the legacy
46+
// configuration language and there are lots of examples out there
47+
// showing that usage, so we'll sniff for that situation here and
48+
// produce a specialized error message for it to help users find
49+
// the new correct form.
50+
if exprIsNativeQuotedString(expr) {
51+
diags = append(diags, &hcl.Diagnostic{
52+
Severity: hcl.DiagError,
53+
Summary: "Invalid provider configuration reference",
54+
Detail: "A provider configuration reference must not be given in quotes.",
55+
Subject: expr.Range().Ptr(),
56+
})
57+
return nil, diags
58+
}
59+
60+
diags = append(diags, &hcl.Diagnostic{
61+
Severity: hcl.DiagError,
62+
Summary: "Invalid provider configuration reference",
63+
Detail: fmt.Sprintf("The %s argument requires a provider type name, optionally followed by a period and then a configuration alias.", argName),
64+
Subject: expr.Range().Ptr(),
65+
})
66+
return nil, diags
67+
}
68+
69+
// verify that the provider local name is normalized
70+
name := traversal.RootName()
71+
nameDiags := checkProviderNameNormalized(name, traversal[0].SourceRange())
72+
diags = append(diags, nameDiags...)
73+
if diags.HasErrors() {
74+
return nil, diags
75+
}
76+
77+
ret := &ProviderConfigRef{
78+
Name: name,
79+
NameRange: traversal[0].SourceRange(),
80+
}
81+
82+
if len(traversal) > 1 {
83+
aliasStep, ok := traversal[1].(hcl.TraverseAttr)
84+
if !ok {
85+
diags = append(diags, &hcl.Diagnostic{
86+
Severity: hcl.DiagError,
87+
Summary: "Invalid provider configuration reference",
88+
Detail: "Provider name must either stand alone or be followed by a period and then a configuration alias.",
89+
Subject: traversal[1].SourceRange().Ptr(),
90+
})
91+
return ret, diags
92+
}
93+
94+
ret.Alias = aliasStep.Name
95+
ret.AliasRange = aliasStep.SourceRange().Ptr()
96+
}
97+
98+
return ret, diags
99+
}
100+
101+
// original code: https://github.com/hashicorp/terraform/blob/3fbedf25430ead97eb42575d344427db3c32d524/internal/configs/compat_shim.go#L21-L92
102+
// shimTraversalInString takes any arbitrary expression and checks if it is
103+
// a quoted string in the native syntax. If it _is_, then it is parsed as a
104+
// traversal and re-wrapped into a synthetic traversal expression and a
105+
// warning is generated. Otherwise, the given expression is just returned
106+
// verbatim.
107+
//
108+
// This function has no effect on expressions from the JSON syntax, since
109+
// traversals in strings are the required pattern in that syntax.
110+
//
111+
// If wantKeyword is set, the generated warning diagnostic will talk about
112+
// keywords rather than references. The behavior is otherwise unchanged, and
113+
// the caller remains responsible for checking that the result is indeed
114+
// a keyword, e.g. using hcl.ExprAsKeyword.
115+
func shimTraversalInString(expr hcl.Expression, wantKeyword bool) (hcl.Expression, hcl.Diagnostics) {
116+
// ObjectConsKeyExpr is a special wrapper type used for keys on object
117+
// constructors to deal with the fact that naked identifiers are normally
118+
// handled as "bareword" strings rather than as variable references. Since
119+
// we know we're interpreting as a traversal anyway (and thus it won't
120+
// matter whether it's a string or an identifier) we can safely just unwrap
121+
// here and then process whatever we find inside as normal.
122+
if ocke, ok := expr.(*hclsyntax.ObjectConsKeyExpr); ok {
123+
expr = ocke.Wrapped
124+
}
125+
126+
if !exprIsNativeQuotedString(expr) {
127+
return expr, nil
128+
}
129+
130+
strVal, diags := expr.Value(nil)
131+
if diags.HasErrors() || strVal.IsNull() || !strVal.IsKnown() {
132+
// Since we're not even able to attempt a shim here, we'll discard
133+
// the diagnostics we saw so far and let the caller's own error
134+
// handling take care of reporting the invalid expression.
135+
return expr, nil
136+
}
137+
138+
// The position handling here isn't _quite_ right because it won't
139+
// take into account any escape sequences in the literal string, but
140+
// it should be close enough for any error reporting to make sense.
141+
srcRange := expr.Range()
142+
startPos := srcRange.Start // copy
143+
startPos.Column++ // skip initial quote
144+
startPos.Byte++ // skip initial quote
145+
146+
traversal, tDiags := hclsyntax.ParseTraversalAbs(
147+
[]byte(strVal.AsString()),
148+
srcRange.Filename,
149+
startPos,
150+
)
151+
diags = append(diags, tDiags...)
152+
153+
if wantKeyword {
154+
diags = append(diags, &hcl.Diagnostic{
155+
Severity: hcl.DiagWarning,
156+
Summary: "Quoted keywords are deprecated",
157+
Detail: "In this context, keywords are expected literally rather than in quotes. Terraform 0.11 and earlier required quotes, but quoted keywords are now deprecated and will be removed in a future version of Terraform. Remove the quotes surrounding this keyword to silence this warning.",
158+
Subject: &srcRange,
159+
})
160+
} else {
161+
diags = append(diags, &hcl.Diagnostic{
162+
Severity: hcl.DiagWarning,
163+
Summary: "Quoted references are deprecated",
164+
Detail: "In this context, references are expected literally rather than in quotes. Terraform 0.11 and earlier required quotes, but quoted references are now deprecated and will be removed in a future version of Terraform. Remove the quotes surrounding this reference to silence this warning.",
165+
Subject: &srcRange,
166+
})
167+
}
168+
169+
return &hclsyntax.ScopeTraversalExpr{
170+
Traversal: traversal,
171+
SrcRange: srcRange,
172+
}, diags
173+
}
174+
175+
// original code: https://github.com/hashicorp/terraform/blob/3fbedf25430ead97eb42575d344427db3c32d524/internal/configs/util.go#L8-L18
176+
// exprIsNativeQuotedString determines whether the given expression looks like
177+
// it's a quoted string in the HCL native syntax.
178+
//
179+
// This should be used sparingly only for situations where our legacy HCL
180+
// decoding would've expected a keyword or reference in quotes but our new
181+
// decoding expects the keyword or reference to be provided directly as
182+
// an identifier-based expression.
183+
func exprIsNativeQuotedString(expr hcl.Expression) bool {
184+
_, ok := expr.(*hclsyntax.TemplateExpr)
185+
return ok
186+
}
187+
188+
// original code: https://github.com/hashicorp/terraform/blob/3fbedf25430ead97eb42575d344427db3c32d524/internal/configs/provider.go#L256-L282
189+
// checkProviderNameNormalized verifies that the given string is already
190+
// normalized and returns an error if not.
191+
func checkProviderNameNormalized(name string, declrange hcl.Range) hcl.Diagnostics {
192+
var diags hcl.Diagnostics
193+
// verify that the provider local name is normalized
194+
normalized, err := IsProviderPartNormalized(name)
195+
if err != nil {
196+
diags = append(diags, &hcl.Diagnostic{
197+
Severity: hcl.DiagError,
198+
Summary: "Invalid provider local name",
199+
Detail: fmt.Sprintf("%s is an invalid provider local name: %s", name, err),
200+
Subject: &declrange,
201+
})
202+
return diags
203+
}
204+
if !normalized {
205+
// we would have returned this error already
206+
normalizedProvider, _ := ParseProviderPart(name)
207+
diags = append(diags, &hcl.Diagnostic{
208+
Severity: hcl.DiagError,
209+
Summary: "Invalid provider local name",
210+
Detail: fmt.Sprintf("Provider names must be normalized. Replace %q with %q to fix this error.", name, normalizedProvider),
211+
Subject: &declrange,
212+
})
213+
}
214+
return diags
215+
}
216+
217+
// original code: https://github.com/hashicorp/terraform/blob/3fbedf25430ead97eb42575d344427db3c32d524/internal/addrs/provider.go#L454-L464
218+
// IsProviderPartNormalized compares a given string to the result of ParseProviderPart(string)
219+
func IsProviderPartNormalized(str string) (bool, error) {
220+
normalized, err := ParseProviderPart(str)
221+
if err != nil {
222+
return false, err
223+
}
224+
if str == normalized {
225+
return true, nil
226+
}
227+
return false, nil
228+
}
229+
230+
// original code: https://github.com/hashicorp/terraform/blob/3fbedf25430ead97eb42575d344427db3c32d524/internal/addrs/provider.go#L385-L442
231+
// ParseProviderPart processes an addrs.Provider namespace or type string
232+
// provided by an end-user, producing a normalized version if possible or
233+
// an error if the string contains invalid characters.
234+
//
235+
// A provider part is processed in the same way as an individual label in a DNS
236+
// domain name: it is transformed to lowercase per the usual DNS case mapping
237+
// and normalization rules and may contain only letters, digits, and dashes.
238+
// Additionally, dashes may not appear at the start or end of the string.
239+
//
240+
// These restrictions are intended to allow these names to appear in fussy
241+
// contexts such as directory/file names on case-insensitive filesystems,
242+
// repository names on GitHub, etc. We're using the DNS rules in particular,
243+
// rather than some similar rules defined locally, because the hostname part
244+
// of an addrs.Provider is already a hostname and it's ideal to use exactly
245+
// the same case folding and normalization rules for all of the parts.
246+
//
247+
// In practice a provider type string conventionally does not contain dashes
248+
// either. Such names are permitted, but providers with such type names will be
249+
// hard to use because their resource type names will not be able to contain
250+
// the provider type name and thus each resource will need an explicit provider
251+
// address specified. (A real-world example of such a provider is the
252+
// "google-beta" variant of the GCP provider, which has resource types that
253+
// start with the "google_" prefix instead.)
254+
//
255+
// It's valid to pass the result of this function as the argument to a
256+
// subsequent call, in which case the result will be identical.
257+
func ParseProviderPart(given string) (string, error) {
258+
if len(given) == 0 {
259+
return "", fmt.Errorf("must have at least one character")
260+
}
261+
262+
// We're going to process the given name using the same "IDNA" library we
263+
// use for the hostname portion, since it already implements the case
264+
// folding rules we want.
265+
//
266+
// The idna library doesn't expose individual label parsing directly, but
267+
// once we've verified it doesn't contain any dots we can just treat it
268+
// like a top-level domain for this library's purposes.
269+
if strings.ContainsRune(given, '.') {
270+
return "", fmt.Errorf("dots are not allowed")
271+
}
272+
273+
// We don't allow names containing multiple consecutive dashes, just as
274+
// a matter of preference: they look weird, confusing, or incorrect.
275+
// This also, as a side-effect, prevents the use of the "punycode"
276+
// indicator prefix "xn--" that would cause the IDNA library to interpret
277+
// the given name as punycode, because that would be weird and unexpected.
278+
if strings.Contains(given, "--") {
279+
return "", fmt.Errorf("cannot use multiple consecutive dashes")
280+
}
281+
282+
result, err := idna.Lookup.ToUnicode(given)
283+
if err != nil {
284+
return "", fmt.Errorf("must contain only letters, digits, and dashes, and may not use leading or trailing dashes")
285+
}
286+
287+
return result, nil
288+
}

0 commit comments

Comments
 (0)