UNPKG

binci

Version:

Utility for running containerized development workflows

178 lines (174 loc) 7.2 kB
'use strict' const proc = require('./proc') const output = require('./output') const cp = require('child_process') const fs = require('fs') const path = require('path') const crypto = require('crypto') const Promise = require('bluebird') const images = { /** * Builds a docker image, naming it according to the parent folder and tagging it * with "bc_" followed by a hash of the Dockerfile used to build it. * @param {string} dockerfile The path (absolute or relative from the current working dir) to * the Dockerfile to be built * @param {Array<string>} [tags=[]] An array of tags with which to tag the new image * @returns {Promise.<string>} A tag that can be used to launch a container of this new image */ buildImage: (dockerfile, tags = []) => { output.info(`Building image from ${dockerfile}`) output.line() const tagArgs = tags.reduce((acc, cur) => acc.concat(['-t', cur.toLowerCase()]), []) return proc.run([ 'build', '-f', path.resolve(process.cwd(), dockerfile) ].concat(tagArgs).concat([process.cwd()]) ).then(() => { output.line() output.success('Image built successfully!') return tags[tags.length - 1] }).catch(e => { output.line() output.error('Build failed') throw e }) }, /** * Deletes a docker image * @param {Promise} imageName The name of the image to be deleted in the format name:tag */ deleteImage: (imageName) => Promise.resolve().then(() => { const delSpinner = output.spinner(`Deleting old image: ${imageName}`) const cmd = `docker rmi ${imageName}` try { cp.execSync(cmd) } catch (e) { delSpinner.fail() throw e } delSpinner.succeed() }), /** * Searches for images that have been been automatically built by binci for the * current project, and returns them as an array of objects containing the fields * "hash" (the truncated sha1 of the Dockerfile that built the image), and "createdAt" * (the Epoch time of image creation). * @returns {Promise.<Array<{id:string},{hash:string},{createdAt:number}>>} the * array of images pertaining to this project */ getBuiltImages: () => Promise.resolve().then(() => { const cmd = [ 'docker images', '--format', `'{{"{"}}"tag":"{{.Tag}}","createdAt":"{{.CreatedAt}}"{{"}"}}'`, '"--filter=reference=' + images.getProjectName() + ':bc_*"' ].join(' ') const out = cp.execSync(cmd).toString() const parsed = JSON.parse('[' + out.replace(/\s*$/g, '').split('\n').join(',') + ']') return parsed.map(elem => ({ hash: elem.tag.substr(3), createdAt: new Date(elem.createdAt).getTime() })) }), /** * Searches for local images matching the names and tags defined in the service config * and prints a courtesy message for whichever are not found saying they will pulled * as part of service start-up. * @param {Array<Object>} svc The array of services defined in the instance config. */ logMissingServiceImages: (svc) => Promise.resolve().then(() => { const missingImages = svc.reduce((arr, i) => { const cmd = [ 'docker images', '"--filter=reference=' + i.args[i.args.length - 1] + '"' ].join(' ') const out = cp.execSync(cmd).toString().split('\n') if (!out[1]) { arr.push(i.name) } return arr }, []) if (missingImages.length) { output.info(`Unable to find local image${missingImages.length > 1 ? 's' : ''} for ${missingImages.join(', ')}. Pulling during start step.`) } }), /** * Gets the SHA-1 checksum, truncated to 12 hexadecimal digits, of the combined contents of the * files at the paths provided. * @param {Array<string>} paths An array of paths for the files to include in the checksum * @returns {Promise.<string>} The sha1 as a hex string. */ getHash: (paths) => { return Promise.reduce(paths, images.updateHash, crypto.createHash('sha1')) .then(hash => hash ? hash.digest('hex').substr(0, 12) : null) }, /** * Updates an existing hash object with the contents of a file at a given path. * @param {Hash} hash A crypto hash object to be updated * @param {string} path The path to a file which will be calculated into the hash * @returns {Promise<Hash>} resolves with the updated Hash object */ updateHash: (hash, path) => new Promise((resolve, reject) => { const stream = fs.createReadStream(path) stream.on('error', err => { if (err.code && err.code === 'ENOENT') resolve(null) else reject(err) }) stream.on('data', data => hash.update(data)) stream.on('close', () => resolve(hash)) }), /** * Gets a valid image name:tag that can be used to run a new docker container. * If an image has already been built for this dockerfile, the existing image will * be returned. If no build has happened yet or the dockerfile has been changed * since the last build, a new build will be run, and the previous image will be * deleted (if one exists). * @param {String} [dockerfile="./Dockerfile"] The path to the dockerfile to be * used for building the new image or retrieving the existing one * @param {Array<string>} [monitorPaths=[]] An optional array of file paths to monitor * for changes, causing a rebuild of the docker container if any of them are updated * @param {Array<string>} [tags=[]] An optional array of tags with which to tag a new * image, if one needs to be built * @returns {Promise.<string>} the name:tag of the image to be used */ getImage: (dockerfile = './Dockerfile', monitorPaths = [], tags = []) => { monitorPaths.push(dockerfile) return Promise.all([ images.getHash(monitorPaths), images.getBuiltImages() ]).then(([ hash, imgs ]) => { if (!hash) { throw new Error(`No "from" specified, and ${dockerfile} does not exist.`) } const [ image ] = imgs.filter(img => img.hash === hash) if (image) return images.getImageNameFromHash(hash) // Find the most recent binci build so we can delete it after the new one builds const mostRecent = imgs.reduce((acc, elem) => { if (acc.createdAt > elem.createdAt) return acc return elem }, {hash: null, createdAt: 0}) tags.push(images.getImageNameFromHash(hash)) return images.buildImage(dockerfile, tags) .then(imageName => { if (mostRecent.hash) { return images.deleteImage(images.getImageNameFromHash(mostRecent.hash)) .then(() => imageName) } return imageName }) }) }, /** * Constructs a full name:tag string for a given Dockerfile hash for this project * @param {string} hash The Dockerfile hash of the build * @returns {string} The fully qualified image name */ getImageNameFromHash: (hash) => `${images.getProjectName()}:bc_${hash}`, /** * Gets the name of the project binci is running for. This is simply the name of * the directory in which binci has been executed. * @returns {string} the project name */ getProjectName: () => path.basename(process.cwd()) } module.exports = images