🌳
pt0/opencodeF/k8sF/codeservLibAI.mts
1import { execSync } from 'node:child_process'
2import * as fs from 'node:fs'
3import * as _ from 'lodash-es'
8import { eptSimpleDfDeploy as doEpSimpleDfDeploy } from "../../deployF/dockerF/deploySimpleDfF.mts"
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 () => {
46 const privKeyPath = pathDownJoin(secretsDir, sshKeySecretName)
47 const pubKeyPath = pathDownJoin(secretsDir, sshPubKeySecretName)
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})
53 }
54 return {privateKey: fs.readFileSync(privKeyPath, 'utf8'), publicKey: fs.readFileSync(pubKeyPath, 'utf8')}
57export const getSecretsForEp = async (epRelPath: string) => {
58 const epFullPath = pathDownJoin(ptDir, epRelPath)
59 assertExists(epFullPath, {epRelPath})
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[] = []
65 if (cluster_name) {
66 clusterSecrets.push(`${cluster_name}.yaml`)
67 if (dockreg_host) {
68 const regCredName = klusterCtx.run({cluster_name}, () => genRegCredSecName(dockreg_host))
69 clusterSecrets.push(regCredName)
70 }
71 }
72 if (hetzn1Kan?.kanikoClusterName && hetzn1Kan.kanikoClusterName !== cluster_name) {
73 clusterSecrets.push(`${hetzn1Kan.kanikoClusterName}.yaml`)
74 }
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)
84 }
85 return {secretNames: _.uniq(allSecrets), clusterSecrets: _.uniq(allClusterSecrets)}
88export const syncCodeserv = async () => {
89 const ctxStore = getReqCodeservCtx()
90 const {wcDomain, codeservHostname: explicitHostname, gitName, gitEmail, deployableEps = [], gitsshRemoteUrl, gitsshK8sName, isThrowawayEp} = ctxStore
91 assertDefined(gitsshK8sName)
92 assertDefined(gitName)
93 assertDefined(gitEmail)
94 const appCfg = getAppCfg()
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!})
103 assertDefined(codeservHostname, {wcDomain, explicitHostname})
105 // Register runtests action - wrapper discards result for PtAction compat, actual result used in action === 'runtests' branch
106 const runtestsAction = async () => { await runCodeservHealthcheck({hostname: codeservHostname}) }
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'))
114 return
115 }
116 const result = await runCodeservHealthcheck({hostname: codeservHostname})
117 if (!result.passed) process.exitCode = 1
118 return
119 }
120 const {privateKey: sshPrivKey, publicKey: sshPubKey} = await genSshKeypairIfMissing()
121 const {secretNames: epSecrets, clusterSecrets} = await collectSecretsForEps(deployableEps)
122 const secretNames = _.uniq([...epSecrets, ...clusterSecrets])
124 setEnvConf({nextSvcPortNo: codeservPort} as any)
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
132 resources.push(secretStubForDelete(authSecName), secretStubForDelete(sshSecName))
133 return
134 }
135 const openrouterKey = getPlainNoMappedSec('openrouter-codeserv' as any)
136 const ocPassword = await genPlainSecretIfMissing({secretName: `${csName}-oc-password` as any, autoYes: true})
138 resources.push(secretTemplate({name: authSecName, secretsH: {
139 OPENROUTER_API_KEY: openrouterKey, GIT_NAME: gitName, GIT_EMAIL: gitEmail,
140 OPENCODE_SERVER_PASSWORD: ocPassword as string,
141 }}))
143 resources.push({
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')},
146 })
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)
155 }
157 const {image} = await imageFromAppCfg(getAppCfg())
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],
163 })
165 volumeMounts.push({name: 'ssh-key', mountPath: '/ssh-key', readOnly: true})
166 }
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'}},
175 })
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,
185 },
186 copyPathsA: [
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'},
189 ],
190 dockerRunCmdsA: [
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',
195 ],
196 })
198 if (action === 'apply' || action === 'info') {
199 const passwordPath = pathDownJoin(secretsDir, `${csName}-oc-password`)
200 const password = await read1File(passwordPath).catch(() => 'unknown')
201 console.log(`
202${csName} ready!
204URL: https://${codeservHostname}
205Username: opencode
206Password: ${password}
208Secrets mounted: ${secretNames.join(', ')}
209`)
210 }