// Copyright IBM Corp. 2014, 2026 // 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() }