-
Notifications
You must be signed in to change notification settings - Fork 28
Fix K8s sidecar registration and stale cluster topology broadcast #512
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d4525f8
fdc87ac
ef1acb0
1b18dfb
4a434b5
3679ca5
694681a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| import { describe, it, afterEach } from "node:test" | ||
| import assert from "node:assert" | ||
| import { | ||
| ORCHESTRATOR_AUTH_DOMAIN, | ||
| ORCHESTRATOR_AUTH_HEADER, | ||
| createOrchestratorAuthCredential | ||
| } from "valkey-common" | ||
| import type { Request, Response } from "express" | ||
|
|
||
| // These tests exercise the Kubernetes-only branches of the orchestrator: | ||
| // - resolveCollectorKey falls back to the shared ORCHESTRATOR_KEY, and | ||
| // - handleRegister admits a sidecar whose nodeId is part of the discovered | ||
| // cluster topology even though the orchestrator never spawned it. | ||
| // isKubernetes is a module-load constant, so DEPLOYMENT_MODE and the shared key | ||
| // must be set BEFORE importing the module. | ||
| process.env.DEPLOYMENT_MODE = "K8" | ||
| process.env.ORCHESTRATOR_KEY = "k8s-shared-secret" | ||
|
|
||
| const { | ||
| metricsServerMap, | ||
| clusterNodesRegistry, | ||
| resolveCollectorKey, | ||
| __test__, | ||
| } = await import("../metrics-orchestrator") | ||
|
|
||
| const NODE_ID = "valkey-6-valkey-headless-valkey-svc-cluster-local-6379" | ||
| const URI = "http://10.42.0.32:3000" | ||
| const SHARED_KEY = "k8s-shared-secret" | ||
|
|
||
| const makeRes = () => { | ||
| const captured: { statusCode: number; body?: unknown } = { statusCode: 200 } | ||
| const res = { | ||
| status(code: number) { captured.statusCode = code; return res }, | ||
| send(body?: unknown) { captured.body = body; return res }, | ||
| sendStatus(code: number) { captured.statusCode = code; return res }, | ||
| } | ||
| return { res: res as unknown as Response, captured } | ||
| } | ||
|
|
||
| const makeReq = (body: unknown, credential?: string) => ({ | ||
| body, | ||
| headers: credential === undefined ? {} : { [ORCHESTRATOR_AUTH_HEADER]: credential }, | ||
| }) as unknown as Request | ||
|
|
||
| const signRegister = (fields: { nodeId?: string; metricsServerUri?: string; timestamp?: number } = {}, key = SHARED_KEY) => | ||
| createOrchestratorAuthCredential(key, ORCHESTRATOR_AUTH_DOMAIN.REGISTER, { | ||
| nodeId: NODE_ID, | ||
| metricsServerUri: URI, | ||
| timestamp: Date.now(), | ||
| ...fields, | ||
| }) as string | ||
|
|
||
| // Register a node into the discovered topology so it is a known cluster member. | ||
| const seedTopology = (nodeId = NODE_ID) => { | ||
| clusterNodesRegistry.set("cluster-1", { | ||
| [nodeId]: { host: "10.42.0.32", port: 6379, tls: false, verifyTlsCertificate: false }, | ||
| }) | ||
| } | ||
|
|
||
| describe("K8s shared-key registration", () => { | ||
| afterEach(() => { | ||
| metricsServerMap.clear() | ||
| clusterNodesRegistry.clear() | ||
| __test__.collectorKeys.clear() | ||
| }) | ||
|
|
||
| describe("resolveCollectorKey", () => { | ||
| it("falls back to the shared ORCHESTRATOR_KEY for a node the orchestrator never spawned", () => { | ||
| assert.strictEqual(resolveCollectorKey("never-spawned-node"), SHARED_KEY) | ||
| }) | ||
|
|
||
| it("still prefers a per-node minted key when one exists", () => { | ||
| __test__.collectorKeys.set("spawned-node", "per-node-key") | ||
| assert.strictEqual(resolveCollectorKey("spawned-node"), "per-node-key") | ||
| }) | ||
| }) | ||
|
|
||
| describe("handleRegister topology-membership gate", () => { | ||
| it("admits a correctly signed sidecar for a known cluster node with no pre-existing entry", () => { | ||
| seedTopology() | ||
| assert.strictEqual(metricsServerMap.has(NODE_ID), false, "precondition: no orchestrator-spawned entry") | ||
|
|
||
| const { res, captured } = makeRes() | ||
| __test__.handleRegister(makeReq({ nodeId: NODE_ID, metricsServerUri: URI, timestamp: Date.now() }, signRegister()), res) | ||
|
|
||
| assert.strictEqual(captured.statusCode, 200, "a known cluster node should be allowed to register") | ||
| assert.strictEqual(metricsServerMap.get(NODE_ID)?.metricsURI, URI, "the sidecar's entry should be created on first register") | ||
| }) | ||
|
|
||
| it("rejects a correctly signed sidecar whose nodeId is not in the discovered topology", () => { | ||
| // Topology known, but for a different node. | ||
| seedTopology("some-other-node-6379") | ||
|
|
||
| const { res, captured } = makeRes() | ||
| __test__.handleRegister(makeReq({ nodeId: NODE_ID, metricsServerUri: URI, timestamp: Date.now() }, signRegister()), res) | ||
|
Comment on lines
+84
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,125p' apps/server/src/__tests__/orchestrator-k8s-register.test.ts
sed -n '150,210p' common/src/orchestrator-auth.ts
sed -n '260,315p' apps/server/src/metrics-orchestrator.tsRepository: valkey-io/valkey-admin Length of output: 9044 🏁 Script executed: sed -n '110,245p' common/src/orchestrator-auth.ts
sed -n '225,285p' apps/server/src/metrics-orchestrator.ts
sed -n '76,101p' apps/server/src/__tests__/orchestrator-k8s-register.test.tsRepository: valkey-io/valkey-admin Length of output: 8571 Use one timestamp for the body and signature.
Create one timestamp and pass it to both the request body and 🤖 Prompt for AI Agents |
||
|
|
||
| assert.strictEqual(captured.statusCode, 401, "an unknown node must be rejected even with a valid signature") | ||
| assert.strictEqual(metricsServerMap.has(NODE_ID), false) | ||
| }) | ||
|
|
||
| it("rejects a known cluster node when the signature uses the wrong key", () => { | ||
| seedTopology() | ||
|
|
||
| const { res, captured } = makeRes() | ||
| __test__.handleRegister( | ||
| makeReq({ nodeId: NODE_ID, metricsServerUri: URI, timestamp: Date.now() }, signRegister({}, "wrong-key")), | ||
| res, | ||
| ) | ||
|
|
||
| assert.strictEqual(captured.statusCode, 401, "a bad signature must be rejected before the membership check") | ||
| assert.strictEqual(metricsServerMap.has(NODE_ID), false) | ||
| }) | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { describe, it, afterEach, mock } from "node:test" | ||
| import assert from "node:assert" | ||
|
|
||
| // preConfiguredConnection is a module-load constant derived from VALKEY_HOST / | ||
| // VALKEY_PORT, so it must be set BEFORE importing the module to exercise the | ||
| // headless-refresh fallback (no live user client → initial client). | ||
| process.env.VALKEY_HOST = "valkey-0.example" | ||
| process.env.VALKEY_PORT = "6379" | ||
|
|
||
| const { | ||
| resolveClusterRefreshTarget, | ||
| setPreconfiguredClusterId, | ||
| initialConnectionDetails, | ||
| __test__, | ||
| } = await import("../metrics-orchestrator") | ||
|
|
||
| const PRECONFIGURED_ID = "preconfigured-cluster" | ||
|
|
||
| describe("resolveClusterRefreshTarget (preconfigured / headless)", () => { | ||
| afterEach(() => { | ||
| mock.restoreAll() | ||
| setPreconfiguredClusterId(undefined) | ||
| }) | ||
|
|
||
| it("refreshes the preconfigured cluster via the initial client + initialConnectionDetails", async () => { | ||
| setPreconfiguredClusterId(PRECONFIGURED_ID) | ||
| const initialClient = { id: "initial-client" } as never | ||
| mock.method(__test__, "getInitialClient", async () => initialClient) | ||
|
|
||
| const target = await resolveClusterRefreshTarget( | ||
| PRECONFIGURED_ID, | ||
| { node1: { host: "10.0.0.1", port: 6379, tls: false, verifyTlsCertificate: false } }, | ||
| undefined, | ||
| ) | ||
|
|
||
| assert.strictEqual(target?.client, initialClient, "the preconfigured cluster should refresh via the initial client") | ||
| assert.strictEqual( | ||
| target?.nodeInfo, | ||
| initialConnectionDetails, | ||
| "the preconfigured cluster decorates with initialConnectionDetails", | ||
| ) | ||
| }) | ||
|
|
||
| it("does NOT use the initial client for a different client-less cluster", async () => { | ||
| // Preconfigured cluster is A; refreshing a stale, client-less cluster B must | ||
| // not rediscover A's topology through the initial client. | ||
| setPreconfiguredClusterId(PRECONFIGURED_ID) | ||
| const getInitial = mock.method(__test__, "getInitialClient", async () => ({ id: "initial-client" } as never)) | ||
|
|
||
| const target = await resolveClusterRefreshTarget( | ||
| "some-other-cluster", | ||
| { node1: { host: "10.9.9.9", port: 6379, tls: false, verifyTlsCertificate: false } }, | ||
| undefined, | ||
| ) | ||
|
|
||
| assert.strictEqual(target, undefined, "an unrelated client-less cluster must be left unchanged") | ||
| assert.strictEqual(getInitial.mock.callCount(), 0, "the initial client must not be consulted for other clusters") | ||
| }) | ||
|
|
||
| it("still prefers a user client over the initial client when one is present", async () => { | ||
| const userClient = { id: "user-client" } as never | ||
| mock.method(__test__, "getInitialClient", async () => ({ id: "initial-client" } as never)) | ||
|
|
||
| const clusterNodes = { node1: { host: "10.0.0.1", port: 6379, tls: true, verifyTlsCertificate: false } } | ||
| const target = await resolveClusterRefreshTarget("any-cluster", clusterNodes, userClient) | ||
|
|
||
| assert.strictEqual(target?.client, userClient) | ||
| assert.deepStrictEqual(target?.nodeInfo, clusterNodes.node1) | ||
| }) | ||
| }) |
Uh oh!
There was an error while loading. Please reload this page.