Skip to content
This repository has been archived by the owner on Feb 12, 2022. It is now read-only.

Commit

Permalink
restore deprecated commands
Browse files Browse the repository at this point in the history
  • Loading branch information
hunterloftis committed Jun 20, 2016
1 parent f528caf commit f946442
Show file tree
Hide file tree
Showing 3 changed files with 375 additions and 1 deletion.
168 changes: 168 additions & 0 deletions commands/init.js
@@ -0,0 +1,168 @@
var fs = require('fs');
var path = require('path');
var child = require('child_process');

var _ = require('lodash');
var exists = require('is-there');
var cli = require('heroku-cli-util');
var YAML = require('yamljs');
var camelcase = require('camelcase');

var docker = require('../lib/docker');
var safely = require('../lib/safely');
var directory = require('../lib/directory');

const ADDONS = require('../lib/addons');

module.exports = function(topic) {
return {
topic: topic,
command: 'init',
description: 'create Dockerfile and docker-compose.yml',
help: 'Creates a Dockerfile and docker-compose.yml for the app specified in app.json',
flags: [
{ name: 'image', char: 'i', description: 'the Docker image from which to inherit', hasValue: true },
{ name: 'force', char: 'f', description: 'overwrite existing Dockerfile and docker-compose.yml', hasValue: false },
{ name: 'dockerfile', char: 'd', description: 'use existing Dockerfile', hasValue: true }
],
run: safely(init)
};
};

function init(context) {
// Check preconditions
abortOnMissing(context.cwd, 'Procfile');
abortOnClobber(context.cwd, docker.filename, context.flags.force || context.flags.dockerfile);
abortOnClobber(context.cwd, docker.composeFilename, context.flags.force);

// Inputs (Procfile & app.json)
var procfile = readFile(context.cwd, 'Procfile', YAML.parse);
var appJSON = exists(path.join(context.cwd, 'app.json')) ?
readFile(context.cwd, 'app.json', JSON.parse) :
createAppJSON(context.cwd, context.flags.image);

// Outputs (app.json & Dockerfile & docker-compose.yml)
appJSON.image = context.flags.image || appJSON.image;
var dockerfile = context.flags.dockerfile ?
readFile(context.cwd, context.flags.dockerfile) :
createDockerfile(appJSON.image);
var dockerCompose = createDockerCompose(procfile, appJSON.addons, appJSON.mount_dir);

// All went well; write all files
writeFile(context.cwd, 'app.json', JSON.stringify(appJSON, null, ' ') + "\n");
writeFile(context.cwd, docker.filename, dockerfile);
writeFile(context.cwd, docker.composeFilename, dockerCompose);
}

function abortOnMissing(dir, filename) {
if (!exists(path.join(dir, filename))) {
throw new Error(`${ filename } required; aborting`);
}
}

function abortOnClobber(dir, filename, force) {
if (exists(path.join(dir, filename)) && !force) {
throw new Error(`${ filename } already exists; use --force to overwrite`);
}
}

function createAppJSON(dir, image) {
return {
name: path.basename(dir),
image: image,
addons: []
};
}

function createDockerfile(image) {
if (!image) {
throw new Error(`docker image required: provide an --image flag or 'image' key in app.json`);
}
return `FROM ${ image }\n`;
}

