@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
195 lines (181 loc) • 6.99 kB
JavaScript
const cds = require('../../lib/cds')
const { path } = cds.utils
const { join } = path
const { execSync } = require('node:child_process')
const { highlight, info } = require('../../lib/util/term')
const { calcDirChecksum } = require('../../lib/util/checksum')
const buildpacks = new Set([
'java', 'nodejs', 'sap-machine', 'executable-jar', 'spring-boot', 'syft'
])
const builders = new Set([
'builder-jammy-base', 'builder-jammy-buildpackless-base', 'builder-jammy-full', 'builder-jammy-buildpackless-full'
])
const keyProcessorMap = {
dockerfile: processDockerfile,
commands: processCommands,
buildpack: processBuildpack
}
const run = (cmd, { continueOnFail = false, silent = false } = {}) => {
cmd = Array.isArray(cmd) ? cmd.join(' ') : cmd
console.log(highlight(cmd))
try {
return execSync(cmd, {
stdio: silent ? 'pipe' : 'inherit',
env: { ...process.env, FORCE_COLOR: cds.utils.colors.enabled }
})
} catch (error) {
if (continueOnFail) {
if (!silent)
console.error(error.message)
return
}
throw error.message
}
}
function appendSubcommand(param, subcommand) {
if (!param) return []
if (subcommand === '--env') {
return Object.entries(param).flatMap(([k, v]) =>
String(v).split(',').map(env => [subcommand, `${k}=${env.trim() || '""'}`])
).flat()
}
return param.split(',').map(e => [subcommand, e.trim()]).flat()
}
function processDockerfile(bp, name, tag) {
if (!bp.dockerfile) return []
return [['docker', 'build',
...appendSubcommand(`${name}:${tag ?? 'latest'}`, '-t'),
...appendSubcommand(bp.dockerfile, '-f'),
'.']]
}
function processCommands(bp) {
return bp.commands ? bp.commands.map(c => c.trim().split(' ')) : []
}
function processBuildpack(bp, name, tag, clearCache = false) {
let { type, path, env, builder } = bp.buildpack
type = type ? type.split(',').map(t => buildpacks.has(t.trim()) ? `paketo-buildpacks/${t.trim()}` : t.trim()) : []
if (builders.has(builder)) builder = `paketobuildpacks/${builder}`
const cacheArgs = clearCache ? ['--clear-cache'] : []
return [['pack',
...appendSubcommand(`${name}:${tag}`, 'build'),
...appendSubcommand(path, '--path'),
...type.flatMap(t => appendSubcommand(t, '--buildpack')),
...appendSubcommand(builder, '--builder'),
...appendSubcommand(env, '--env'),
...cacheArgs
]]
}
/**
* Parses a YAML configuration file to extract Docker build and push commands for modules.
*
* @param {string} filename - The path to the YAML file to parse.
* @param {string} [repoOpt=''] - Optional repository override. If not provided, uses the repository defined in the YAML file.
* @param {boolean} [clearCache=false] - Whether to clear cache for buildpack builds.
* @returns {Promise<{
* commands: Array<{
* name: string,
* tag: string,
* buildCmd: Array<string>,
* tagCmd: Array<string>,
* pushCmd: Array<string>,
* image: string,
* checksum: string|null
* path: string
* }>,
* before_all: Array<Array<string>>,
* repository: string
* }>} An object containing the commands for each module, any global pre-commands, and the repository name.
* @throws {Error} If modules are not defined in the YAML file or if build-parameters are invalid.
*/
async function parseYAML(filename, repoOpt = '', options) {
const file = cds.load.yaml(join(cds.root, filename))
const { repository: repoFile, modules, tag: globalTag = 'latest' } = file
const repository = repoOpt || repoFile
const before_all = file['before-all'] ? file['before-all'].map(c => c.trim().split(' ')) : []
if (!modules) throw `no modules defined in ${filename}`
const commands = await Promise.all(modules.map(async (m) => {
const name = m.name, tag = m.tag || globalTag, bp = m['build-parameters']
const rawImg = `${repository}/${name}`, source = `${name}:${tag}`, repoImg = `${repository}/${source}`
const key = Object.keys(bp)[0]
const buildType = keyProcessorMap[key]
if (!buildType) throw `'build-parameters' ${Object.keys(bp)} in ${filename} is invalid`
return {
name,
tag,
buildCmd: buildType(bp, name, tag, key === 'buildpack' && options.clearCache),
pushCmd: ['docker', 'push', repoImg],
tagCmd: [['docker', 'tag', source, repoImg]],
image: rawImg,
source,
path: bp.buildpack?.path
}
}))
return { commands, before_all, repository }
}
/**
* Executes the containerization commands for each module.
* @param {*} modules - The list of modules with their respective build, tag, and push commands.
* @param {*} options - Options object, e.g., { push: true } to push images after building.
*/
async function executeModules(modules, options) {
for (const m of modules) {
if (m.checksum && process.env.SKIP_CHECK != 'true') {
try {
run(`docker inspect --format='{{index .RepoTags 0}}' ${m.image}:${m.checksum}`, { silent: true })
console.log(info(`image ${m.image}:${m.checksum} already exists – skipping build and push`))
continue
} catch { /* ignore */ }
}
for (const cmd of m.buildCmd) run(cmd)
if (options.push) {
for (const cmd of m.tagCmd) run(cmd)
run(m.pushCmd)
}
}
}
/**
* Calculates checksums for the specified modules based on their directory contents.
* @param {*} modules - The list of modules to calculate checksums for.
* @param {string} filename - The filename to include in the checksum calculation.
*/
async function calculateChecksums(modules, filename) {
for (const m of modules) {
if (m.path) {
m.checksum = await calcDirChecksum(
join(cds.root, m.path),
(dir) => !dir.includes('node_modules'),
'sha256',
[
join(cds.root, filename),
join(cds.root, 'package.json')
]
)
m.tagCmd = m.tagCmd
.concat(m.checksum ? [['docker', 'tag', m.source, `${m.image}:${m.checksum}`]] : [])
}
}
}
/**
* Main function to containerize modules based on a YAML configuration file.
* @param {string} filename - The path to the YAML configuration file.
* @param {{ push: boolean, clearCache: boolean }} options - Options object, e.g., { push: true } to push images after building.
* @returns {Promise<Array<{
* name: string,
* tag: string,
* buildCmd: Array<string>,
* tagCmd: Array<string>,
* pushCmd: Array<string>,
* image: string,
* checksum: string|null
* path: string
* }>} - The list of modules that were containerized.
*/
async function containerize(filename = 'containerize.yaml', options = { clearCache: !!process.env.CLEAR_CACHE, push: true }) {
const { repository, before_all, commands: modules } = await parseYAML(filename, '', options)
if (!repository || !modules) return
for (const cmd of before_all) run(cmd)
await calculateChecksums(modules, filename)
await executeModules(modules, options)
return modules
}
module.exports = { containerize }