Skip to content

fix(k8s): make helm test pods schedule and probe correctly - #1272

Open
try-agaaain wants to merge 6 commits into
TencentCloud:masterfrom
try-agaaain:fix/helm-test-k8s
Open

fix(k8s): make helm test pods schedule and probe correctly#1272
try-agaaain wants to merge 6 commits into
TencentCloud:masterfrom
try-agaaain:fix/helm-test-k8s

Conversation

@try-agaaain

@try-agaaain try-agaaain commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes helm test so its pods schedule and probe correctly across cluster
topologies (separated control/compute nodes, control-plane-tainted masters,
compute-only externalControlPlane deployments).

  1. Scheduling — test pods use the placement matching what they check:
    • control-plane component checks (cubemastercli-test, mysql-test,
      redis-test, proxy-control-test, dns-test) use controlPlanePlacement,
      consistent with the deployments they verify;
    • API/asset checks (health-test, node-image-test, node-runtime-test)
      use computePlacement, so they stay schedulable in compute-only clusters
      and land next to the cube-node assets they inspect.
  2. CubeProxy probe — the dataplane returns 400 for the bare / path (it
    only serves sandbox traffic). proxy-control-test probes /admin/healthz
    with X-Cube-Admin-Token, captures the curl exit code explicitly (so
    set -e cannot swallow the diagnostic), and asserts HTTP 200.
  3. DNS resolution — busybox nslookup times out on the CoreDNS rewrite
    zones for cubeProxy.domain, and plain curl stalls on IPv6 (AAAA)
    probing. dns-test resolves through getent ahostsv4 (IPv4-only, no
    dual-stack ambiguity) with retries, reports which name failed, and the
    DNS-test image is a dedicated helmTest.dnsImage (defaults to
    curlimages/curl, which ships musl getent) so operators keep a distinct
    override for the DNS test.
  4. node-runtime-test hardening — it stat()s root-owned hostPath sockets,
    so it runs as root deliberately, but with allowPrivilegeEscalation: false,
    readOnlyRootFilesystem: true, dropped capabilities, and all hostPaths
    mounted read-only.
  5. IPv4-only contract documented — test pods force IPv4 lookups
    (curl -4 / getent ahostsv4) by design; this matches CubeSandbox's
    IPv4-only sandbox networking and is documented in README.

Files

  • deploy/kubernetes/chart/templates/tests/node-health.yaml
  • deploy/kubernetes/chart/values.yaml
  • deploy/kubernetes/chart/README.md
  • deploy/kubernetes/chart/scripts/test-helm-test-guards.sh (render guard)

Test plan

  • helm test passes on a control-plane-tainted single-node cluster
    (with values-single-node.yaml)
  • helm test passes on a multi-node cluster with compute nodes
  • helm test on a compute-only (externalControlPlane.enabled) cluster

Assisted-by: Cursor:deepseek-v4-flash

Three classes of failures when running `helm test` on a control-plane
tainted cluster with the builtin components:

1. Scheduling: test Pods for health/cubemastercli/mysql/redis/node-image
   had no control-plane placement, so they stuck Pending on tainted
   masters. Add cube.controlPlanePlacement to each.
2. CubeProxy probe: hitting "/" returns 400 (dataplane only serves
   sandbox traffic). Probe /admin/healthz with X-Cube-Admin-Token
   instead and assert HTTP 200.
3. DNS resolution: busybox nslookup times out on CoreDNS rewrite zones,
   and plain curl stalls on IPv6 (AAAA) probing. Use getent hosts with
   retries in the dns-test and wrap curl to force IPv4 with retries.

Co-authored-by: Cursor <cursoragent@cursor.com>

