terraform: Shadow interface, properly string through errors at the right

time
This commit is contained in:
Mitchell Hashimoto 2016-10-05 13:39:02 -07:00
parent c92ee5a8bd
commit 3edb8599b1
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
7 changed files with 176 additions and 36 deletions

View File

@ -2,7 +2,6 @@ package terraform
import (
"fmt"
"io"
"log"
"sort"
"strings"
@ -615,7 +614,7 @@ func (c *Context) walk(
// If we have a shadow graph, walk that as well
var shadowCtx *Context
var shadowCh chan error
var shadowCloser io.Closer
var shadowCloser Shadow
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
@ -647,13 +646,16 @@ func (c *Context) walk(
// 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 {
if err := shadowCloser.CloseShadow(); err != nil {
c.shadowErr = multierror.Append(c.shadowErr, err)
}
// Wait for the walk to end
log.Printf("[DEBUG] Waiting for shadow graph to complete...")
if err := <-shadowCh; err != nil {
shadowWalkErr := <-shadowCh
// Get any shadow errors
if err := shadowCloser.ShadowError(); err != nil {
c.shadowErr = multierror.Append(c.shadowErr, err)
}
@ -662,6 +664,22 @@ func (c *Context) walk(
c.shadowErr = multierror.Append(c.shadowErr, err)
}
// At this point, if we're supposed to fail on error, then
// we PANIC. Some tests just verify that there is an error,
// so simply appending it to realErr and returning could hide
// shadow problems.
//
// This must be done BEFORE appending shadowWalkErr since the
// shadowWalkErr may include expected errors.
if c.shadowErr != nil && contextFailOnShadowError {
panic(multierror.Prefix(c.shadowErr, "shadow graph:"))
}
// Now, if we have a walk error, we append that through
if shadowWalkErr != nil {
c.shadowErr = multierror.Append(c.shadowErr, shadowWalkErr)
}
if c.shadowErr == nil {
log.Printf("[INFO] Shadow graph success!")
} else {

View File

@ -318,10 +318,10 @@ func TestContext2Apply_computedAttrRefTypeMismatch(t *testing.T) {
}
_, err := ctx.Apply()
if err == nil {
t.Fatalf("Expected err, got none!")
}
expected := "Expected ami to be string"
if !strings.Contains(err.Error(), expected) {
t.Fatalf("expected:\n\n%s\n\nto contain:\n\n%s", err, expected)

28
terraform/shadow.go Normal file
View File

@ -0,0 +1,28 @@
package terraform
// Shadow is the interface that any "shadow" structures must implement.
//
// A shadow structure is an interface implementation (typically) that
// shadows a real implementation and verifies that the same behavior occurs
// on both. The semantics of this behavior are up to the interface itself.
//
// A shadow NEVER modifies real values or state. It must always be safe to use.
//
// For example, a ResourceProvider shadow ensures that the same operations
// are done on the same resources with the same configurations.
//
// The typical usage of a shadow following this interface is to complete
// the real operations, then call CloseShadow which tells the shadow that
// the real side is done. Then, once the shadow is also complete, call
// ShadowError to find any errors that may have been caught.
type Shadow interface {
// CloseShadow tells the shadow that the REAL implementation is
// complete. Therefore, any calls that would block should now return
// immediately since no more changes will happen to the real side.
CloseShadow() error
// ShadowError returns the errors that the shadow has found.
// This should be called AFTER CloseShadow and AFTER the shadow is
// known to be complete (no more calls to it).
ShadowError() error
}

View File

@ -112,6 +112,38 @@ func (f *shadowComponentFactory) CloseShadow() error {
return result
}
func (f *shadowComponentFactory) ShadowError() error {
// If we aren't the shadow, just return
if !f.Shadow {
return nil
}
// Lock ourselves so we don't modify state
f.lock.Lock()
defer f.lock.Unlock()
// Grab our shared state
shared := f.shadowComponentFactoryShared
// If we're not closed, its an error
if !shared.closed {
return fmt.Errorf("component factory must be closed to retrieve errors")
}
// Close all the providers and provisioners and return the error
var result error
for _, n := range shared.providerKeys {
_, shadow, err := shared.ResourceProvider(n, n)
if err == nil && shadow != nil {
if err := shadow.ShadowError(); err != nil {
result = multierror.Append(result, err)
}
}
}
return result
}
// shadowComponentFactoryShared is shared data between the two factories.
//
// It is NOT SAFE to run any function on this struct in parallel. Lock

View File

@ -2,7 +2,6 @@ package terraform
import (
"fmt"
"io"
"reflect"
"github.com/hashicorp/go-multierror"
@ -13,14 +12,13 @@ import (
// 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.
// 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, io.Closer) {
func newShadowContext(c *Context) (*Context, *Context, Shadow) {
// Copy the targets
targetRaw, err := copystructure.Copy(c.targets)
if err != nil {
@ -99,6 +97,10 @@ type shadowContextCloser struct {
}
// Close closes the shadow context.
func (c *shadowContextCloser) Close() error {
func (c *shadowContextCloser) CloseShadow() error {
return c.Components.CloseShadow()
}
func (c *shadowContextCloser) ShadowError() error {
return c.Components.ShadowError()
}

View File

@ -16,14 +16,7 @@ import (
// be used directly.
type shadowResourceProvider interface {
ResourceProvider
// CloseShadow should be called when the _real_ side is complete.
// This will immediately end any blocked calls and return any errors.
//
// Any operations on the shadow provider after this is undefined. It
// could be fine, it could result in crashes, etc. Do not use the
// shadow after this is called.
CloseShadow() error
Shadow
}
// newShadowResourceProvider creates a new shadowed ResourceProvider.
@ -174,6 +167,7 @@ type shadowResourceProviderShared struct {
Input shadow.Value
Validate shadow.Value
Configure shadow.Value
ValidateResource shadow.KeyedValue
Apply shadow.KeyedValue
Diff shadow.KeyedValue
Refresh shadow.KeyedValue
@ -182,7 +176,7 @@ type shadowResourceProviderShared struct {
func (p *shadowResourceProviderShared) Close() error {
closers := []io.Closer{
&p.CloseErr, &p.Input, &p.Validate,
&p.Configure, &p.Apply, &p.Diff,
&p.Configure, &p.ValidateResource, &p.Apply, &p.Diff,
&p.Refresh,
}
@ -199,13 +193,16 @@ func (p *shadowResourceProviderShared) Close() error {
}
func (p *shadowResourceProviderShadow) CloseShadow() error {
result := p.Error
if err := p.Shared.Close(); err != nil {
result = multierror.Append(result, fmt.Errorf(
"close error: %s", err))
err := p.Shared.Close()
if err != nil {
err = fmt.Errorf("close error: %s", err)
}
return result
return err
}
func (p *shadowResourceProviderShadow) ShadowError() error {
return p.Error
}
func (p *shadowResourceProviderShadow) Resources() []ResourceType {
@ -313,6 +310,57 @@ func (p *shadowResourceProviderShadow) Configure(c *ResourceConfig) error {
return result.Result
}
func (p *shadowResourceProviderShadow) ValidateResource(t string, c *ResourceConfig) ([]string, []error) {
// Unique key
key := t
// Get the initial value
raw := p.Shared.ValidateResource.Value(key)
// Find a validation with our configuration
var result *shadowResourceProviderValidateResource
for {
// Get the value
if raw == nil {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'ValidateResource' call for %q:\n\n%#v",
key, c))
return nil, nil
}
wrapper, ok := raw.(*shadowResourceProviderValidateResourceWrapper)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'ValidateResource' shadow value: %#v", raw))
return nil, nil
}
// Look for the matching call with our configuration
wrapper.RLock()
for _, call := range wrapper.Calls {
if call.Config.Equal(c) {
result = call
break
}
}
wrapper.RUnlock()
// If we found a result, exit
if result != nil {
break
}
// Wait for a change so we can get the wrapper again
raw = p.Shared.ValidateResource.WaitForChange(key)
}
return result.Warns, result.Errors
}
func (p *shadowResourceProviderShadow) Apply(
info *InstanceInfo,
state *InstanceState,
@ -438,10 +486,6 @@ func (p *shadowResourceProviderShadow) Refresh(
// TODO
// TODO
func (p *shadowResourceProviderShadow) ValidateResource(t string, c *ResourceConfig) ([]string, []error) {
return nil, nil
}
func (p *shadowResourceProviderShadow) ImportState(info *InstanceInfo, id string) ([]*InstanceState, error) {
return nil, nil
}
@ -482,6 +526,18 @@ type shadowResourceProviderConfigure struct {
Result error
}
type shadowResourceProviderValidateResourceWrapper struct {
sync.RWMutex
Calls []*shadowResourceProviderValidateResource
}
type shadowResourceProviderValidateResource struct {
Config *ResourceConfig
Warns []string
Errors []error
}
type shadowResourceProviderApply struct {
State *InstanceState
Diff *InstanceDiff

View File

@ -7,6 +7,10 @@ import (
"time"
)
func TestShadowResourceProvider_impl(t *testing.T) {
var _ Shadow = new(shadowResourceProviderShadow)
}
func TestShadowResourceProvider_cachedValues(t *testing.T) {
mock := new(MockResourceProvider)
real, shadow := newShadowResourceProvider(mock)