|
1 | 1 | package opentofu |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "fmt" |
| 5 | + |
4 | 6 | "github.com/hashicorp/go-version" |
5 | 7 | "github.com/hashicorp/hcl/v2" |
6 | 8 | "github.com/hashicorp/hcl/v2/gohcl" |
7 | 9 | "github.com/hashicorp/hcl/v2/hclsyntax" |
| 10 | + hcljson "github.com/hashicorp/hcl/v2/json" |
| 11 | + regaddr "github.com/opentofu/registry-address" |
8 | 12 | "github.com/terraform-linters/tflint-plugin-sdk/hclext" |
| 13 | + "github.com/zclconf/go-cty/cty" |
9 | 14 | ) |
10 | 15 |
|
11 | 16 | // ModuleCall represents a "module" block. |
@@ -66,26 +71,208 @@ type Local struct { |
66 | 71 |
|
67 | 72 | // ProviderRef represents a reference to a provider like `provider = google.europe` in a resource or module. |
68 | 73 | 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 |
71 | 79 | } |
72 | 80 |
|
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...) |
76 | 131 | if diags.HasErrors() { |
77 | 132 | return nil, diags |
78 | 133 | } |
79 | 134 |
|
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...) |
81 | 224 | if diags.HasErrors() { |
82 | 225 | return nil, diags |
83 | 226 | } |
84 | 227 |
|
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 |
89 | 276 | } |
90 | 277 |
|
91 | 278 | // @see https://github.com/hashicorp/terraform/blob/v1.2.5/internal/configs/compat_shim.go#L34 |
|
0 commit comments