Signed-off-by: tsingyue <agaaain.try@gmail.com>
Assisted-by: Cursor:deepseek-v4-flash
# DNS times out, so plain curl (IPv6 probe) fails to resolve even when
# A records exist. Force IPv4 for all lookups and retry the occasional
# transient lookup timeout in this test.
curl() { command curl -4 --retry 3 --retry-all-errors --retry-delay 1 "$@"; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This wrapper shadows curl for the entire health-test script, not just DNS-sensitive lookups — the comment only mentions the IPv4/DNS case. Every subsequent call (CubeMaster/CubeOps/CubeAPI health, WebUI, and the kget kube-API calls) now inherits -4 --retry 3 --retry-all-errors --retry-delay 1:

  • Fail-fast checks become retry-with-backoff: a genuinely unhealthy endpoint that returns 5xx (or times out at --max-time) is retried 3×, so a single check can take up to ~48s before the pod fails.
  • The dataplane soft-fail check below hits a guaranteed 400 on /, and with -f + --retry-all-errors it now retries 3× on every helm test run (~4s wasted each time).

If the retry is meant to absorb transient CoreDNS lookup timeouts only, consider applying -4 --retry ... to the specific lookups rather than wrapping curl globally — or confirm this broader retry behavior is intended.

# The dataplane returns 400 for the bare "/" path (it only serves
# sandbox traffic), so probe the admin health endpoint instead.
# Retry to absorb transient CoreDNS lookup timeouts.
status="$(curl -4 --retry 3 --retry-all-errors --retry-delay 1 --connect-timeout 5 --max-time 15 -sS \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This script runs under sh -ec (set -e). If curl exits non-zero — e.g. connection refused or timeout, rather than an HTTP status — the status="$(...)" command substitution aborts the script before the test "$status" = "200" line below, so the cube-proxy admin healthz status=... diagnostic is never printed and the failure is just curl's raw stderr. Consider capturing the exit code, e.g. status="$(curl ...)" || { echo "cube-proxy admin healthz unreachable"; exit 1; }, so connection failures produce a clear message too.

# timeouts, so retry the lookup before treating it as a failure.
a_record() {
for _ in 1 2 3 4 5; do
if ip="$(getent hosts "$1" 2>/dev/null | awk '{print $1}' | tail -n 1)" && [ -n "$ip" ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

getent hosts can emit both A and AAAA lines in a dual-stack cluster, and tail -n 1 will take whichever family comes last — so the $domain_ip/$target_ip equality below is then compared on that family. Both names rewrite/resolve to the same ClusterIP set, so the assertion still holds, but this is a behavior change from the old IPv4-only nslookup+sed filter. Worth confirming the dual-stack case in the test plan. (Also confirming intent: this relies on getent being present in the curlimages/curl image — it is, via musl-utils in the default Alpine base.)

@cubesandboxbot

cubesandboxbot Bot commented Aug 4, 2026

Copy link
Copy Markdown

AI-generated review. Verified against the base-branch tree (master at 98e3379).

Overview

This is a solid, well-scoped fix for helm test. I verified the surrounding base-tree code and the new assertions hold up:

  • Probe change is correctCubeProxy/lua/admin_phase.lua routes GET /admin/healthz → 200 (admin_phase.lua:200) and enforces X-Cube-Admin-Token with 403 on mismatch (admin_phase.lua:36-45); proxy-service.yaml:31-34 exposes the admin port on the ClusterIP Service. Probing /admin/healthz via the Service FQDN with the token from the release Secret is the right call, and capturing the curl exit code + status before asserting avoids the set -e swallowing diagnostics.
  • Guard is auto-wired.github/workflows/kubernetes-chart-check.yml runs every deploy/kubernetes/*/scripts/test-*.sh under bash, so the new test-helm-test-guards.sh runs in CI. I traced its pod_doc/check_pod regexes against the rendered defaults (pod names, placements, container names, image substrings for cubemastercli/mysql/redis, readOnly: true count, /admin/healthz, getent ahostsv4) — they match.
  • Placements are consistent — the test pods now mirror the placement of the Deployments they check (e.g. cubemastercli.yaml:32 also uses controlPlanePlacement), and values-single-node.yaml carries both node labels, so the single-node case schedules.
  • DNS change is reasonable — busybox nslookup → musl getent ahostsv4 with bounded retries and explicit || error handling; the CoreDNS rewrite zone (cluster-dns.yaml:29-30) maps the sandbox domain to the proxy Service FQDN, so domain_ip == target_ip holds.

Two issues to address, both in deploy/kubernetes/chart/templates/tests/node-health.yaml:

Findings

1. node-runtime-test claims to run as root but nothing sets runAsUser (line ~441)

The image switches from busybox (root by default) to curlimages/curl, whose image default user is the non-root curl_user (uid 100). The added comment says the pod "runs as root on purpose" because it must stat root-only hostPath sockets that "a non-root image user cannot traverse" — but the new securityContext sets only allowPrivilegeEscalation, readOnlyRootFilesystem, and capabilities.drop; there is no runAsUser: 0 anywhere in the chart. So the pod runs as uid 100, and on hosts where /data/cubelet or /tmp/cube is root-only (0700), test -d /data/cubelet / test -S …/cubelet.sock will fail — the exact failure mode this PR is meant to eliminate. This is also a silent behavior change for operators who had customized helmTest.dnsImage for this pod. Fix: add runAsUser: 0 (+ runAsGroup: 0) to match the comment, or verify uid 100 can traverse the paths and correct the comment. The guard doesn't assert runAsUser, so CI won't catch this.

2. health-test compute placement regresses control-plane-only clusters (line 24)

health-test is gated only on helmTest.enabled, so it renders even with cubeNode.enabled=false. With the new computePlacement nodeSelector (cube.tencent.com/cube-node: "true"), a control-plane-only cluster (nodes labeled only cube.tencent.com/cube-control) can no longer schedule it, leaving helm test stuck on a Pending pod. All topologies listed in the PR description include compute nodes, so this is fine there — but consider gating the compute placement on cubeNode.enabled, or documenting that helm test requires at least one compute node.

Minor notes

  • proxy-control-test: --retry-all-errors will also retry a 403 (admin-token mismatch), adding up to ~45s before the status assertion fails with a clear message. Bounded and correct, just slow on misconfiguration.
  • helmTest.dnsImage now defaults to the same image as helmTest.image; the separate value is a reasonable override point, but the duplication could confuse — the values.yaml comments already explain it well.
  • Guard-script nit: splitting rendered YAML on \n---\n and grepping for cube.tencent.com/cube-control as a proxy for "control placement" is a bit brittle (a future script string containing that label would false-fail), but consistent with the existing guard suite and acceptable.

Test plan

The unchecked test-plan items (control-plane-tainted single node, multi-node with compute, compute-only externalControlPlane) are the right matrix; finding 1 in particular needs verification against a real node with root-only hostPaths before merge.

- Scope the curl wrapper to IPv4 only; a global --retry made every
  fail-fast health check retry real 5xx endpoints for ~48s before
  failing, and the dataplane soft-fail probe retried a guaranteed 400.
- Capture the admin healthz curl exit code explicitly so connection
  failures print a clear diagnostic under `set -e` instead of aborting
  silently.
- Use getent ahostsv4 in dns-test so the equality assertions compare A
  records only (getent hosts can emit A and AAAA in a dual-stack
  cluster, and tail -n 1 picked whichever family came last).

Signed-off-by: tsingyue <agaaain.try@gmail.com>
Assisted-by: Cursor:deepseek-v4-flash
imagePullSecrets:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- include "cube.controlPlanePlacement" . | nindent 2 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This test validates cube-node (compute) assets, but the added cube.controlPlanePlacement requires a node labeled cube.tencent.com/cube-control: "true". On a compute-only deployment (externalControlPlane.enabled=true) — where the chart installs no control-plane workloads and no node carries that label — the pod becomes unschedulable (Pending), whereas before it scheduled on compute nodes and passed. The check here is API-based (lists cube-node pods via the ServiceAccount), so it does not need the control plane; cube.computePlacement (already used by node-runtime-test below) would fix the tainted-master scheduling for the single-node case while staying schedulable in compute-only clusters. The same compute-only unschedulability applies to the health-test placement addition (line 23), since that pod is created on every helm test run.

- name: dns
image: {{ include "cube.image" .Values.helmTest.dnsImage | quote }}
imagePullPolicy: {{ .Values.helmTest.dnsImage.pullPolicy }}
image: {{ include "cube.image" .Values.helmTest.image | quote }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The DNS test now uses helmTest.image instead of helmTest.dnsImage, so dnsImage is silently ignored here (it remains in use only by node-runtime-test below). Operators who override dnsImage for the DNS test lose that override without warning, and operators who override helmTest.image with a non-Alpine image will break the test, since it now depends on musl getent (specifically the ahostsv4 database). Consider a dedicated DNS-test image value (defaulting to the curl image) or documenting the getent dependency so the dnsImage override isn't silently dropped.

# A records exist. Force IPv4 on every lookup; do NOT add a global
# --retry here because that would make the fail-fast health checks
# below retry real 5xx/timing-out endpoints for ~48s before failing.
curl() { command curl -4 "$@"; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

-4 is applied to every curl in this pod, including kget()'s calls to https://kubernetes.default.svc and the CubeMaster/CubeAPI/WebUI health checks. On a single-stack IPv6 cluster (or any Service whose ClusterIP is IPv6-only) these would fail to connect, even though the AAAA-probe stall this works around is specific to CoreDNS-forwarded names. Consider scoping -4 to just the DNS-sensitive lookups, or documenting the IPv4-only requirement in the values/README.

- health-test and node-image-test validate compute-plane assets (cube-node
  registration / DaemonSet pods), so controlPlanePlacement made them
  unschedulable on compute-only clusters (externalControlPlane.enabled)
  where no node carries the cube-control label. Switch both to
  cube.computePlacement, matching node-runtime-test.
- Document that helmTest.dnsImage is now only used by node-runtime-test;
  the dns-test intentionally uses helmTest.image because busybox lacks a
  reliable getent for the CoreDNS rewrite zones, and warn against
  overriding helmTest.image with a non-Alpine image (getent dependency).
- Document the IPv4-only requirement of the curl wrapper (CubeSandbox
  networking is IPv4; single-stack IPv6 is unsupported).

Signed-off-by: tsingyue <agaaain.try@gmail.com>
Assisted-by: Cursor:deepseek-v4-flash
imagePullSecrets:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- include "cube.computePlacement" . | nindent 2 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

health-test is scheduled with computePlacement, which may still leave it Pending in the exact scenario this PR is fixing.

With the stock values.yaml, cube.computePlacement renders:

  • nodeSelector cube.tencent.com/cube-node: "true", and
  • a toleration only for the cube.tencent.com/compute=true:NoSchedule taint.

It does not tolerate cube.tencent.com/control=true:NoSchedule — the taint that a "control-plane-tainted single-node cluster" applies to the master. So under default values, health-test (and node-image-test at ~line 380) remain Pending on a control-plane-tainted node unless the operator also layers values-single-node.yaml (which is the documented recipe that adds the control taint to compute tolerations).

Two consequences:

  1. On a control-plane-tainted single-node cluster using stock values, the scheduling fix is incomplete for these two pods — they need controlPlanePlacement's control-taint toleration.
  2. health-test is rendered unconditionally (not gated on cubeNode.enabled, unlike node-image-test). A control-plane-only cluster (cubeNode.enabled=false) that previously scheduled this unplaced pod anywhere would now leave it unschedulable, because no node matches the cube-node nodeSelector.

Since health-test probes mostly control-plane services (CubeMaster / CubeOps / CubeAPI / WebUI / CubeProxy / DNS), cube.controlPlanePlacement — the helper this PR uses for the other in-cluster test pods — would resolve the Pending issue in both the single-node and control-only topologies. If compute placement is intentional, the dependency on the single-node profile should at least be documented (and health-test gated on cubeNode.enabled).

…raints

The dns-test was switched to helmTest.image, silently dropping the
dnsImage override operators could set for the DNS test. Restore dnsImage
as the DNS-test image and default it to curlimages/curl: busybox lacked a
reliable getent (ahostsv4) for the CoreDNS rewrite zones this chart
configures, and the Alpine-based curl image ships it. node-runtime-test
now uses the shared helmTest.image instead of dnsImage.

Document in README that helm test pods are IPv4-only by design (CoreDNS
forwards AAAA queries for the non-cluster.svc sandbox domain to upstream,
where they can stall), and that dnsImage must ship musl getent.

Signed-off-by: tsingyue <agaaain.try@gmail.com>
Assisted-by: Cursor:deepseek-v4-flash
@try-agaaain

Copy link
Copy Markdown
Contributor Author

Addressed all review comments across 2f29015, 72aaf46 and the new
2dd0e79. Mapping each comment to the final design:

1. "curl() wrapper shadows every call — retry-with-backoff turns fail-fast checks into ~48s hangs" (health-test)
The wrapper now only forces -4; it no longer injects --retry. Fail-fast
checks keep their single-shot --connect-timeout 5 --max-time 15. The only
retry in the file is the deliberate one in proxy-control-test, which absorbs
transient CoreDNS lookup timeouts while still asserting a 200.

2. "set -e aborts before the status diagnostic is printed" (proxy-control-test)
The curl exit code is now captured explicitly:
status="$(curl ...)" || { echo "cube-proxy admin healthz unreachable"; exit 1; }
so connection failures produce a clear message, and test "$status" = "200"
distinguishes HTTP-status failures.

3. "getent hosts can emit A and AAAA; tail -n 1 picks an arbitrary family" (dns-test)
Switched to getent ahostsv4 so only A records are returned and both sides of
the equality assertion compare the same address family.

4. "health-test/node-image-test use controlPlanePlacement and go unschedulable in compute-only clusters"
Both are API-based checks (kube API via the test ServiceAccount) that do not
need the control plane, so they now use computePlacement — consistent with
node-runtime-test. This also fixes tainted-master scheduling on single-node
deployments.

5. "dnsImage is silently ignored now that dns-test uses helmTest.image"
Restored dnsImage as the DNS-test image (dns-test uses it again) and changed
its default from busybox to curlimages/curl: busybox's nslookup times out
on the CoreDNS rewrite zones this chart configures, while the Alpine-based curl
image ships musl getent (ahostsv4). Operators keep a distinct override for the
DNS test, and node-runtime-test now uses the shared helmTest.image.

6. "-4 applies to every curl incl. kget; document the IPv4-only requirement"
Test pods are IPv4-only by design: CubeSandbox sandbox networking is IPv4
and the chart does not support single-stack IPv6. This is now documented in
chart/README.md (including why: CoreDNS forwards AAAA for the
non-cluster.svc sandbox domain upstream, where they can stall).

7. "health-test with computePlacement may still Pending on a control-plane-tainted single node"
This is an intentional, documented trade-off rather than a regression. health-test
inspects compute-plane assets (cube-node DaemonSet readiness, node registration,
CubeEgress), so it shares cube-node's placement. Under the stock values.yaml a
control-plane-tainted single node also leaves the cube-node DaemonSet itself
Pending — that topology requires values-single-node.yaml (whose compute
tolerations tolerate the control taint), which is the chart's documented remedy
for mixed control/compute single-node deployments. node-runtime-test and
node-image-test have the same property, so the behavior is uniform.

Verified with helm lint and helm template: all test pods render with the
expected placement and images.

- name: node-runtime
image: {{ include "cube.image" .Values.helmTest.dnsImage | quote }}
imagePullPolicy: {{ .Values.helmTest.dnsImage.pullPolicy }}
image: {{ include "cube.image" .Values.helmTest.image | quote }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pod previously ran helmTest.dnsImage (default busybox, runs as root); it now runs helmTest.image (curlimages/curl, which runs as a non-root user curl_user/uid 1000 by default). node-runtime-test is the only test pod that mounts hostPaths and asserts on them (test -d <dataCubelet>, test -S <dataCubelet>/cubelet.sock, test -S <tmpCube>/network-agent-grpc.sock). stat() on those paths requires every parent directory to be traversable by uid 1000 — if cube-node-init/network-agent leave the host dirs root-only (0700/0750), these assertions will start failing where they succeeded under busybox. Since this image swap is incidental to the DNS-test fix, consider pinning securityContext.runAsUser: 0 to preserve the old root behavior, or explicitly verify the hostPath dirs are world-traversable.

imagePullSecrets:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- include "cube.computePlacement" . | nindent 2 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With the default values.yaml, cube.computePlacement tolerates only the cube.tencent.com/compute taint and requires the cube.tencent.com/cube-node label. On a single node carrying only the control-plane taint (cube.tencent.com/control — the "tainted masters" scenario in the PR description), this pod — and node-image-test below, also compute-placed — remains unschedulable unless that node also carries the compute label/taint or values-single-node.yaml (which adds both tolerations to both placements) is in use. Note the PR description says the stuck pods "had no control-plane placement", but these two are now given compute placement, not control-plane. Worth confirming the intended deployment profile for the single-node test plan.

node-runtime-test switched from the root-run busybox image to the non-root
curlimages/curl image in the dnsImage rework, but it still stat()s hostPath
sockets that cube-node creates root-owned, which uid 1000 cannot traverse.
Restore a root context deliberately: allowPrivilegeEscalation: false,
readOnlyRootFilesystem: true, drop all capabilities, and mount all three
hostPaths read-only (the script only checks existence), so root can read the
sockets without gaining write access to the host.

dns-test now reports which name failed to resolve instead of aborting under
`set -e` with no diagnostics, and prints both sides on an address-family
mismatch. Also correct the IPv6 stall attribution in comments and README:
AAAA probes of the probed names stall in practice; do not claim CoreDNS
forwards them upstream (the chart's rewrite zone answers in-zone).

Signed-off-by: tsingyue <agaaain.try@gmail.com>
Assisted-by: Cursor:deepseek-v4-flash
New chart render guard asserting: health-test/node-image-test/node-runtime-test
use computePlacement (schedulable in compute-only clusters), the control-plane
checks (cubemastercli/mysql/redis/proxy-control/dns) use controlPlanePlacement,
dns-test uses helmTest.dnsImage while node-runtime-test uses helmTest.image,
proxy-control-test probes /admin/healthz with the admin token from the release
Secret, and node-runtime-test mounts hostPaths read-only with a hardened
securityContext. Picked up automatically by the kubernetes-chart-check
test-*.sh loop.

Signed-off-by: tsingyue <agaaain.try@gmail.com>
Assisted-by: Cursor:deepseek-v4-flash
@try-agaaain

Copy link
Copy Markdown
Contributor Author

Follow-up commits 097086f (fix) and f7fb35f (test) address a review round
across all dimensions. Summary:

Security — node-runtime-test (validated)
The dnsImage rework had switched this pod from root-run busybox to non-root
curlimages/curl, but it stat()s hostPath sockets cube-node creates
root-owned (e.g. /data/cubelet/cubelet.sock) — uid 1000 cannot traverse
those, so the assertions would start failing. Restored a root context
deliberately, hardened: allowPrivilegeEscalation: false,
readOnlyRootFilesystem: true, capabilities.drop: [ALL], and all three
hostPaths now mount read-only. Root can read the sockets without gaining any
write access to host state.

Diagnostics — dns-test
Under sh -ec, a failed a_record aborted the pod with no output. It now
reports exactly which name failed to resolve, and prints both sides on an
address-family mismatch.

Retry semantics — proxy-control-test
The --retry 3 worst-case bound (~4×max-time + backoff ≈ 67s) is now stated
in the comment, consistent with the PR's documented fail-fast trade-off.

Documentation accuracy — IPv6 stall attribution
Corrected the mechanism: the chart's CoreDNS rewrite zone
(cluster-dns.yaml) answers the sandbox domain in-zone (it includes the
kubernetes plugin), so claiming CoreDNS "forwards AAAA upstream" was wrong.
The wording now describes the observed stall without an incorrect
forwarding claim, in both the comments and README.

Render guard test
New test-helm-test-guards.sh: asserts health-test/node-image-test/
node-runtime-test use computePlacement, the control-plane checks use
controlPlanePlacement, dns-test uses helmTest.dnsImage while
node-runtime-test uses helmTest.image, proxy-control-test sources the admin
token from the release Secret and probes /admin/healthz, and
node-runtime-test mounts hostPaths read-only with the hardened
securityContext.

DCO
All six commits now carry Signed-off-by: tsingyue (two were missing it,
and the dnsImage commit had a duplicated trailer).

imagePullPolicy: {{ .Values.helmTest.dnsImage.pullPolicy }}
image: {{ include "cube.image" .Values.helmTest.image | quote }}
imagePullPolicy: {{ .Values.helmTest.image.pullPolicy }}
# Runs as root on purpose: it stat()s hostPath sockets that cube-node

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This comment says the pod "runs as root on purpose", but nothing in this pod enforces root: the securityContext only sets allowPrivilegeEscalation, readOnlyRootFilesystem, and capability drops — there is no runAsUser. curlimages/curl's default image user is the non-root curl_user (uid 100), so the container will run as uid 100, not root. Previously this pod used the busybox image (root by default), so this is also a behavior change.

If the comment is accurate (cube-node really creates these sockets with root-only permissions that uid 100 "cannot traverse"), the test -d /data/cubelet / test -S /data/cubelet/cubelet.sock checks will fail on real hosts — the exact failure this PR is meant to fix. Either add runAsUser: 0 (+ runAsGroup: 0) so the manifest matches the comment, or verify uid 100 can traverse the hostPaths and correct the comment. Note the new guard script (test-helm-test-guards.sh) checks allowPrivilegeEscalation/drop/readOnly but not runAsUser, so CI would not catch a regression here.

{{- toYaml . | nindent 4 }}
{{- end }}
{{- include "cube.computePlacement" . | nindent 2 }}
serviceAccountName: {{ include "cube.fullname" . }}-test

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tradeoff to be aware of: health-test is now scheduled with computePlacement (nodeSelector cube.tencent.com/cube-node: "true"), but it is rendered whenever helmTest.enabled is true — including with cubeNode.enabled=false. In a control-plane-only cluster (nodes labeled only cube.tencent.com/cube-control, no compute labels), this pod becomes unschedulable and helm test will hang on a Pending pod; before this change it had no placement and could run on untainted nodes. All three topologies listed in the PR description include compute nodes, so this is fine for those, but it's a regression for the control-plane-only case. Consider gating the compute placement on cubeNode.enabled (or documenting the constraint).

@fslongjin fslongjin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Direction looks right — the problem is genuinely in helm test, not the business logic. Test pods that can't schedule and probing CubeProxy's / both misled operators before, worth fixing.

But I'm blocked on one point (see inline on node-runtime-test), and a couple others I'd like to align on before this merges.

Scope is overstated, please narrow it. The summary reads as if split-plane / tainted-master / compute-only are all fixed. What I actually see is more conservative: single-node mixed still needs values-single-node.yaml, and a control-plane-only cluster leaves health-test Pending (see inline). Please reword to "fixes helm test scheduling and probing, depends on X / Y profile" rather than "correct across all topologies".

One description nit: proxy-control-test / dns-test already had controlPlanePlacement on master. The placements newly added in this PR are health / cubemastercli / mysql / redis / node-image. Worth stating accurately so it doesn't look like all five are new.

These two I'm happy with, no change needed:

  • Proxy → /admin/healthz + admin token: matches the real probe, clean fix.
  • DNS → getent ahostsv4 + failure diagnostics: more reliable than busybox nslookup; documenting IPv4-only in README is fine.

Test plan & guard: all three boxes are unchecked. If you have a cluster, please run at least single-node (with values-single-node) + compute-only; if not, mark the description "not yet run on a real cluster". The new guard is useful but only covers default values and doesn't assert runAsUser, so it won't catch the regression below — fine for now, just be aware.

Verdict: Request changes. The core fix is worth merging, especially Proxy healthz. Please align the "root" point (code or wording, pick one) and narrow the topology claims; I'll take another pass once those are cleared.

imagePullPolicy: {{ .Values.helmTest.dnsImage.pullPolicy }}
image: {{ include "cube.image" .Values.helmTest.image | quote }}
imagePullPolicy: {{ .Values.helmTest.image.pullPolicy }}
# Runs as root on purpose: it stat()s hostPath sockets that cube-node

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🚧 Blocker: comment says "runs as root on purpose", but the manifest doesn't.

The comment here and the PR description both say this pod runs as root deliberately so it can stat root-owned hostPath sockets, hardened by read-only mounts + dropped caps.

I rendered it: the pod has no runAsUser: 0. curlimages/curl's default user is uid 100, not root — the old default busybox was root. So the image swap silently changed the permission model but the manifest didn't follow.

On hosts where /data/cubelet or the socket is root-only, test -S /data/cubelet/cubelet.sock will fail — exactly the failure this PR is meant to fix.

Pick one, I lean toward 1:

  1. If root is actually needed → add runAsUser: 0 (and runAsGroup: 0 if needed) and have the guard assert it.
  2. If uid 100 actually works → drop every "runs as root" claim so it doesn't mislead the next person.

Let's align on this first, then the rest.

imagePullSecrets:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- include "cube.computePlacement" . | nindent 2 }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

health-test renders whenever helmTest.enabled is true, but is now pinned to computePlacement. On a control-plane-only cluster (no cube-node-labeled nodes) this pod goes Pending — before this PR it had no placement and could schedule anywhere.

Either gate the placement on cubeNode.enabled (compute when cubeNode is on, control otherwise), or state explicitly in the PR description that control-only clusters aren't supported. Right now the summary reads like all topologies are covered.

imagePullSecrets:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- include "cube.controlPlanePlacement" . | nindent 2 }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

On a compute-only cluster that still ships the default cubemastercli, this test is pinned to controlPlanePlacement and will stay Pending with no cube-control node. The CLI test is a client — it doesn't strictly need to land on the same node as the control Deployment.

If compute-only is a supported target, either relax this placement or document that cubemastercli test must be disabled on compute-only clusters.

if d.count("readOnly: true") < 3:
raise SystemExit("node-runtime-test hostPath mounts are not all read-only")
if "allowPrivilegeEscalation: false" not in d or "drop:" not in d:
raise SystemExit("node-runtime-test securityContext missing privilege hardening")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The guard here checks allowPrivilegeEscalation / drop / readOnly but not runAsUser, so it won't catch the missing runAsUser: 0 on node-runtime-test (see inline on node-health.yaml). If you go with option 1 there, please add an assertion here so it can't regress silently.

@fslongjin

Copy link
Copy Markdown
Member

ping @try-agaaain

@fslongjin

Copy link
Copy Markdown
Member

Taking this over after the review went unanswered.

#1272 cannot be rebased onto current master from the fork branch (conflicts + the placement model still left health-test Pending on control-only and cubemastercli-test Pending on compute-only, and node-runtime-test still had no runAsUser: 0).

Replacement: #1388

What changed vs this PR:

  • Most test pods use a new cube.testPlacement (both plane taint tolerations, no nodeSelector) instead of pinning computePlacement / controlPlanePlacement.
  • node-runtime-test keeps busybox + computePlacement, and now pins runAsUser: 0.
  • DNS test uses helmTest.image + getent ahostsv4; dnsImage stays the runtime-test image.
  • Proxy /admin/healthz probe is kept.

Please close this PR in favor of #1388 if that looks right.

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