Skip to content

test(e2e): e2e k8s environment tests using chainsaw#365

Open
ravisoundar wants to merge 2 commits into
mainfrom
rs-chainsaw
Open

test(e2e): e2e k8s environment tests using chainsaw#365
ravisoundar wants to merge 2 commits into
mainfrom
rs-chainsaw

Conversation

@ravisoundar

Copy link
Copy Markdown
Collaborator

Description

End-2-end tests for the Kubernetes environment using Chainsaw.
Addresses issue #263

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.
  • All commits are signed off per DCO (git commit -s).

@ravisoundar
ravisoundar requested a review from dmitsh as a code owner June 26, 2026 03:52
@copy-pr-bot

copy-pr-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a full Chainsaw-based E2E test suite covering six scenarios across the Kubernetes and Slinky engines, plus a CI workflow, Makefile targets, and documentation updates. Previously raised concerns about image tag sanitization, E2E_IMAGE_TAG variable propagation, and the absence of Chainsaw checksum verification have all been resolved in the current diff.

  • Workflow (e2e.yml): uses git rev-parse --short HEAD (always a valid Docker tag), passes E2E_IMAGE_TAG as a Make variable override (make e2e E2E_IMAGE_TAG="$IMAGE_TAG"), and verifies the Chainsaw tarball via sha256sum before install.
  • Makefile: adds e2e, e2e-local, and kind-load targets; e2e-local propagates the exit status and deletes the cluster in a finally-style ; chain; kind create … || kind get clusters | grep -q is more precise than the old || true.
  • Test suites: six declarative Chainsaw tests exercising label-application, label-truncation, tree-topology, DRA-provider, block-complement, and dynamic-nodes scenarios; all run sequentially (parallel: 1) to avoid global-Node collisions.

Confidence Score: 5/5

Safe to merge; all changes are additive test infrastructure with no modifications to production code paths.

All six Chainsaw suites are additive; the workflow, Makefile targets, and documentation introduce no changes to the production binary. The previously raised concerns about image-tag formatting, variable propagation to chainsaw, and binary checksum verification have each been resolved in this revision. The only observations are non-critical quality nits in the CI workflow itself.

No files require special attention; the e2e.yml workflow has two minor style observations (kind install method and action pinning) but neither affects correctness.

Important Files Changed

