fix: k3s install automation — skip Cilium on join, Longhorn via server, default root user
Some checks failed
CI/CD / typecheck (push) Failing after 10s
CI/CD / test (push) Failing after 9s
CI/CD / lint (push) Failing after 22s
CI/CD / build (push) Has been skipped
CI/CD / publish-rpm (push) Has been skipped
CI/CD / publish-deb (push) Has been skipped

- Skip Cilium install for joining servers (already in cluster via daemonset)
- Longhorn annotation for workers: SSH to server node from CLI to apply
  kubectl annotation (workers don't have kubectl access)
- Default SSH user for k3s/app commands changed to 'root' (operations
  need root privileges, using 'lab' user broke installs)
- k3s server config: cluster-init for initial server, server+token for joins

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-04-01 16:02:19 +01:00
parent a68d6d617e
commit 06fc40a857
5 changed files with 30 additions and 13 deletions

View File

@@ -70,7 +70,7 @@ export function registerAppCommand(program: Command): void {
.command("install <target>") .command("install <target>")
.description("Install k3s on a target machine (hostname, IP, or MAC)") .description("Install k3s on a target machine (hostname, IP, or MAC)")
.option("--role <role>", "k3s role: infra (server) or worker (agent)", "infra") .option("--role <role>", "k3s role: infra (server) or worker (agent)", "infra")
.option("--user <user>", "SSH user", "lab") .option("--user <user>", "SSH user", "root")
.option("--k3s-server <url>", "k3s server URL (required for worker role)") .option("--k3s-server <url>", "k3s server URL (required for worker role)")
.option("--k3s-token <token>", "k3s join token (required for worker role)") .option("--k3s-token <token>", "k3s join token (required for worker role)")
.action(async (target: string, opts: { .action(async (target: string, opts: {
@@ -164,7 +164,7 @@ export function registerAppCommand(program: Command): void {
k3sCmd k3sCmd
.command("health [target]") .command("health [target]")
.description("Check k3s health (all hosts if no target given)") .description("Check k3s health (all hosts if no target given)")
.option("--user <user>", "SSH user", "lab") .option("--user <user>", "SSH user", "root")
.action(async (target: string | undefined, opts: { user: string }) => { .action(async (target: string | undefined, opts: { user: string }) => {
const sshKey = findSshKey(); const sshKey = findSshKey();
@@ -304,7 +304,7 @@ export function registerAppCommand(program: Command): void {
k3sCmd k3sCmd
.command("list") .command("list")
.description("List installed machines and their k3s status") .description("List installed machines and their k3s status")
.option("--user <user>", "SSH user", "lab") .option("--user <user>", "SSH user", "root")
.action(async (opts: { user: string }) => { .action(async (opts: { user: string }) => {
let state: BastionState; let state: BastionState;
try { try {

View File

@@ -59,7 +59,7 @@ export function registerAsahiCommand(parent: Command): void {
console.log(` labvg/longhorn (remaining space)${RESET}`); console.log(` labvg/longhorn (remaining space)${RESET}`);
console.log(""); console.log("");
console.log(` After first boot, SSH in and run the firstboot script:`); console.log(` After first boot, SSH in and run the firstboot script:`);
console.log(` ${BOLD}ssh lab@<ip> 'curl -sf ${bastionUrl}/asahi/firstboot.sh | sudo bash'${RESET}`); console.log(` ${BOLD}ssh root@<ip> 'curl -sf ${bastionUrl}/asahi/firstboot.sh | bash'${RESET}`);
console.log(""); console.log("");
console.log(` This sets up LVM, detects hostname/MAC, and self-registers.`); console.log(` This sets up LVM, detects hostname/MAC, and self-registers.`);
console.log(` Then install k3s:`); console.log(` Then install k3s:`);

View File

@@ -38,7 +38,7 @@ export function registerLabcontrollerCommands(appCmd: Command): void {
lcCmd lcCmd
.command("deploy <target>") .command("deploy <target>")
.description("Deploy labcontroller stack to a k3s node") .description("Deploy labcontroller stack to a k3s node")
.option("--user <user>", "SSH user", "lab") .option("--user <user>", "SSH user", "root")
.option("--crdb-replicas <n>", "CockroachDB replicas", "1") .option("--crdb-replicas <n>", "CockroachDB replicas", "1")
.action(async (target: string, opts: { .action(async (target: string, opts: {
user: string; user: string;
@@ -193,7 +193,7 @@ export function registerLabcontrollerCommands(appCmd: Command): void {
lcCmd lcCmd
.command("status [target]") .command("status [target]")
.description("Check labcontroller deployment status (all hosts if no target)") .description("Check labcontroller deployment status (all hosts if no target)")
.option("--user <user>", "SSH user", "lab") .option("--user <user>", "SSH user", "root")
.action(async (target: string | undefined, opts: { user: string }) => { .action(async (target: string | undefined, opts: { user: string }) => {
const sshKey = findSshKey(); const sshKey = findSshKey();
const sshOpts = sshKey ? { keyPath: sshKey } : {}; const sshOpts = sshKey ? { keyPath: sshKey } : {};

View File

@@ -78,9 +78,10 @@ export class K3sModule implements Module {
return toModuleResult("install", [...prepResults, ...k3sResults], start); return toModuleResult("install", [...prepResults, ...k3sResults], start);
} }
// Phase 3: Networking (server only — agents don't install Cilium) // Phase 3: Networking (initial server only — joining servers get Cilium via daemonset)
let netResults: OperationResult[] = []; let netResults: OperationResult[] = [];
if (isServer) { const isJoiningServer = isServer && !!opCtx.config.k3sServerUrl;
if (isServer && !isJoiningServer) {
netResults = await runNetworking(opCtx); netResults = await runNetworking(opCtx);
} }

View File

@@ -3,6 +3,7 @@
import type { Operation, OperationResult } from "../types.js"; import type { Operation, OperationResult } from "../types.js";
import { sshOpts } from "../utils.js"; import { sshOpts } from "../utils.js";
import { sshExec as remoteSshExec } from "../../../../src/ssh.js";
export const configureLonghornDisk: Operation = async (ctx): Promise<OperationResult> => { export const configureLonghornDisk: Operation = async (ctx): Promise<OperationResult> => {
// Check if /var/lib/longhorn exists on this node // Check if /var/lib/longhorn exists on this node
@@ -15,12 +16,11 @@ export const configureLonghornDisk: Operation = async (ctx): Promise<OperationRe
const nodeNameResult = await ctx.ssh.exec("hostname -f 2>/dev/null || hostname", sshOpts(ctx)); const nodeNameResult = await ctx.ssh.exec("hostname -f 2>/dev/null || hostname", sshOpts(ctx));
const nodeName = nodeNameResult.stdout.trim(); const nodeName = nodeNameResult.stdout.trim();
// Apply the annotation via kubectl (works on server nodes, or via KUBECONFIG on agents)
const kubectlPrefix = "k3s kubectl";
const annotation = JSON.stringify([{ path: "/var/lib/longhorn", allowScheduling: true }]); const annotation = JSON.stringify([{ path: "/var/lib/longhorn", allowScheduling: true }]);
// Try kubectl locally first (works on server nodes)
const result = await ctx.ssh.exec( const result = await ctx.ssh.exec(
`${kubectlPrefix} annotate node "${nodeName}" "node.longhorn.io/default-disks-config=${annotation}" --overwrite 2>&1 || true`, `k3s kubectl annotate node "${nodeName}" "node.longhorn.io/default-disks-config=${annotation}" --overwrite 2>&1 || true`,
sshOpts(ctx), sshOpts(ctx),
); );
@@ -28,7 +28,23 @@ export const configureLonghornDisk: Operation = async (ctx): Promise<OperationRe
return { success: true, changed: true, message: `Longhorn disk annotation applied to ${nodeName}` }; return { success: true, changed: true, message: `Longhorn disk annotation applied to ${nodeName}` };
} }
// If kubectl isn't available (agent node without server access), that's OK — // For worker/agent nodes without local kubectl: apply via the server
// the label is set, annotation can be applied from the server later if (ctx.config.k3sServerUrl) {
// The CLI has SSH access to the server — use sshExec from there
const serverHost = new URL(ctx.config.k3sServerUrl).hostname;
try {
const remoteResult = await remoteSshExec(
serverHost, "root",
`k3s kubectl annotate node "${nodeName}" "node.longhorn.io/default-disks-config=${annotation}" --overwrite`,
{ ...(ctx.ssh.keyPath ? { keyPath: ctx.ssh.keyPath } : {}), timeoutMs: 15_000 },
);
if (remoteResult.stdout.includes("annotated") || remoteResult.stdout.includes("unchanged")) {
return { success: true, changed: true, message: `Longhorn disk annotation applied to ${nodeName} (via server)` };
}
} catch {
// Fall through to manual instruction
}
}
return { success: true, changed: false, message: "Longhorn disk label set (annotation requires server kubectl)" }; return { success: true, changed: false, message: "Longhorn disk label set (annotation requires server kubectl)" };
}; };