1import { pvcAndVolumeMounts } from '../k8sF/pvcAndVolumeMountsF.mts'2import { klusterCtx, type KlusterCfg } from "../k8sF/ctxF/klusterCtxF.mts"3import { wrapKubeCoalesce } from "../k8sF/kubeCoalesceF.mts"4import { watchFlogs } from '../k8sF/logsF/watchFlogsF.mts'5import { syncOtherNextAppResources } from '../../nextF/deployF/syncOtherNextAppResourcesF.mts'6import { dockerBuildAndRet } from "./dockerImageF.mts"7import * as _ from 'lodash-es'8import { appCfgCtx } from "../k8sF/ctxF/appCfgCtxF.mts"9import { nextVolMntsCtx } from "../../nextF/deployF/nextVolMntsCtxF.mts"10import { applyAndWaitRollout } from "../../nextF/deployF/mkNextDeploymentF.mts"11import { setPtSubdirForImportMetaUrl } from "../setPtDirForImportMetaUrlF.mts"12import { read1File } from '../../serverF/libFs/read1FileF.mts'13import { nextContCtx } from "../libF/nextContCtxF.mts"14import { getImportMetaUrlPath } from "../../serverF/isDirectlyRunF.mts"15import { getParentDir } from "../../serverF/libFs/getParentDirF.mts"16import { initDockFile } from "./dockFileF.mts"17import { getDockerBaseStr } from './getDockerBaseStrF.mts'18import { genericNodePortTmpl } from '../k8sF/generic1NodePortTmplF.mts'19import { assertDefined } from "../../sharedF/assertsF/assertDefinedF.mts"20import { assertEmpty } from "../../sharedF/assertsF/assertEmptyF.mts"21import { getUncommittedInPathSet } from '../libF/gitF/gitDirtyPathsAI.mts'22import { resourcesAction } from '../k8sF/resourcesActionF.mts'23import { getHeadGitSha } from '../libF/gitF/gitRepoF.mts'24import { cliHelp } from '../cliF/cliHelpF.mts'25import { resourcesActionsH, resourcesActionNames } from '../k8sF/isActionSupportedF.mts'26import { printDeployedUrl } from '../../serverF/briefForLlmF.mts'27import { cliValidatedCtx } from '../ctxF/cliValidatedCtxF.mts'28import { isDeployAction } from '../testsF/actionResultsAI.mts'29import { getDeployActions, ptDeployActionsCtx } from '../ptDeployActions/doPtDeployActionF.mts'30import { doPtDeployAction, tryDispatchAction } from '../ptDeployActions/do1PtDeployActionF.mts'31import { epsToPathsGitCached } from './dfFromMadgeF.mts'32import { dockerTarPaths } from './dockerTarPathsF.mts'33import { deployActionGensDf } from './deployActionGensDfF.mts'34import { nodeJsDockerImage } from './nodeJsDockerImageF.mts'35import { dockerApproot } from '../constantsF.mts'36import { tsAbsPath } from '../../ptDirF.mts'37import { isPtCodeFileExt } from '../../sharedF/isPtCodeFileExtF.mts'38import { dockerActions } from '../ptDeployActions/dockerActionsAI.mts'39import type { CopyablePtPath } from '../../serverF/libF/ptAnchorPathF.mts'41type CopyPathEntry = {src: CopyablePtPath, dest: string} | CopyablePtPath43export const eptSimpleDfDeployK8sRes = {kind: 'Deployment', apiVersion: 'apps/v1'}44export const eptSimpleDfDeploy = (props: Record<string, any>) => {45 return wrapKubeCoalesce(doDeploySimpleDf, props)46}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} = props53 ptDeployActionsCtx.enterWith(dockerActions)55 if (!cliValidatedCtx.getStore()?.validated) {56 const actionsH = {...resourcesActionsH(), ...getDeployActions()}58 }60 const relDir = getParentDir(getImportMetaUrlPath(props.importMetaUrl))61 klusterCtx.enterWith(props as KlusterCfg)63 // Extract JS entry files from copyPathsA for import scanning64 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 : undefined68 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 escape77 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)101 df = initDockFile()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'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 setup147 if (await tryDispatchAction()) return148 // Handle ptDeployActions (lscode, diff, dockerfile, etc)149 if (await doPtDeployAction()) return151 if (action === 'apply') {152 const {dockerFilesA, dockerBuilder} = props153 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} = props162 let pvcVolMnts163 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})172 await applyAndWaitRollout({173 ...props,174 git_sha175 })176 const {nodePortNo, kube_extHostname, svcPortNo, cluster_name} = props177 if (nodePortNo || kube_extHostname) {178 assertDefined(svcPortNo)180 if (kube_extHostname) {181 await syncOtherNextAppResources()182 } else if (nodePortNo) {183 await resourcesAction({resources: [184 genericNodePortTmpl({name, nodePort: nodePortNo, svcPort: svcPortNo})185 ]})186 const {clusterVip, nodeIpsExtHost} = props187 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 exposed193please specify "nodePortNo" or "kube_extHostname"`)194 }195 }196 await watchFlogs()197}