Filename Overview
.github/workflows/e2e.yml New CI workflow for Chainsaw E2E on kind; image-tag sanitization and variable-passing issues from previous reviews are fixed; Chainsaw binary now verified with sha256sum; kind is installed via go install @latest (non-pinned, slow from source)
Makefile Adds e2e/e2e-local/kind-load/chainsaw-install targets; IMAGE_TAG now defaults to short commit SHA; image-build hardcodes TARGETOS=linux (previously $(GOOS)); e2e-local properly preserves exit status before cluster teardown
tests/chainsaw/.chainsaw.yaml Global Chainsaw config: parallel: 1 prevents race conditions on shared cluster-scoped Node objects across suites; timeouts are generous but appropriate for Helm deployments
tests/chainsaw/k8s/label-application/chainsaw-test.yaml Node Observer auto-triggers generation via fake nodes; uses native Chainsaw assert with label matching; catch and finally blocks provide good diagnostics and cleanup
tests/chainsaw/k8s/label-truncation/chainsaw-test.yaml Uses a shell polling loop to assert label ≤63 chars and starts with x (FNV64a hash); polling approach is appropriate since native Chainsaw assert can't express length/prefix conditions
tests/chainsaw/slinky/dra-provider/chainsaw-test.yaml Fake nodes carry nvidia.com/gpu.clique labels for DRA discovery; slurmd pods status-patched to Ready; asserts correct two-clique BlockName/BlockSizes in slurm-topology ConfigMap
tests/chainsaw/slinky/block-complement/chainsaw-test.yaml Three-clique model with two absent nodes; asserts complement placeholder BlockName=block004 is added when only 3 of 4 base-block slots are filled; namespace injected via --set for ConfigMap routing
tests/chainsaw/slinky/dynamic-nodes/chainsaw-test.yaml Skeleton-only ConfigMap and per-node topology.slinky.slurm.net/spec annotation assertions; "default:block001" is the SLURM partition-qualified block name, not a K8s namespace reference
tests/chainsaw/slinky/tree-topology/chainsaw-test.yaml Pattern C — fires on real kind worker nodes to trigger generation; topology.conf derived entirely from model switch structure so actual cluster node names don't appear in output; no fake nodes or slurmd pods needed
tests/chainsaw/README.md Comprehensive docs for all three patterns, prerequisites, and env var reference; make image-build IMAGE_TAG=my-tag / make kind-load E2E_IMAGE_TAG=my-tag correctly separated — previous doc inaccuracy resolved

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant GH as GitHub Actions / make e2e-local
    participant Kind as kind cluster
    participant Helm as helm upgrade --install
    participant NO as Node Observer
    participant TG as Topograph pod
    participant K8s as Kubernetes API

    GH->>Kind: kind create cluster (kind-config.yaml)
    GH->>GH: make image-build (short-SHA tag)
    GH->>Kind: kind load docker-image
    GH->>K8s: kubectl apply fake Nodes + ConfigMap (topology-model)
    GH->>K8s: kubectl apply fake slurmd Pods (Slinky suites)
    GH->>K8s: kubectl patch pod status to Ready
    GH->>Helm: helm upgrade --install topograph (values.yaml + --set image/ns)
    Helm-->>TG: pod starts (imagePullPolicy: Never)
    Helm-->>NO: node-observer starts
    NO->>K8s: "watch Nodes (kubernetes.io/os=linux)"
    K8s-->>NO: Added events for fake/real nodes
    NO->>TG: POST /v1/generate (modelFileName)
    TG->>TG: "run provider -> build graph -> run engine"
    TG->>K8s: patch Node labels (k8s engine) OR create slurm-topology ConfigMap (Slinky) OR annotate Nodes (dynamic-nodes)
    GH->>K8s: chainsaw assert Node labels / ConfigMap content / Node annotations
    GH->>Helm: helm uninstall (finally block)
    GH->>Kind: kind delete cluster
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant GH as GitHub Actions / make e2e-local
    participant Kind as kind cluster
    participant Helm as helm upgrade --install
    participant NO as Node Observer
    participant TG as Topograph pod
    participant K8s as Kubernetes API

    GH->>Kind: kind create cluster (kind-config.yaml)
    GH->>GH: make image-build (short-SHA tag)
    GH->>Kind: kind load docker-image
    GH->>K8s: kubectl apply fake Nodes + ConfigMap (topology-model)
    GH->>K8s: kubectl apply fake slurmd Pods (Slinky suites)
    GH->>K8s: kubectl patch pod status to Ready
    GH->>Helm: helm upgrade --install topograph (values.yaml + --set image/ns)
    Helm-->>TG: pod starts (imagePullPolicy: Never)
    Helm-->>NO: node-observer starts
    NO->>K8s: "watch Nodes (kubernetes.io/os=linux)"
    K8s-->>NO: Added events for fake/real nodes
    NO->>TG: POST /v1/generate (modelFileName)
    TG->>TG: "run provider -> build graph -> run engine"
    TG->>K8s: patch Node labels (k8s engine) OR create slurm-topology ConfigMap (Slinky) OR annotate Nodes (dynamic-nodes)
    GH->>K8s: chainsaw assert Node labels / ConfigMap content / Node annotations
    GH->>Helm: helm uninstall (finally block)
    GH->>Kind: kind delete cluster
Loading

Reviews (10): Last reviewed commit: "Incorporated review comments" | Re-trigger Greptile

@ravisoundar

Copy link
Copy Markdown
Collaborator Author

