cloud-custodian / cloud-custodian/tfparse
each.value.<attr> unresolved when the for_each collection contains a module output reference
- Dominant language
- Go
- Stars
- 57
- Forks
- 19
- Avg merge
- 2h 1m
- Merged PRs (30d)
- 1
Description
### Summary
When a `for_each` collection contains a **module output reference**, the collection value itself resolves correctly, but `each.value.` inside the body does not — it stays an unresolved marker:
```json
"labels": {"__attribute__": "each.value.labels", "__name__": "value"}
```
This looks like an evaluation-ordering problem: module outputs appear to be substituted *after* `for_each` / `each.value` binding, so the binding captures a pre-resolution value even though the resolved value is present in the graph by the end.
### Minimal reproduction
No modules are needed for the deref itself — a root-level resource is enough:
```hcl
module "labels" {
source = "./labels"
name = "a"
}
locals {
keys = {
storage = {
labels = module.labels.out
}
}
}
resource "google_storage_bucket" "x" {
for_each = local.keys
name = "static-name-${each.key}"
location = "US"
labels = each.value.labels
}
```
`labels/main.tf`:
```hcl
variable "name" { type = string }
```
`labels/outputs.tf`:
```hcl
output "out" {
value = { name = var.name, static = "x" }
}
```
### Actual
```json
{
"path": "google_storage_bucket.x",
"labels": {"__attribute__": "each.value.labels", "__name__": "value"}
}
```
Note the path is `google_storage_bucket.x` with **no instance key** — the `for_each` did not expand into instances at all in this variant.
### Expected
`labels` should be `{"name": "a", "static": "x"}`, and the resource should expand to `google_storage_bucket.x["storage"]`. `tofu validate` succeeds on this configuration.
### What the graph shows
In the module-crossing variant of this bug, everything resolves right up to the resource body — the module output is *not* lost:
- `module.labels["a"].output.out` → `{"name": "a", "static": "x"}` ✅
- `module.child["a"].keys` → `{"storage": {"labels": {"name": "a", "static": "x"}}}` ✅
- the resource's own `for_each` → the same fully-resolved map ✅
- but `labels` (from `each.value.labels`) → the `__attribute__` marker ❌
So the resolved value is demonstrably available; the `each.value` binding just doesn't see it.
### Isolation
Bisecting the variables independently — only the value's *provenance* matters, not module nesting:
| # | module `for_each` | value source | deref | result |
|---|---|---|---|---|
| a | none (no module) | literal map | `each.value.labels` | ✅ resolves |
| b | yes | literal map | `each.value.labels` | ✅ resolves |
| c | no | module output | `each.value.labels` | ❌ marker |
| d | no module at all | module output | `each.value.labels` | ❌ marker |
| e | no | module output | `var.keys.storage.labels` (direct, no `each`) | ✅ resolves |
So: a module output dereferenced via `each.value` fails; the same module output dereferenced directly succeeds; and a literal value dereferenced via `each.value` succeeds. The combination of the two is what breaks.
### Impact
The resource is present and evaluated, but one attribute value is wrong, so policies checking that attribute produce **false positives** on correct configuration. Filtering the `__attribute__` marker in the consumer is possible but blunt — the marker is identical whether the underlying value is compliant or genuinely absent, so guarding it trades false positives for false negatives.
### Version
tfparse 0.6.20 (latest on PyPI at time of filing), reproduced both via `tfparse.load_from_path()` directly and through c7n-left 0.3.38.
### Possibly related
- #185 (handling module inputs from other module outputs) and #194 (its nested-module follow-up) — this looks like a remaining gap in that same area. The module *input* now resolves correctly; what still fails is the `each.value` dereference of it.
- #256 (missing resource while parsing) — different reference kind (resource-to-resource rather than module output), but also a `for_each` whose collection holds a reference, and also order-sensitive.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by reproducing the configuration from the issue through tfparse.load_from_path(), then trace how the resource for_each collection, each.value binding, and module output are evaluated. Done means the resource expands to google_storage_bucket.x["storage"] and labels resolves to {"name":"a","static":"x"} instead of an __attribute__ marker.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, terraform
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100