Task · SKT-0012.02

skcapture: read real node pool identity instead of synthesising groups per instance type

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

Definition of Done

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.

View the source file on GitHub