Skip to content

fix(terraform): evaluateStep to correctly set EvalContext for multiple instances of blocks #8555

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

Emyrk
Copy link
Contributor

@Emyrk Emyrk commented Mar 14, 2025

Before this change, all data blocks set their values in the EvalContext with the key b.Labels()[0]. If the block is an instance of a block (via count param), the label looks like <name>[<idx>].

This is different to how expandBlockCounts handles this.

https://github.com/aquasecurity/trivy/blob/main/pkg/iac/scanners/terraform/parser/evaluator.go#L422-L422

expandBlockCounts handles this correctly, as an hcl.TraverseIndex expects a tuple (assuming the key is a number).

This PR updates evaluateStep to override the <name> in the EvalContext with a new tuple containing every instance of the data block that is found.

TODO:

This same fix probably needs to be applied for for_each and maybe dynamic blocks.

Testing when count = 0 likely also needs to be tested. I think this is already ok, because if count = 0, then expandBlockCounts will not set the EvalContext. And the block is omitted from the list with any key index. Worst case, the key isNull and it is saved under the name without a tuple. So the context has a value, but not a tuple type, so traversals will still fail.

Checklist

  • I've read the guidelines for contributing to this repository.
  • I've followed the conventions in the PR title.
  • I've added tests that prove my fix is effective or that my feature works.
  • I've updated the documentation with the relevant information (if needed).
  • I've added usage information (if the PR introduces new options)
  • I've included a "before" and "after" example to the description (if the PR is a user interface change).

Emyrk added 8 commits March 14, 2025 12:09
Referencing a block that uses `count`, and accessing it's arguments
returns a nil value.
Instances of a block were being saved as "<name>[<idx>] = val" to the
eval context. `expandBlocks` correctly saves it as a tuple
    "<name> = [val, ...]"
@Emyrk Emyrk requested review from simar7 and nikpivkin as code owners March 14, 2025 23:50
Comment on lines 575 to 590
if rawKey.Type().Equals(cty.Number) {
existing := valueMap[ref.NameLabel()]
asInt, _ := rawKey.AsBigFloat().Int64()

valueMap[ref.NameLabel()] = insertTupleElement(existing, int(asInt), b.Values())
}

// The default behavior, that being no key or an unimplemented key type, is to
// save the block as the name value. The name label value contains the key.
//
// Eg: If the block has the index '4', the string [4] is contained
// within 'b.Labels()[1]'
// TODO: This is always done to maintain backwards compatibility.
// I think this can be omitted if the switch statement above
// sets the appropriate tuple value in the valueMap.
valueMap[b.Labels()[1]] = b.Values()
Copy link
Contributor Author

@Emyrk Emyrk Mar 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really think if this if statement branch is executed, we should not do valueMap[b.Labels()[1]] = b.Values().

In an abundance of caution, I kept the previous context setting as well. The previous context is incorrect for an hcl traversal though. So I feel like it was never being hit.

How the code would look if we remove the current behavior entirely.

