opentofu/internal/tofu/node_resource_abstract.go
Ronny Orot dcc1fa3b88
Support for_each syntax in import block (#1492)
Signed-off-by: RLRabinowitz <rlrabinowitz2@gmail.com>
Signed-off-by: Ronny Orot <ronny.orot@gmail.com>
Co-authored-by: RLRabinowitz <rlrabinowitz2@gmail.com>
2024-04-17 17:12:10 +03:00

605 lines
22 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 tofu
import (
"fmt"
"log"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/dag"
"github.com/opentofu/opentofu/internal/lang"
"github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// ConcreteResourceNodeFunc is a callback type used to convert an
// abstract resource to a concrete one of some type.
type ConcreteResourceNodeFunc func(*NodeAbstractResource) dag.Vertex
// GraphNodeConfigResource is implemented by any nodes that represent a resource.
// The type of operation cannot be assumed, only that this node represents
// the given resource.
type GraphNodeConfigResource interface {
ResourceAddr() addrs.ConfigResource
}
// ConcreteResourceInstanceNodeFunc is a callback type used to convert an
// abstract resource instance to a concrete one of some type.
type ConcreteResourceInstanceNodeFunc func(*NodeAbstractResourceInstance) dag.Vertex
// GraphNodeResourceInstance is implemented by any nodes that represent
// a resource instance. A single resource may have multiple instances if,
// for example, the "count" or "for_each" argument is used for it in
// configuration.
type GraphNodeResourceInstance interface {
ResourceInstanceAddr() addrs.AbsResourceInstance
// StateDependencies returns any inter-resource dependencies that are
// stored in the state.
StateDependencies() []addrs.ConfigResource
}
// NodeAbstractResource represents a resource that has no associated
// operations. It registers all the interfaces for a resource that common
// across multiple operation types.
type NodeAbstractResource struct {
Addr addrs.ConfigResource
// The fields below will be automatically set using the Attach
// interfaces if you're running those transforms, but also be explicitly
// set if you already have that information.
Schema *configschema.Block // Schema for processing the configuration body
SchemaVersion uint64 // Schema version of "Schema", as decided by the provider
Config *configs.Resource // Config is the resource in the config
// ProviderMetas is the provider_meta configs for the module this resource belongs to
ProviderMetas map[addrs.Provider]*configs.ProviderMeta
ProvisionerSchemas map[string]*configschema.Block
// Set from GraphNodeTargetable
Targets []addrs.Targetable
// Set from AttachDataResourceDependsOn
dependsOn []addrs.ConfigResource
forceDependsOn bool
// The address of the provider this resource will use
ResolvedProvider addrs.AbsProviderConfig
// storedProviderConfig is the provider address retrieved from the
// state. This is defined here for access within the ProvidedBy method, but
// will be set from the embedding instance type when the state is attached.
storedProviderConfig addrs.AbsProviderConfig
// This resource may expand into instances which need to be imported.
importTargets []*ImportTarget
// generateConfigPath tells this node which file to write generated config
// into. If empty, then config should not be generated.
generateConfigPath string
}
var (
_ GraphNodeReferenceable = (*NodeAbstractResource)(nil)
_ GraphNodeReferencer = (*NodeAbstractResource)(nil)
_ GraphNodeProviderConsumer = (*NodeAbstractResource)(nil)
_ GraphNodeProvisionerConsumer = (*NodeAbstractResource)(nil)
_ GraphNodeConfigResource = (*NodeAbstractResource)(nil)
_ GraphNodeAttachResourceConfig = (*NodeAbstractResource)(nil)
_ GraphNodeAttachResourceSchema = (*NodeAbstractResource)(nil)
_ GraphNodeAttachProvisionerSchema = (*NodeAbstractResource)(nil)
_ GraphNodeAttachProviderMetaConfigs = (*NodeAbstractResource)(nil)
_ GraphNodeTargetable = (*NodeAbstractResource)(nil)
_ graphNodeAttachDataResourceDependsOn = (*NodeAbstractResource)(nil)
_ dag.GraphNodeDotter = (*NodeAbstractResource)(nil)
)
// NewNodeAbstractResource creates an abstract resource graph node for
// the given absolute resource address.
func NewNodeAbstractResource(addr addrs.ConfigResource) *NodeAbstractResource {
return &NodeAbstractResource{
Addr: addr,
}
}
var (
_ GraphNodeModuleInstance = (*NodeAbstractResourceInstance)(nil)
_ GraphNodeReferenceable = (*NodeAbstractResourceInstance)(nil)
_ GraphNodeReferencer = (*NodeAbstractResourceInstance)(nil)
_ GraphNodeRootReferencer = (*NodeAbstractResourceInstance)(nil)
_ GraphNodeProviderConsumer = (*NodeAbstractResourceInstance)(nil)
_ GraphNodeProvisionerConsumer = (*NodeAbstractResourceInstance)(nil)
_ GraphNodeConfigResource = (*NodeAbstractResourceInstance)(nil)
_ GraphNodeResourceInstance = (*NodeAbstractResourceInstance)(nil)
_ GraphNodeAttachResourceState = (*NodeAbstractResourceInstance)(nil)
_ GraphNodeAttachResourceConfig = (*NodeAbstractResourceInstance)(nil)
_ GraphNodeAttachResourceSchema = (*NodeAbstractResourceInstance)(nil)
_ GraphNodeAttachProvisionerSchema = (*NodeAbstractResourceInstance)(nil)
_ GraphNodeAttachProviderMetaConfigs = (*NodeAbstractResourceInstance)(nil)
_ GraphNodeTargetable = (*NodeAbstractResourceInstance)(nil)
_ dag.GraphNodeDotter = (*NodeAbstractResourceInstance)(nil)
)
func (n *NodeAbstractResource) Name() string {
return n.ResourceAddr().String()
}
// GraphNodeModulePath
func (n *NodeAbstractResource) ModulePath() addrs.Module {
return n.Addr.Module
}
// GraphNodeReferenceable
func (n *NodeAbstractResource) ReferenceableAddrs() []addrs.Referenceable {
return []addrs.Referenceable{n.Addr.Resource}
}
// GraphNodeReferencer
func (n *NodeAbstractResource) References() []*addrs.Reference {
var result []*addrs.Reference
// If we have a config then we prefer to use that.
if c := n.Config; c != nil {
result = append(result, n.DependsOn()...)
if n.Schema == nil {
// Should never happen, but we'll log if it does so that we can
// see this easily when debugging.
log.Printf("[WARN] no schema is attached to %s, so config references cannot be detected", n.Name())
}
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, c.Count)
result = append(result, refs...)
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, c.ForEach)
result = append(result, refs...)
for _, expr := range c.TriggersReplacement {
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, expr)
result = append(result, refs...)
}
// ReferencesInBlock() requires a schema
if n.Schema != nil {
refs, _ = lang.ReferencesInBlock(addrs.ParseRef, c.Config, n.Schema)
result = append(result, refs...)
}
if c.Managed != nil {
if c.Managed.Connection != nil {
refs, _ = lang.ReferencesInBlock(addrs.ParseRef, c.Managed.Connection.Config, connectionBlockSupersetSchema)
result = append(result, refs...)
}
for _, p := range c.Managed.Provisioners {
if p.When != configs.ProvisionerWhenCreate {
continue
}
if p.Connection != nil {
refs, _ = lang.ReferencesInBlock(addrs.ParseRef, p.Connection.Config, connectionBlockSupersetSchema)
result = append(result, refs...)
}
schema := n.ProvisionerSchemas[p.Type]
if schema == nil {
log.Printf("[WARN] no schema for provisioner %q is attached to %s, so provisioner block references cannot be detected", p.Type, n.Name())
}
refs, _ = lang.ReferencesInBlock(addrs.ParseRef, p.Config, schema)
result = append(result, refs...)
}
}
for _, check := range c.Preconditions {
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, check.Condition)
result = append(result, refs...)
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, check.ErrorMessage)
result = append(result, refs...)
}
for _, check := range c.Postconditions {
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, check.Condition)
result = append(result, refs...)
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, check.ErrorMessage)
result = append(result, refs...)
}
}
return result
}
// referencesInImportAddress find all references relevant to the node in an import target address expression.
// The only references we care about here are the references that exist in the keys of hclsyntax.IndexExpr.
// For example, if the address is module.my_module1[expression1].aws_s3_bucket.bucket[expression2], then we would only
// consider references in expression1 and expression2, as the rest of the expression is the static part of the current
// resource's address
func referencesInImportAddress(expr hcl.Expression) (refs []*addrs.Reference, diags tfdiags.Diagnostics) {
switch e := expr.(type) {
case *hclsyntax.IndexExpr:
r, d := referencesInImportAddress(e.Collection)
diags = diags.Append(d)
refs = append(refs, r...)
r, _ = lang.ReferencesInExpr(addrs.ParseRef, e.Key)
refs = append(refs, r...)
case *hclsyntax.RelativeTraversalExpr:
r, d := referencesInImportAddress(e.Source)
refs = append(refs, r...)
diags = diags.Append(d)
// We don't care about the traversal part of the relative expression
// as it should not contain any references in the index keys
case *hclsyntax.ScopeTraversalExpr:
// Static traversals should not contain any references in the index keys
default:
// This should not happen, as it should have failed validation earlier, in config.absTraversalForImportToExpr
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
}
func (n *NodeAbstractResource) RootReferences() []*addrs.Reference {
var root []*addrs.Reference
for _, importTarget := range n.importTargets {
// References are only possible in import targets originating from an import block
if !importTarget.IsFromImportBlock() {
continue
}
refs, _ := referencesInImportAddress(importTarget.Config.To)
root = append(root, refs...)
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, importTarget.Config.ForEach)
root = append(root, refs...)
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, importTarget.Config.ID)
root = append(root, refs...)
}
return root
}
func (n *NodeAbstractResource) DependsOn() []*addrs.Reference {
var result []*addrs.Reference
if c := n.Config; c != nil {
for _, traversal := range c.DependsOn {
ref, diags := addrs.ParseRef(traversal)
if diags.HasErrors() {
// We ignore this here, because this isn't a suitable place to return
// errors. This situation should be caught and rejected during
// validation.
log.Printf("[ERROR] Can't parse %#v from depends_on as reference: %s", traversal, diags.Err())
continue
}
result = append(result, ref)
}
}
return result
}
func (n *NodeAbstractResource) SetProvider(p addrs.AbsProviderConfig) {
n.ResolvedProvider = p
}
// GraphNodeProviderConsumer
func (n *NodeAbstractResource) ProvidedBy() (addrs.ProviderConfig, bool) {
// Once the provider is fully resolved, we can return the known value.
if n.ResolvedProvider.Provider.Type != "" {
return n.ResolvedProvider, true
}
// If we have a config we prefer that above all else
if n.Config != nil {
relAddr := n.Config.ProviderConfigAddr()
return addrs.LocalProviderConfig{
LocalName: relAddr.LocalName,
Alias: relAddr.Alias,
}, false
}
// See if we have a valid provider config from the state.
if n.storedProviderConfig.Provider.Type != "" {
// An address from the state must match exactly, since we must ensure
// we refresh/destroy a resource with the same provider configuration
// that created it.
return n.storedProviderConfig, true
}
// We might have an import target that is providing a specific provider,
// this is okay as we know there is nothing else potentially providing a
// provider configuration.
if len(n.importTargets) > 0 {
// The import targets should either all be defined via config or none
// of them should be. They should also all have the same provider, so it
// shouldn't matter which we check here, as they'll all give the same.
if n.importTargets[0].Config != nil && n.importTargets[0].Config.ProviderConfigRef != nil {
return addrs.LocalProviderConfig{
LocalName: n.importTargets[0].Config.ProviderConfigRef.Name,
Alias: n.importTargets[0].Config.ProviderConfigRef.Alias,
}, false
}
}
// No provider configuration found; return a default address
return addrs.AbsProviderConfig{
Provider: n.Provider(),
Module: n.ModulePath(),
}, false
}
// GraphNodeProviderConsumer
func (n *NodeAbstractResource) Provider() addrs.Provider {
if n.Config != nil {
return n.Config.Provider
}
if n.storedProviderConfig.Provider.Type != "" {
return n.storedProviderConfig.Provider
}
if len(n.importTargets) > 0 {
// The import targets should either all be defined via config or none
// of them should be. They should also all have the same provider, so it
// shouldn't matter which we check here, as they'll all give the same.
if n.importTargets[0].Config != nil {
return n.importTargets[0].Config.Provider
}
}
return addrs.ImpliedProviderForUnqualifiedType(n.Addr.Resource.ImpliedProvider())
}
// GraphNodeProvisionerConsumer
func (n *NodeAbstractResource) ProvisionedBy() []string {
// If we have no configuration, then we have no provisioners
if n.Config == nil || n.Config.Managed == nil {
return nil
}
// Build the list of provisioners we need based on the configuration.
// It is okay to have duplicates here.
result := make([]string, len(n.Config.Managed.Provisioners))
for i, p := range n.Config.Managed.Provisioners {
result[i] = p.Type
}
return result
}
// GraphNodeProvisionerConsumer
func (n *NodeAbstractResource) AttachProvisionerSchema(name string, schema *configschema.Block) {
if n.ProvisionerSchemas == nil {
n.ProvisionerSchemas = make(map[string]*configschema.Block)
}
n.ProvisionerSchemas[name] = schema
}
// GraphNodeResource
func (n *NodeAbstractResource) ResourceAddr() addrs.ConfigResource {
return n.Addr
}
// GraphNodeTargetable
func (n *NodeAbstractResource) SetTargets(targets []addrs.Targetable) {
n.Targets = targets
}
// graphNodeAttachDataResourceDependsOn
func (n *NodeAbstractResource) AttachDataResourceDependsOn(deps []addrs.ConfigResource, force bool) {
n.dependsOn = deps
n.forceDependsOn = force
}
// GraphNodeAttachResourceConfig
func (n *NodeAbstractResource) AttachResourceConfig(c *configs.Resource) {
n.Config = c
}
// GraphNodeAttachResourceSchema impl
func (n *NodeAbstractResource) AttachResourceSchema(schema *configschema.Block, version uint64) {
n.Schema = schema
n.SchemaVersion = version
}
// GraphNodeAttachProviderMetaConfigs impl
func (n *NodeAbstractResource) AttachProviderMetaConfigs(c map[addrs.Provider]*configs.ProviderMeta) {
n.ProviderMetas = c
}
// GraphNodeDotter impl.
func (n *NodeAbstractResource) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
return &dag.DotNode{
Name: name,
Attrs: map[string]string{
"label": n.Name(),
"shape": "box",
},
}
}
// writeResourceState ensures that a suitable resource-level state record is
// present in the state, if that's required for the "each mode" of that
// resource.
//
// This is important primarily for the situation where count = 0, since this
// eval is the only change we get to set the resource "each mode" to list
// in that case, allowing expression evaluation to see it as a zero-element list
// rather than as not set at all.
func (n *NodeAbstractResource) writeResourceState(ctx EvalContext, addr addrs.AbsResource) (diags tfdiags.Diagnostics) {
state := ctx.State()
// We'll record our expansion decision in the shared "expander" object
// so that later operations (i.e. DynamicExpand and expression evaluation)
// can refer to it. Since this node represents the abstract module, we need
// to expand the module here to create all resources.
expander := ctx.InstanceExpander()
switch {
case n.Config != nil && n.Config.Count != nil:
count, countDiags := evaluateCountExpression(n.Config.Count, ctx)
diags = diags.Append(countDiags)
if countDiags.HasErrors() {
return diags
}
state.SetResourceProvider(addr, n.ResolvedProvider)
expander.SetResourceCount(addr.Module, n.Addr.Resource, count)
case n.Config != nil && n.Config.ForEach != nil:
forEach, forEachDiags := evaluateForEachExpression(n.Config.ForEach, ctx)
diags = diags.Append(forEachDiags)
if forEachDiags.HasErrors() {
return diags
}
// This method takes care of all of the business logic of updating this
// while ensuring that any existing instances are preserved, etc.
state.SetResourceProvider(addr, n.ResolvedProvider)
expander.SetResourceForEach(addr.Module, n.Addr.Resource, forEach)
default:
state.SetResourceProvider(addr, n.ResolvedProvider)
expander.SetResourceSingle(addr.Module, n.Addr.Resource)
}
return diags
}
// readResourceInstanceState reads the current object for a specific instance in
// the state.
func (n *NodeAbstractResource) readResourceInstanceState(ctx EvalContext, addr addrs.AbsResourceInstance) (*states.ResourceInstanceObject, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
if err != nil {
diags = diags.Append(err)
return nil, diags
}
log.Printf("[TRACE] readResourceInstanceState: reading state for %s", addr)
src := ctx.State().ResourceInstanceObject(addr, states.CurrentGen)
if src == nil {
// Presumably we only have deposed objects, then.
log.Printf("[TRACE] readResourceInstanceState: no state present for %s", addr)
return nil, nil
}
schema, currentVersion := (providerSchema).SchemaForResourceAddr(addr.Resource.ContainingResource())
if schema == nil {
// Shouldn't happen since we should've failed long ago if no schema is present
return nil, diags.Append(fmt.Errorf("no schema available for %s while reading state; this is a bug in OpenTofu and should be reported", addr))
}
src, upgradeDiags := upgradeResourceState(addr, provider, src, schema, currentVersion)
if n.Config != nil {
upgradeDiags = upgradeDiags.InConfigBody(n.Config.Config, addr.String())
}
diags = diags.Append(upgradeDiags)
if diags.HasErrors() {
return nil, diags
}
obj, err := src.Decode(schema.ImpliedType())
if err != nil {
diags = diags.Append(err)
}
return obj, diags
}
// readResourceInstanceStateDeposed reads the deposed object for a specific
// instance in the state.
func (n *NodeAbstractResource) readResourceInstanceStateDeposed(ctx EvalContext, addr addrs.AbsResourceInstance, key states.DeposedKey) (*states.ResourceInstanceObject, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
if err != nil {
diags = diags.Append(err)
return nil, diags
}
if key == states.NotDeposed {
return nil, diags.Append(fmt.Errorf("readResourceInstanceStateDeposed used with no instance key; this is a bug in OpenTofu and should be reported"))
}
log.Printf("[TRACE] readResourceInstanceStateDeposed: reading state for %s deposed object %s", addr, key)
src := ctx.State().ResourceInstanceObject(addr, key)
if src == nil {
// Presumably we only have deposed objects, then.
log.Printf("[TRACE] readResourceInstanceStateDeposed: no state present for %s deposed object %s", addr, key)
return nil, diags
}
schema, currentVersion := (providerSchema).SchemaForResourceAddr(addr.Resource.ContainingResource())
if schema == nil {
// Shouldn't happen since we should've failed long ago if no schema is present
return nil, diags.Append(fmt.Errorf("no schema available for %s while reading state; this is a bug in OpenTofu and should be reported", addr))
}
src, upgradeDiags := upgradeResourceState(addr, provider, src, schema, currentVersion)
if n.Config != nil {
upgradeDiags = upgradeDiags.InConfigBody(n.Config.Config, addr.String())
}
diags = diags.Append(upgradeDiags)
if diags.HasErrors() {
// Note that we don't have any channel to return warnings here. We'll
// accept that for now since warnings during a schema upgrade would
// be pretty weird anyway, since this operation is supposed to seem
// invisible to the user.
return nil, diags
}
obj, err := src.Decode(schema.ImpliedType())
if err != nil {
diags = diags.Append(err)
}
return obj, diags
}
// graphNodesAreResourceInstancesInDifferentInstancesOfSameModule is an
// annoyingly-task-specific helper function that returns true if and only if
// the following conditions hold:
// - Both of the given vertices represent specific resource instances, as
// opposed to unexpanded resources or any other non-resource-related object.
// - The module instance addresses for both of the resource instances belong
// to the same static module.
// - The module instance addresses for both of the resource instances are
// not equal, indicating that they belong to different instances of the
// same module.
//
// This result can be used as a way to compensate for the effects of
// conservative analysis passes in our graph builders which make their
// decisions based only on unexpanded addresses, often so that they can behave
// correctly for interactions between expanded and not-yet-expanded objects.
//
// Callers of this helper function will typically skip adding an edge between
// the two given nodes if this function returns true.
func graphNodesAreResourceInstancesInDifferentInstancesOfSameModule(a, b dag.Vertex) bool {
aRI, aOK := a.(GraphNodeResourceInstance)
bRI, bOK := b.(GraphNodeResourceInstance)
if !(aOK && bOK) {
return false
}
aModInst := aRI.ResourceInstanceAddr().Module
bModInst := bRI.ResourceInstanceAddr().Module
aMod := aModInst.Module()
bMod := bModInst.Module()
if !aMod.Equal(bMod) {
return false
}
return !aModInst.Equal(bModInst)
}