Description
Found by the SKT-0012 validation run against the EKS lab cluster, 2026-08-27.
internal/capture/k8s.go:394 keys node groups on {instanceType, provisioner, os} and :415 names a group <instanceType>-<provisioner> whenever no eks.amazonaws.com/nodegroup label is present. Karpenter nodes carry no such label, so every Karpenter-provisioned node collapses into an instance-type bucket and the real NodePool identity is discarded.
Observed on the lab cluster: three distinct Karpenter NodePools with different roles and sizes, plus one EKS managed nodegroup. The capture emitted four groups named after instance types. karpenter.sh/nodepool is present on every Karpenter node and is never read.
Separately and in the same code path: the one genuinely EKS-managed nodegroup was emitted with provisioner: karpenter. Its nodes carry eks.amazonaws.com/nodegroup and no karpenter.sh/nodepool, so the provisioner attribution is simply wrong for it — a node that is not Karpenter-provisioned is being described as though it were.
Consequence: the forged blueprint models a node topology the cluster does not have. Node group identity feeds node-scoped metric labels, so this propagates into the emitted data rather than staying a cosmetic naming difference.
Acceptance Criteria
- #1 Karpenter-provisioned nodes group by their real NodePool, read from karpenter.sh/nodepool
- #2 EKS managed nodegroups continue to group by eks.amazonaws.com/nodegroup
- #3 The provisioner recorded for a group reflects how those nodes are actually provisioned
- #4 A cluster mixing Karpenter and managed nodegroups produces both kinds correctly in one capture
- #5 Verified against the lab cluster: captured groups match the real NodePools and managed nodegroup, by name and node count
Definition of Done
- #1 make gate (build vet test race rw-proto-check spdx-check forbidden-words)
- #2 make blueprint-schema (only if a blueprint field or construct/workload config struct changed)
- #3 DRY_RUN=true go run ./cmd/synthkit -once -dump — inventory diffed against signals/
Implementation Notes
FIXED 2026-08-27 (lane L10), each with a failing test first and a live before/after against the EKS lab cluster.
nodePoolIdentity reads the pool off the node own labels and groupNodes now keys on {poolName, provisioner, os} instead of {instanceType, provisioner, os}.
ROOT CAUSE OF THE WRONG PROVISIONER, and it is not what it looked like: the old test was a PREFIX match on the whole karpenter.sh/ family. The EKS-managed nodes carry karpenter.sh/controller — the nodeSelector label marking where the Karpenter controller itself is scheduled — and no karpenter.sh/nodepool. So a prefix test over that family mislabels a managed nodegroup as Karpenter-provisioned. The fix reads the two specific pool labels, EKS first, since a node in a managed nodegroup is managed by definition.
A node declaring neither label is now provisioner “unknown” rather than “managed” — the capture no longer claims a k3d node is an EKS managed nodegroup.
Mixed-type pools handled rather than flattened: one lab pool genuinely runs two instance types at once, so InstanceType is now the dominant type with a stable tie-break and a new InstanceTypes field carries the full observed set.
LIVE: captured groups match a kubectl ground truth taken immediately before the capture, exactly, on name, provisioner, count and instance types.
CORRECTION TO THIS TASK OWN GROUND TRUTH, worth keeping for whoever re-verifies: the node counts recorded when this task was filed were true at that moment only. The gh-runner pools are GitHub Actions runners and Karpenter scaled them during the session — 10 to 9 to 7 to 6 nodes across runs. Re-verification must read kubectl get nodes and capture BACK-TO-BACK; comparing against a ground truth taken minutes earlier shows a spurious diff on those pools.
Final Summary
Node groups now key on real pool identity rather than instance type, so distinct Karpenter NodePools no longer collapse into instance-type buckets.
The provisioner bug root cause was not what it looked like: the old test was a PREFIX match on the whole karpenter.sh/ family, and EKS-managed nodes carry karpenter.sh/controller — the nodeSelector marking where the Karpenter controller is scheduled — with no karpenter.sh/nodepool. Reading the two specific pool labels fixes it, EKS first since a node in a managed nodegroup is managed by definition. A node with neither label is now unknown rather than managed, so a k3d node is no longer reported as an EKS managed nodegroup.
Mixed-type pools are handled rather than flattened: one lab pool genuinely runs two instance types, so the dominant type is reported with a stable tie-break and a new field carries the full observed set.
Verified against a kubectl ground truth taken immediately before the capture: exact match on name, provisioner, count and instance types. Recorded for future re-verification that the lab GitHub Actions runner pools scale under Karpenter — 10 to 6 nodes across one session — so a ground truth taken minutes earlier shows a spurious diff.