cloud-custodian / cloud-custodian/tfparse

each.value.<attr> unresolved when the for_each collection contains a module output reference

Open
#283 0 comments 0 reactions 0 assignees View on GitHub
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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.