/ok-to-test 9fee2e8

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.36%. Comparing base (1875ab8) to head (5a07060).
⚠️ Report is 86 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #365      +/-   ##
==========================================
+ Coverage   68.46%   72.36%   +3.90%     
==========================================
  Files          82       86       +4     
  Lines        4842     5312     +470     
==========================================
+ Hits         3315     3844     +529     
+ Misses       1395     1278     -117     
- Partials      132      190      +58     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ravisoundar
ravisoundar force-pushed the rs-chainsaw branch 2 times, most recently from 31d1489 to 91eea39 Compare June 26, 2026 04:45
@ravisoundar

Copy link
Copy Markdown
Collaborator Author

/ok-to-test 91eea39

@github-actions

Copy link
Copy Markdown
Contributor

@ravisoundar

Copy link
Copy Markdown
Collaborator Author

/ok-to-test a10bc7d

@ravisoundar
ravisoundar force-pushed the rs-chainsaw branch 2 times, most recently from b7ac4d8 to 5a07060 Compare June 27, 2026 01:08
@ravisoundar

Copy link
Copy Markdown
Collaborator Author

/ok-to-test 5a07060

@ArangoGutierrez ArangoGutierrez left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Found 5 issues.

  1. Every suite's values.yaml disables the node-data-broker DaemonSet but leaves node-observer.topograph.nodeDataBroker.enabled at its default true, so the observer's allBrokerPodsReady() waits forever for broker pods that never exist and /v1/generate is never triggered - all six suites should time out on their assert steps. Per the node-data-broker subchart's values comment, also set node-observer.topograph.nodeDataBroker.enabled: false (same fix in the other five values.yaml files). (tests/chainsaw/k8s/label-application/values.yaml:19)
  2. Chainsaw only auto-discovers a file named .chainsaw.yaml, and neither the e2e/e2e-local Makefile targets nor the workflow pass --config, so this file - including parallel: 1 - is silently ignored. With default parallelism the six suites race: all use the fixed release name topograph, so the release-scoped ClusterRole/ClusterRoleBinding names collide and each suite's pre-install kubectl delete clusterrole tears down RBAC another suite owns. Renaming to .chainsaw.yaml or passing --config fixes it. (tests/chainsaw/chainsaw-config.yaml:14)
  3. kind get kubeconfig prints kubeconfig content to stdout, not a path, so this sets KUBECONFIG to inline YAML and every kubectl/helm/chainsaw call in e2e-local fails to find a config. Write it to a temp file (or use kind export kubeconfig) and point KUBECONFIG at that. (Makefile:152)
  4. go.mod requires Go 1.25.11, and the four pins in go.yml were deliberately bumped from 1.25.9 to 1.25.11 (cb0362e) to clear reachable stdlib vulnerabilities - this reintroduces the pre-fix toolchain. Pin 1.25.11 here too. (.github/workflows/e2e.yml:32)
  5. This row claims label-application verifies accelerator labels, but the suite's topology model has no NVLink data and its assert step only checks leaf and spine (tests/chainsaw/README.md lists it correctly). The table also omits the slinky/block-complement and slinky/dynamic-nodes suites shipped in this PR. (docs/engines/k8s.md:357)

kubernetes.io/os: linux

node-data-broker:
enabled: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Every suite's values.yaml disables the node-data-broker DaemonSet but leaves node-observer.topograph.nodeDataBroker.enabled at its default true, so the observer's allBrokerPodsReady() waits forever for broker pods that never exist and /v1/generate is never triggered - all six suites should time out on their assert steps. Per the node-data-broker subchart's values comment, also set node-observer.topograph.nodeDataBroker.enabled: false (same fix in the other five values.yaml files).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

With the new chart changes node-observer.topograph.nodeDataBroker.enabled is not required.

