1import { execSync } from 'node:child_process' 2import * as fs from 'node:fs' 3import * as _ from 'lodash-es' 34const defaultCodeservName = 'codeserv', codeservPort = 8080 35const getCodeservName = () => codeservCtx.getStore()?.name || defaultCodeservName 36const sshKeySecretName = 'codeserv-ssh-key', sshPubKeySecretName = 'codeserv-ssh-key-pub' 38const ingressAnnotationsH = { 39 'nginx.ingress.kubernetes.io/proxy-read-timeout': '86400', 40 'nginx.ingress.kubernetes.io/proxy-send-timeout': '86400', 41 'nginx.ingress.kubernetes.io/proxy-http-version': '1.1', 42 'nginx.ingress.kubernetes.io/proxy-buffering': 'off', 45export const genSshKeypairIfMissing = async () => { 48 if (!fs.existsSync(privKeyPath)) { 49 betLog('Generating SSH keypair for codeserv...') 50 execSync(`ssh-keygen -t ed25519 -f "${privKeyPath}" -N "" -C "codeserv@k8s"`, {stdio: 'pipe'}) 51 fs.renameSync(privKeyPath + '.pub', pubKeyPath) 52 betLog('Generated SSH keypair', {privKeyPath, pubKeyPath}) 54 return {privateKey: fs.readFileSync(privKeyPath, 'utf8'), publicKey: fs.readFileSync(pubKeyPath, 'utf8')} 57export const getSecretsForEp = async (epRelPath: string) => { 60 const mod = await import(epFullPath) 61 const appConfig = mod.default 62 const secretNames: string[] = appConfig?.secretNames || [] 63 const {cluster_name, dockreg_host} = appConfig 64 const clusterSecrets: string[] = [] 66 clusterSecrets.push(`${cluster_name}.yaml`) 69 clusterSecrets.push(regCredName) 72 if (hetzn1Kan?.kanikoClusterName && hetzn1Kan.kanikoClusterName !== cluster_name) { 73 clusterSecrets.push(`${hetzn1Kan.kanikoClusterName}.yaml`) 75 return {secretNames, clusterSecrets} 78export const collectSecretsForEps = async (deployableEps: string[]) => { 79 const allSecrets: string[] = [], allClusterSecrets: string[] = [] 80 for (const epPath of deployableEps) { 81 const {secretNames, clusterSecrets} = await getSecretsForEp(epPath) 82 allSecrets.push(...secretNames) 83 allClusterSecrets.push(...clusterSecrets) 85 return {secretNames: _.uniq(allSecrets), clusterSecrets: _.uniq(allClusterSecrets)} 88export const syncCodeserv = async () => { 90 const {wcDomain, codeservHostname: explicitHostname, gitName, gitEmail, deployableEps = [], gitsshRemoteUrl, gitsshK8sName, isThrowawayEp} = ctxStore 95 const {action, cluster_name} = appCfg 96 const csName = getCodeservName() 97 const csSizeGb = ctxStore.sizeGb || 50 99 if (action === 'help') return doEpSimpleDfDeploy({...appCfg, importMetaUrl: import.meta.url, name: csName}) 101 // Use explicit hostname if provided, otherwise derive from wcDomain 102 const codeservHostname = explicitHostname ?? await getDerivedHostname({name: csName, wcDomain: wcDomain!}) 105 // Register runtests action - wrapper discards result for PtAction compat, actual result used in action === 'runtests' branch 107 runtestsAction.cliDescript = 'verify HTTPS responds' 108 availActionsCtx.enterWith({...availActionsCtx.getStore(), runtests: runtestsAction}) 110 if (action === 'runtests') { 111 if (!isThrowawayEp) { 112 console.log(chalkYellow('\n⚠️ SKIP: runtests is only available on throwaway entrypoints')) 113 console.log(chalkYellow(' Use sync_throwaway.mjs for destructive deploy tests\n')) 117 if (!result.passed) process.exitCode = 1 120 const {privateKey: sshPrivKey, publicKey: sshPubKey} = await genSshKeypairIfMissing() 121 const {secretNames: epSecrets, clusterSecrets} = await collectSecretsForEps(deployableEps) 122 const secretNames = _.uniq([...epSecrets, ...clusterSecrets]) 126 const authSecName = `${csName}-auth`, sshSecName = `${csName}-ssh` 127 const gitsshRepoPath = gitsshRemoteUrl?.replace(/^ssh:\/\/[^/]+/, '') || `/home/git/repos/${ossRepoGitPath}` 129 const modVolsFnc = async ({volumeMounts, volumes, initContainers, resources}: {volumeMounts: any[], volumes: any[], initContainers: any[], resources: any[]}) => { 130 if (action !== 'apply') { 131 // Only apply needs full secret data; delete/delpvcs/info just need resource metadata 139 OPENROUTER_API_KEY: openrouterKey, GIT_NAME: gitName, GIT_EMAIL: gitEmail, 140 OPENCODE_SERVER_PASSWORD: ocPassword as string, 144 apiVersion: 'v1', kind: 'Secret', metadata: {name: sshSecName}, type: 'Opaque', 145 data: {'id_ed25519': Buffer.from(sshPrivKey).toString('base64'), 'id_ed25519.pub': Buffer.from(sshPubKey).toString('base64')}, 148 volumes.push({name: 'ssh-key', secret: {secretName: sshSecName, defaultMode: 0o600}}) 150 if (secretNames.length > 0) { 151 const secretsVols = getVolumesForSecrets({secretNames, envConf: undefined, cluster_name: cluster_name!, name: csName, action, appRoot: '/workspace/monor'}) 152 volumes.push(...secretsVols.volumes) 153 volumeMounts.push(...secretsVols.volumeMounts) 154 await Promise.all(secretsVols.promises) 158 initContainers.push({ 159 name: 'init-workspace', image, command: ['node', '/init-workspace.mjs'], 160 envFrom: [{secretRef: {name: authSecName}}], 161 env: [{name: 'GITSSH_HOST', value: gitsshK8sName}, {name: 'GITSSH_REPO', value: gitsshRepoPath}], 162 volumeMounts: [{name: 'ssh-key', mountPath: '/ssh-key', readOnly: true}, ...volumeMounts], 165 volumeMounts.push({name: 'ssh-key', mountPath: '/ssh-key', readOnly: true}) 168 nextContCtx.enterWith({ 169 ...nextContCtx.getStore(), 170 command: ['/entrypoint.sh'], 171 ports: [{containerPort: codeservPort}], 172 envFrom: [{secretRef: {name: authSecName}}], 173 env: [{name: 'GITSSH_HOST', value: gitsshK8sName}, {name: 'GITSSH_REPO', value: gitsshRepoPath}, {name: 'GITSSH_SSH_SECRET', value: sshSecName}, {name: 'OCWEB', value: '1'}, {name: 'NODE_OPTIONS', value: '--disable-warning=ExperimentalWarning'}], 174 resources: {requests: {cpu: '100m', memory: '256Mi'}, limits: {cpu: '2000m', memory: '4Gi'}}, 177 ptKubeCtx.enterWith({...ptKubeCtx.getStore(), modVolsFnc}) 179 // pvcEncName set by caller via pvcTypeCtx (e.g. syncGitSshOcWeb or standalone sync) 181 ...appCfg, mountPath: '/workspace', sizeGb: csSizeGb, kube_extHostname: codeservHostname, 182 svcPortNo: codeservPort, importMetaUrl: import.meta.url, name: csName, ingressAnnotationsH: { 183 ...ingressAnnotationsH, 184 'nginx.ingress.kubernetes.io/websocket-services': csName, 187 {src: ptAnchorPath('pt0/opencodeF/k8sF/copyToDocker/entrypoint-ocweb.sh'), dest: '/entrypoint.sh'}, 188 {src: ptAnchorPath('pt0/opencodeF/k8sF/copyToDocker/initWorkspace.mjs'), dest: '/init-workspace.mjs'}, 191 'apt-get update && apt-get install -y --no-install-recommends git curl openssh-client vim procps && npx playwright install-deps chromium && rm -rf /var/lib/apt/lists/*', 192 corepackInstallLine.replace('RUN ', ''), 193 'curl -fsSL https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-linux-x64.tar.gz | tar -xz -C /usr/local/bin', 194 'curl -fsSL -o /usr/local/bin/kubectl "https://dl.k8s.io/release/$(curl -fsSL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && chmod +x /usr/local/bin/kubectl', 198 if (action === 'apply' || action === 'info') { 200 const password = await read1File(passwordPath).catch(() => 'unknown') 204URL: https://${codeservHostname} 208Secrets mounted: ${secretNames.join(', ')}