opentofu/terraform/shadow_context.go
Mitchell Hashimoto aa5d16be79
terraform: shadow errors with UUID() must be ignored
People with `uuid()` usage in their configurations would receive shadow
errors every time on plan because the UUID would change.

This is hacky fix but I also believe correct: if a shadow error contains
uuid() then we ignore the shadow error completely. This feels wrong but
I'll explain why it is likely right:

The "right" feeling solution is to create deterministic random output
across graph runs. This would require using math/rand and seeding it
with the same value each run. However, this alone probably won't work
due to Terraform's parallelism and potential to call uuid() in different
orders. In addition to this, you can't seed crypto/rand and its unlikely
that we'll NEVER use crypto/rand in the future even if we switched
uuid() to use math/rand.

Therefore, the solution is simple: if there is no shadow error, no
problem. If there is a shadow error and it contains uuid(), then ignore
it.
2016-11-14 10:20:26 -08:00

156 lines
4.3 KiB
Go

package terraform
import (
"fmt"
"strings"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/copystructure"
)
// newShadowContext creates a new context that will shadow the given context
// when walking the graph. The resulting context should be used _only once_
// for a graph walk.
//
// The returned Shadow should be closed after the graph walk with the
// real context is complete. Errors from the shadow can be retrieved there.
//
// Most importantly, any operations done on the shadow context (the returned
// context) will NEVER affect the real context. All structures are deep
// copied, no real providers or resources are used, etc.
func newShadowContext(c *Context) (*Context, *Context, Shadow) {
// Copy the targets
targetRaw, err := copystructure.Copy(c.targets)
if err != nil {
panic(err)
}
// Copy the variables
varRaw, err := copystructure.Copy(c.variables)
if err != nil {
panic(err)
}
// Copy the provider inputs
providerInputRaw, err := copystructure.Copy(c.providerInputConfig)
if err != nil {
panic(err)
}
// The factories
componentsReal, componentsShadow := newShadowComponentFactory(c.components)
// Create the shadow
shadow := &Context{
components: componentsShadow,
destroy: c.destroy,
diff: c.diff.DeepCopy(),
hooks: nil,
module: c.module,
state: c.state.DeepCopy(),
targets: targetRaw.([]string),
variables: varRaw.(map[string]interface{}),
// NOTE(mitchellh): This is not going to work for shadows that are
// testing that input results in the proper end state. At the time
// of writing, input is not used in any state-changing graph
// walks anyways, so this checks nothing. We set it to this to avoid
// any panics but even a "nil" value worked here.
uiInput: new(MockUIInput),
// Hardcoded to 4 since parallelism in the shadow doesn't matter
// a ton since we're doing far less compared to the real side
// and our operations are MUCH faster.
parallelSem: NewSemaphore(4),
providerInputConfig: providerInputRaw.(map[string]map[string]interface{}),
}
// Create the real context. This is effectively just a copy of
// the context given except we need to modify some of the values
// to point to the real side of a shadow so the shadow can compare values.
real := &Context{
// The fields below are changed.
components: componentsReal,
// The fields below are direct copies
destroy: c.destroy,
diff: c.diff,
// diffLock - no copy
hooks: c.hooks,
module: c.module,
sh: c.sh,
state: c.state,
// stateLock - no copy
targets: c.targets,
uiInput: c.uiInput,
variables: c.variables,
// l - no copy
parallelSem: c.parallelSem,
providerInputConfig: c.providerInputConfig,
runCh: c.runCh,
shadowErr: c.shadowErr,
}
return real, shadow, &shadowContextCloser{
Components: componentsShadow,
}
}
// shadowContextVerify takes the real and shadow context and verifies they
// have equal diffs and states.
func shadowContextVerify(real, shadow *Context) error {
var result error
// The states compared must be pruned so they're minimal/clean
real.state.prune()
shadow.state.prune()
// Compare the states
if !real.state.Equal(shadow.state) {
result = multierror.Append(result, fmt.Errorf(
"Real and shadow states do not match! "+
"Real state:\n\n%s\n\n"+
"Shadow state:\n\n%s\n\n",
real.state, shadow.state))
}
// Compare the diffs
if !real.diff.Equal(shadow.diff) {
result = multierror.Append(result, fmt.Errorf(
"Real and shadow diffs do not match! "+
"Real diff:\n\n%s\n\n"+
"Shadow diff:\n\n%s\n\n",
real.diff, shadow.diff))
}
return result
}
// shadowContextCloser is the io.Closer returned by newShadowContext that
// closes all the shadows and returns the results.
type shadowContextCloser struct {
Components *shadowComponentFactory
}
// Close closes the shadow context.
func (c *shadowContextCloser) CloseShadow() error {
return c.Components.CloseShadow()
}
func (c *shadowContextCloser) ShadowError() error {
err := c.Components.ShadowError()
if err == nil {
return nil
}
// This is a sad edge case: if the configuration contains uuid() at
// any point, we cannot reason aboyt the shadow execution. Tested
// with Context2Plan_shadowUuid.
if strings.Contains(err.Error(), "uuid()") {
err = nil
}
return err
}