From 9ae9f208d1aa1c1481de974399acf1e7285f1809 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 1 Oct 2016 16:51:00 -0700 Subject: [PATCH] terraform: Context knows how to walk a shadow graph and report errors --- terraform/context.go | 111 ++++++++++++++++++++++++++++++++---- terraform/context_import.go | 2 +- terraform/shadow_context.go | 20 +++++++ terraform/terraform_test.go | 4 ++ 4 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 terraform/shadow_context.go diff --git a/terraform/context.go b/terraform/context.go index 5940dd3f4..fc7e4c994 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -2,6 +2,7 @@ package terraform import ( "fmt" + "io" "log" "sort" "strings" @@ -32,6 +33,12 @@ const ( InputModeStd = InputModeVar | InputModeProvider ) +var ( + // contextFailOnShadowError will cause Context operations to return + // errors when shadow operations fail. This is only used for testing. + contextFailOnShadowError = false +) + // ContextOpts are the user-configurable options to create a context with // NewContext. type ContextOpts struct { @@ -74,6 +81,7 @@ type Context struct { parallelSem Semaphore providerInputConfig map[string]map[string]interface{} runCh <-chan struct{} + shadowErr error } // NewContext creates a new Context structure. @@ -190,6 +198,33 @@ func (c *Context) graphBuilder(g *ContextGraphOpts) GraphBuilder { } } +// ShadowError returns any errors caught during a shadow operation. +// +// A shadow operation is an operation run in parallel to a real operation +// that performs the same tasks using new logic on copied state. The results +// are compared to ensure that the new logic works the same as the old logic. +// The shadow never affects the real operation or return values. +// +// The result of the shadow operation are only available through this function +// call after a real operation is complete. +// +// For API consumers of Context, you can safely ignore this function +// completely if you have no interest in helping report experimental feature +// errors to Terraform maintainers. Otherwise, please call this function +// after every operation and report this to the user. +// +// IMPORTANT: Shadow errors are _never_ critical: they _never_ affect +// the real state or result of a real operation. They are purely informational +// to assist in future Terraform versions being more stable. Please message +// this effectively to the end user. +// +// This must be called only when no other operation is running (refresh, +// plan, etc.). The result can be used in parallel to any other operation +// running. +func (c *Context) ShadowError() error { + return c.shadowErr +} + // Input asks for input to fill variables and provider configurations. // This modifies the configuration in-place, so asking for Input twice // may result in different UI output showing different current values. @@ -300,7 +335,7 @@ func (c *Context) Input(mode InputMode) error { } // Do the walk - if _, err := c.walk(graph, walkInput); err != nil { + if _, err := c.walk(graph, nil, walkInput); err != nil { return err } } @@ -329,9 +364,9 @@ func (c *Context) Apply() (*State, error) { // Do the walk var walker *ContextGraphWalker if c.destroy { - walker, err = c.walk(graph, walkDestroy) + walker, err = c.walk(graph, nil, walkDestroy) } else { - walker, err = c.walk(graph, walkApply) + walker, err = c.walk(graph, nil, walkApply) } if len(walker.ValidationErrors) > 0 { @@ -396,7 +431,7 @@ func (c *Context) Plan() (*Plan, error) { } // Do the walk - walker, err := c.walk(graph, operation) + walker, err := c.walk(graph, nil, operation) if err != nil { return nil, err } @@ -434,7 +469,7 @@ func (c *Context) Refresh() (*State, error) { } // Do the walk - if _, err := c.walk(graph, walkRefresh); err != nil { + if _, err := c.walk(graph, nil, walkRefresh); err != nil { return nil, err } @@ -502,7 +537,7 @@ func (c *Context) Validate() ([]string, []error) { } // Walk - walker, err := c.walk(graph, walkValidate) + walker, err := c.walk(graph, nil, walkValidate) if err != nil { return nil, multierror.Append(errs, err).Errors } @@ -541,8 +576,13 @@ func (c *Context) acquireRun() chan<- struct{} { c.l.Lock() } + // Create the new channel ch := make(chan struct{}) c.runCh = ch + + // Reset the shadow errors + c.shadowErr = nil + return ch } @@ -556,11 +596,62 @@ func (c *Context) releaseRun(ch chan<- struct{}) { } func (c *Context) walk( - graph *Graph, operation walkOperation) (*ContextGraphWalker, error) { - // Walk the graph + graph, shadow *Graph, operation walkOperation) (*ContextGraphWalker, error) { + // Keep track of the "real" context which is the context that does + // the real work: talking to real providers, modifying real state, etc. + realCtx := c + + // If we have a shadow graph, walk that as well + var shadowCh chan error + var shadowCloser io.Closer + if shadow != nil { + // Build the shadow context. In the process, override the real context + // with the one that is wrapped so that the shadow context can verify + // the results of the real. + var shadowCtx *Context + realCtx, shadowCtx, shadowCloser = newShadowContext(c) + + // Build the graph walker for the shadow. + shadowWalker := &ContextGraphWalker{ + Context: shadowCtx, + Operation: operation, + } + + // Kick off the shadow walk. This will block on any operations + // on the real walk so it is fine to start first. + shadowCh = make(chan error) + go func() { + shadowCh <- shadow.Walk(shadowWalker) + }() + } + + // Build the real graph walker log.Printf("[DEBUG] Starting graph walk: %s", operation.String()) - walker := &ContextGraphWalker{Context: c, Operation: operation} - return walker, graph.Walk(walker) + walker := &ContextGraphWalker{Context: realCtx, Operation: operation} + + // Walk the real graph, this will block until it completes + realErr := graph.Walk(walker) + + // If we have a shadow graph, wait for that to complete + if shadowCloser != nil { + // Notify the shadow that we're done + if err := shadowCloser.Close(); err != nil { + c.shadowErr = multierror.Append(c.shadowErr, err) + } + + // Wait for the walk to end + if err := <-shadowCh; err != nil { + c.shadowErr = multierror.Append(c.shadowErr, err) + } + + // If we're supposed to fail on shadow errors, then report it + if contextFailOnShadowError && c.shadowErr != nil { + realErr = multierror.Append(realErr, multierror.Prefix( + c.shadowErr, "shadow graph:")) + } + } + + return walker, realErr } // parseVariableAsHCL parses the value of a single variable as would have been specified diff --git a/terraform/context_import.go b/terraform/context_import.go index 20969ae00..eb9534c1a 100644 --- a/terraform/context_import.go +++ b/terraform/context_import.go @@ -63,7 +63,7 @@ func (c *Context) Import(opts *ImportOpts) (*State, error) { } // Walk it - if _, err := c.walk(graph, walkImport); err != nil { + if _, err := c.walk(graph, nil, walkImport); err != nil { return c.state, err } diff --git a/terraform/shadow_context.go b/terraform/shadow_context.go new file mode 100644 index 000000000..6e02eb3ec --- /dev/null +++ b/terraform/shadow_context.go @@ -0,0 +1,20 @@ +package terraform + +import ( + "io" +) + +// 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 io.Closer should be closed after the graph walk with the +// real context is complete. The result of the Close function will be any +// errors caught during the shadowing operation. +// +// 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, io.Closer) { + return c, nil, nil +} diff --git a/terraform/terraform_test.go b/terraform/terraform_test.go index 880de448a..910005d2a 100644 --- a/terraform/terraform_test.go +++ b/terraform/terraform_test.go @@ -23,6 +23,7 @@ const fixtureDir = "./test-fixtures" func TestMain(m *testing.M) { flag.Parse() + if testing.Verbose() { // if we're verbose, use the logging requested by TF_LOG logging.SetOutput() @@ -31,6 +32,9 @@ func TestMain(m *testing.M) { log.SetOutput(ioutil.Discard) } + // Make sure shadow operations fail our real tests + contextFailOnShadowError = true + os.Exit(m.Run()) }