🌳
pt0/deployF/dockerF/deploySimpleDfF.mts
2import { klusterCtx, type KlusterCfg } from "../k8sF/ctxF/klusterCtxF.mts"
7import * as _ from 'lodash-es'
36import { tsAbsPath } from '../../ptDirF.mts'
41type CopyPathEntry = {src: CopyablePtPath, dest: string} | CopyablePtPath
43export const eptSimpleDfDeployK8sRes = {kind: 'Deployment', apiVersion: 'apps/v1'}
44export const eptSimpleDfDeploy = (props: Record<string, any>) => {
45 return wrapKubeCoalesce(doDeploySimpleDf, props)
48const doDeploySimpleDf = async ({dockerfileDirPath, copyPathsA, dockerRunCmdsA, usePnpm, appPath, aptPkgNames, extraDockerSetupLine, fromImgName, bundleEntryPtPath, ...props}: {dockerfileDirPath?: string, copyPathsA?: CopyPathEntry[], dockerRunCmdsA?: string[], usePnpm?: boolean, appPath?: string, aptPkgNames?: string[], extraDockerSetupLine?: string, fromImgName?: string, bundleEntryPtPath?: string, [k: string]: any}) => {
49 props = setPtSubdirForImportMetaUrl(props as any)
51 const {action} = props
53 ptDeployActionsCtx.enterWith(dockerActions)
55 if (!cliValidatedCtx.getStore()?.validated) {
56 const actionsH = {...resourcesActionsH(), ...getDeployActions()}
57 cliHelp({action, actionsH})
58 }
60 const relDir = getParentDir(getImportMetaUrlPath(props.importMetaUrl))
61 klusterCtx.enterWith(props as KlusterCfg)
63 // Extract JS entry files from copyPathsA for import scanning
64 const srcPaths = copyPathsA?.map((p: any) => p.src || p) || []
65 const jsEntryPaths = srcPaths.filter((p: string) => isPtCodeFileExt(p))
66 const epPtPathsA = jsEntryPaths.length > 0 ? jsEntryPaths : undefined
68 props = {...props, epPtPathsA, usesExplicitEpPtPaths: true}
69 appCfgCtx.enterWith(props)
71 const git_sha = await getHeadGitSha({filterPathList: [relDir]})
73 // Build dockerfile from import tree (extracted for lazy/immediate use)
74 const buildImportTreeDf = async () => {
75 if (bundleEntryPtPath) {
76 // esbuild single-file bundle image (no node_modules layer) — the multi-GB pnpm-deploy escape
77 assertDefined(appPath, {bundleEntryPtPath})
78 return [await getDockerBaseStr({afterYarnInstallDockPathsA: [], appPath, aptPkgNames, extraDockerSetupLine, fromImgName, bundle: true, entrypointAbsPath: tsAbsPath(bundleEntryPtPath), bundleName: props.name})]
79 }
80 const importTreePaths = await epsToPathsGitCached({epPtPathsA: epPtPathsA!})
81 const nonJsPaths = srcPaths.filter((p: string) => !isPtCodeFileExt(p))
82 const allDockerPaths = [...importTreePaths, ...nonJsPaths]
84 const destMappings = copyPathsA?.filter((p): p is {src: CopyablePtPath, dest: string} => typeof p !== 'string' && 'dest' in p) || []
85 const pushExtraRunCmds = (df: ReturnType<typeof initDockFile>) => {
86 if (destMappings.length) {
87 const destDirs = [...new Set(destMappings.map(p => p.dest.replace(/\/[^/]+$/, '')))].filter(Boolean)
88 const lnCmds = destMappings.map(p => `ln -sf ${dockerApproot}/${p.src} ${p.dest}`).join(' && ')
89 df.pushLine(`RUN ${destDirs.length ? destDirs.map(d => `mkdir -p ${d}`).join(' && ') + ' && ' : ''}${lnCmds}`)
90 }
91 for (const cmd of dockerRunCmdsA || []) df.pushLine(`RUN ${cmd}`)
92 }
94 let df: ReturnType<typeof initDockFile>
95 if (usePnpm && appPath) {
96 // pnpm-workspace path: resolves cross-package npm deps via the workspace (getDockerBaseStr)
97 df = await getDockerBaseStr({afterYarnInstallDockPathsA: allDockerPaths, appPath, aptPkgNames, extraDockerSetupLine, fromImgName})
98 pushExtraRunCmds(df)
99 } else {
100 // madge-tar path: self-contained leaf services (cross-package npm deps not resolved)
102 const tarResult = await dockerTarPaths({dockerPathsA: allDockerPaths})
103 df.pushLine(`FROM ${nodeJsDockerImage()}`)
104 df.pushLine(`WORKDIR ${dockerApproot}`)
105 df.pushCpTar(tarResult)
106 pushExtraRunCmds(df)
107 }
109 const firstEntry = jsEntryPaths[0]
110 if (firstEntry) {
111 df.pushLine(`WORKDIR ${dockerApproot}/${getParentDir(firstEntry)}`)
112 }
113 return [df]
114 }
116 // If we have JS entry files, set up dockerfile (immediate or lazy)
117 if (epPtPathsA) {
118 if (deployActionGensDf({action})) {
119 props = {...props, dockerFilesA: await buildImportTreeDf(), git_sha}
120 } else {
121 props = {...props, dockerBuilder: buildImportTreeDf, git_sha}
122 }
123 } else {
124 // Legacy: use explicit Dockerfile (no JS entry files)
125 const dfToBuild = initDockFile()
126 const dockerfilePath = dockerfileDirPath ? dockerfileDirPath + '/Dockerfile' : relDir + '/Dockerfile'
127 const dockerfileContent = await read1File(tsAbsPath(dockerfilePath))
128 dfToBuild.pushLine(dockerfileContent)
130 if (copyPathsA && copyPathsA.length > 0) {
131 if (isDeployAction(action)) {
132 const uncommittedInCopyPaths = getUncommittedInPathSet(srcPaths)
133 assertEmpty(uncommittedInCopyPaths, {uncommittedInCopyPaths})
134 }
135 const copyPathsSha = await getHeadGitSha({filterPathList: srcPaths})
136 dfToBuild.pushLine(`LABEL copy_paths_sha=${copyPathsSha}`)
137 for (const p of copyPathsA) {
138 if (typeof p !== 'string' && p.src && p.dest) dfToBuild.pushLine(`COPY ${p.src} ${p.dest}`)
139 }
140 }
141 props = {...props, dockerFilesA: [dfToBuild], git_sha}
142 }
144 appCfgCtx.enterWith(props)
146 // Handle extraActions (rotatewchn, etc) that don't need docker setup
147 if (await tryDispatchAction()) return
148 // Handle ptDeployActions (lscode, diff, dockerfile, etc)
149 if (await doPtDeployAction()) return
151 if (action === 'apply') {
152 const {dockerFilesA, dockerBuilder} = props
153 const dfA = dockerFilesA || (dockerBuilder && await dockerBuilder())
154 if (dfA) {
155 props = {...props, dockerFilesA: dfA}
156 appCfgCtx.enterWith(props)
157 await dockerBuildAndRet({dfToBuild: dfA[0]})
158 }
159 }
161 const {name, mountPath, sizeGb, pvcStorClassName} = props
162 let pvcVolMnts
163 if (mountPath && sizeGb) {
164 pvcVolMnts = await pvcAndVolumeMounts({name, mountPath, sizeGb, ...(pvcStorClassName && {pvcStorClassName})})
165 }
167 nextVolMntsCtx.enterWith(pvcVolMnts!)
168 const existingContCtx = nextContCtx.getStore() || {}
169 // Only set default sshd command if no command/ports specified (for gitssh-style deploys)
170 const defaultContCtx = existingContCtx.ports ? {} : {command: ['/usr/sbin/sshd', '-D', '-e'], ports: [{containerPort: 22}]}
171 nextContCtx.enterWith({...defaultContCtx, ...existingContCtx})
173 ...props,
174 git_sha
175 })
176 const {nodePortNo, kube_extHostname, svcPortNo, cluster_name} = props
177 if (nodePortNo || kube_extHostname) {
178 assertDefined(svcPortNo)
180 if (kube_extHostname) {
182 } else if (nodePortNo) {
183 await resourcesAction({resources: [
184 genericNodePortTmpl({name, nodePort: nodePortNo, svcPort: svcPortNo})
185 ]})
186 const {clusterVip, nodeIpsExtHost} = props
187 const accessHost = clusterVip || nodeIpsExtHost || `<node-ip>`
188 printDeployedUrl({action, nodePortUrl: `${accessHost}:${nodePortNo}`})
189 }
190 } else {
191 if (action != 'delete') {
192 console.log(`\nYour app is hosted but not exposed
193please specify "nodePortNo" or "kube_extHostname"`)
194 }
195 }
196 await watchFlogs()