diff --git a/internal/stacks/stackruntime/internal/stackeval/change_exec.go b/internal/stacks/stackruntime/internal/stackeval/change_exec.go index bf6250088e..6702571026 100644 --- a/internal/stacks/stackruntime/internal/stackeval/change_exec.go +++ b/internal/stacks/stackruntime/internal/stackeval/change_exec.go @@ -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. diff --git a/internal/stacks/stackruntime/internal/stackeval/change_exec_test.go b/internal/stacks/stackruntime/internal/stackeval/change_exec_test.go index 626b0ca042..b4dff88649 100644 --- a/internal/stacks/stackruntime/internal/stackeval/change_exec_test.go +++ b/internal/stacks/stackruntime/internal/stackeval/change_exec_test.go @@ -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 })