fix: k3s install automation — skip Cilium on join, Longhorn via server, default root user
Some checks failed
Some checks failed
- 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:
@@ -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 {
|
||||||
|
|||||||
@@ -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:`);
|
||||||
|
|||||||
@@ -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 } : {};
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)" };
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user