stackeval: ChangeExec returns states.State from component instance change

Previously we were returning just a value representing all of the
component's output values, but that's the wrong level of detail.

In the plan phase we have the corresponding function of ComponentInstance
return a plans.Plan object and then derive the object value representing
outputs from it only on request. Having the apply result be a *states.State
is the analogous level of detail, and then again we'll construct the
cty object representing all of the output values only on request during
expression evaluation.
pull/34738/head
Martin Atkins 3 years ago
parent 959b02daa3
commit 985b110afa

@ -8,8 +8,8 @@ import (
"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
// ChangeExec is a helper for making concurrent changes to a set of objects
@ -65,7 +65,7 @@ func ChangeExec[Main any](
results: ChangeExecResults{
componentInstances: collections.NewMap[
stackaddrs.AbsComponentInstance,
func(ctx context.Context) (withDiagnostics[cty.Value], error),
func(ctx context.Context) (withDiagnostics[*states.State], error),
](),
},
}
@ -104,9 +104,9 @@ type ChangeExecRegistry[Main any] struct {
func (r *ChangeExecRegistry[Main]) RegisterComponentInstanceChange(
ctx context.Context,
addr stackaddrs.AbsComponentInstance,
run func(ctx context.Context, main Main) (cty.Value, tfdiags.Diagnostics),
run func(ctx context.Context, main Main) (*states.State, tfdiags.Diagnostics),
) {
resultProvider, waitResult := promising.NewPromise[withDiagnostics[cty.Value]](ctx)
resultProvider, waitResult := promising.NewPromise[withDiagnostics[*states.State]](ctx)
r.mu.Lock()
if r.results.componentInstances.HasKey(addr) {
// This is always a bug in the caller.
@ -117,7 +117,7 @@ func (r *ChangeExecRegistry[Main]) RegisterComponentInstanceChange(
// The asynchronous execution task is responsible for resolving waitResult
// through resultProvider.
promising.AsyncTask(ctx, resultProvider, func(ctx context.Context, resultProvider promising.PromiseResolver[withDiagnostics[cty.Value]]) {
promising.AsyncTask(ctx, resultProvider, func(ctx context.Context, resultProvider promising.PromiseResolver[withDiagnostics[*states.State]]) {
// We'll hold here until the ChangeExec caller signals that it's
// time to begin, by providing a Main object to the begin-execution
// callback that ChangeExec returned.
@ -126,16 +126,16 @@ func (r *ChangeExecRegistry[Main]) RegisterComponentInstanceChange(
// If we get here then that suggests that there was a self-reference
// error or other promise-related inconsistency, so we'll just
// bail out with a placeholder value and the error.
resultProvider.Resolve(ctx, withDiagnostics[cty.Value]{Result: cty.DynamicVal}, err)
resultProvider.Resolve(ctx, withDiagnostics[*states.State]{Result: nil}, err)
return
}
// Now the registered task can begin running, with access to the Main
// object that is presumably by now configured to retrieve apply-phase
// results from our corresponding [ChangeExecResults] object.
v, diags := run(ctx, main)
resultProvider.Resolve(ctx, withDiagnostics[cty.Value]{
Result: v,
newState, diags := run(ctx, main)
resultProvider.Resolve(ctx, withDiagnostics[*states.State]{
Result: newState,
Diagnostics: diags,
}, nil)
})
@ -151,14 +151,14 @@ func (r *ChangeExecRegistry[Main]) RegisterComponentInstanceChange(
type ChangeExecResults struct {
componentInstances collections.Map[
stackaddrs.AbsComponentInstance,
func(context.Context) (withDiagnostics[cty.Value], error),
func(context.Context) (withDiagnostics[*states.State], error),
]
}
func (r *ChangeExecResults) ComponentInstanceResult(ctx context.Context, addr stackaddrs.AbsComponentInstance) (cty.Value, tfdiags.Diagnostics, error) {
func (r *ChangeExecResults) ComponentInstanceResult(ctx context.Context, addr stackaddrs.AbsComponentInstance) (*states.State, tfdiags.Diagnostics, error) {
getter, ok := r.componentInstances.GetOk(addr)
if !ok {
return cty.DynamicVal, nil, ErrChangeExecUnregistered{addr}
return nil, nil, ErrChangeExecUnregistered{addr}
}
// This call will block until the corresponding execution function has
// completed and resolved this promise.

@ -6,8 +6,10 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
@ -45,21 +47,33 @@ func TestChangeExec(t *testing.T) {
},
},
}
valueAddr := addrs.OutputValue{Name: "v"}.Absolute(addrs.RootModuleInstance)
_, err := promising.MainTask(ctx, func(ctx context.Context) (FakeMain, error) {
changeResults, begin := ChangeExec(ctx, func(ctx context.Context, reg *ChangeExecRegistry[FakeMain]) {
t.Logf("begin setup phase")
reg.RegisterComponentInstanceChange(ctx, instAAddr, func(ctx context.Context, main FakeMain) (cty.Value, tfdiags.Diagnostics) {
reg.RegisterComponentInstanceChange(ctx, instAAddr, func(ctx context.Context, main FakeMain) (*states.State, tfdiags.Diagnostics) {
t.Logf("producing result for A")
return cty.StringVal("a"), nil
return states.BuildState(func(ss *states.SyncState) {
ss.SetOutputValue(valueAddr, cty.StringVal("a"), false)
}), nil
})
reg.RegisterComponentInstanceChange(ctx, instBAddr, func(ctx context.Context, main FakeMain) (cty.Value, tfdiags.Diagnostics) {
reg.RegisterComponentInstanceChange(ctx, instBAddr, func(ctx context.Context, main FakeMain) (*states.State, tfdiags.Diagnostics) {
t.Logf("B is waiting for A")
aVal, _, err := main.results.ComponentInstanceResult(ctx, instAAddr)
aState, _, err := main.results.ComponentInstanceResult(ctx, instAAddr)
if err != nil {
return cty.DynamicVal, nil
return nil, nil
}
t.Logf("producing result for B")
return cty.TupleVal([]cty.Value{aVal, cty.StringVal("b")}), nil
aOutputVal := aState.OutputValue(valueAddr)
if aOutputVal == nil {
return nil, nil
}
return states.BuildState(func(ss *states.SyncState) {
ss.SetOutputValue(
valueAddr, cty.TupleVal([]cty.Value{aOutputVal.Value, cty.StringVal("b")}), false,
)
}), nil
})
t.Logf("end setup phase")
})
@ -78,24 +92,24 @@ func TestChangeExec(t *testing.T) {
// the "B" task depends on the result from the "A" task.
var wg sync.WaitGroup
wg.Add(3)
var gotAVal, gotBVal, gotCVal cty.Value
var gotAState, gotBState, gotCState *states.State
var errA, errB, errC error
promising.AsyncTask(ctx, promising.NoPromises, func(ctx context.Context, _ promising.PromiseContainer) {
t.Logf("requesting result C")
gotCVal, _, errC = main.results.ComponentInstanceResult(ctx, instCAddr)
t.Logf("C is %#v", gotCVal)
gotCState, _, errC = main.results.ComponentInstanceResult(ctx, instCAddr)
t.Logf("got result C")
wg.Done()
})
promising.AsyncTask(ctx, promising.NoPromises, func(ctx context.Context, _ promising.PromiseContainer) {
t.Logf("requesting result B")
gotBVal, _, errB = main.results.ComponentInstanceResult(ctx, instBAddr)
t.Logf("B is %#v", gotBVal)
gotBState, _, errB = main.results.ComponentInstanceResult(ctx, instBAddr)
t.Logf("got result B")
wg.Done()
})
promising.AsyncTask(ctx, promising.NoPromises, func(ctx context.Context, _ promising.PromiseContainer) {
t.Logf("requesting result A")
gotAVal, _, errA = main.results.ComponentInstanceResult(ctx, instAAddr)
t.Logf("A is %#v", gotAVal)
gotAState, _, errA = main.results.ComponentInstanceResult(ctx, instAAddr)
t.Logf("got result A")
wg.Done()
})
wg.Wait()
@ -109,19 +123,38 @@ func TestChangeExec(t *testing.T) {
if diff := cmp.Diff(ErrChangeExecUnregistered{instCAddr}, errC); diff != "" {
t.Errorf("wrong error for C\n%s", diff)
}
if errA != nil || errB != nil {
t.FailNow()
}
if gotAState == nil {
t.Fatal("A state is nil")
}
if gotBState == nil {
t.Fatal("B state is nil")
}
if gotCState != nil {
t.Fatal("C state isn't nil, but should have been")
}
gotAOutputVal := gotAState.OutputValue(valueAddr)
if gotAOutputVal == nil {
t.Fatal("A state has no value")
}
gotBOutputVal := gotBState.OutputValue(valueAddr)
if gotBOutputVal == nil {
t.Fatal("B state has no value")
}
gotAVal := gotAOutputVal.Value
wantAVal := cty.StringVal("a")
gotBVal := gotBOutputVal.Value
wantBVal := cty.TupleVal([]cty.Value{wantAVal, cty.StringVal("b")})
wantCVal := cty.DynamicVal
if diff := cmp.Diff(wantAVal, gotAVal, ctydebug.CmpOptions); diff != "" {
t.Errorf("wrong result for A\n%s", diff)
}
if diff := cmp.Diff(wantBVal, gotBVal, ctydebug.CmpOptions); diff != "" {
t.Errorf("wrong result for B\n%s", diff)
}
if diff := cmp.Diff(wantCVal, gotCVal, ctydebug.CmpOptions); diff != "" {
t.Errorf("wrong result for C\n%s", diff)
}
return main, nil
})

Loading…
Cancel
Save