mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
fix(elbv2): make NLB nginx health-check pathway robust in CI
E2E NLB suites timed out at "0/2 healthy" while ALB suites on the same target instances went healthy in ~5s. The host daemon journal shows the NLB agent reporting an empty server list (no probe results at all), which happens when the lb-agent never enters nginx mode — i.e. `nginx -t`/start fails closed inside the LB microVM, so the agent never probes and reports nothing. The data plane itself is verified working; this is purely the active-health-check pathway failing to converge in CI. - lbagent: pre-create nginx runtime prefix dirs (pid dir + /var/lib/nginx, /var/lib/nginx/tmp, /var/log/nginx) before `nginx -t`, mirroring reloadHAProxy's socket-dir pre-create. A stripped Alpine initramfs may lack these, dead-ending the whole NLB health path. - build-microvm-image.sh: assert the nginx binary and ngx_stream_module.so are present in the image and fail the build loudly, instead of surfacing as an opaque e2e health timeout. - e2e: capture the LB VM serial console to the artifact bundle on NLB health-wait failure. The agent's engine activation and any `reload nginx:` error land on the guest console, which the host journal never sees.
This commit is contained in:
@@ -173,6 +173,21 @@ else
|
||||
echo "[build-microvm-image] fw_cfg: module found in lib/modules"
|
||||
fi
|
||||
|
||||
# --- Assert nginx + stream module (NLB L4 data plane) ---
|
||||
# The lb-agent runs `nginx -c` with a config that `load_module`s the stream
|
||||
# module; if either the binary or the .so is missing the agent never enters
|
||||
# nginx mode, never probes, and NLB targets stay at 0/N healthy. Fail the build
|
||||
# loudly here rather than discover it as an opaque e2e health timeout.
|
||||
if [ ! -x "$CHROOT_DIR/usr/sbin/nginx" ] && [ ! -x "$CHROOT_DIR/usr/bin/nginx" ]; then
|
||||
echo "ERROR: nginx binary missing from image — NLB data plane will not start" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! find "$CHROOT_DIR/usr/lib/nginx/modules" -name "ngx_stream_module.so" 2>/dev/null | grep -q .; then
|
||||
echo "ERROR: ngx_stream_module.so missing from image — NLB stream config fails to load" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[build-microvm-image] nginx: binary + stream module present"
|
||||
|
||||
# --- Strip kernel modules (Phase 2 keep-list approach) ---
|
||||
# Only modules needed by a virtio-mmio microvm guest are kept:
|
||||
# virtio*.ko* — virtio bus, net, blk, rng, console, and helpers
|
||||
|
||||
@@ -413,6 +413,15 @@ func reloadHAProxy(configPath, pidPath string) error {
|
||||
// `nginx -t` fails closed before any reload, mirroring HAProxy's
|
||||
// validate-before-bind guarantee (a bad config never replaces a good one).
|
||||
func reloadNginx(configPath, pidPath string) error {
|
||||
// On a stripped Alpine microvm initramfs the nginx runtime prefix dirs may
|
||||
// be absent, so `nginx -t`/start fails closed on a missing pid dir or temp
|
||||
// path — and the agent never enters nginx mode, never probes, and reports
|
||||
// empty health (NLB targets stuck at 0/N). Pre-create them, mirroring
|
||||
// reloadHAProxy's socket-dir pre-create.
|
||||
for _, dir := range []string{filepath.Dir(pidPath), "/var/lib/nginx", "/var/lib/nginx/tmp", "/var/log/nginx"} {
|
||||
_ = os.MkdirAll(dir, 0o750)
|
||||
}
|
||||
|
||||
test := exec.Command("nginx", "-t", "-c", configPath)
|
||||
if out, err := test.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("nginx -t: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
|
||||
@@ -92,6 +92,32 @@ func dumpConsoleOutput(t *testing.T, c *AWSClient, instanceID string) {
|
||||
}
|
||||
}
|
||||
|
||||
// DumpInstanceConsole writes an instance's full serial console (base64-decoded)
|
||||
// to <dir>/<name>. The lb-agent logs its data-plane engine activation and any
|
||||
// `reload nginx:` error to the guest console, which the host daemon journal
|
||||
// never sees — capturing it makes an NLB health-check timeout diagnosable from
|
||||
// CI artifacts alone. Best-effort: failures log, they don't fail the test.
|
||||
func DumpInstanceConsole(t *testing.T, c *AWSClient, instanceID, dir, name string) {
|
||||
t.Helper()
|
||||
out, err := c.EC2.GetConsoleOutput(&ec2.GetConsoleOutputInput{
|
||||
InstanceId: aws.String(instanceID),
|
||||
})
|
||||
if err != nil {
|
||||
t.Logf("console capture: GetConsoleOutput(%s) failed: %v", instanceID, err)
|
||||
return
|
||||
}
|
||||
encoded := aws.StringValue(out.Output)
|
||||
if encoded == "" {
|
||||
t.Logf("console capture: %s empty", instanceID)
|
||||
return
|
||||
}
|
||||
raw, derr := base64.StdEncoding.DecodeString(encoded)
|
||||
if derr != nil {
|
||||
raw = []byte(encoded)
|
||||
}
|
||||
DumpFile(t, dir, name, raw)
|
||||
}
|
||||
|
||||
func dumpOVNState(t *testing.T, instanceID string) {
|
||||
t.Helper()
|
||||
if _, err := exec.LookPath("ovn-nbctl"); err != nil {
|
||||
|
||||
@@ -506,6 +506,9 @@ func runLBSuite(t *testing.T, c *harness.AWSClient, f *sharedFixture, kind lbKin
|
||||
}
|
||||
|
||||
harness.WaitForLBActive(t, c, lb.ARN, label, 5*time.Minute)
|
||||
if kind == kindNLB {
|
||||
captureLBConsoleOnFailure(t, c, eniDescPrefix, lb)
|
||||
}
|
||||
harness.WaitForTargetsHealthy(t, c, tgArn, 2, label, 2*time.Minute)
|
||||
|
||||
eni := lbENI(t, c, eniDescPrefix, lb)
|
||||
@@ -554,6 +557,7 @@ func runUDPNLBSuite(t *testing.T, c *harness.AWSClient, f *sharedFixture) {
|
||||
assert.Contains(t, lb.ARN, "/net/", label+" ARN must contain /net/")
|
||||
|
||||
harness.WaitForLBActive(t, c, lb.ARN, label, 5*time.Minute)
|
||||
captureLBConsoleOnFailure(t, c, "net", lb)
|
||||
// The key assertion: NLB targets reach healthy via the agent's active
|
||||
// prober. Pre-feature this timed out (nginx reported empty server lists).
|
||||
harness.WaitForTargetsHealthy(t, c, tgArn, 2, label, 2*time.Minute)
|
||||
@@ -1300,6 +1304,27 @@ func lbENI(t *testing.T, c *harness.AWSClient, prefix string, lb lbInfo) *ec2.Ne
|
||||
return eni
|
||||
}
|
||||
|
||||
// captureLBConsoleOnFailure registers an on-failure dump of the NLB VM's serial
|
||||
// console to the subtest artifact dir. The lb-agent's nginx engine activation
|
||||
// (and any `reload nginx:` error) lands on the guest console, not in the host
|
||||
// daemon journal — so an NLB "0/N healthy" timeout is otherwise undiagnosable
|
||||
// from CI artifacts. Call after the LB is active (the VM/ENI exists by then).
|
||||
func captureLBConsoleOnFailure(t *testing.T, c *harness.AWSClient, eniDescPrefix string, lb lbInfo) {
|
||||
t.Helper()
|
||||
eni := lbENI(t, c, eniDescPrefix, lb)
|
||||
if eni.Attachment == nil {
|
||||
return
|
||||
}
|
||||
instanceID := aws.StringValue(eni.Attachment.InstanceId)
|
||||
if instanceID == "" {
|
||||
return
|
||||
}
|
||||
dir := harness.ArtifactDir(t, harness.LoadEnv(t))
|
||||
harness.OnFailure(t, func() {
|
||||
harness.DumpInstanceConsole(t, c, instanceID, dir, "lb-vm-console.log")
|
||||
})
|
||||
}
|
||||
|
||||
func publicIP(eni *ec2.NetworkInterface) string {
|
||||
if eni == nil || eni.Association == nil {
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user