Skip to content

Commit 9ce0166

Browse files
committed
chore: update Kubernetes output tests to include secrets handling and adjust expected counts
1 parent 6bc831f commit 9ce0166

3 files changed

Lines changed: 174 additions & 17 deletions

File tree

charts/network-bootstrapper/templates/role.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ rules:
1010
- ""
1111
resources:
1212
- configmaps
13+
- secrets
1314
verbs:
1415
- get
1516
- list

src/cli/output.test.ts

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ const HEX_RADIX = 16;
7777
const SAMPLE_VALIDATOR_INDEX = 1;
7878
const SAMPLE_RPC_INDEX = 2;
7979
const SAMPLE_FAUCET_INDEX = 99;
80-
const EXPECTED_CONFIGMAP_COUNT = 8;
80+
const EXPECTED_CONFIGMAP_COUNT = 9;
81+
const EXPECTED_SECRET_COUNT = 3;
82+
const HEX_PREFIX_PATTERN = /^0x/;
8183
const TEST_CHAIN_ID = 1;
8284
const HTTP_CONFLICT_STATUS = 409;
8385
const HTTP_INTERNAL_ERROR_STATUS = 500;
@@ -153,17 +155,23 @@ describe("outputResult", () => {
153155
await rm("out", { recursive: true, force: true });
154156
});
155157

