🌳
pt0/deployF/dockerF/dockFileF.mts
1import * as _ from 'lodash-es'
6export type BakedDf = {
7 isBakedDf: true
8 repo_name_tag: string
9 push_repo_name_tag: string
10 dockerfileContent: string
11 dfDockPathsA: string[]
12 tarSpecsA: object[]
13 includeShaInTag?: boolean
14 getRnt: () => Promise<BakedDf>
17export type DockFile = {
18 isDockFile: true
19 pushLine: (line: string | BakedDf) => void
20 pushEnv: (key: string, value: string, opts?: {append?: boolean, allowOverwrite?: boolean}) => void
21 pushCpTar: (arg: {dfCpTarStr: string, dockerPathsA: string[], tarSpec?: object}) => void
22 getRnt: () => Promise<BakedDf>
25const unquoteEnvVal = (v: string) => v.trim().replace(/^(['"])((?:\\.|(?!\1).)*)\1$/, '$2')
27const renderEnvLine = (key: string, value: string) =>
28 `ENV ${key}=${/\s/.test(value) ? `"${value}"` : value}`
30export const initDockFile = (initialLine?: string, {includeShaInTag}: {includeShaInTag?: boolean} = {}): DockFile => {
31 const linesA: string[] = initialLine ? [initialLine] : []
33 let workDir: string | undefined
34 let stageEnvH: Record<string, string> = {}
35 const dfDockPathsA: string[] = []
36 const tarSpecsA: object[] = []
37 const pushDockPath = (path: string) => dfDockPathsA.push(path)
38 const pushTarSpec = (tarSpec: object) => tarSpecsA.push(tarSpec)
39 let frozenRez: BakedDf | undefined
41 const trackEnvLine = (line: string) => {
42 if (line.startsWith('FROM ')) {
43 stageEnvH = {}
44 return
45 }
46 const m = line.match(/^ENV ([A-Za-z_][A-Za-z0-9_]*)=(.*)$/)
47 if (!m) return
48 const [, key, rawVal] = m as unknown as [string, string, string]
49 const val = unquoteEnvVal(rawVal)
50 const prev = stageEnvH[key]
51 throwIf(() => prev !== undefined && prev !== val && val !== '', {key, prevValue: prev, clobberLine: line})
52 stageEnvH[key] = val
53 }
55 const pushLine = (lineStr: string | BakedDf) => {
56 throwIf(() => frozenRez, {frozenRez})
58 if ((lineStr as BakedDf)?.isBakedDf) {
59 const df = lineStr as BakedDf
60 for (const l of df.dockerfileContent.split('\n')) trackEnvLine(l)
61 linesA.push(df.dockerfileContent)
62 for (const p of df.dfDockPathsA) pushDockPath(p)
63 for (const t of df.tarSpecsA) pushTarSpec(t)
64 return
65 }
66 assertString(lineStr, {lineStr})
67 const workdirMatch = lineStr.match(/WORKDIR (.+)/)
68 if (workdirMatch) {
69 const [, newWorkdir] = workdirMatch
70 if (newWorkdir == workDir) return
71 workDir = newWorkdir
72 }
73 for (const l of lineStr.split('\n')) trackEnvLine(l)
74 linesA.push(lineStr)
75 }
77 const pushEnv = (key: string, value: string, {append, allowOverwrite}: {append?: boolean, allowOverwrite?: boolean} = {}) => {
78 throwIf(() => frozenRez, {frozenRez})
79 const prev = stageEnvH[key]
80 if (append && prev !== undefined) {
81 const flagsA = prev.split(/\s+/)
82 const newFlagsA = value.split(/\s+/).filter(f => f && !flagsA.includes(f))
83 if (!newFlagsA.length) return
84 stageEnvH[key] = [...flagsA, ...newFlagsA].join(' ')
85 } else {
86 if (prev === value) return
87 throwIf(() => prev !== undefined && !allowOverwrite, {key, prevValue: prev, newValue: value})
88 stageEnvH[key] = value
89 }
90 linesA.push(renderEnvLine(key, stageEnvH[key]!))
91 }
92 const pushCpTar = ({dfCpTarStr, dockerPathsA, tarSpec}: {dfCpTarStr: string, dockerPathsA: string[], tarSpec?: object}) => {
93 pushLine(dfCpTarStr)
94 for (const p of dockerPathsA) pushDockPath(p)
95 if (tarSpec) pushTarSpec(tarSpec)
96 }
98 const getRnt = async (): Promise<BakedDf> => {
99 if (frozenRez) return frozenRez
100 const dockerfileContent = _.chain(linesA).map(_.trim).compact().value().join("\n")
101 const {repo_name_tag, push_repo_name_tag} = await get2RepoNameTag({dockerfileContent, includeShaInTag})
102 frozenRez = {repo_name_tag, push_repo_name_tag, dockerfileContent, dfDockPathsA, tarSpecsA, isBakedDf: true, includeShaInTag, getRnt}
103 return frozenRez
104 }
106 return {pushLine, pushEnv, pushCpTar, isDockFile: true, getRnt}