mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 16:31:10 -06:00
terraform: ResourceProvisioner shadow
This commit is contained in:
parent
50e0647c53
commit
4de803622d
271
terraform/shadow_resource_provisioner.go
Normal file
271
terraform/shadow_resource_provisioner.go
Normal 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)
|
||||
}
|
178
terraform/shadow_resource_provisioner_test.go
Normal file
178
terraform/shadow_resource_provisioner_test.go
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user