From 4de803622df6c66804aa912d5d4d0a673e3c9ff0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 6 Oct 2016 14:26:51 -0700 Subject: [PATCH] terraform: ResourceProvisioner shadow --- terraform/shadow_resource_provisioner.go | 271 ++++++++++++++++++ terraform/shadow_resource_provisioner_test.go | 178 ++++++++++++ 2 files changed, 449 insertions(+) create mode 100644 terraform/shadow_resource_provisioner.go create mode 100644 terraform/shadow_resource_provisioner_test.go diff --git a/terraform/shadow_resource_provisioner.go b/terraform/shadow_resource_provisioner.go new file mode 100644 index 0000000000..6e405c09db --- /dev/null +++ b/terraform/shadow_resource_provisioner.go @@ -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) +} diff --git a/terraform/shadow_resource_provisioner_test.go b/terraform/shadow_resource_provisioner_test.go new file mode 100644 index 0000000000..7e37d264a4 --- /dev/null +++ b/terraform/shadow_resource_provisioner_test.go @@ -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) + } +}