opentofu/internal/configs/import.go
Nathan Baulch ea558d9d4b
Fix typos (#1905)
Signed-off-by: Nathan Baulch <nathan.baulch@gmail.com>
Signed-off-by: Christian Mesh <christianmesh1@gmail.com>
Co-authored-by: Christian Mesh <christianmesh1@gmail.com>
2024-08-29 13:20:33 -04:00

211 lines
8.3 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package configs
import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
hcljson "github.com/hashicorp/hcl/v2/json"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs/hcl2shim"
"github.com/opentofu/opentofu/internal/tfdiags"
)
type Import struct {
ID hcl.Expression
// To is the address HCL expression given in the `import` block configuration.
// It supports the following address formats:
// - aws_s3_bucket.my_bucket
// - module.my_module.aws_s3_bucket.my_bucket
// - aws_s3_bucket.my_bucket["static_key"]
// - module.my_module[0].aws_s3_bucket.my_buckets["static_key"]
// - aws_s3_bucket.my_bucket[expression]
// - module.my_module[expression].aws_s3_bucket.my_buckets[expression]
// A dynamic instance key supports a dynamic expression like - a variable, a local, a condition (for example,
// ternary), a resource block attribute, a data block attribute, etc.
To hcl.Expression
// StaticTo is the corresponding resource and module that the address is referring to. When decoding, as long
// as the `to` field is in the accepted format, we could determine the actual modules and resource that the
// address represents. However, we do not yet know for certain what module instance and resource instance this
// address refers to. So, Static import is mainly used to figure out the Module and Resource, and Provider of the
// import target resource
// If we could not determine the StaticTo when decoding the block, then the address is in an unacceptable format
StaticTo addrs.ConfigResource
// ResolvedTo will be a reference to the resource instance of the import target, if it can be resolved when decoding
// the `import` block. If the `to` field does not represent a static address
// (for example: module.my_module[var.var1].aws_s3_bucket.bucket), then this will be nil.
// However, if the address is static and can be fully resolved at decode time
// (for example: module.my_module[2].aws_s3_bucket.bucket), then this will be a reference to the resource instance's
// address
// Mainly used for early validations on the import block address, for example making sure there are no duplicate
// import blocks targeting the same resource
ResolvedTo *addrs.AbsResourceInstance
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["to"]; exists {
toExpr := attr.Expr
// Since we are manually parsing the 'to' argument, we need to specially
// handle json configs, in which case the values will be json strings
// rather than hcl
isJSON := hcljson.IsJSONExpression(attr.Expr)
if isJSON {
convertedExpr, convertDiags := hcl2shim.ConvertJSONExpressionToHCL(toExpr)
diags = append(diags, convertDiags...)
if diags.HasErrors() {
return imp, diags
}
toExpr = convertedExpr
}
imp.To = toExpr
staticAddress, addressDiags := staticImportAddress(toExpr)
diags = append(diags, addressDiags.ToHCL()...)
// Exit early if there are issues resolving the static address part. We wouldn't be able to validate the provider in such a case
if addressDiags.HasErrors() {
return imp, diags
}
imp.StaticTo = staticAddress
imp.ResolvedTo = resolvedImportAddress(imp.To)
}
if attr, exists := content.Attributes["provider"]; exists {
if len(imp.StaticTo.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 attr, exists := content.Attributes["for_each"]; exists {
imp.ForEach = attr.Expr
}
return imp, diags
}
var importBlockSchema = &hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "provider",
},
{
Name: "id",
Required: true,
},
{
Name: "to",
Required: true,
},
{
Name: "for_each",
},
},
}
// absTraversalForImportToExpr returns a static traversal of an import block's "to" field.
// It is inspired by hcl.AbsTraversalForExpr and by tofu.triggersExprToTraversal
// The use-case here is different - we want to also allow for hclsyntax.IndexExpr to be allowed,
// but we don't really care about the key part of it. We just want a traversal that could be converted to an address
// of a resource, so we could determine the module + resource + provider
//
// Currently, there are 4 types of HCL expressions that support AsTraversal:
// - hclsyntax.ScopeTraversalExpr - Simply returns the Traversal. Same for our use-case here
// - hclsyntax.RelativeTraversalExpr - Calculates hcl.AbsTraversalForExpr for the Source, and adds the Traversal to it. Same here, with absTraversalForImportToExpr instead
// - hclsyntax.LiteralValueExpr - Mainly for null/false/true values. Not relevant in our use-case, as it's could not really be part of a reference (unless it is inside of an index, which is irrelevant here anyway)
// - hclsyntax.ObjectConsKeyExpr - Not relevant here
//
// In addition to these, we need to also support hclsyntax.IndexExpr. For it - we do not care about what's in the index.
// We need only know the traversal parts of it the "Collection", as the index doesn't affect which resource/module this is
func absTraversalForImportToExpr(expr hcl.Expression) (traversal hcl.Traversal, diags tfdiags.Diagnostics) {
switch e := expr.(type) {
case *hclsyntax.IndexExpr:
t, d := absTraversalForImportToExpr(e.Collection)
diags = diags.Append(d)
traversal = append(traversal, t...)
case *hclsyntax.RelativeTraversalExpr:
t, d := absTraversalForImportToExpr(e.Source)
diags = diags.Append(d)
traversal = append(traversal, t...)
traversal = append(traversal, e.Traversal...)
case *hclsyntax.ScopeTraversalExpr:
traversal = append(traversal, e.Traversal...)
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid import address expression",
Detail: "Import address must be a reference to a resource's address, and only allows for indexing with dynamic keys. For example: module.my_module[expression1].aws_s3_bucket.my_buckets[expression2] for resources inside of modules, or simply aws_s3_bucket.my_bucket for a resource in the root module",
Subject: expr.Range().Ptr(),
})
}
return
}
// staticImportAddress returns an addrs.ConfigResource representing the module and resource of the import target.
// If the address is of an unacceptable format, the function will return error diags
func staticImportAddress(expr hcl.Expression) (addrs.ConfigResource, tfdiags.Diagnostics) {
traversal, diags := absTraversalForImportToExpr(expr)
if diags.HasErrors() {
return addrs.ConfigResource{}, diags
}
absResourceInstance, diags := addrs.ParseAbsResourceInstance(traversal)
return absResourceInstance.ConfigResource(), diags
}
// resolvedImportAddress attempts to find the resource instance of the import target, if possible.
// Here, we attempt to resolve the address as though it is a static absolute traversal, if that's possible.
// This would only be possible if the `import` block's "to" field does not rely on any data that is dynamic
func resolvedImportAddress(expr hcl.Expression) *addrs.AbsResourceInstance {
var diags hcl.Diagnostics
traversal, traversalDiags := hcl.AbsTraversalForExpr(expr)
diags = append(diags, traversalDiags...)
if diags.HasErrors() {
return nil
}
to, toDiags := addrs.ParseAbsResourceInstance(traversal)
diags = append(diags, toDiags.ToHCL()...)
if diags.HasErrors() {
return nil
}
return &to
}