if rawKey.Type().Equals(cty.Number) {
  existing := valueMap[ref.NameLabel()]
  asInt, _ := rawKey.AsBigFloat().Int64()

  valueMap[ref.NameLabel()] = insertTupleElement(existing, int(asInt), b.Values())
} else {
  valueMap[b.Labels()[1]] = b.Values()
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the old behavior should be removed after adding the handling of all cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nikpivkin by "all the cases", do you mean handling for_each and possibly dynamic cases as well? (TBD if dynamic needs any changes, I am unsure off the top of my head)

@nikpivkin
Copy link
Contributor

Hi @Emyrk !

Indeed, I was not even aware of this problem. Now the last part of the path when inserting values into a context contains an index, which is incorrect. Your approach looks good, but this problem applies not only to data blocks, but to all types of blocks with for_each and count meta-arguments. When using for_each, instances of the block should be exported as an object, and the index can be a number, so we should distinguish between these cases.

If you don't mind I can change this PR to add support for the rest of the cases.

@Emyrk
Copy link
Contributor Author

Emyrk commented Mar 19, 2025

Indeed, I was not even aware of this problem. Now the last part of the path when inserting values into a context contains an index, which is incorrect. Your approach looks good, but this problem applies not only to data blocks, but to all types of blocks with for_each and count meta-arguments. When using for_each, instances of the block should be exported as an object, and the index can be a number, so we should distinguish between these cases.

If you don't mind I can change this PR to add support for the rest of the cases.

Yes! I was aware this problem is larger than my fix. I'm still getting my footing/bearings in this codebase, so just wanted to tackle this in the smallest incremental improvement I could contribute.

Feel free to contribute to my branch directly if you want to fix it all in 1 go. We could also solve the for_each in a second PR. I will defer to your judgement.

@Emyrk
Copy link
Contributor Author

Emyrk commented Mar 20, 2025

@nikpivkin
Copy link
Contributor

@Emyrk Yes, I will make the fixes for the modules in the other PR as it requires a slightly different approach. I already have a draft.

@Emyrk
Copy link
Contributor Author

Emyrk commented Mar 20, 2025

@Emyrk Yes, I will make the fixes for the modules in the other PR as it requires a slightly different approach. I already have a draft.

Whoops! @nikpivkin I will revert my test and change, and keep this PR to what it was before my comment.

// lastState needs to be captured after applying outputs – so that they
// don't get treated as changes – but before running post-submodule
// evaluation, so that changes from that can trigger re-evaluations of
// the submodule if/when they feed back into inputs.
e.ctx.Set(outputs, "module", sm.definition.Name)
ref := sm.definition.Definition.Reference()
e.ctx.Set(blockInstanceValues(sm.definition.Definition, valueMap, outputs), "module", ref.NameLabel())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My draft has exactly the same implementation :)

@nikpivkin
Copy link
Contributor

@simar7 Can you take a look?

}

for _, tt := range tests {
tt := tt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

capturing loop variable is no longer required https://go.dev/blog/loopvar-preview

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL ❤️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Emyrk can we remove this then?

Comment on lines +612 to +613
idx, _ := key.AsBigFloat().Int64()
return insertTupleElement(typeValues[ref.NameLabel()], int(idx), b.Values())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check if idx is within bounds before we invoke insertTupleElement?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AsBigFloat() will panic if it is not valid type. Maybe we should assert it first?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Element may be larger than the tuple capacity when the context does not yet contain all instances of the block, and then we need to grow it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AsBigFloat() will panic if it is not valid type. Maybe we should assert it first?

We check that it is a number. Also the key cannot be unknown, it is either Nil or a known value.

@Emyrk Emyrk force-pushed the stevenmasley/count_argument_references branch 2 times, most recently from 25daecd to d2fff87 Compare April 3, 2025 20:49
@Emyrk
Copy link
Contributor Author

Emyrk commented Apr 3, 2025

Really sorry, I made a mistake and force pushed to this branch by accident. I got the branch back to the state that was reviewed 🤦‍♂️

@simar7
Copy link
Member

simar7 commented Apr 8, 2025

Really sorry, I made a mistake and force pushed to this branch by accident. I got the branch back to the state that was reviewed 🤦‍♂️

No worries @Emyrk - were there any other updates to the comments I had left or you have pushed everything you had?

@Emyrk
Copy link
Contributor Author

Emyrk commented Apr 8, 2025

@simar7 just read through all the comments. Added 1 little test syntax tweak.

Everything I have for this issue is pushed up 👍

@Emyrk Emyrk mentioned this pull request Apr 8, 2025
@simar7 simar7 self-requested a review April 8, 2025 19:41
This was referenced Apr 9, 2025
@simar7 simar7 requested a review from nikpivkin April 10, 2025 04:43
Copy link
Member

@simar7 simar7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, thanks for the PR! @nikpivkin could you also take another look at it?

@nikpivkin nikpivkin added this pull request to the merge queue Apr 10, 2025
Merged via the queue into aquasecurity:main with commit e25de25 Apr 10, 2025
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants