@@ -15,6 +15,8 @@ import (
1515 tfjson "github.com/hashicorp/terraform-json"
1616 "github.com/zclconf/go-cty/cty"
1717 "github.com/zclconf/go-cty/cty/gocty"
18+
19+ "github.com/coder/preview/hclext"
1820)
1921
2022func PlanJSONHook (dfs fs.FS , input Input ) (func (ctx * tfcontext.Context , blocks terraform.Blocks , inputVars map [string ]cty.Value ), error ) {
@@ -31,29 +33,45 @@ func PlanJSONHook(dfs fs.FS, input Input) (func(ctx *tfcontext.Context, blocks t
3133 return nil , fmt .Errorf ("unable to parse plan JSON: %w" , err )
3234 }
3335
34- var _ = plan
3536 return func (ctx * tfcontext.Context , blocks terraform.Blocks , inputVars map [string ]cty.Value ) {
37+ loaded := make (map [* tfjson.StateModule ]bool )
38+
3639 // Do not recurse to child blocks.
3740 // TODO: Only load into the single parent context for the module.
41+ // And do not load context for a module more than once
3842 for _ , block := range blocks {
43+ // TODO: Maybe switch to the 'configuration' block
3944 planMod := priorPlanModule (plan , block )
4045 if planMod == nil {
4146 continue
4247 }
4348
44- // TODO: Nested blocks might have an issue here with the correct context.
45- // We want the "module context", which is the parent of the top level
46- // block. Maybe there is a way to discover what that is via some
47- // var set in the context.
48- err = loadResourcesToContext (block .Context ().Parent (), planMod .Resources )
49+ if loaded [planMod ] {
50+ // No need to load this module into state again
51+ continue
52+ }
53+
54+ rootCtx := block .Context ()
55+ for {
56+ if rootCtx .Parent () != nil {
57+ rootCtx = rootCtx .Parent ()
58+ continue
59+ }
60+ break
61+ }
62+
63+ // Load state into the context
64+ err := loadResourcesToContext (rootCtx , planMod .Resources )
4965 if err != nil {
5066 // TODO: Somehow handle this error
5167 panic (fmt .Sprintf ("unable to load resources to context: %v" , err ))
5268 }
69+ loaded [planMod ] = true
5370 }
5471 }, nil
5572}
5673
74+ // priorPlanModule returns the state data of the module a given block is in.
5775func priorPlanModule (plan * tfjson.Plan , block * terraform.Block ) * tfjson.StateModule {
5876 if ! block .InModule () {
5977 return plan .PriorState .Values .RootModule
@@ -85,6 +103,22 @@ func priorPlanModule(plan *tfjson.Plan, block *terraform.Block) *tfjson.StateMod
85103 return current
86104}
87105
106+ func matchingBlock (block * terraform.Block , planMod * tfjson.StateModule ) * tfjson.StateResource {
107+ ref := block .Reference ()
108+ matchKey := keyMatcher (ref .RawKey ())
109+
110+ for _ , resource := range planMod .Resources {
111+ if ref .BlockType ().ShortName () == string (resource .Mode ) &&
112+ ref .TypeLabel () == resource .Type &&
113+ ref .NameLabel () == resource .Name &&
114+ matchKey (resource .Index ) {
115+
116+ return resource
117+ }
118+ }
119+ return nil
120+ }
121+
88122func loadResourcesToContext (ctx * tfcontext.Context , resources []* tfjson.StateResource ) error {
89123 for _ , resource := range resources {
90124 if resource .Mode != "data" {
@@ -96,12 +130,35 @@ func loadResourcesToContext(ctx *tfcontext.Context, resources []*tfjson.StateRes
96130 continue
97131 }
98132
133+ path := []string {string (resource .Mode ), resource .Type , resource .Name }
134+
135+ // Always merge with any existing values
136+ existing := ctx .Get (path ... )
137+
99138 val , err := toCtyValue (resource .AttributeValues )
100139 if err != nil {
101140 return fmt .Errorf ("unable to determine value of resource %q: %w" , resource .Address , err )
102141 }
103142
104- ctx .Set (val , string (resource .Mode ), resource .Type , resource .Name )
143+ var merged cty.Value
144+ switch resource .Index .(type ) {
145+ case int , int32 , int64 , float32 , float64 :
146+ asInt , ok := toInt (resource .Index )
147+ if ! ok {
148+ return fmt .Errorf ("unable to convert index '%v' to int" , resource .Index )
149+ }
150+
151+ if ! existing .Type ().IsTupleType () {
152+ continue
153+ }
154+ merged = hclext .MergeWithTupleElement (existing , int (asInt ), val )
155+ case nil :
156+ merged = hclext .MergeObjects (existing , val )
157+ default :
158+ return fmt .Errorf ("unsupported index type %T" , resource .Index )
159+ }
160+
161+ ctx .Set (merged , string (resource .Mode ), resource .Type , resource .Name )
105162 }
106163 return nil
107164}
@@ -172,3 +229,51 @@ func TrivyParsePlanJSON(reader io.Reader) (*tfjson.Plan, error) {
172229
173230 return nil , err
174231}
232+
233+ func keyMatcher (key cty.Value ) func (to any ) bool {
234+ switch {
235+ case key .Type ().Equals (cty .Number ):
236+ idx , _ := key .AsBigFloat ().Int64 ()
237+ return func (to any ) bool {
238+ asInt , ok := toInt (to )
239+ return ok && asInt == idx
240+ }
241+
242+ case key .Type ().Equals (cty .String ):
243+ // TODO: handle key strings
244+ }
245+
246+ return func (to any ) bool {
247+ return true
248+ }
249+ }
250+
251+ func toInt (to any ) (int64 , bool ) {
252+ switch typed := to .(type ) {
253+ case uint :
254+ return int64 (typed ), true
255+ case uint8 :
256+ return int64 (typed ), true
257+ case uint16 :
258+ return int64 (typed ), true
259+ case uint32 :
260+ return int64 (typed ), true
261+ case uint64 :
262+ return int64 (typed ), true
263+ case int :
264+ return int64 (typed ), true
265+ case int8 :
266+ return int64 (typed ), true
267+ case int16 :
268+ return int64 (typed ), true
269+ case int32 :
270+ return int64 (typed ), true
271+ case int64 :
272+ return typed , true
273+ case float32 :
274+ return int64 (typed ), true
275+ case float64 :
276+ return int64 (typed ), true
277+ }
278+ return 0 , false
279+ }
0 commit comments