From 3ed20990839e4157977034a4485abbda444f4269 Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 7 Sep 2026 23:48:04 +0100 Subject: [PATCH] labctl: export the k3s config generators + a render CLI Exports generateServerConfig/generateAgentConfig (previously module-private) and adds bin/render-config.ts, which produces /etc/rancher/k3s/config.yaml from env vars using the PRODUCTION generator, with no SSH/OperationContext. This is the linchpin of the labsim 3-server-etcd rehearsal: the sim must drive its nodes through this exact generator, not a parallel set of INSTALL_K3S_EXEC flags, or it rehearses a mechanism production does not run. Verified rendering all three shapes -- cluster-init server, joining server (server:+token:), and dual-stack (dual node-ip + cluster/service CIDRs). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DMVzWZgiKW2wquf5z8S1yH --- .../modules/modules/k3s/bin/render-config.ts | 42 +++++++++++++++++++ .../modules/k3s/src/operations/index.ts | 2 +- .../modules/k3s/src/operations/k3s-config.ts | 8 +++- 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 bastion/src/modules/modules/k3s/bin/render-config.ts diff --git a/bastion/src/modules/modules/k3s/bin/render-config.ts b/bastion/src/modules/modules/k3s/bin/render-config.ts new file mode 100644 index 0000000..ec53c0b --- /dev/null +++ b/bastion/src/modules/modules/k3s/bin/render-config.ts @@ -0,0 +1,42 @@ +#!/usr/bin/env node +// Render /etc/rancher/k3s/config.yaml using the PRODUCTION generator. +// +// Exists so labsim (and anything else) can produce the exact config.yaml a node +// would get from `labctl install`, without an SSH/OperationContext. Driving the +// rehearsal through this means the sim tests the same code path production runs +// -- if generateServerConfig ever changes shape, the sim moves with it. +// +// All input via env, so a sim's cloud-init or a shell can call it plainly: +// +// ROLE=infra HOSTNAME=k8s1 IP=172.31.2.11 render-config.ts # cluster-init server +// ROLE=infra HOSTNAME=k8s2 IP=172.31.2.12 \ +// K3S_SERVER_URL=https://172.31.2.11:6443 K3S_TOKEN=... render-config.ts # joining server +// ROLE=worker HOSTNAME=k8s4 IP=172.31.2.14 K3S_SERVER_URL=... K3S_TOKEN=... render-config.ts # agent +// +// Dual-stack is opt-in and matches K3sConfig exactly: set IPV6, CLUSTER_CIDR, +// SERVICE_CIDR (comma-separated families) and the generator emits the dual +// node-ip + CIDRs. Omit them and the output is byte-identical to a v4-only node. +import { generateServerConfig, generateAgentConfig } from "../src/operations/k3s-config.js"; +import type { K3sConfig } from "../src/types.js"; +import type { Role } from "@lab/shared"; + +const env = process.env; +const role = (env.ROLE ?? "worker") as Role; +const isServer = role === "infra" || role === "labcontroller"; + +const splitCsv = (v: string | undefined): string[] | undefined => + v ? v.split(",").map((s) => s.trim()).filter(Boolean) : undefined; + +const cfg: K3sConfig = { + hostname: env.HOSTNAME ?? "node", + ip: env.IP ?? (() => { throw new Error("IP is required"); })(), + role, + k3sServerUrl: env.K3S_SERVER_URL, + k3sToken: env.K3S_TOKEN, + tlsSans: splitCsv(env.TLS_SANS), + ipv6: env.IPV6, + clusterCidr: splitCsv(env.CLUSTER_CIDR), + serviceCidr: splitCsv(env.SERVICE_CIDR), +}; + +process.stdout.write(isServer ? generateServerConfig(cfg) : generateAgentConfig(cfg)); diff --git a/bastion/src/modules/modules/k3s/src/operations/index.ts b/bastion/src/modules/modules/k3s/src/operations/index.ts index d4783de..c6616d1 100644 --- a/bastion/src/modules/modules/k3s/src/operations/index.ts +++ b/bastion/src/modules/modules/k3s/src/operations/index.ts @@ -5,7 +5,7 @@ export { growRancherLv } from "./rancher-storage.js"; export { enableIscsi } from "./iscsi.js"; export { disableFirewall } from "./firewall.js"; export { setSelinuxPermissive } from "./selinux.js"; -export { writeK3sConfig } from "./k3s-config.js"; +export { writeK3sConfig, generateServerConfig, generateAgentConfig } from "./k3s-config.js"; export { writeAuditPolicy } from "./audit-policy.js"; export { cleanupStaleCni } from "./cni-cleanup.js"; export { installK3sBinary } from "./k3s-install.js"; diff --git a/bastion/src/modules/modules/k3s/src/operations/k3s-config.ts b/bastion/src/modules/modules/k3s/src/operations/k3s-config.ts index d5392b6..32d2e7f 100644 --- a/bastion/src/modules/modules/k3s/src/operations/k3s-config.ts +++ b/bastion/src/modules/modules/k3s/src/operations/k3s-config.ts @@ -38,7 +38,11 @@ function addressFamilyLines(config: K3sConfig, opts: { cidrs: boolean }): string return lines.length ? `${lines.join("\n")}\n` : ""; } -function generateServerConfig(config: K3sConfig): string { +// Exported so the exact production config.yaml can be rendered outside an SSH +// context -- notably by the labsim 3-server-etcd rehearsal, which must drive its +// nodes through THIS generator rather than a parallel set of INSTALL_K3S_EXEC +// flags, or it proves a mechanism production does not run. +export function generateServerConfig(config: K3sConfig): string { // The IPv6 address goes in the cert too. Without it, anything that reaches // this apiserver over v6 -- a peer server joining, or kubectl against the v6 // address -- fails TLS verification, and the error names the certificate @@ -96,7 +100,7 @@ ${tlsSans.map((s) => ` - "${s}"`).join("\n")} // cluster, gets no IPv6 pod CIDR, and the failure surfaces later as pods on that // node being unreachable over v6 while the node itself reads Ready. // It takes no cluster/service CIDRs -- those are server-side only. -function generateAgentConfig(config: K3sConfig): string { +export function generateAgentConfig(config: K3sConfig): string { return `${addressFamilyLines(config, { cidrs: false })}protect-kernel-defaults: true node-label: - "node-role.kubernetes.io/worker=true"