terraform: ResourceProvisioner shadow

This commit is contained in:
Mitchell Hashimoto 2016-10-06 14:26:51 -07:00
parent 50e0647c53
commit 4de803622d
No known key found for this signature in database
GPG Key ID: 744E147AA52F5B0A
2 changed files with 449 additions and 0 deletions

View File

@ -0,0 +1,271 @@
package terraform
import (
"fmt"
"io"
"log"
"sync"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/helper/shadow"
)
// shadowResourceProvisioner implements ResourceProvisioner for the shadow
// eval context defined in eval_context_shadow.go.
//
// This is used to verify behavior with a real provisioner. This shouldn't
// be used directly.
type shadowResourceProvisioner interface {
ResourceProvisioner
Shadow
}
// newShadowResourceProvisioner creates a new shadowed ResourceProvisioner.
func newShadowResourceProvisioner(
p ResourceProvisioner) (ResourceProvisioner, shadowResourceProvisioner) {
// Create the shared data
shared := shadowResourceProvisionerShared{
Validate: shadow.ComparedValue{
Func: shadowResourceProvisionerValidateCompare,
},
}
// Create the real provisioner that does actual work
real := &shadowResourceProvisionerReal{
ResourceProvisioner: p,
Shared: &shared,
}
// Create the shadow that watches the real value
shadow := &shadowResourceProvisionerShadow{
Shared: &shared,
}
return real, shadow
}
// shadowResourceProvisionerReal is the real resource provisioner. Function calls
// to this will perform real work. This records the parameters and return
// values and call order for the shadow to reproduce.
type shadowResourceProvisionerReal struct {
ResourceProvisioner
Shared *shadowResourceProvisionerShared
}
func (p *shadowResourceProvisionerReal) Close() error {
var result error
if c, ok := p.ResourceProvisioner.(ResourceProvisionerCloser); ok {
result = c.Close()
}
p.Shared.CloseErr.SetValue(result)
return result
}
func (p *shadowResourceProvisionerReal) Validate(c *ResourceConfig) ([]string, []error) {
warns, errs := p.ResourceProvisioner.Validate(c)
p.Shared.Validate.SetValue(&shadowResourceProvisionerValidate{
Config: c,
ResultWarn: warns,
ResultErr: errs,
})
return warns, errs
}
func (p *shadowResourceProvisionerReal) Apply(
output UIOutput, s *InstanceState, c *ResourceConfig) error {
err := p.ResourceProvisioner.Apply(output, s, c)
// Write the result, grab a lock for writing. This should nver
// block long since the operations below don't block.
p.Shared.ApplyLock.Lock()
defer p.Shared.ApplyLock.Unlock()
key := s.ID
raw, ok := p.Shared.Apply.ValueOk(key)
if !ok {
// Setup a new value
raw = &shadow.ComparedValue{
Func: shadowResourceProvisionerApplyCompare,
}
// Set it
p.Shared.Apply.SetValue(key, raw)
}
compareVal, ok := raw.(*shadow.ComparedValue)
if !ok {
// Just log and return so that we don't cause the real side
// any side effects.
log.Printf("[ERROR] unknown value in 'apply': %#v", raw)
return err
}
// Write the resulting value
compareVal.SetValue(&shadowResourceProvisionerApply{
Config: c,
ResultErr: err,
})
return err
}
// shadowResourceProvisionerShadow is the shadow resource provisioner. Function
// calls never affect real resources. This is paired with the "real" side
// which must be called properly to enable recording.
type shadowResourceProvisionerShadow struct {
Shared *shadowResourceProvisionerShared
Error error // Error is the list of errors from the shadow
ErrorLock sync.Mutex
}
type shadowResourceProvisionerShared struct {
// NOTE: Anytime a value is added here, be sure to add it to
// the Close() method so that it is closed.
CloseErr shadow.Value
Validate shadow.ComparedValue
Apply shadow.KeyedValue
ApplyLock sync.Mutex // For writing only
}
func (p *shadowResourceProvisionerShared) Close() error {
closers := []io.Closer{
&p.CloseErr,
}
for _, c := range closers {
// This should never happen, but we don't panic because a panic
// could affect the real behavior of Terraform and a shadow should
// never be able to do that.
if err := c.Close(); err != nil {
return err
}
}
return nil
}
func (p *shadowResourceProvisionerShadow) CloseShadow() error {
err := p.Shared.Close()
if err != nil {
err = fmt.Errorf("close error: %s", err)
}
return err
}
func (p *shadowResourceProvisionerShadow) ShadowError() error {
return p.Error
}
func (p *shadowResourceProvisionerShadow) Close() error {
v := p.Shared.CloseErr.Value()
if v == nil {
return nil
}
return v.(error)
}
func (p *shadowResourceProvisionerShadow) Validate(c *ResourceConfig) ([]string, []error) {
// Get the result of the validate call
raw := p.Shared.Validate.Value(c)
if raw == nil {
return nil, nil
}
result, ok := raw.(*shadowResourceProvisionerValidate)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'validate' shadow value: %#v", raw))
return nil, nil
}
// We don't need to compare configurations because we key on the
// configuration so just return right away.
return result.ResultWarn, result.ResultErr
}
func (p *shadowResourceProvisionerShadow) Apply(
output UIOutput, s *InstanceState, c *ResourceConfig) error {
// Get the value based on the key
key := s.ID
raw := p.Shared.Apply.Value(key)
if raw == nil {
return nil
}
compareVal, ok := raw.(*shadow.ComparedValue)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'apply' shadow value: %#v", raw))
return nil
}
// With the compared value, we compare against our config
raw = compareVal.Value(c)
if raw == nil {
return nil
}
result, ok := raw.(*shadowResourceProvisionerApply)
if !ok {
p.ErrorLock.Lock()
defer p.ErrorLock.Unlock()
p.Error = multierror.Append(p.Error, fmt.Errorf(
"Unknown 'apply' shadow value: %#v", raw))
return nil
}
return result.ResultErr
}
// The structs for the various function calls are put below. These structs
// are used to carry call information across the real/shadow boundaries.
type shadowResourceProvisionerValidate struct {
Config *ResourceConfig
ResultWarn []string
ResultErr []error
}
type shadowResourceProvisionerApply struct {
Config *ResourceConfig
ResultErr error
}
func shadowResourceProvisionerValidateCompare(k, v interface{}) bool {
c, ok := k.(*ResourceConfig)
if !ok {
return false
}
result, ok := v.(*shadowResourceProvisionerValidate)
if !ok {
return false
}
return c.Equal(result.Config)
}
func shadowResourceProvisionerApplyCompare(k, v interface{}) bool {
c, ok := k.(*ResourceConfig)
if !ok {
return false
}
result, ok := v.(*shadowResourceProvisionerApply)
if !ok {
return false
}
return c.Equal(result.Config)
}