156-
test("kubernetes output creates configmaps", async () => {
158+
test("kubernetes output creates configmaps and secrets", async () => {
157159
const originalLoad = (KubeConfig.prototype as any).loadFromCluster;
158160
const originalMake = (KubeConfig.prototype as any).makeApiClient;
159161
const originalFile = Bun.file;
160162

161-
const created: Array<{
163+
const createdConfigMaps: Array<{
162164
namespace: string;
163165
name: string;
164166
data: Record<string, string>;
165167
}> = [];
166-
const listedNamespaces: string[] = [];
168+
const createdSecrets: Array<{
169+
namespace: string;
170+
name: string;
171+
data: Record<string, string>;
172+
}> = [];
173+
const listedConfigNamespaces: string[] = [];
174+
const listedSecretNamespaces: string[] = [];
167175

168176
try {
169177
(KubeConfig.prototype as any).loadFromCluster =
@@ -174,7 +182,11 @@ describe("outputResult", () => {
174182
(KubeConfig.prototype as any).makeApiClient = function makeApiClient() {
175183
const client = {
176184
listNamespacedConfigMap: ({ namespace }: { namespace: string }) => {
177-
listedNamespaces.push(namespace);
185+
listedConfigNamespaces.push(namespace);
186+
return Promise.resolve();
187+
},
188+
listNamespacedSecret: ({ namespace }: { namespace: string }) => {
189+
listedSecretNamespaces.push(namespace);
178190
return Promise.resolve();
179191
},
180192
createNamespacedConfigMap: ({
@@ -184,13 +196,27 @@ describe("outputResult", () => {
184196
namespace: string;
185197
body: any;
186198
}) => {
187-
created.push({
199+
createdConfigMaps.push({
188200
namespace,
189201
name: body?.metadata?.name ?? "",
190202
data: body?.data ?? {},
191203
});
192204
return Promise.resolve();
193205
},
206+
createNamespacedSecret: ({
207+
namespace,
208+
body,
209+
}: {
210+
namespace: string;
211+
body: any;
212+
}) => {
213+
createdSecrets.push({
214+
namespace,
215+
name: body?.metadata?.name ?? "",
216+
data: body?.stringData ?? {},
217+
});
218+
return Promise.resolve();
219+
},
194220
};
195221
return client as unknown as CoreV1Api;
196222
};
@@ -202,11 +228,26 @@ describe("outputResult", () => {
202228

203229
await outputResult("kubernetes", samplePayload);
204230

205-
expect(listedNamespaces).toEqual(["test-namespace"]);
206-
expect(created).toHaveLength(EXPECTED_CONFIGMAP_COUNT);
207-
const names = created.map((entry) => entry.name).sort();
208-
expect(names).toContain("besu-node-validator-1-address");
209-
expect(names).toContain("besu-node-rpc-node-2-private-key");
231+
expect(listedConfigNamespaces).toEqual(["test-namespace"]);
232+
expect(listedSecretNamespaces).toEqual(["test-namespace"]);
233+
expect(createdConfigMaps).toHaveLength(EXPECTED_CONFIGMAP_COUNT);
234+
expect(createdSecrets).toHaveLength(EXPECTED_SECRET_COUNT);
235+
const mapNames = createdConfigMaps.map((entry) => entry.name).sort();
236+
expect(mapNames).toContain("besu-node-validator-1-address");
237+
expect(mapNames).toContain("besu-genesis");
238+
expect(mapNames).toContain("besu-faucet-address");
239+
expect(mapNames).toContain("besu-faucet-pubkey");
240+
expect(mapNames).not.toContain("besu-faucet-enode");
241+
const secretNames = createdSecrets.map((entry) => entry.name).sort();
242+
expect(secretNames).toEqual([
243+
"besu-faucet-private-key",
244+
"besu-node-rpc-node-2-private-key",
245+
"besu-node-validator-1-private-key",
246+
]);
247+
const privateKeySecret = createdSecrets.find((entry) =>
248+
entry.name.endsWith("validator-1-private-key")
249+
);
250+
expect(privateKeySecret?.data?.privateKey).toMatch(HEX_PREFIX_PATTERN);
210251
} finally {
211252
(KubeConfig.prototype as any).loadFromCluster = originalLoad;
212253
(KubeConfig.prototype as any).makeApiClient = originalMake;
@@ -227,6 +268,7 @@ describe("outputResult", () => {
227268
(KubeConfig.prototype as any).makeApiClient = function makeApiClient() {
228269
const client = {
229270
listNamespacedConfigMap: () => Promise.resolve(),
271+
listNamespacedSecret: () => Promise.resolve(),
230272
createNamespacedConfigMap: () => {
231273
const error = new Error("already exists");
232274
(
@@ -239,6 +281,7 @@ describe("outputResult", () => {
239281
};
240282
throw error;
241283
},
284+
createNamespacedSecret: () => Promise.resolve(),
242285
};
243286
return client as unknown as CoreV1Api;
244287
};
@@ -258,6 +301,52 @@ describe("outputResult", () => {
258301
}
259302
});
260303

304+
test("kubernetes output surfaces secret conflict errors", async () => {
305+
const originalLoad = (KubeConfig.prototype as any).loadFromCluster;
306+
const originalMake = (KubeConfig.prototype as any).makeApiClient;
307+
const originalFile = Bun.file;
308+
309+
try {
310+
(KubeConfig.prototype as any).loadFromCluster =
311+
function loadFromCluster(): void {
312+
/* no-op for tests */
313+
};
314+
(KubeConfig.prototype as any).makeApiClient = function makeApiClient() {
315+
const client = {
316+
listNamespacedConfigMap: () => Promise.resolve(),
317+
listNamespacedSecret: () => Promise.resolve(),
318+
createNamespacedConfigMap: () => Promise.resolve(),
319+
createNamespacedSecret: () => {
320+
const error = new Error("already exists");
321+
(
322+
error as {
323+
response?: { statusCode: number; body: { message: string } };
324+
}
325+
).response = {
326+
statusCode: HTTP_CONFLICT_STATUS,
327+
body: { message: "already exists" },
328+
};
329+
throw error;
330+
},
331+
};
332+
return client as unknown as CoreV1Api;
333+
};
334+
335+
(Bun as any).file = () =>
336+
({
337+
text: () => Promise.resolve("secret-conflict-namespace"),
338+
}) as unknown as ReturnType<typeof Bun.file>;
339+
340+
await expect(outputResult("kubernetes", samplePayload)).rejects.toThrow(
341+
"Secret besu-node-validator-1-private-key already exists. Delete it or choose a different output target."
342+
);
343+
} finally {
344+
(KubeConfig.prototype as any).loadFromCluster = originalLoad;
345+
(KubeConfig.prototype as any).makeApiClient = originalMake;
346+
(Bun as any).file = originalFile;
347+
}
348+
});
349+
261350
test("kubernetes output fails without cluster credentials", async () => {
262351
const originalLoad = (KubeConfig.prototype as any).loadFromCluster;
263352
const originalFile = Bun.file;
@@ -346,6 +435,7 @@ describe("outputResult", () => {
346435
(KubeConfig.prototype as any).makeApiClient = function makeApiClient() {
347436
const client = {
348437
listNamespacedConfigMap: () => Promise.reject(new Error("forbidden")),
438+
listNamespacedSecret: () => Promise.resolve(),
349439
};
350440
return client as unknown as CoreV1Api;
351441
};
@@ -377,9 +467,11 @@ describe("outputResult", () => {
377467
(KubeConfig.prototype as any).makeApiClient = function makeApiClient() {
378468
const client = {
379469
listNamespacedConfigMap: () => Promise.resolve(),
470+
listNamespacedSecret: () => Promise.resolve(),
380471
createNamespacedConfigMap: () => {
381472
throw new Error("boom");
382473
},
474+
createNamespacedSecret: () => Promise.resolve(),
383475
};
384476
return client as unknown as CoreV1Api;
385477
};
@@ -411,12 +503,14 @@ describe("outputResult", () => {
411503
(KubeConfig.prototype as any).makeApiClient = function makeApiClient() {
412504
const client = {
413505
listNamespacedConfigMap: () => Promise.resolve(),
506+
listNamespacedSecret: () => Promise.resolve(),
414507
createNamespacedConfigMap: () => {
415508
const error = new Error("failed");
416509
(error as { statusCode?: number }).statusCode =
417510
HTTP_INTERNAL_ERROR_STATUS;
418511
throw error;
419512
},
513+
createNamespacedSecret: () => Promise.resolve(),
420514
};
421515
return client as unknown as CoreV1Api;
422516
};
@@ -448,6 +542,7 @@ describe("outputResult", () => {
448542
(KubeConfig.prototype as any).makeApiClient = function makeApiClient() {
449543
const client = {
450544
listNamespacedConfigMap: () => Promise.resolve(),
545+
listNamespacedSecret: () => Promise.resolve(),
451546
createNamespacedConfigMap: () => {
452547
const error = new Error("response error");
453548
Object.defineProperty(error, "message", { value: undefined });
@@ -456,6 +551,7 @@ describe("outputResult", () => {
456551
};
457552
throw error;
458553
},
554+
createNamespacedSecret: () => Promise.resolve(),
459555
};
460556
return client as unknown as CoreV1Api;
461557
};
@@ -487,6 +583,7 @@ describe("outputResult", () => {
487583
(KubeConfig.prototype as any).makeApiClient = function makeApiClient() {
488584
const client = {
489585
listNamespacedConfigMap: () => Promise.resolve(),
586+
listNamespacedSecret: () => Promise.resolve(),
490587
createNamespacedConfigMap: () => {
491588
const error = new Error("body error");
492589
Object.defineProperty(error, "message", { value: undefined });
@@ -495,6 +592,7 @@ describe("outputResult", () => {
495592
};
496593
throw error;
497594
},
595+
createNamespacedSecret: () => Promise.resolve(),
498596
};
499597
return client as unknown as CoreV1Api;
500598
};

src/cli/output.ts

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { mkdir } from "node:fs/promises";
22
import { join } from "node:path";
3-
import type { V1ConfigMap } from "@kubernetes/client-node";
3+
import type { V1ConfigMap, V1Secret } from "@kubernetes/client-node";
44
import { CoreV1Api, KubeConfig } from "@kubernetes/client-node";
55

66
import type { GeneratedNodeKey } from "../keys/node-key-factory.ts";
@@ -23,6 +23,8 @@ type ConfigMapSpec = {
2323
value: string;
2424
};
2525

26+
type SecretSpec = ConfigMapSpec;
27+
2628
const OUTPUT_DIR = "out";
2729
const NAMESPACE_PATH =
2830
"/var/run/secrets/kubernetes.io/serviceaccount/namespace";
@@ -133,13 +135,27 @@ const outputToKubernetes = async (payload: OutputPayload): Promise<void> => {
133135
const validatorSpecs = createSpecsForGroup("validator", payload.validators);
134136
const rpcSpecs = createSpecsForGroup("rpc-node", payload.rpcNodes);
135137
const allSpecs = [...validatorSpecs, ...rpcSpecs];
138+
const configMapSpecs = [
139+
...allSpecs.filter((spec) => spec.key !== "privateKey"),
140+
...createFaucetConfigSpecs(payload.faucet),
141+
{
142+
name: "besu-genesis",
143+
key: "genesis.json",
144+
value: JSON.stringify(payload.genesis, null, 2),
145+
},
146+
];
147+
const secretSpecs = [
148+
...allSpecs.filter((spec) => spec.key === "privateKey"),
149+
...createFaucetSecretSpecs(payload.faucet),
150+
];
136151

137-
await Promise.all(
138-
allSpecs.map((spec) => upsertConfigMap(client, namespace, spec))
139-
);
152+
await Promise.all([
153+
...configMapSpecs.map((spec) => upsertConfigMap(client, namespace, spec)),
154+
...secretSpecs.map((spec) => upsertSecret(client, namespace, spec)),
155+
]);
140156

141157
process.stdout.write(
142-
`Applied ${allSpecs.length} ConfigMaps in namespace ${namespace}.\n`
158+
`Applied ${configMapSpecs.length} ConfigMaps and ${secretSpecs.length} Secrets in namespace ${namespace}.\n`
143159
);
144160
};
145161

@@ -165,6 +181,19 @@ const createSpecsForGroup = (
165181
});
166182
};
167183

184+
const createFaucetConfigSpecs = (faucet: GeneratedNodeKey): ConfigMapSpec[] => [
185+
{ name: "besu-faucet-address", key: "address", value: faucet.address },
186+
{ name: "besu-faucet-pubkey", key: "publicKey", value: faucet.publicKey },
187+
];
188+
189+
const createFaucetSecretSpecs = (faucet: GeneratedNodeKey): SecretSpec[] => [
190+
{
191+
name: "besu-faucet-private-key",
192+
key: "privateKey",
193+
value: faucet.privateKey,
194+
},
195+
];
196+
168197
const createKubernetesClient = async (): Promise<{
169198
client: CoreV1Api;
170199
namespace: string;
@@ -195,7 +224,10 @@ const createKubernetesClient = async (): Promise<{
195224

196225
const client = kubeConfig.makeApiClient(CoreV1Api);
197226
try {
198-
await client.listNamespacedConfigMap({ namespace, limit: 1 });
227+
await Promise.all([
228+
client.listNamespacedConfigMap({ namespace, limit: 1 }),
229+
client.listNamespacedSecret({ namespace, limit: 1 }),
230+
]);
199231
} catch (error) {
200232
throw new Error(
201233
`Kubernetes permissions check failed: ${extractKubernetesError(error)}`
@@ -230,6 +262,32 @@ const upsertConfigMap = async (
230262
}
231263
};
232264

265+
const upsertSecret = async (
266+
client: CoreV1Api,
267+
namespace: string,
268+
spec: SecretSpec
269+
): Promise<void> => {
270+
const body: V1Secret = {
271+
metadata: { name: spec.name },
272+
stringData: { [spec.key]: spec.value },
273+
type: "Opaque",
274+
};
275+
276+
try {
277+
await client.createNamespacedSecret({ namespace, body });
278+
} catch (error) {
279+
if (getStatusCode(error) === HTTP_CONFLICT_STATUS) {
280+
throw new Error(
281+
`Secret ${spec.name} already exists. Delete it or choose a different output target.`
282+
);
283+
}
284+
285+
throw new Error(
286+
`Failed to create Secret ${spec.name}: ${extractKubernetesError(error)}`
287+
);
288+
}
289+
};
290+
233291
const extractKubernetesError = (error: unknown): string => {
234292
if (typeof error === "string") {
235293
return error;

0 commit comments

Comments
 (0)