Skip to content

[test/otel/pernode] Add per-node + zero-step CRD E2E suite and EKS harness#720

Open
wenegiemepraise wants to merge 13 commits into
aws:mainfrom
wenegiemepraise:pernode-e2e
Open

[test/otel/pernode] Add per-node + zero-step CRD E2E suite and EKS harness#720
wenegiemepraise wants to merge 13 commits into
aws:mainfrom
wenegiemepraise:pernode-e2e

Conversation

@wenegiemepraise

Copy link
Copy Markdown

Summary

Add the test/otel/pernode integration suite plus a Terraform EKS harness that validates
the ServiceMonitor/PodMonitor scraping path end to end on a live cluster: per-node target
locality, zero-step CRD bundling, and Target Allocator resilience to missing CRDs.

What's included

Suite (test/otel/pernode/, build tag integration):

  • per_node_test.go — every SM/PM series is node-local: target_node == @resource.k8s.node.name; targets span ≥2 nodes.
  • crd_bundling_test.go — SM + PM CRDs are served/established after a plain chart install (harness never installs them separately).
  • ta_resilience_test.go — TA Deployment Available, no CrashLoopBackOff, 0 restarts on a fresh cluster; and it actually discovers monitors.
  • setup_test.go, k8s_helpers_test.go, metrics_test.go — shared clientset/ground-truth, CRD discovery, otelmetrics client bootstrap.
  • resources/workload.yaml — sm-app/pm-app workloads + SM/PM with the target_node relabel + load generator.

Harness (terraform/eks/daemon/otel-pernode/): provisions EKS, installs the chart with the
per-node-capable operator/TA images, applies the workloads, and runs the suite. Because it
exercises unreleased code it takes operator_image_*, ta_image, helm_chart_repo/branch vars.

Verification method (per-node)

target_node             = scraped POD's node   (SM/PM relabel from __meta_kubernetes_pod_node_name)
@resource.k8s.node.name = scraping AGENT's node (from ${env:K8S_NODE_NAME})
per-node holds  ⇔  for every series, target_node == @resource.k8s.node.name

Dependencies / notes

  • Validates the feature PRs (operator per-node + CRD resilience, helm bundling) — most useful
    once those and their custom images are available; tests are skip-guarded so they don't break
    the broader suite where fixtures are absent.
  • go vet -tags integration ./test/otel/pernode/... is clean against current main.

Add an integration suite (test/otel/pernode) and Terraform harness
(terraform/eks/daemon/otel-pernode) that validate the Target Allocator
per-node allocation strategy end to end, plus the zero-step
ServiceMonitor/PodMonitor CRD bundling (G1) and TA resilience to missing
CRDs (G2).

Suite:
- per_node_test.go asserts every scraped series is collected by the agent on
  the scraped pod's own node (target_node == @resource.k8s.node.name) and that
  the workload spans >= 2 nodes.
- crd_bundling_test.go asserts the SM/PM CRDs are served via discovery after a
  plain chart install (no prometheus-operator prerequisite).
- ta_resilience_test.go asserts the TA Deployment is Available with zero
  container restarts and that it discovers the monitors once the CRDs exist.
- resources/workload.yaml: sm-app/pm-app behind a ServiceMonitor and PodMonitor
  with a target_node relabel, plus a load generator.

Harness installs a helm-charts checkout that bundles the CRDs (no separate CRD
install step, by design), deploys custom operator and Target Allocator images
carrying the per-node + CRD-watch code, forces the per-node strategy on the CR,
applies the workload, and runs the suite.
Validated the pernode suite against a live per-node cluster:
- Select Target Allocator pods by app.kubernetes.io/name=cloudwatch-agent-
  target-allocator (the operator labels component as
  amazon-cloudwatch-agent-target-allocator, so the previous selector matched
  nothing).
- Assert the TA is currently Ready/Running and not in CrashLoopBackOff instead
  of requiring a lifetime restart count of 0. The readiness/crashloop check
  still catches a TA that died on a missing CRD, but is portable across reruns
  on long-lived clusters; the lifetime restart count is now logged
  informationally (expected 0 only on a freshly provisioned harness cluster).
- Add local_chart_path var to install the chart from a local checkout
  (skipping the git clone) so unpushed working-tree changes can be exercised.
- Raise the helm_release timeout to 900s for the multi-deployment fresh
  install (operator, agent DaemonSet, Target Allocator, webhook).

// Informational: on a freshly provisioned cluster (the otel-pernode harness)
// this should be 0, evidencing the TA never restarted to pick up the CRDs.
t.Logf("Target Allocator lifetime container restarts: %d (expected 0 on a fresh harness cluster)", totalRestarts(pods))

@musa-asad musa-asad Jul 21, 2026

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 harness installs the chart with CRDs already bundled and then rollout restarts the target-allocator, so this never opens the missing CRD window it's meant to test, and the restart zeroes the one signal (lifetime restart count). Could we relabel it a smoke test on a bundled install rather than CRD resilience?

Comment thread test/otel/pernode/crd_bundling_test.go Outdated
// ServiceMonitor and PodMonitor CRDs are present and established (served by the
// API server) with no manual prerequisite. The otel-pernode harness installs the
// chart onto a cluster that does NOT pre-install these CRDs, so their presence
// here is attributable to the chart's bundling.

@musa-asad musa-asad Jul 21, 2026

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.

crdServed only checks the CRDs are served, not that this chart installed them, so a rerun or a preexisting prometheus-operator passes even when the chart bundles nothing. Could we assert the CRD carries this release's app.kubernetes.io/managed-by=Helm and treat CRD absence as a precondition?

topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway

