From d7cf20283749613979ff0e72431608074ac59cd2 Mon Sep 17 00:00:00 2001 From: Hunter Loftis Date: Fri, 30 Sep 2016 14:28:43 -0400 Subject: [PATCH] push multiple, recursive, choose nearest, warn multiple, fail ambiguous Dockerfile matches --- commands/login.js | 6 +-- commands/logout.js | 5 +- commands/push.js | 114 ++++++++++++++++++++++++++++++++++++--------- lib/log.js | 6 +++ package.json | 1 + 5 files changed, 103 insertions(+), 29 deletions(-) create mode 100644 lib/log.js diff --git a/commands/login.js b/commands/login.js index df154c7..51fb7cc 100644 --- a/commands/login.js +++ b/commands/login.js @@ -3,6 +3,7 @@ const cli = require('heroku-cli-util'); const co = require('co'); const child = require('child_process'); +const log = require('../lib/log'); module.exports = function(topic) { return { @@ -26,6 +27,7 @@ function* login(context, heroku) { } catch (err) { cli.error(`Error: docker login exited with ${ err }`); + cli.hush(err.stack || err); } } @@ -37,9 +39,7 @@ function dockerLogin(registry, password, verbose) { `--password=${ password }`, registry ]; - if (verbose) { - console.log(['> docker'].concat(args).join(' ')); - } + log(verbose, args); child.spawn('docker', args, { stdio: 'inherit' }) .on('exit', (code, signal) => { if (signal || code) reject(signal || code); diff --git a/commands/logout.js b/commands/logout.js index 5f15187..da4e7ba 100644 --- a/commands/logout.js +++ b/commands/logout.js @@ -3,6 +3,7 @@ const cli = require('heroku-cli-util'); const co = require('co'); const child = require('child_process'); +const log = require('../lib/log'); module.exports = function(topic) { return { @@ -34,9 +35,7 @@ function dockerLogout(registry, verbose) { 'logout', registry ]; - if (verbose) { - console.log(['> docker'].concat(args).join(' ')); - } + log(verbose, args); child.spawn('docker', args, { stdio: 'inherit' }) .on('exit', (code, signal) => { if (signal || code) reject(signal || code); diff --git a/commands/push.js b/commands/push.js index e816541..872aad8 100644 --- a/commands/push.js +++ b/commands/push.js @@ -3,6 +3,12 @@ const cli = require('heroku-cli-util'); const co = require('co'); const child = require('child_process'); +const log = require('../lib/log'); +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +const DOCKERFILE_REGEX = /\/Dockerfile(.\w*)?$/; module.exports = function(topic) { return { @@ -11,8 +17,10 @@ module.exports = function(topic) { description: 'Builds, then pushes a Docker image to deploy your Heroku app', needsApp: true, needsAuth: true, - args: [{ name: 'process', optional: true }], - flags: [{ name: 'verbose', char: 'v', hasValue: false }], + variableArgs: true, + flags: [ + { name: 'verbose', char: 'v', hasValue: false } + ], run: cli.command(co.wrap(push)) }; }; @@ -20,37 +28,46 @@ module.exports = function(topic) { function* push(context, heroku) { let herokuHost = process.env.HEROKU_HOST || 'heroku.com'; let registry = `registry.${ herokuHost }`; - let proc = context.args.process || 'web'; - let resource = `${ registry }/${ context.app }/${ proc }`; + let dockerfiles = getDockerfiles(context.cwd, true); + let possibleJobs = getJobs(`${ registry }/${ context.app }`, context.args, dockerfiles); + let jobs = chooseJobs(possibleJobs); + + if (!jobs.length) { + cli.warn('No images to push'); + process.exit(); + } try { - let build = yield buildImage(resource, context.cwd, context.flags.verbose); + for (let job of jobs) { + cli.log(`Building ${ job.name } (${ job.dockerfile })`); + yield buildImage(job.dockerfile, job.resource, context.flags.verbose); + } } catch (err) { cli.error(`Error: docker build exited with ${ err }`); + cli.hush(err.stack || err); process.exit(1); } try { - let push = yield pushImage(resource, context.flags.verbose); + cli.log(`Pushing ${ jobs.length } images...`); + for (let job of jobs) { + cli.log(`Pushing ${ job.name } (${ job.dockerfile })`); + yield pushImage(job.resource, context.flags.verbose); + } } catch (err) { cli.error(`Error: docker push exited with ${ err }`); + cli.hush(err.stack || err); process.exit(1); } } -function buildImage(resource, cwd, verbose) { +function buildImage(dockerfile, resource, verbose) { return new Promise((resolve, reject) => { - let args = [ - 'build', - '-t', - resource, - cwd - ]; - if (verbose) { - console.log(['> docker'].concat(args).join(' ')); - } + let cwd = path.dirname(dockerfile); + let args = [ 'build', '-f', dockerfile, '-t', resource, cwd ]; + log(verbose, args); child.spawn('docker', args, { stdio: 'inherit' }) .on('exit', (code, signal) => { if (signal || code) reject(signal || code); @@ -61,13 +78,8 @@ function buildImage(resource, cwd, verbose) { function pushImage(resource, verbose) { return new Promise((resolve, reject) => { - let args = [ - 'push', - resource - ]; - if (verbose) { - console.log(['> docker'].concat(args).join(' ')); - } + let args = [ 'push', resource ]; + log(verbose, args); child.spawn('docker', args, { stdio: 'inherit' }) .on('exit', (code, signal) => { if (signal || code) reject(signal || code); @@ -75,3 +87,59 @@ function pushImage(resource, verbose) { }); }); } + +function getDockerfiles(dir, recursive) { + let match = recursive ? '**/Dockerfile?(.)*' : 'Dockerfile?(.)*'; + let dockerfiles = glob.sync(match, { cwd: dir, nonull: false, nodir: true }); + return dockerfiles.map(file => path.join(dir, file)); +} + +function getJobs(resourceRoot, procs, dockerfiles) { + return dockerfiles + // convert all Dockerfiles into job Objects + .map((dockerfile) => { + let match = dockerfile.match(DOCKERFILE_REGEX); + if (!match) return; + let proc = (match[1] || '.web').slice(1); + return { + name: proc, + resource: `${ resourceRoot }/${ proc }`, + dockerfile: dockerfile, + postfix: path.basename(dockerfile) === 'Dockerfile' ? 0 : 1, + depth: path.normalize(dockerfile).split(path.sep).length + }; + }) + // if process types have been specified, filter non matches out + .filter(job => { + return job && (!procs.length || procs.indexOf(job.name) !== -1); + }) + // prefer closer Dockerfiles, then prefer Dockerfile over Dockerfile.web + .sort((a, b) => { + return a.depth - b.depth || a.postfix - b.postfix; + }) + // group all Dockerfiles for the same process type together + .reduce((jobs, job) => { + jobs[job.name] = jobs[job.name] || []; + jobs[job.name].push(job); + return jobs; + }, {}); +} + +function chooseJobs(jobs) { + return Object.keys(jobs).map(name => { + let group = jobs[name]; + if (group.length > 1) { + let ambiguous = group.map(job => job.dockerfile); + if (group[1].depth === group[0].depth) { + if (group[1].postfix === group[0].postfix) { + cli.error(`Cannot build with ambiguous Dockerfiles:\n${ ambiguous.join('\n') }`); + process.exit(1); + } + } + cli.warn(`Using nearest match for the '${ group[0].name }' process:`); + cli.warn(`${ ambiguous[0] } (used)`); + cli.warn(`${ ambiguous.slice(1).join('\n') }`); + } + return group[0]; + }); +} diff --git a/lib/log.js b/lib/log.js new file mode 100644 index 0000000..fa0e6eb --- /dev/null +++ b/lib/log.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = (visible, args) => { + if (!visible) return; + console.log(`> docker ${ args.join(' ') }`); +}; diff --git a/package.json b/package.json index fedb521..9c6acfb 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "camelcase": "1.0.2", "co": "4.6.0", "dotenv": "^1.1.0", + "glob": "7.1.0", "heroku-cli-util": "^1.8.1", "heroku-client": "^1.9.1", "is-there": "4.0.0",