blob: f7efcef3a49b51a077fdbac77611491b80065398 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package configs
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
hcljson "github.com/hashicorp/hcl/v2/json"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
type Import struct {
ID hcl.Expression
Identity hcl.Expression
To hcl.Expression
// The To address may not be resolvable immediately if it contains dynamic
// index expressions, so we will extract the ConfigResource address and
// store it here for reference.
ToResource addrs.ConfigResource
ForEach hcl.Expression
ProviderConfigRef *ProviderConfigRef
Provider addrs.Provider
DeclRange hcl.Range
ProviderDeclRange hcl.Range
}
func decodeImportBlock(block *hcl.Block) (*Import, hcl.Diagnostics) {
var diags hcl.Diagnostics
imp := &Import{
DeclRange: block.DefRange,
}
content, moreDiags := block.Body.Content(importBlockSchema)
diags = append(diags, moreDiags...)
if attr, exists := content.Attributes["id"]; exists {
imp.ID = attr.Expr
}
if attr, exists := content.Attributes["identity"]; exists {
imp.Identity = attr.Expr
}
if attr, exists := content.Attributes["to"]; exists {
toExpr, jsDiags := unwrapJSONRefExpr(attr.Expr)
diags = diags.Extend(jsDiags)
if diags.HasErrors() {
return imp, diags
}
imp.To = toExpr
addr, toDiags := parseConfigResourceFromExpression(imp.To)
diags = diags.Extend(toDiags.ToHCL())
if addr.Resource.Mode != addrs.ManagedResourceMode {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid import address",
Detail: "Only managed resources can be imported.",
Subject: attr.Range.Ptr(),
})
}
imp.ToResource = addr
}
if attr, exists := content.Attributes["for_each"]; exists {
imp.ForEach = attr.Expr
}
if attr, exists := content.Attributes["provider"]; exists {
if len(imp.ToResource.Module) > 0 {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid import provider argument",
Detail: "The provider argument can only be specified in import blocks that will generate configuration.\n\nUse the providers argument within the module block to configure providers for all resources within a module, including imported resources.",
Subject: attr.Range.Ptr(),
})
}
var providerDiags hcl.Diagnostics
imp.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider")
imp.ProviderDeclRange = attr.Range
diags = append(diags, providerDiags...)
}
if imp.ID == nil && imp.Identity == nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid import block",
Detail: "At least one of 'id' or 'identity' must be specified.",
Subject: block.DefRange.Ptr(),
})
}
if imp.ID != nil && imp.Identity != nil {
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid import block",
Detail: "Only one of 'id' or 'identity' can be specified.",
Subject: block.DefRange.Ptr(),
})
}
return imp, diags
}
var importBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "provider",
},
{
Name: "for_each",
},
{
Name: "id",
},
{
Name: "to",
Required: true,
},
{
Name: "identity",
},
},
}
// parseResourceInstanceFromExpression takes an arbitrary expression
// representing a resource instance, and parses out the static ConfigResource
// skipping an variable index expressions. This is used to connect an import
// block's "to" to the configuration address before the full instance
// expressions are evaluated.
func parseConfigResourceFromExpression(expr hcl.Expression) (addrs.ConfigResource, tfdiags.Diagnostics) {
traversal, hcdiags := exprToResourceTraversal(expr)
if hcdiags.HasErrors() {
return addrs.ConfigResource{}, tfdiags.Diagnostics(nil).Append(hcdiags)
}
addr, diags := addrs.ParseAbsResourceInstance(traversal)
if diags.HasErrors() {
return addrs.ConfigResource{}, diags
}
return addr.ConfigResource(), diags
}
// unwrapJSONRefExpr takes a string expression from a JSON configuration,
// and re-evaluates the string as HCL. If the expression is not JSON, the
// original expression is returned directly.
func unwrapJSONRefExpr(expr hcl.Expression) (hcl.Expression, hcl.Diagnostics) {
if !hcljson.IsJSONExpression(expr) {
return expr, nil
}
// We can abuse the hcl json api and rely on the fact that calling
// Value on a json expression with no EvalContext will return the
// raw string. We can then parse that as normal hcl syntax, and
// continue with the decoding.
v, diags := expr.Value(nil)
if diags.HasErrors() {
return nil, diags
}
// the JSON representation can only be a string
if v.Type() != cty.String {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid reference expression",
Detail: "A single reference string is required.",
Subject: expr.Range().Ptr(),
})
return nil, diags
}
rng := expr.Range()
expr, ds := hclsyntax.ParseExpression([]byte(v.AsString()), rng.Filename, rng.Start)
diags = diags.Extend(ds)
return expr, diags
}
// exprToResourceTraversal is used to parse the import block's to expression,
// which must be a resource instance, but may contain limited variables with
// index expressions. Since we only need the ConfigResource to connect the
// import to the configuration, we skip any index expressions.
func exprToResourceTraversal(expr hcl.Expression) (hcl.Traversal, hcl.Diagnostics) {
var trav hcl.Traversal
var diags hcl.Diagnostics
switch e := expr.(type) {
case *hclsyntax.RelativeTraversalExpr:
t, d := exprToResourceTraversal(e.Source)
diags = diags.Extend(d)
trav = append(trav, t...)
trav = append(trav, e.Traversal...)
case *hclsyntax.ScopeTraversalExpr:
// a static reference, we can just append the traversal
trav = append(trav, e.Traversal...)
case *hclsyntax.IndexExpr:
// Get the collection from the index expression, we don't need the
// index for a ConfigResource
t, d := exprToResourceTraversal(e.Collection)
diags = diags.Extend(d)
if diags.HasErrors() {
return nil, diags
}
trav = append(trav, t...)
default:
// if we don't recognise the expression type (which means we are likely
// dealing with a test mock), try and interpret this as an absolute
// traversal
t, d := hcl.AbsTraversalForExpr(e)
diags = diags.Extend(d)
trav = append(trav, t...)
}
return trav, diags
}
// parseImportToStatic attempts to parse the To address of an import block
// statically to get the resource address. This returns false when the address
// cannot be parsed, which is usually a result of dynamic index expressions
// using for_each.
func parseImportToStatic(expr hcl.Expression) (addrs.AbsResourceInstance, bool) {
// we may have a nil expression in some error cases, which we can just
// false to avoid the parsing
if expr == nil {
return addrs.AbsResourceInstance{}, false
}
var toDiags tfdiags.Diagnostics
traversal, hd := hcl.AbsTraversalForExpr(expr)
toDiags = toDiags.Append(hd)
to, td := addrs.ParseAbsResourceInstance(traversal)
toDiags = toDiags.Append(td)
return to, !toDiags.HasErrors()
}