error: 30s
exec: 120s
fullName: true
parallel: 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Chainsaw only auto-discovers a file named .chainsaw.yaml, and neither the e2e/e2e-local Makefile targets nor the workflow pass --config, so this file - including parallel: 1 - is silently ignored. With default parallelism the six suites race: all use the fixed release name topograph, so the release-scoped ClusterRole/ClusterRoleBinding names collide and each suite's pre-install kubectl delete clusterrole tears down RBAC another suite owns. Renaming to .chainsaw.yaml or passing --config fixes it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Renamed it to .chainsaw.yaml now

Comment thread Makefile Outdated
--config tests/chainsaw/kind-config.yaml --wait 120s \
|| kind get clusters | grep -q "^$(KIND_CLUSTER)$$"
kind load docker-image $(IMAGE_REPO):$(E2E_IMAGE_TAG) --name $(KIND_CLUSTER)
KUBECONFIG="$$(kind get kubeconfig --name $(KIND_CLUSTER))" \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

kind get kubeconfig prints kubeconfig content to stdout, not a path, so this sets KUBECONFIG to inline YAML and every kubectl/helm/chainsaw call in e2e-local fails to find a config. Write it to a temp file (or use kind export kubeconfig) and point KUBECONFIG at that.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed it now.

Comment thread .github/workflows/e2e.yml Outdated
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.25.9'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

go.mod requires Go 1.25.11, and the four pins in go.yml were deliberately bumped from 1.25.9 to 1.25.11 (cb0362e) to clear reachable stdlib vulnerabilities - this reintroduces the pre-fix toolchain. Pin 1.25.11 here too.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed it now.

Comment thread docs/engines/k8s.md Outdated

| Suite | What it checks |
|---|---|
| `k8s/label-application` | `leaf`, `spine`, and `accelerator` labels applied to nodes after generation |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This row claims label-application verifies accelerator labels, but the suite's topology model has no NVLink data and its assert step only checks leaf and spine (tests/chainsaw/README.md lists it correctly). The table also omits the slinky/block-complement and slinky/dynamic-nodes suites shipped in this PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed it now.

Signed-off-by: Ravi Shankar <ravish@nvidia.com>
@ravisoundar

Copy link
Copy Markdown
Collaborator Author

/ok-to-test 9e38d2c

Signed-off-by: Ravi Shankar <ravish@nvidia.com>

@ArangoGutierrez ArangoGutierrez left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Found 6 issues.

  1. This pin went stale mid-review: #395 has since bumped the repo to Go 1.26.5 (go.mod and all four pins in go.yml). On runners where toolchain auto-fetch is restricted this fails outright; everywhere else it silently downloads 1.26.5 anyway, so best to align it. My earlier 1.25.11 suggestion predates the #395 bump. (.github/workflows/e2e.yml:32)
  2. This delete (and the same pattern in all six suites, setup and cleanup) removes cluster-scoped objects that share names with a real Topograph deployment's RBAC, against whatever cluster KUBECONFIG currently points at. Since the README documents running make e2e against an arbitrary context, a stale KUBECONFIG would silently strip a live cluster's Topograph RBAC - consider guarding on cluster identity, e.g. only proceed when the current context is the expected kind cluster. (tests/chainsaw/k8s/label-application/chainsaw-test.yaml:80)
  3. This kind load runs as its own recipe line, outside the shell block that captures E2E_STATUS and deletes the cluster, so a load failure aborts the target and orphans the just-created kind cluster. Folding it into the guarded block keeps the teardown promise. (Makefile:151)
  4. kind@latest floats, so a new kind release can change cluster behavior between runs with no way to pin or bisect. Worth the same treatment as the chainsaw install below: a pinned, overridable version. (.github/workflows/e2e.yml:35)
  5. This comment points users at TOPOGRAPH_IMAGE_REPO/TOPOGRAPH_IMAGE_TAG, but the recipe unconditionally overwrites both from $(IMAGE_REPO)/$(E2E_IMAGE_TAG), and the new README explicitly warns against setting them as env vars. Recommending the IMAGE_REPO/E2E_IMAGE_TAG make overrides here would keep the two in agreement. (Makefile:136)
  6. The suite list names node-observer, but no such suite exists under tests/chainsaw/ - the actual suites are k8s/{label-application,label-truncation} and slinky/{block-complement,dra-provider,dynamic-nodes,tree-topology}. The synced line in .claude/CLAUDE.md needs the same fix. (AGENTS.md:46)

