fix(k8s): read ApiException.code when classifying client-node errors

@kubernetes/client-node v1 raises ApiException, which carries the HTTP status
on `.code`. Every check in the orchestrator read only `.statusCode` — the
pre-1.0 HttpError field — so `status` came back undefined and each "expected"
404/409 was rethrown instead of handled.

Found in production: the first start of a server with a volume read a
not-yet-existing PVC, got a 404 that should have meant "create it", and failed
the instance instead. The same latent bug sat in removeContainer, where a
missing pod failed the delete rather than being treated as already gone.

httpStatusOf() checks .code, .statusCode and .response.statusCode, and falls
back to parsing the rendered "HTTP-Code: NNN" message for wrappers that keep
none of them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
This commit is contained in:
Michal
2026-08-10 12:20:56 +01:00
parent b0233918ff
commit bb4b0b910f
2 changed files with 54 additions and 9 deletions

View File

@@ -87,7 +87,7 @@ vi.mock('@kubernetes/client-node', () => {
});
// Import after mock
import { KubernetesOrchestrator } from '../src/services/k8s/kubernetes-orchestrator.js';
import { KubernetesOrchestrator, httpStatusOf } from '../src/services/k8s/kubernetes-orchestrator.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const k8sMock = await import('@kubernetes/client-node') as any;
const { setHandler, clearHandlers, mockCore } = k8sMock.__testHelpers;
@@ -356,3 +356,26 @@ describe('KubernetesOrchestrator', () => {
});
});
});
describe('httpStatusOf', () => {
it('reads .code, which is what client-node v1 ApiException actually sets', () => {
// The bug this exists for: reading only .statusCode returned undefined, so
// an expected 404 was rethrown and a first-start PVC create never happened.
expect(httpStatusOf({ code: 404 })).toBe(404);
expect(httpStatusOf({ code: 409 })).toBe(409);
});
it('still reads the pre-1.0 HttpError shapes', () => {
expect(httpStatusOf({ statusCode: 404 })).toBe(404);
expect(httpStatusOf({ response: { statusCode: 409 } })).toBe(409);
});
it('falls back to parsing the rendered message', () => {
expect(httpStatusOf(new Error('HTTP-Code: 403\nMessage: Unknown API Status Code!'))).toBe(403);
});
it('returns undefined when there is no status to find', () => {
expect(httpStatusOf(new Error('socket hang up'))).toBeUndefined();
expect(httpStatusOf({ code: 'ECONNREFUSED' })).toBeUndefined();
});
});