🌳
pt0/klustersF/hetzn1/epSetupHetznerAI.mjs
1import { execSync } from 'node:child_process'
2import * as fs from 'node:fs'
13const sshKeySecretName = 'hetzn1-ssh-key', sshPubKeySecretName = 'hetzn1-ssh-key-pub'
14const klusterDir = `${ptDir}/pt0/klustersF/hetzn1`
15const kubeconfigPath = pathDownJoin(secretsDir, 'hetzn1.yaml')
17const sshCmd = (cmd) => execSync(
18 `ssh -i ${pathDownJoin(secretsDir, sshKeySecretName)} -o StrictHostKeyChecking=accept-new root@${serverIp} '${cmd}'`,
19 {encoding: 'utf8', stdio: ['pipe', 'pipe', 'inherit']}
20).trim()
22// 1. Generate SSH keypair if missing
23const genSshKeypairIfMissing = () => {
24 const privKeyPath = pathDownJoin(secretsDir, sshKeySecretName)
25 const pubKeyPath = pathDownJoin(secretsDir, sshPubKeySecretName)
26 if (!fs.existsSync(privKeyPath)) {
27 betLog('Generating SSH keypair for hetzn1...')
28 execSync(`ssh-keygen -t ed25519 -f "${privKeyPath}" -N "" -C "hetzn1@k3s"`, {stdio: 'pipe'})
29 fs.renameSync(privKeyPath + '.pub', pubKeyPath)
30 betLog('Generated SSH keypair', {privKeyPath: absPathToPtPath(privKeyPath), pubKeyPath: absPathToPtPath(pubKeyPath)})
31 } else {
32 betLog('SSH keypair exists', {privKeyPath: absPathToPtPath(privKeyPath)})
33 }
34 return {privateKey: fs.readFileSync(privKeyPath, 'utf8'), publicKey: fs.readFileSync(pubKeyPath, 'utf8')}
37// 2. Set hostname (idempotent)
38const setHostname = () => {
39 const current = sshCmd('hostname')
40 if (current !== 'hetzn1') {
41 betLog('Setting hostname to hetzn1...')
42 sshCmd('hostnamectl set-hostname hetzn1')
43 } else {
44 betLog('Hostname already set', {current})
45 }
48// 3. Install k3s (idempotent)
49const installK3s = () => {
50 const hasK3s = sshCmd('which k3s || echo ""')
51 if (!hasK3s) {
52 betLog('Installing k3s...')
53 sshCmd('curl -sfL https://get.k3s.io | sh -')
54 betLog('k3s installed')
55 } else {
56 betLog('k3s already installed', {hasK3s})
57 }
60// 3b. Disable traefik (idempotent) - we use ingress-nginx instead
61const disableTraefik = () => {
62 const configPath = '/etc/rancher/k3s/config.yaml'
63 const hasConfig = sshCmd(`test -f ${configPath} && echo "exists" || echo ""`)
64 const configContent = hasConfig ? sshCmd(`cat ${configPath}`) : ''
66 if (configContent.includes('disable:') && configContent.includes('traefik')) {
67 betLog('traefik already disabled in k3s config')
68 return false
69 }
71 betLog('Disabling traefik in k3s...')
72 // Use echo -e for proper newline interpretation
73 if (!hasConfig) {
74 sshCmd(`mkdir -p /etc/rancher/k3s && echo -e "disable:\\n - traefik" > ${configPath}`)
75 } else {
76 sshCmd(`echo -e "\\ndisable:\\n - traefik" >> ${configPath}`)
77 }
79 betLog('Restarting k3s to apply config...')
80 sshCmd('systemctl restart k3s')
81 betLog('k3s restarted, traefik disabled')
82 return true
85// 3c. Registry mirrors via regcache (idempotent) - rewrite maps upstream repo paths onto zot's /<host>/ destinations
86const writeRegistriesYaml = () => {
87 const registriesYaml = ['mirrors:', ...upstreamRegHosts.flatMap((host) => [
88 ` ${host}:`,
89 ` endpoint:`,
90 ` - "https://${regcacheHost}"`,
91 ` rewrite:`,
92 ` "^(.+)": "${host}/$1"`,
93 ])].join('\n') + '\n'
94 const current = sshCmd('cat /etc/rancher/k3s/registries.yaml 2>/dev/null || echo ""')
95 if (current.trim() === registriesYaml.trim()) {
96 betLog('registries.yaml already up to date')
97 return
98 }
99 betLog('Writing k3s registries.yaml (registry mirrors via regcache)...')
100 const b64 = Buffer.from(registriesYaml).toString('base64')
101 sshCmd(`mkdir -p /etc/rancher/k3s && echo ${b64} | base64 -d > /etc/rancher/k3s/registries.yaml`)
102 sshCmd('systemctl restart k3s')
103 betLog('k3s restarted, registries.yaml applied')
106// 4. Extract kubeconfig (idempotent)
107const extractKubeconfig = () => {
108 if (!fs.existsSync(kubeconfigPath)) {
109 betLog('Extracting kubeconfig...')
110 let kubeconfig = sshCmd('cat /etc/rancher/k3s/k3s.yaml')
111 kubeconfig = kubeconfig.replace('127.0.0.1', serverIp)
112 fs.writeFileSync(kubeconfigPath, kubeconfig)
113 betLog('Kubeconfig saved', {kubeconfigPath: absPathToPtPath(kubeconfigPath)})
114 } else {
115 betLog('Kubeconfig exists', {kubeconfigPath: absPathToPtPath(kubeconfigPath)})
116 }
119// 5. Create common.mjs (idempotent)
120const createCommonMjs = () => {
121 const path = `${klusterDir}/common.mjs`
122 if (fs.existsSync(path)) return betLog('common.mjs exists')
123 fs.writeFileSync(path, `export const cluster_name = 'hetzn1'
124export const k8sCloudName = 'k3s'
125export const serverIp = '${serverIp}'
126`)
127 betLog('Created common.mjs')
130// 6. Create cli.mjs (idempotent)
131const createCliMjs = () => {
132 const path = `${klusterDir}/cli.mjs`
133 if (fs.existsSync(path)) return betLog('cli.mjs exists')
134 fs.writeFileSync(path, `import { eptKubeCli } from '../../../deployF/k8sF/cliF/epKubeCliF.mts'
136import * as klusterCfg from './common.mjs'
137import { getProcArgv } from '../../../serverF/isDirectlyRunF.mts'
139klusterCtx.enterWith(klusterCfg)
140await eptKubeCli(getProcArgv(2))
141`)
142 betLog('Created cli.mjs')
145// 7. Create path_bin/hetzn1 (idempotent)
146const createPathBin = () => {
147 const path = `${ptDir}/pt0/path_bin/hetzn1`
148 if (fs.existsSync(path)) return betLog('path_bin/hetzn1 exists')
149 fs.writeFileSync(path, `#!/bin/sh
150ptnode pt0/klustersF/hetzn1/epCli.mjs hetzn1 "$@"
151`)
152 fs.chmodSync(path, 0o755)
153 betLog('Created path_bin/hetzn1')
156// 8. Verify cluster
157const verifyCluster = () => {
158 betLog('Verifying cluster...')
159 const nodes = execSync(`kubectl --kubeconfig=${kubeconfigPath} get nodes -o wide`, {encoding: 'utf8'})
160 console.log(nodes)
163// Teardown - reverse of setup
164const teardown = () => {
165 const hasK3s = sshCmd('which k3s || echo ""')
166 if (hasK3s) {
167 betLog('Uninstalling k3s...')
168 sshCmd('/usr/local/bin/k3s-uninstall.sh')
169 betLog('k3s uninstalled')
170 } else {
171 betLog('k3s not installed, skipping uninstall')
172 }
174 betLog('Resetting hostname...')
175 sshCmd('hostnamectl set-hostname localhosttorndown')
177 if (fs.existsSync(kubeconfigPath)) {
178 fs.unlinkSync(kubeconfigPath)
179 betLog('Removed local kubeconfig', {kubeconfigPath: absPathToPtPath(kubeconfigPath)})
180 }
182 console.log('\n=== Teardown complete. hetzn1 is now a fresh node. ===\n')
184teardown.cliDescript = 'uninstall k3s and reset node to fresh state'
186// Check SSH connectivity before proceeding
187const checkSshConnectivity = (publicKey) => {
188 try {
189 sshCmd('echo ok')
190 return true
191 } catch {
192 console.log('\nSSH not working yet. Add the public key above to Hetzner Robot.')
193 console.log('Then re-run: ptnode pt0/klustersF/hetzn1/epSetupHetznerAI.mjs setup\n')
194 return false
195 }
198// Setup - run all steps
199const setup = () => {
200 const {publicKey} = genSshKeypairIfMissing()
201 console.log('\n=== SSH Public Key (for Hetzner Robot) ===\n')
202 console.log(publicKey)
204 if (!checkSshConnectivity(publicKey)) return
206 setHostname()
207 installK3s()
208 disableTraefik()
209 writeRegistriesYaml()
210 extractKubeconfig()
211 createCommonMjs()
212 createCliMjs()
213 createPathBin()
214 verifyCluster()
216 console.log('\n=== Done! Test with: hetzn1 get nodes ===\n')
218setup.cliDescript = 'install k3s and configure local kubeconfig (idempotent)'
220const ep = {importMetaUrl: import.meta.url, epName: 'hetzn1-setup'}
221export default ep
223if (isDirectlyRun(import.meta.url)) {
224 const actionsH = {setup, teardown}
225 const {action} = cliBase({actionNames: ['setup', 'teardown', 'help'], actionsH, exampleName: ep.epName})
226 if (action === 'setup') setup()
227 else if (action === 'teardown') teardown()