function createDockerCompose(procfile, addons, mountDir) {
var volumeMount = path.join('/app/user', mountDir || '');

// get the base addon name, ignoring plan types
var addonNames = _.map(addons, nameWithoutPlan);

// process only the addons that we have mappings for
var mappedAddons = _.filter(addonNames, _.has.bind(this, ADDONS));

// hyphens are not valid in link names for docker-compose
var links = _.map(mappedAddons || [], camelcase);

// reduce all addon env vars into a single object
var envs = _.reduce(mappedAddons, reduceEnv, {});

// compile a list of process types from the procfile
var processServices = _.mapValues(procfile, processToService(volumeMount, links, envs));

// add a 'shell' process for persistent changes, one-off tasks
processServices.shell = _.extend(_.cloneDeep(processServices.web), {
command: 'bash',
volumes: [`.:${ volumeMount }`]
});

// zip all the addons into an object
var addonServices = _.zipObject(links, _.map(mappedAddons, addonToService));

// combine processes and addons into a list of all services
var services = _.extend({}, processServices, addonServices);

// create docker-compose contents from the list of services
return YAML.stringify(services, 4, 2);

function nameWithoutPlan(addon) {
return addon.split(':')[0];
}

function reduceEnv(env, addon) {
_.extend(env, ADDONS[addon].env);
return env;
}

function processToService(mountDir, links, envs) {
return function(command, procName) {
var port = procName === 'web' ? docker.port : undefined;
return _.pick({
build: '.',
command: `bash -c '${command}'`,
working_dir: mountDir,
dockerfile: undefined, // TODO: docker.filename (once docker-compose 1.3.0 is released)
environment: _.extend(port ? { PORT: port } : {}, envs),
ports: port ? [`${ port }:${ port }`] : undefined,
links: links.length ? links : undefined,
envFile: undefined // TODO: detect an envFile?
}, _.identity);
};
}

function addonToService(addon) {
return {
image: ADDONS[addon].image
};
}
}

function readFile(dir, filename, transform) {
try {
var contents = fs.readFileSync(path.join(dir, filename), { encoding: 'utf8' });
return transform ? transform(contents) : contents;
}
catch (e) {
throw new Error(`Error reading ${ filename } (${ e.message })`);
}
}

function writeFile(dir, filename, contents) {
try {
var file = path.join(dir, filename);
fs.writeFileSync(file, contents, { encoding: 'utf8' });
`Wrote ${ filename }`
}
catch (e) {
throw new Error(`Error writing ${ filename }: (${ e.message })`);
}
}
204 changes: 204 additions & 0 deletions commands/release.js
@@ -0,0 +1,204 @@
var child = require('child_process');
var path = require('path');
var os = require('os');
var fs = require('fs');
var Heroku = require('heroku-client');
var request = require('request');
var agent = require('superagent');
var cli = require('heroku-cli-util');
var _ = require('lodash');
var ProgressBar = require('progress');

var directory = require('../lib/directory');
var docker = require('../lib/docker');
var safely = require('../lib/safely');

const ADDONS = require('../lib/addons');

module.exports = function(topic) {
return {
topic: topic,
command: 'release',
description: 'create and release slug to app',
help: 'Create slug tarball from Docker image and release it to Heroku app',
needsApp: true,
needsAuth: true,
run: safely(release)
};
};