Comment thread .github/workflows/e2e.yml
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.25.11'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pin went stale mid-review: #395 has since bumped the repo to Go 1.26.5 (go.mod and all four pins in go.yml). On runners where toolchain auto-fetch is restricted this fails outright; everywhere else it silently downloads 1.26.5 anyway, so best to align it. My earlier 1.25.11 suggestion predates the #395 bump.

IMAGE_TAG="${TOPOGRAPH_IMAGE_TAG:-}"
PULL_POLICY="${TOPOGRAPH_IMAGE_PULL_POLICY:-IfNotPresent}"

kubectl delete clusterrole topograph topograph-node-observer \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This delete (and the same pattern in all six suites, setup and cleanup) removes cluster-scoped objects that share names with a real Topograph deployment's RBAC, against whatever cluster KUBECONFIG currently points at. Since the README documents running make e2e against an arbitrary context, a stale KUBECONFIG would silently strip a live cluster's Topograph RBAC - consider guarding on cluster identity, e.g. only proceed when the current context is the expected kind cluster.

Comment thread Makefile
kind create cluster --name $(KIND_CLUSTER) \
--config tests/chainsaw/kind-config.yaml --wait 120s \
|| kind get clusters | grep -q "^$(KIND_CLUSTER)$$"
kind load docker-image $(IMAGE_REPO):$(E2E_IMAGE_TAG) --name $(KIND_CLUSTER)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This kind load runs as its own recipe line, outside the shell block that captures E2E_STATUS and deletes the cluster, so a load failure aborts the target and orphans the just-created kind cluster. Folding it into the guarded block keeps the teardown promise.

Comment thread .github/workflows/e2e.yml
go-version: '1.25.11'

- name: Install kind
run: go install sigs.k8s.io/kind@latest

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

kind@latest floats, so a new kind release can change cluster behavior between runs with no way to pin or bisect. Worth the same treatment as the chainsaw install below: a pinned, overridable version.

Comment thread Makefile
kind load docker-image $(IMAGE_REPO):$(E2E_IMAGE_TAG) --name $(KIND_CLUSTER)

# Run all Chainsaw E2E suites against the current KUBECONFIG context.
# For a pre-pushed registry image: set TOPOGRAPH_IMAGE_REPO and TOPOGRAPH_IMAGE_TAG.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This comment points users at TOPOGRAPH_IMAGE_REPO/TOPOGRAPH_IMAGE_TAG, but the recipe unconditionally overwrites both from $(IMAGE_REPO)/$(E2E_IMAGE_TAG), and the new README explicitly warns against setting them as env vars. Recommending the IMAGE_REPO/E2E_IMAGE_TAG make overrides here would keep the two in agreement.

Comment thread AGENTS.md
CHANGELOG.md # Release history (Keep a Changelog format); update [Unreleased] for user-facing PRs
docs/ # Public-facing docs — overview.md, architecture.md, api.md + providers/, engines/, reference/ subdirectories
tests/models/ # YAML simulation fixtures
tests/chainsaw/ # Chainsaw E2E test suites (label-application, label-truncation, node-observer, slinky)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The suite list names node-observer, but no such suite exists under tests/chainsaw/ - the actual suites are k8s/{label-application,label-truncation} and slinky/{block-complement,dra-provider,dynamic-nodes,tree-topology}. The synced line in .claude/CLAUDE.md needs the same fix.

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.

2 participants