@musa-asad musa-asad Jul 21, 2026

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 workload only soft prefers spreading (ScheduleAnyway), but TestPerNodeCoverageAcrossNodes needs two target_nodes, so both replicas can land on one node and flake it. Could we force the spread with DoNotSchedule or a kubernetes.io/hostname podAntiAffinity? Same for pm-app at line 94.


var results []otelmetrics.MetricResult
var usedMetric string
for _, m := range perNodeMetrics {

@musa-asad musa-asad Jul 21, 2026

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 callers break on the first perNodeMetrics, and sm-app/pm-app share metric names, so one path passing greens the test while the other is broken, and queryWorkloadMetric never checks the test context so it can hang. Could we filter on app/job for one series per path and thread a context.Context through?


variable "k8s_version" {
type = string
default = "1.35"

@musa-asad musa-asad Jul 21, 2026

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.

A few defaults are footguns: k8s_version 1.35 may be ahead of what EKS offers in a region (so a default apply fails), and the helm_chart_* defaults point at upstream main. Could we pin a GA version and a real chart ref, and drop the dev registry path in operator_image_repo, before this goes public?

data "external" "clone_helm_chart" {
count = var.local_chart_path == "" ? 1 : 0
program = ["bash", "-c", <<-EOT
rm -rf ./helm-charts

@musa-asad musa-asad Jul 21, 2026

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.

data.external.clone_helm_chart runs rm -rf then git clone on every plan, which is a side effect in a data source and can wipe a local checkout in the cwd. Could we move it to a null_resource with a triggers guard that clones into path.module behind a [ -d ] check instead?

provisioner "local-exec" {
command = <<-EOT
set -e
sleep 30

@musa-asad musa-asad Jul 21, 2026

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.

null_resource.patch_cr waits on fixed sleeps instead of readiness (same with validator and the workload apply racing CRD creation), so on a slow cluster kubectl patch fails not found before the CR exists. Could we switch to kubectl wait on the CR and Established CRDs plus kubectl rollout status?

return groundTruth
}

func buildGroundTruth() (*k8sGroundTruth, error) {

@musa-asad musa-asad Jul 21, 2026

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 groundTruth and TestMain helpers here duplicate the standard suite, so any fallback fix has to land twice, and buildGroundTruth's pod List and nodeNames() are dead code. Could we pull the shared helpers into a common package, drop the dead code, and collapse crdServed's redundant not found branch?

TestTargetAllocatorHealthy claimed to verify G2 CRD-install-ordering resilience,
but the harness bundles the CRDs at install so the missing-CRD window is never
opened, and a rollout restart would zero the lifetime restart signal. Rename to
TestTargetAllocatorHealthyOnBundledInstall and reword the assertions/comments to
describe what it actually checks (TA healthy on a bundled install). The missing-
CRD tolerance itself is covered by the operator's Target Allocator unit tests.
crdServed only checked the CRDs were served, so a rerun or a pre-existing
prometheus-operator passed even when the chart bundled nothing. Fetch the CRD
objects via a dynamic client and assert app.kubernetes.io/managed-by=Helm and
meta.helm.sh/release-namespace == the release namespace, so only CRDs this chart
release installed count. Treat CRD absence as a precondition failure. Drop the
now-unused discovery-based crdServed helper.
sm-app/pm-app used topologySpreadConstraints with whenUnsatisfiable:
ScheduleAnyway (soft), so both replicas could land on one node and flake
TestPerNodeCoverageAcrossNodes, which needs two distinct target_nodes. Switch
both to DoNotSchedule (hard spread on kubernetes.io/hostname) so the two
replicas are guaranteed on separate nodes.
queryWorkloadMetric broke on the first metric and sm-app/pm-app share metric
names, so the ServiceMonitor path passing greened the test while a broken
PodMonitor path went unnoticed; it also used context.Background() and could hang.
Stamp a deterministic app label per monitor (sm-app/pm-app) via relabel, filter
queries by app, validate each path independently (subtests), and thread a
context.Context with a bounded deadline through queryWorkloadMetric.
Pin k8s_version to a GA EKS version (1.35 -> 1.31) so a default apply does not
fail in regions that do not yet offer it; scrub the dev registry path from the
operator_image_repo example; and correct the helm chart-source comment (it does
not default to a fork) to warn that upstream main lacks the feature until the
stack merges and to pin a fixed ref / use local_chart_path.
The helm chart was fetched in a data source that ran rm -rf ./helm-charts then
git clone on every plan -- a side effect in a data source that could wipe a
checkout in the caller's cwd. Replace with a null_resource that clones into
path.module behind a git-dir guard, triggered only when the repo/branch changes
(fetch+checkout on change, clone once otherwise). Point chart_path and the
helm_release dependency at it.
patch_cr slept 30s hoping the CR existed and the workload apply raced CRD
creation. Replace with kubectl wait --for=condition=Established on the
AmazonCloudWatchAgent CRD (then poll for the CR) before patching, and on the
bundled ServiceMonitor/PodMonitor CRDs before applying the workload. Drop the
validator's fixed 3-minute propagation sleep -- the tests poll CloudWatch with a
bounded retry. terraform validate passes.
buildGroundTruth listed all pods into a pods map that nothing reads, and
nodeNames() was never called. Remove the pods field, the pod List, and
nodeNames(); ground truth now only holds nodes (all that the per-node checks
use). (crdServed and its redundant not-found branch were already removed when
the CRD-bundling test moved to a Helm-ownership check.)
wenegiemepraise added a commit to wenegiemepraise/amazon-cloudwatch-agent-test that referenced this pull request Jul 24, 2026
Propagate the aws#720 review fixes (bundled-install smoke test, CRD Helm-ownership
assertion, workload DoNotSchedule spread, both-path metric validation + context,
terraform footguns/clone/readiness, dead-code removal) so the routing suite
builds on the updated harness.
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