|
| 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