UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

195 lines (181 loc) 6.99 kB
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 }