View File

@ -0,0 +1,178 @@
package terraform
import (
"errors"
"fmt"
"reflect"
"testing"
"time"
)
func TestShadowResourceProvisioner_impl(t *testing.T) {
var _ Shadow = new(shadowResourceProvisionerShadow)
}
func TestShadowResourceProvisionerValidate(t *testing.T) {
mock := new(MockResourceProvisioner)
real, shadow := newShadowResourceProvisioner(mock)
// Test values
config := testResourceConfig(t, map[string]interface{}{
"foo": "bar",
})
returnWarns := []string{"foo"}
returnErrs := []error{fmt.Errorf("bar")}
// Configure the mock
mock.ValidateReturnWarns = returnWarns
mock.ValidateReturnErrors = returnErrs
// Verify that it blocks until the real func is called
var warns []string
var errs []error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
warns, errs = shadow.Validate(config)
}()
select {
case <-doneCh:
t.Fatal("should block until finished")
case <-time.After(10 * time.Millisecond):
}
// Call the real func
realWarns, realErrs := real.Validate(config)
if !reflect.DeepEqual(realWarns, returnWarns) {
t.Fatalf("bad: %#v", realWarns)
}
if !reflect.DeepEqual(realErrs, returnErrs) {
t.Fatalf("bad: %#v", realWarns)
}
// The shadow should finish now
<-doneCh
// Verify the shadow returned the same values
if !reflect.DeepEqual(warns, returnWarns) {
t.Fatalf("bad: %#v", warns)
}
if !reflect.DeepEqual(errs, returnErrs) {
t.Fatalf("bad: %#v", errs)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
}
func TestShadowResourceProvisionerValidate_diff(t *testing.T) {
mock := new(MockResourceProvisioner)
real, shadow := newShadowResourceProvisioner(mock)
// Test values
config := testResourceConfig(t, map[string]interface{}{
"foo": "bar",
})
returnWarns := []string{"foo"}
returnErrs := []error{fmt.Errorf("bar")}
// Configure the mock
mock.ValidateReturnWarns = returnWarns
mock.ValidateReturnErrors = returnErrs
// Run a real validation with a config
real.Validate(testResourceConfig(t, map[string]interface{}{"bar": "baz"}))
// Verify that it blocks until the real func is called
var warns []string
var errs []error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
warns, errs = shadow.Validate(config)
}()
select {
case <-doneCh:
t.Fatal("should block until finished")
case <-time.After(10 * time.Millisecond):
}
// Call the real func
realWarns, realErrs := real.Validate(config)
if !reflect.DeepEqual(realWarns, returnWarns) {
t.Fatalf("bad: %#v", realWarns)
}
if !reflect.DeepEqual(realErrs, returnErrs) {
t.Fatalf("bad: %#v", realWarns)
}
// The shadow should finish now
<-doneCh
// Verify the shadow returned the same values
if !reflect.DeepEqual(warns, returnWarns) {
t.Fatalf("bad: %#v", warns)
}
if !reflect.DeepEqual(errs, returnErrs) {
t.Fatalf("bad: %#v", errs)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
}
func TestShadowResourceProvisionerApply(t *testing.T) {
mock := new(MockResourceProvisioner)
real, shadow := newShadowResourceProvisioner(mock)
// Test values
output := new(MockUIOutput)
state := &InstanceState{ID: "foo"}
config := testResourceConfig(t, map[string]interface{}{"foo": "bar"})
mockReturn := errors.New("err")
// Configure the mock
mock.ApplyReturnError = mockReturn
// Verify that it blocks until the real func is called
var err error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
err = shadow.Apply(output, state, config)
}()
select {
case <-doneCh:
t.Fatal("should block until finished")
case <-time.After(10 * time.Millisecond):
}
// Call the real func
realErr := real.Apply(output, state, config)
if realErr != mockReturn {
t.Fatalf("bad: %#v", realErr)
}
// The shadow should finish now
<-doneCh
// Verify the shadow returned the same values
if err != mockReturn {
t.Errorf("bad: %#v", err)
}
// Verify we have no errors
if err := shadow.CloseShadow(); err != nil {
t.Fatalf("bad: %s", err)
}
if err := shadow.ShadowError(); err != nil {
t.Fatalf("bad: %s", err)
}
}