🌳
pt0/deployF/harvF/setupHarvesterF.mts
1import { createHmac } from 'crypto'
19import { tsAbsPath } from '../../ptDirF.mts'
22import type { HarvKlusterCfg } from './harvSetupCtxF.mts'
24export const harvDashboardHostnamePrefix = (cluster_name: string, dashboardSecret: string) => {
25 const hmac = createHmac('sha256', dashboardSecret)
26 hmac.update(cluster_name)
27 return `harvester-${hmac.digest('hex').slice(0, 8)}`
30export const printHarvDashboardLogin = ({cluster_name, vipHost, lanHost, extHost}: {
31 cluster_name: string, vipHost?: string, lanHost?: string, extHost?: string
32}) => {
33 const secName = `harv-dashboard-${cluster_name}`
34 if (vipHost) {
35 console.log(chalkGreen(`Harvester Dashboard: https://${vipHost}`))
36 } else if (lanHost && extHost) {
37 console.log(`Harvester Dashboard: https://${lanHost} (lan) or https://${extHost} (ext)`)
38 }
39 console.log(`Login: admin`)
40 console.log(`Password: # cat ${secretsRelDir}/${secName}`)
43export const plsDlKubeConfig = (hostname: string) => {
44 const {cluster_name} = getKlusterCtx()
45 console.log(`1) Goto https://${hostname}/dashboard/harvester/c/local/support`)
46 console.log(`2) Download KubeConfig to ${getKubeConfigRelPath(cluster_name)}`)
49const sshExecQuiet = async (sshHost: string, cmd: string, stepName: string) => {
50 const {stdout} = await liveSpawnThrow({
51 cmd: `ssh ${sshHost} '${cmd}'`,
52 isQuiet: true, noOutCmd: true,
53 }).catch(err => { err.message = `${stepName}: ${err.message}`; throw err })
54 return stdout.trim()
57const rancherApiUrl = (clusterVip: string) => `https://${clusterVip}`
59export const k8sHarvDashboardPw = async ({nodeHostname}: {nodeHostname?: string} = {}) => {
60 const ctx = getKlusterCtx() as HarvKlusterCfg & {sshNodeHostname?: string}
61 const {cluster_name, clusterVip} = ctx
62 nodeHostname ||= ctx.sshNodeHostname || ctx.nodeHostname!
63 assertDefined(clusterVip, {cluster_name})
64 const secName = `harv-dashboard-${cluster_name}`
65 const stablePw = getGenPlainSec(secName)
66 const sshHost = `rancher@${nodeHostname}`
67 const rancherApi = rancherApiUrl(clusterVip)
68 const rancherPodCmd = `sudo -i kubectl -n cattle-system get pods -l app=rancher --no-headers | head -1 | awk "{ print \\$1 }"`
69 const bootstrap = await sshExecQuiet(sshHost,
70 `sudo -i kubectl -n cattle-system exec $( ${rancherPodCmd} ) -c rancher -- reset-password 2>/dev/null | tail -1`,
71 'reset-password')
72 const token = await sshExecQuiet(sshHost,
73 `curl -sk ${rancherApi}/v3-public/localProviders/local?action=login -H "Content-Type: application/json" -d "{\\"username\\":\\"admin\\",\\"password\\":\\"${bootstrap}\\"}" | jq -r .token`,
74 'rancher-login')
75 await sshExecQuiet(sshHost,
76 `curl -sk ${rancherApi}/v3/users?action=changepassword -H "Authorization: Bearer ${token}" -H "Content-Type: application/json" -d "{\\"currentPassword\\":\\"${bootstrap}\\",\\"newPassword\\":\\"${stablePw}\\"}" > /dev/null`,
77 'change-password')
78 await sshExecQuiet(sshHost,
79 `sudo -i kubectl patch settings.management.cattle.io first-login -p "{\\"value\\":\\"false\\"}" --type=merge`,
80 'patch-first-login')
81 printHarvDashboardLogin({cluster_name, vipHost: clusterVip})
83export { k8sHarvDashboardPw as resetHarvesterDashboardPw }
85export const dlKubeConfig = async ({nodeHostname}: {nodeHostname?: string} = {}) => {
86 const ctx = getKlusterCtx() as HarvKlusterCfg & {sshNodeHostname?: string}
87 const {cluster_name, clusterVip} = ctx
88 nodeHostname ||= ctx.sshNodeHostname || ctx.nodeHostname!
89 const kcAbsPath = getKubeConfigPath(cluster_name)
90 const kcRelPath = getKubeConfigRelPath(cluster_name)
92 if (await fileExists(tsAbsPath(kcAbsPath))) {
93 const {isSuccess} = await liveSpawn({
94 cmd: `kubectl --kubeconfig=${kcAbsPath} get nodes --request-timeout=5s`,
95 isQuiet: true, noOutCmd: true, timeoutAfterSec: 10,
96 })
97 if (isSuccess) {
98 console.log(chalkGreen(`✓ ${kcRelPath} valid and connectable`))
99 return
100 }
101 console.log(chalkYellow(`${kcRelPath} exists but not connectable, re-downloading...`))
102 }
104 const secName = `harv-dashboard-${cluster_name}`
105 const dashboardPw = getGenPlainSec(secName)
106 const sshHost = `rancher@${nodeHostname}`
107 const vipOrHostname = clusterVip || nodeHostname
108 const rancherApi = rancherApiUrl(clusterVip!)
109 const token = await sshExecQuiet(sshHost,
110 `curl -sk ${rancherApi}/v3-public/localProviders/local?action=login -H "Content-Type: application/json" -d "{\\"username\\":\\"admin\\",\\"password\\":\\"${dashboardPw}\\"}" | jq -r .token`,
111 'rancher-login').catch(() => '')
112 const rawConfig = token ? await sshExecQuiet(sshHost,
113 `curl -sk ${rancherApi}/v1/management.cattle.io.clusters/local?action=generateKubeconfig -H "Authorization: Bearer ${token}" -X POST | jq -r .config`,
114 'generate-kubeconfig').catch(() => '') : ''
115 if (!rawConfig || rawConfig === 'null') {
116 console.log(chalkYellow(`failed to generate kubeconfig via API, run k8sHarvDashboardPw first`))
117 plsDlKubeConfig(vipOrHostname)
118 return
119 }
120 const kubeConfigStr = replaceAll(rawConfig, 'https://localhost/', `https://${vipOrHostname}/`)
121 await fs1Promises.writeFile(tsAbsPath(kcAbsPath), kubeConfigStr)
122 console.log(chalkGreen(`downloaded kubeconfig to ${kcRelPath}`))
123 console.log(`( fyi if this doesn't work - you can also manually download kubeconfig from https://${vipOrHostname}/dashboard/harvester/c/local/support )`)
126export const k8sUpdateKubeconfig = async () => {
127 const ctx = getKlusterCtx() as HarvKlusterCfg & {klustCertsH?: Record<string, string | string[]>}
128 const {cluster_name, clusterVip, klusterVipHostname, klustCertsH} = ctx
129 assertDefined(clusterVip, {cluster_name})
130 const kcAbsPath = getKubeConfigPath(cluster_name)
131 const kcRelPath = getKubeConfigRelPath(cluster_name)
132 const kcContent = await read1File(tsAbsPath(kcAbsPath))
133 const vipUrl = `https://${clusterVip}/`
134 if (!kcContent.includes(vipUrl)) {
135 console.log(chalkGreen(`✓ ${kcRelPath} already uses hostname`))
136 return
137 }
138 const lanSuffix = klusterVipHostname?.replace('*.', '')
139 const extWcSubdomain = Object.values(klustCertsH || {}).flat().map(d => d.replace('*.', '')).find(d => d !== lanSuffix)
140 assertDefined(extWcSubdomain, {cluster_name, klustCertsH})
141 const dashboardSec = getGenPlainSec(`harv-dashboard-${cluster_name}`)
142 const dashboardPrefix = harvDashboardHostnamePrefix(cluster_name, dashboardSec)
143 const extHost = `${dashboardPrefix}.${extWcSubdomain}`
144 try { await dnsLookup(extHost) } catch {
145 console.log(chalkYellow(`${extHost} not resolving yet, skipping kubeconfig update`))
146 return
147 }
148 const hostnameUrl = `https://${extHost}/`
149 let updatedKc = kcContent.replaceAll(vipUrl, hostnameUrl)
150 updatedKc = updatedKc.replace(/\n\s+certificate-authority-data:\s+"[^"]+"/g, '')
151 const tmpPath = kcAbsPath + '.tmp'
152 await fs1Promises.writeFile(tsAbsPath(tmpPath as any), updatedKc)
153 const {isSuccess} = await liveSpawn({
154 cmd: `kubectl --kubeconfig=${tmpPath} get nodes --request-timeout=5s`,
155 isQuiet: true, noOutCmd: true, timeoutAfterSec: 10,
156 })
157 if (!isSuccess) {
158 await fs1Promises.unlink(tsAbsPath(tmpPath as any))
159 console.log(chalkYellow(`kubeconfig with ${extHost} not connectable, keeping VIP`))
160 return
161 }
162 await fs1Promises.rename(tsAbsPath(tmpPath as any), tsAbsPath(kcAbsPath))
163 console.log(chalkGreen(`updated ${kcRelPath}: ${clusterVip} → ${extHost}`))
166export const k8sSetupCli = async () => {
167 const importMetaUrl = (importMetaUrlCtx.getStore() as {importMetaUrl?: string} | undefined)?.importMetaUrl
168 assertDefined(importMetaUrl)
169 const {cluster_name} = getKlusterCtx({})
171 const callerDir = dirName(urlToPath(importMetaUrl))
172 const cliMjsAbsPath = callerDir + '/cli.mjs'
173 const cliMjsRelPath = absPathToPtPath(tsAbsPath(cliMjsAbsPath))
175 if (!await fileExists(tsAbsPath(cliMjsAbsPath))) {
176 const callerPtPath = absPathToPtPath(tsAbsPath(callerDir))
177 const isPt0 = callerPtPath.startsWith('pt0/')
178 const depthFromPt0 = isPt0 ? callerPtPath.slice(4).split('/').length : 0
179 const pt0RelPath = isPt0
180 ? '../'.repeat(depthFromPt0).slice(0, -1)
181 : callerPtPath.replace(/[^/]+/g, '..') + '/pt0'
182 await write1File(tsAbsPath(cliMjsAbsPath),
183`import { eptKubeCli } from '${pt0RelPath}/deployF/k8sF/cliF/epKubeCliF.mts'
184import { klusterCtx } from '${pt0RelPath}/deployF/k8sF/ctxF/klusterCtxF.mts'
185import * as klusterCfg from './common.mjs'
186import { getProcArgv } from '${pt0RelPath}/serverF/isDirectlyRunF.mts'
188klusterCtx.enterWith(klusterCfg)
189await eptKubeCli(getProcArgv(2))
190`)
191 console.log(`created ${cliMjsRelPath}`)
192 }
194 const binRelPath = `pt0/path_bin/${cluster_name}`
195 const binAbsPath = ptDir + '/' + binRelPath
196 if (!await fileExists(tsAbsPath(binAbsPath))) {
197 await write1File(tsAbsPath(binAbsPath),
198`#!/bin/sh
199ptnode ${cliMjsRelPath} ${cluster_name} "$@"
200`)
201 await liveSpawnThrow({cmd: `chmod +x ${binAbsPath}`, isQuiet: true, noOutCmd: true})
202 console.log(`created ${binRelPath}`)
203 }