mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
fd371d838d
Previously we had the evaluate methods accept directly an addrs.InstanceKey and had our evaluator infer a suitable value for count.index for it, but that prevents us from setting the index to be unknown in the validation scenario where we may not be able to predict the number of instances yet but we still want to be able to check that the configuration block is type-safe for all possible count values. To achieve this, we separate the concern of deciding on a value for count.index from the concern of evaluating it, which then allows for other implementations of this in future. For the purpose of this commit there is no change in behavior, with the count.index value being populated whenever the instance key is a number. This commit does a little more groundwork for the future implementation of the for_each feature (which'll support each.key and each.value) but still doesn't yet implement it, leaving it just stubbed out for the moment.
594 lines
15 KiB
Go
594 lines
15 KiB
Go
package terraform
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl2/hcl"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/configs"
|
|
"github.com/hashicorp/terraform/tfdiags"
|
|
"github.com/hashicorp/terraform/version"
|
|
)
|
|
|
|
// EvalCompareDiff is an EvalNode implementation that compares two diffs
|
|
// and errors if the diffs are not equal.
|
|
type EvalCompareDiff struct {
|
|
Addr addrs.ResourceInstance
|
|
One, Two **InstanceDiff
|
|
}
|
|
|
|
// TODO: test
|
|
func (n *EvalCompareDiff) Eval(ctx EvalContext) (interface{}, error) {
|
|
one, two := *n.One, *n.Two
|
|
|
|
// If either are nil, let them be empty
|
|
if one == nil {
|
|
one = new(InstanceDiff)
|
|
one.init()
|
|
}
|
|
if two == nil {
|
|
two = new(InstanceDiff)
|
|
two.init()
|
|
}
|
|
oneId, _ := one.GetAttribute("id")
|
|
twoId, _ := two.GetAttribute("id")
|
|
one.DelAttribute("id")
|
|
two.DelAttribute("id")
|
|
defer func() {
|
|
if oneId != nil {
|
|
one.SetAttribute("id", oneId)
|
|
}
|
|
if twoId != nil {
|
|
two.SetAttribute("id", twoId)
|
|
}
|
|
}()
|
|
|
|
if same, reason := one.Same(two); !same {
|
|
log.Printf("[ERROR] %s: diffs didn't match", n.Addr)
|
|
log.Printf("[ERROR] %s: reason: %s", n.Addr, reason)
|
|
log.Printf("[ERROR] %s: diff one: %#v", n.Addr, one)
|
|
log.Printf("[ERROR] %s: diff two: %#v", n.Addr, two)
|
|
return nil, fmt.Errorf(
|
|
"%s: diffs didn't match during apply. This is a bug with "+
|
|
"Terraform and should be reported as a GitHub Issue.\n"+
|
|
"\n"+
|
|
"Please include the following information in your report:\n"+
|
|
"\n"+
|
|
" Terraform Version: %s\n"+
|
|
" Resource ID: %s\n"+
|
|
" Mismatch reason: %s\n"+
|
|
" Diff One (usually from plan): %#v\n"+
|
|
" Diff Two (usually from apply): %#v\n"+
|
|
"\n"+
|
|
"Also include as much context as you can about your config, state, "+
|
|
"and the steps you performed to trigger this error.\n",
|
|
n.Addr, version.Version, n.Addr, reason, one, two)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// EvalDiff is an EvalNode implementation that does a refresh for
|
|
// a resource.
|
|
type EvalDiff struct {
|
|
Addr addrs.ResourceInstance
|
|
Config *configs.Resource
|
|
Provider *ResourceProvider
|
|
ProviderSchema **ProviderSchema
|
|
State **InstanceState
|
|
PreviousDiff **InstanceDiff
|
|
|
|
OutputDiff **InstanceDiff
|
|
OutputValue *cty.Value
|
|
OutputState **InstanceState
|
|
|
|
Stub bool
|
|
}
|
|
|
|
// TODO: test
|
|
func (n *EvalDiff) Eval(ctx EvalContext) (interface{}, error) {
|
|
state := *n.State
|
|
config := *n.Config
|
|
provider := *n.Provider
|
|
providerSchema := *n.ProviderSchema
|
|
|
|
if providerSchema == nil {
|
|
return nil, fmt.Errorf("provider schema is unavailable for %s", n.Addr)
|
|
}
|
|
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// The provider and hook APIs still expect our legacy InstanceInfo type.
|
|
legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path()))
|
|
|
|
// State still uses legacy-style internal ids, so we need to shim to get
|
|
// a suitable key to use.
|
|
stateId := NewLegacyResourceInstanceAddress(n.Addr.Absolute(ctx.Path())).stateId()
|
|
|
|
// Call pre-diff hook
|
|
if !n.Stub {
|
|
err := ctx.Hook(func(h Hook) (HookAction, error) {
|
|
return h.PreDiff(legacyInfo, state)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// The state for the diff must never be nil
|
|
diffState := state
|
|
if diffState == nil {
|
|
diffState = new(InstanceState)
|
|
}
|
|
diffState.init()
|
|
|
|
// Evaluate the configuration
|
|
schema := providerSchema.ResourceTypes[n.Addr.Resource.Type]
|
|
if schema == nil {
|
|
// Should be caught during validation, so we don't bother with a pretty error here
|
|
return nil, fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Type)
|
|
}
|
|
keyData := EvalDataForInstanceKey(n.Addr.Key)
|
|
configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData)
|
|
diags = diags.Append(configDiags)
|
|
if configDiags.HasErrors() {
|
|
return nil, diags.Err()
|
|
}
|
|
|
|
// The provider API still expects our legacy ResourceConfig type.
|
|
legacyRC := NewResourceConfigShimmed(configVal, schema)
|
|
|
|
// Diff!
|
|
diff, err := provider.Diff(legacyInfo, diffState, legacyRC)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if diff == nil {
|
|
diff = new(InstanceDiff)
|
|
}
|
|
|
|
// Set DestroyDeposed if we have deposed instances
|
|
_, err = readInstanceFromState(ctx, stateId, nil, func(rs *ResourceState) (*InstanceState, error) {
|
|
if len(rs.Deposed) > 0 {
|
|
diff.DestroyDeposed = true
|
|
}
|
|
|
|
return nil, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Preserve the DestroyTainted flag
|
|
if n.PreviousDiff != nil {
|
|
diff.SetTainted((*n.PreviousDiff).GetDestroyTainted())
|
|
}
|
|
|
|
// Require a destroy if there is an ID and it requires new.
|
|
if diff.RequiresNew() && state != nil && state.ID != "" {
|
|
diff.SetDestroy(true)
|
|
}
|
|
|
|
// If we're creating a new resource, compute its ID
|
|
if diff.RequiresNew() || state == nil || state.ID == "" {
|
|
var oldID string
|
|
if state != nil {
|
|
oldID = state.Attributes["id"]
|
|
}
|
|
|
|
// Add diff to compute new ID
|
|
diff.init()
|
|
diff.SetAttribute("id", &ResourceAttrDiff{
|
|
Old: oldID,
|
|
NewComputed: true,
|
|
RequiresNew: true,
|
|
Type: DiffAttrOutput,
|
|
})
|
|
}
|
|
|
|
// filter out ignored attributes
|
|
if err := n.processIgnoreChanges(diff); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Call post-refresh hook
|
|
if !n.Stub {
|
|
err = ctx.Hook(func(h Hook) (HookAction, error) {
|
|
return h.PostDiff(legacyInfo, diff)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Update our output if we care
|
|
if n.OutputDiff != nil {
|
|
*n.OutputDiff = diff
|
|
}
|
|
|
|
if n.OutputValue != nil {
|
|
*n.OutputValue = configVal
|
|
}
|
|
|
|
// Update the state if we care
|
|
if n.OutputState != nil {
|
|
*n.OutputState = state
|
|
|
|
// Merge our state so that the state is updated with our plan
|
|
if !diff.Empty() && n.OutputState != nil {
|
|
*n.OutputState = state.MergeDiff(diff)
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (n *EvalDiff) processIgnoreChanges(diff *InstanceDiff) error {
|
|
if diff == nil || n.Config == nil || n.Config.Managed == nil {
|
|
return nil
|
|
}
|
|
ignoreChanges := n.Config.Managed.IgnoreChanges
|
|
ignoreAll := n.Config.Managed.IgnoreAllChanges
|
|
|
|
if len(ignoreChanges) == 0 && !ignoreAll {
|
|
return nil
|
|
}
|
|
|
|
// If we're just creating the resource, we shouldn't alter the
|
|
// Diff at all
|
|
if diff.ChangeType() == DiffCreate {
|
|
return nil
|
|
}
|
|
|
|
// If the resource has been tainted then we don't process ignore changes
|
|
// since we MUST recreate the entire resource.
|
|
if diff.GetDestroyTainted() {
|
|
return nil
|
|
}
|
|
|
|
attrs := diff.CopyAttributes()
|
|
|
|
// get the complete set of keys we want to ignore
|
|
ignorableAttrKeys := make(map[string]bool)
|
|
for k := range attrs {
|
|
if ignoreAll {
|
|
ignorableAttrKeys[k] = true
|
|
continue
|
|
}
|
|
for _, ignoredTraversal := range ignoreChanges {
|
|
ignoredKey := legacyFlatmapKeyForTraversal(ignoredTraversal)
|
|
if k == ignoredKey || strings.HasPrefix(k, ignoredKey+".") {
|
|
ignorableAttrKeys[k] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the resource was being destroyed, check to see if we can ignore the
|
|
// reason for it being destroyed.
|
|
if diff.GetDestroy() {
|
|
for k, v := range attrs {
|
|
if k == "id" {
|
|
// id will always be changed if we intended to replace this instance
|
|
continue
|
|
}
|
|
if v.Empty() || v.NewComputed {
|
|
continue
|
|
}
|
|
|
|
// If any RequiresNew attribute isn't ignored, we need to keep the diff
|
|
// as-is to be able to replace the resource.
|
|
if v.RequiresNew && !ignorableAttrKeys[k] {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Now that we know that we aren't replacing the instance, we can filter
|
|
// out all the empty and computed attributes. There may be a bunch of
|
|
// extraneous attribute diffs for the other non-requires-new attributes
|
|
// going from "" -> "configval" or "" -> "<computed>".
|
|
// We must make sure any flatmapped containers are filterred (or not) as a
|
|
// whole.
|
|
containers := groupContainers(diff)
|
|
keep := map[string]bool{}
|
|
for _, v := range containers {
|
|
if v.keepDiff(ignorableAttrKeys) {
|
|
// At least one key has changes, so list all the sibling keys
|
|
// to keep in the diff
|
|
for k := range v {
|
|
keep[k] = true
|
|
// this key may have been added by the user to ignore, but
|
|
// if it's a subkey in a container, we need to un-ignore it
|
|
// to keep the complete containter.
|
|
delete(ignorableAttrKeys, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
for k, v := range attrs {
|
|
if (v.Empty() || v.NewComputed) && !keep[k] {
|
|
ignorableAttrKeys[k] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Here we undo the two reactions to RequireNew in EvalDiff - the "id"
|
|
// attribute diff and the Destroy boolean field
|
|
log.Printf("[DEBUG] Removing 'id' diff and setting Destroy to false " +
|
|
"because after ignore_changes, this diff no longer requires replacement")
|
|
diff.DelAttribute("id")
|
|
diff.SetDestroy(false)
|
|
|
|
// If we didn't hit any of our early exit conditions, we can filter the diff.
|
|
for k := range ignorableAttrKeys {
|
|
log.Printf("[DEBUG] [EvalIgnoreChanges] %s: Ignoring diff attribute: %s", n.Addr.String(), k)
|
|
diff.DelAttribute(k)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// legacyFlagmapKeyForTraversal constructs a key string compatible with what
|
|
// the flatmap package would generate for an attribute addressable by the given
|
|
// traversal.
|
|
//
|
|
// This is used only to shim references to attributes within the diff and
|
|
// state structures, which have not (at the time of writing) yet been updated
|
|
// to use the newer HCL-based representations.
|
|
func legacyFlatmapKeyForTraversal(traversal hcl.Traversal) string {
|
|
var buf bytes.Buffer
|
|
first := true
|
|
for _, step := range traversal {
|
|
if !first {
|
|
buf.WriteByte('.')
|
|
}
|
|
switch ts := step.(type) {
|
|
case hcl.TraverseRoot:
|
|
buf.WriteString(ts.Name)
|
|
case hcl.TraverseAttr:
|
|
buf.WriteString(ts.Name)
|
|
case hcl.TraverseIndex:
|
|
val := ts.Key
|
|
switch val.Type() {
|
|
case cty.Number:
|
|
bf := val.AsBigFloat()
|
|
buf.WriteString(bf.String())
|
|
case cty.String:
|
|
s := val.AsString()
|
|
buf.WriteString(s)
|
|
default:
|
|
// should never happen, since no other types appear in
|
|
// traversals in practice.
|
|
buf.WriteByte('?')
|
|
}
|
|
default:
|
|
// should never happen, since we've covered all of the types
|
|
// that show up in parsed traversals in practice.
|
|
buf.WriteByte('?')
|
|
}
|
|
first = false
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// a group of key-*ResourceAttrDiff pairs from the same flatmapped container
|
|
type flatAttrDiff map[string]*ResourceAttrDiff
|
|
|
|
// we need to keep all keys if any of them have a diff that's not ignored
|
|
func (f flatAttrDiff) keepDiff(ignoreChanges map[string]bool) bool {
|
|
for k, v := range f {
|
|
ignore := false
|
|
for attr := range ignoreChanges {
|
|
if strings.HasPrefix(k, attr) {
|
|
ignore = true
|
|
}
|
|
}
|
|
|
|
if !v.Empty() && !v.NewComputed && !ignore {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// sets, lists and maps need to be compared for diff inclusion as a whole, so
|
|
// group the flatmapped keys together for easier comparison.
|
|
func groupContainers(d *InstanceDiff) map[string]flatAttrDiff {
|
|
isIndex := multiVal.MatchString
|
|
containers := map[string]flatAttrDiff{}
|
|
attrs := d.CopyAttributes()
|
|
// we need to loop once to find the index key
|
|
for k := range attrs {
|
|
if isIndex(k) {
|
|
// add the key, always including the final dot to fully qualify it
|
|
containers[k[:len(k)-1]] = flatAttrDiff{}
|
|
}
|
|
}
|
|
|
|
// loop again to find all the sub keys
|
|
for prefix, values := range containers {
|
|
for k, attrDiff := range attrs {
|
|
// we include the index value as well, since it could be part of the diff
|
|
if strings.HasPrefix(k, prefix) {
|
|
values[k] = attrDiff
|
|
}
|
|
}
|
|
}
|
|
|
|
return containers
|
|
}
|
|
|
|
// EvalDiffDestroy is an EvalNode implementation that returns a plain
|
|
// destroy diff.
|
|
type EvalDiffDestroy struct {
|
|
Addr addrs.ResourceInstance
|
|
State **InstanceState
|
|
Output **InstanceDiff
|
|
OutputState **InstanceState
|
|
}
|
|
|
|
// TODO: test
|
|
func (n *EvalDiffDestroy) Eval(ctx EvalContext) (interface{}, error) {
|
|
state := *n.State
|
|
|
|
// If there is no state or we don't have an ID, we're already destroyed
|
|
if state == nil || state.ID == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
// The provider and hook APIs still expect our legacy InstanceInfo type.
|
|
legacyInfo := NewInstanceInfo(n.Addr.Absolute(ctx.Path()))
|
|
|
|
// Call pre-diff hook
|
|
err := ctx.Hook(func(h Hook) (HookAction, error) {
|
|
return h.PreDiff(legacyInfo, state)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// The diff
|
|
diff := &InstanceDiff{Destroy: true}
|
|
|
|
// Call post-diff hook
|
|
err = ctx.Hook(func(h Hook) (HookAction, error) {
|
|
return h.PostDiff(legacyInfo, diff)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update our output
|
|
*n.Output = diff
|
|
|
|
if n.OutputState != nil {
|
|
// Record our proposed new state, which is nil because we're destroying.
|
|
*n.OutputState = nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// EvalDiffDestroyModule is an EvalNode implementation that writes the diff to
|
|
// the full diff.
|
|
type EvalDiffDestroyModule struct {
|
|
Path addrs.ModuleInstance
|
|
}
|
|
|
|
// TODO: test
|
|
func (n *EvalDiffDestroyModule) Eval(ctx EvalContext) (interface{}, error) {
|
|
diff, lock := ctx.Diff()
|
|
|
|
// Acquire the lock so that we can do this safely concurrently
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
// Write the diff
|
|
modDiff := diff.ModuleByPath(n.Path)
|
|
if modDiff == nil {
|
|
modDiff = diff.AddModule(n.Path)
|
|
}
|
|
modDiff.Destroy = true
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// EvalFilterDiff is an EvalNode implementation that filters the diff
|
|
// according to some filter.
|
|
type EvalFilterDiff struct {
|
|
// Input and output
|
|
Diff **InstanceDiff
|
|
Output **InstanceDiff
|
|
|
|
// Destroy, if true, will only include a destroy diff if it is set.
|
|
Destroy bool
|
|
}
|
|
|
|
func (n *EvalFilterDiff) Eval(ctx EvalContext) (interface{}, error) {
|
|
if *n.Diff == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
input := *n.Diff
|
|
result := new(InstanceDiff)
|
|
|
|
if n.Destroy {
|
|
if input.GetDestroy() || input.RequiresNew() {
|
|
result.SetDestroy(true)
|
|
}
|
|
}
|
|
|
|
if n.Output != nil {
|
|
*n.Output = result
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// EvalReadDiff is an EvalNode implementation that writes the diff to
|
|
// the full diff.
|
|
type EvalReadDiff struct {
|
|
Name string
|
|
Diff **InstanceDiff
|
|
}
|
|
|
|
func (n *EvalReadDiff) Eval(ctx EvalContext) (interface{}, error) {
|
|
diff, lock := ctx.Diff()
|
|
|
|
// Acquire the lock so that we can do this safely concurrently
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
// Write the diff
|
|
modDiff := diff.ModuleByPath(ctx.Path())
|
|
if modDiff == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
*n.Diff = modDiff.Resources[n.Name]
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// EvalWriteDiff is an EvalNode implementation that writes the diff to
|
|
// the full diff.
|
|
type EvalWriteDiff struct {
|
|
Name string
|
|
Diff **InstanceDiff
|
|
}
|
|
|
|
// TODO: test
|
|
func (n *EvalWriteDiff) Eval(ctx EvalContext) (interface{}, error) {
|
|
diff, lock := ctx.Diff()
|
|
|
|
// The diff to write, if its empty it should write nil
|
|
var diffVal *InstanceDiff
|
|
if n.Diff != nil {
|
|
diffVal = *n.Diff
|
|
}
|
|
if diffVal.Empty() {
|
|
diffVal = nil
|
|
}
|
|
|
|
// Acquire the lock so that we can do this safely concurrently
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
|
|
// Write the diff
|
|
modDiff := diff.ModuleByPath(ctx.Path())
|
|
if modDiff == nil {
|
|
modDiff = diff.AddModule(ctx.Path())
|
|
}
|
|
if diffVal != nil {
|
|
modDiff.Resources[n.Name] = diffVal
|
|
} else {
|
|
delete(modDiff.Resources, n.Name)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|