function release(context) {
var procfile = directory.readProcfile(context.cwd);
var mountDir = directory.determineMountDir(context.cwd);
var modifiedProc = _.mapValues(procfile, prependMountDir(mountDir));
var heroku = context.heroku || new Heroku({ token: context.auth.password });
var app = context.heroku ? context.app : heroku.apps(context.app);
var appJSONLocation = path.join(context.cwd, 'app.json');
var appJSON = JSON.parse(fs.readFileSync(appJSONLocation, { encoding: 'utf8' }));

request = context.request || request;

if (!procfile) throw new Error('Procfile required. Aborting');

return app.info()
.then(readRemoteAddons)
.then(compareLocalAddons)
.then(addMissingAddons)
.then(createLocalSlug)
.then(createRemoteSlug)
.then(uploadSlug)
.then(releaseSlug)
.then(showMessage);

function prependMountDir(mountDir) {
return function(cmd) {
return `cd ${ mountDir } && ${ cmd }`
}
}

function readRemoteAddons() {
return app.addons().list();
}

function compareLocalAddons(remoteAddons) {
var remoteNames = _.map(remoteAddons, getServiceName);
var localNames = appJSON.addons || [];
var missingAddons = _.filter(localNames, isMissingFrom.bind(this, remoteNames));

console.log(`Remote addons: ${ remoteNames.join(', ')} (${ remoteNames.length })`);
console.log(`Local addons: ${ localNames.join(', ') } (${ localNames.length })`);
console.log(`Missing addons: ${ missingAddons.join(', ') } (${ missingAddons.length })`);

return Promise.resolve(missingAddons);

function getServiceName(addon) {
return addon.addon_service.name;
}

function isMissingFrom(list, addon) {
var name = addon.split(':')[0];
return list.indexOf(name) === -1;
}
}

function addMissingAddons(addons) {
return Promise.all(addons.map(createAddon));

function createAddon(name) {
console.log(`Provisioning ${ name }...`)
return app.addons().create({
plan: name
});
}
}

function createLocalSlug() {
cli.log('Creating local slug...');

return new Promise(function(resolve, reject) {
var slugPath = os.tmpdir();
var output = '';
var build = child.spawn('docker-compose', ['build', 'web']);

build.stdout.pipe(process.stdout);
build.stderr.pipe(process.stderr);
build.stdout.on('data', saveOutput);
build.on('exit', onBuildExit);

function saveOutput(data) {
output += data;
}

function onBuildExit(code) {
if (code !== 0) {
cli.log('Build failed. Make sure `docker-compose build web` returns a 0 exit status.');
process.exit(1);
}

var tokens = output.match(/\S+/g);
var fromMatch = output.match(/FROM ([^\s]+)/) || [];
var imageName = fromMatch[1];
var imageId = tokens[tokens.length - 1];
tar(imageName, imageId);
}

function tar(imageName, imageId) {
cli.log('extracting slug from container...');
var containerId = child.execSync(`docker run -d ${imageId} tar cfvz /tmp/slug.tgz -C / --exclude=.git --exclude=.cache --exclude=.buildpack ./app`, {
encoding: 'utf8'
}).trim();
child.execSync(`docker wait ${containerId}`);
child.execSync(`docker cp ${containerId}:/tmp/slug.tgz ${slugPath}`);
child.execSync(`docker rm -f ${containerId}`);
resolve({
path: path.join(slugPath, 'slug.tgz'),
name: imageName
});
}
});
}

function createRemoteSlug(slug) {
var lang = `heroku-container-tools (${ slug.name || 'unknown'})`;
cli.log(`creating remote slug...`);
cli.log(`language-pack: ${ lang }`);
cli.log('remote process types:', modifiedProc);
var slugInfo = app.slugs().create({
process_types: modifiedProc,
buildpack_provided_description: lang
});
return Promise.all([slug.path, slugInfo])
}

function uploadSlug(slug) {
var slugPath = slug[0];
var slugInfo = slug[1];
var size = fs.statSync(slugPath).size;
var mbs = Math.round(size / 1024 / 1024)
var bar = new ProgressBar(`uploading slug [:bar] :percent of ${ mbs } MB, :etas`, {
width: 20,
total: size
});

return new Promise(function(resolve, reject) {
var outStream = request({
method: 'PUT',
url: slugInfo.blob.url,
headers: {
'content-type': '',
'content-length': size
}
});

fs.createReadStream(slugPath)
.on('error', reject)
.on('data', updateProgress)
.pipe(outStream)
.on('error', reject)
.on('response', resolve.bind(this, slugInfo.id));

function updateProgress(chunk) {
bar.tick(chunk.length);
}
});
}

function releaseSlug(id) {
cli.log('releasing slug...');

return heroku.request({
method: 'POST',
path: `${ app.path }/releases`,
headers: {
'Heroku-Deploy-Type': 'heroku-container-tools'
},
body: {
slug: id
}
});
}

function showMessage(results) {
console.log(`Successfully released ${ results.app.name }!`);
}
}
4 changes: 3 additions & 1 deletion index.js
Expand Up @@ -4,6 +4,8 @@ module.exports = {
commands: [
require('./commands/index')(pkg),
require('./commands/login')(pkg.topic),
require('./commands/push')(pkg.topic)
require('./commands/push')(pkg.topic),
require('./commands/init')(pkg.topic),
require('./commands/release')(pkg.topic)
]
};

0 comments on commit f946442

Please sign in to comment.