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` 17const sshCmd = (cmd) => execSync( 19 {encoding: 'utf8', stdio: ['pipe', 'pipe', 'inherit']} 22// 1. Generate SSH keypair if missing 23const genSshKeypairIfMissing = () => { 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) 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') 44 betLog('Hostname already set', {current}) 48// 3. Install k3s (idempotent) 49const installK3s = () => { 50 const hasK3s = sshCmd('which k3s || echo ""') 53 sshCmd('curl -sfL https://get.k3s.io | sh -') 56 betLog('k3s already installed', {hasK3s}) 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') 71 betLog('Disabling traefik in k3s...') 72 // Use echo -e for proper newline interpretation 74 sshCmd(`mkdir -p /etc/rancher/k3s && echo -e "disable:\\n - traefik" > ${configPath}`) 76 sshCmd(`echo -e "\\ndisable:\\n - traefik" >> ${configPath}`) 79 betLog('Restarting k3s to apply config...') 80 sshCmd('systemctl restart k3s') 81 betLog('k3s restarted, traefik disabled') 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) => [ 92 ` "^(.+)": "${host}/$1"`, 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') 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)) { 110 let kubeconfig = sshCmd('cat /etc/rancher/k3s/k3s.yaml') 111 kubeconfig = kubeconfig.replace('127.0.0.1', serverIp) 112 fs.writeFileSync(kubeconfigPath, kubeconfig) 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' 130// 6. Create cli.mjs (idempotent) 131const createCliMjs = () => { 132 const path = `${klusterDir}/cli.mjs` 133 if (fs.existsSync(path)) return betLog('cli.mjs exists') 139klusterCtx.enterWith(klusterCfg) 140await eptKubeCli(getProcArgv(2)) 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 "$@" 152 fs.chmodSync(path, 0o755) 157const verifyCluster = () => { 159 const nodes = execSync(`kubectl --kubeconfig=${kubeconfigPath} get nodes -o wide`, {encoding: 'utf8'}) 163// Teardown - reverse of setup 164const teardown = () => { 165 const hasK3s = sshCmd('which k3s || echo ""') 168 sshCmd('/usr/local/bin/k3s-uninstall.sh') 171 betLog('k3s not installed, skipping uninstall') 175 sshCmd('hostnamectl set-hostname localhosttorndown') 177 if (fs.existsSync(kubeconfigPath)) { 178 fs.unlinkSync(kubeconfigPath) 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) => { 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') 198// Setup - run all steps 200 const {publicKey} = genSshKeypairIfMissing() 201 console.log('\n=== SSH Public Key (for Hetzner Robot) ===\n') 202 console.log(publicKey) 204 if (!checkSshConnectivity(publicKey)) return 209 writeRegistriesYaml() 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'} 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()