github.com/opentofu/opentofu@v1.7.1/internal/configs/import.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package configs
     7  
     8  import (
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/hashicorp/hcl/v2/hclsyntax"
    11  	"github.com/opentofu/opentofu/internal/addrs"
    12  	"github.com/opentofu/opentofu/internal/tfdiags"
    13  )
    14  
    15  type Import struct {
    16  	ID hcl.Expression
    17  
    18  	// To is the address HCL expression given in the `import` block configuration.
    19  	// It supports the following address formats:
    20  	// - aws_s3_bucket.my_bucket
    21  	// - module.my_module.aws_s3_bucket.my_bucket
    22  	// - aws_s3_bucket.my_bucket["static_key"]
    23  	// - module.my_module[0].aws_s3_bucket.my_buckets["static_key"]
    24  	// - aws_s3_bucket.my_bucket[expression]
    25  	// - module.my_module[expression].aws_s3_bucket.my_buckets[expression]
    26  	// A dynamic instance key supports a dynamic expression like - a variable, a local, a condition (for example,
    27  	//  ternary), a resource block attribute, a data block attribute, etc.
    28  	To hcl.Expression
    29  	// StaticTo is the corresponding resource and module that the address is referring to. When decoding, as long
    30  	// as the `to` field is in the accepted format, we could determine the actual modules and resource that the
    31  	// address represents. However, we do not yet know for certain what module instance and resource instance this
    32  	// address refers to. So, Static import is mainly used to figure out the Module and Resource, and Provider of the
    33  	// import target resource
    34  	// If we could not determine the StaticTo when decoding the block, then the address is in an unacceptable format
    35  	StaticTo addrs.ConfigResource
    36  	// ResolvedTo will be a reference to the resource instance of the import target, if it can be resolved when decoding
    37  	// the `import` block. If the `to` field does not represent a static address
    38  	// (for example: module.my_module[var.var1].aws_s3_bucket.bucket), then this will be nil.
    39  	// However, if the address is static and can be fully resolved at decode time
    40  	// (for example: module.my_module[2].aws_s3_bucket.bucket), then this will be a reference to the resource instance's
    41  	// address
    42  	// Mainly used for early validations on the import block address, for example making sure there are no duplicate
    43  	// import blocks targeting the same resource
    44  	ResolvedTo *addrs.AbsResourceInstance
    45  
    46  	ForEach hcl.Expression
    47  
    48  	ProviderConfigRef *ProviderConfigRef
    49  	Provider          addrs.Provider
    50  
    51  	DeclRange         hcl.Range
    52  	ProviderDeclRange hcl.Range
    53  }
    54  
    55  func decodeImportBlock(block *hcl.Block) (*Import, hcl.Diagnostics) {
    56  	var diags hcl.Diagnostics
    57  	imp := &Import{
    58  		DeclRange: block.DefRange,
    59  	}
    60  
    61  	content, moreDiags := block.Body.Content(importBlockSchema)
    62  	diags = append(diags, moreDiags...)
    63  
    64  	if attr, exists := content.Attributes["id"]; exists {
    65  		imp.ID = attr.Expr
    66  	}
    67  
    68  	if attr, exists := content.Attributes["to"]; exists {
    69  		imp.To = attr.Expr
    70  		staticAddress, addressDiags := staticImportAddress(attr.Expr)
    71  		diags = append(diags, addressDiags.ToHCL()...)
    72  
    73  		// Exit early if there are issues resolving the static address part. We wouldn't be able to validate the provider in such a case
    74  		if addressDiags.HasErrors() {
    75  			return imp, diags
    76  		}
    77  		imp.StaticTo = staticAddress
    78  
    79  		imp.ResolvedTo = resolvedImportAddress(imp.To)
    80  	}
    81  
    82  	if attr, exists := content.Attributes["provider"]; exists {
    83  		if len(imp.StaticTo.Module) > 0 {
    84  			diags = append(diags, &hcl.Diagnostic{
    85  				Severity: hcl.DiagError,
    86  				Summary:  "Invalid import provider argument",
    87  				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.",
    88  				Subject:  attr.Range.Ptr(),
    89  			})
    90  		}
    91  
    92  		var providerDiags hcl.Diagnostics
    93  		imp.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider")
    94  		imp.ProviderDeclRange = attr.Range
    95  		diags = append(diags, providerDiags...)
    96  	}
    97  
    98  	if attr, exists := content.Attributes["for_each"]; exists {
    99  		imp.ForEach = attr.Expr
   100  	}
   101  
   102  	return imp, diags
   103  }
   104  
   105  var importBlockSchema = &hcl.BodySchema{
   106  	Attributes: []hcl.AttributeSchema{
   107  		{
   108  			Name: "provider",
   109  		},
   110  		{
   111  			Name:     "id",
   112  			Required: true,
   113  		},
   114  		{
   115  			Name:     "to",
   116  			Required: true,
   117  		},
   118  		{
   119  			Name: "for_each",
   120  		},
   121  	},
   122  }
   123  
   124  // absTraversalForImportToExpr returns a static traversal of an import block's "to" field.
   125  // It is inspired by hcl.AbsTraversalForExpr and by tofu.triggersExprToTraversal
   126  // The use-case here is different - we want to also allow for hclsyntax.IndexExpr to be allowed,
   127  // but we don't really care about the key part of it. We just want a traversal that could be converted to an address
   128  // of a resource, so we could determine the module + resource + provider
   129  //
   130  // Currently, there are 4 types of HCL epressions that support AsTraversal:
   131  // - hclsyntax.ScopeTraversalExpr - Simply returns the Traversal. Same for our use-case here
   132  // - hclsyntax.RelativeTraversalExpr - Calculates hcl.AbsTraversalForExpr for the Source, and adds the Traversal to it. Same here, with absTraversalForImportToExpr instead
   133  // - 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)
   134  // - hclsyntax.ObjectConsKeyExpr - Not relevant here
   135  //
   136  // In addition to these, we need to also support hclsyntax.IndexExpr. For it - we do not care about what's in the index.
   137  // We need only know the traversal parts of it the "Collection", as the index doesn't affect which resource/module this is
   138  func absTraversalForImportToExpr(expr hcl.Expression) (traversal hcl.Traversal, diags tfdiags.Diagnostics) {
   139  	switch e := expr.(type) {
   140  	case *hclsyntax.IndexExpr:
   141  		t, d := absTraversalForImportToExpr(e.Collection)
   142  		diags = diags.Append(d)
   143  		traversal = append(traversal, t...)
   144  	case *hclsyntax.RelativeTraversalExpr:
   145  		t, d := absTraversalForImportToExpr(e.Source)
   146  		diags = diags.Append(d)
   147  		traversal = append(traversal, t...)
   148  		traversal = append(traversal, e.Traversal...)
   149  	case *hclsyntax.ScopeTraversalExpr:
   150  		traversal = append(traversal, e.Traversal...)
   151  	default:
   152  		diags = diags.Append(&hcl.Diagnostic{
   153  			Severity: hcl.DiagError,
   154  			Summary:  "Invalid import address expression",
   155  			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",
   156  			Subject:  expr.Range().Ptr(),
   157  		})
   158  	}
   159  	return
   160  }
   161  
   162  // staticImportAddress returns an addrs.ConfigResource representing the module and resource of the import target.
   163  // If the address is of an unacceptable format, the function will return error diags
   164  func staticImportAddress(expr hcl.Expression) (addrs.ConfigResource, tfdiags.Diagnostics) {
   165  	traversal, diags := absTraversalForImportToExpr(expr)
   166  	if diags.HasErrors() {
   167  		return addrs.ConfigResource{}, diags
   168  	}
   169  
   170  	absResourceInstance, diags := addrs.ParseAbsResourceInstance(traversal)
   171  	return absResourceInstance.ConfigResource(), diags
   172  }
   173  
   174  // resolvedImportAddress attempts to find the resource instance of the import target, if possible.
   175  // Here, we attempt to resolve the address as though it is a static absolute traversal, if that's possible.
   176  // This would only be possible if the `import` block's "to" field does not rely on any data that is dynamic
   177  func resolvedImportAddress(expr hcl.Expression) *addrs.AbsResourceInstance {
   178  	var diags hcl.Diagnostics
   179  	traversal, traversalDiags := hcl.AbsTraversalForExpr(expr)
   180  	diags = append(diags, traversalDiags...)
   181  	if diags.HasErrors() {
   182  		return nil
   183  	}
   184  
   185  	to, toDiags := addrs.ParseAbsResourceInstance(traversal)
   186  	diags = append(diags, toDiags.ToHCL()...)
   187  	if diags.HasErrors() {
   188  		return nil
   189  	}
   190  	return &to
   191  }