mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-01-29 15:44:26 +00:00
Add ability to build and deploy image locally using resin-cli
Using `resin build` a user can now build an image on their own docker daemon. The daemon can be accessed via a local socket, a remote host and a remote host over a TLS socket. Project type resolution is supported. Nocache and tagging of images is also supported. Using `resin deploy` a user can now deploy an image to their fleet. The image can either be built by `resin-cli`, plain Docker, or from a remote source. Change-type: minor Signed-off-by: Cameron Diver <cameron@resin.io>
This commit is contained in:
parent
2e042499af
commit
d3772386bf
@ -3,9 +3,13 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
### Added
|
||||
|
||||
- Add ability to build and deploy an image to resin's infrastructure
|
||||
|
||||
### Fixed
|
||||
|
||||
- Capture and report errors happening during the program initialization, like parsing invalid YAML config
|
||||
- Capture and report errors happening during the program initialization, like parsing invalid YAML config
|
||||
|
||||
## [5.7.2] - 2017-04-18
|
||||
|
||||
|
57
build/actions/build.js
Normal file
57
build/actions/build.js
Normal file
@ -0,0 +1,57 @@
|
||||
// Generated by CoffeeScript 1.12.5
|
||||
var Promise, dockerUtils, getBundleInfo;
|
||||
|
||||
Promise = require('bluebird');
|
||||
|
||||
dockerUtils = require('../utils/docker');
|
||||
|
||||
getBundleInfo = Promise.method(function(options) {
|
||||
var helpers;
|
||||
helpers = require('../utils/helpers');
|
||||
if (options.application != null) {
|
||||
return helpers.getAppInfo(options.application).then(function(app) {
|
||||
return [app.arch, app.device_type];
|
||||
});
|
||||
} else if ((options.arch != null) && (options.deviceType != null)) {
|
||||
return [options.arch, options.deviceType];
|
||||
} else {
|
||||
return void 0;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
signature: 'build [source]',
|
||||
description: 'Build a container locally',
|
||||
permission: 'user',
|
||||
help: 'Use this command to build a container with a provided docker daemon.\n\nYou must provide either an application or a device-type/architecture\npair to use the resin Dockerfile pre-processor\n(e.g. Dockerfile.template -> Dockerfile).\n\nExamples:\n\n $ resin build\n $ resin build ./source/\n $ resin build --deviceType raspberrypi3 --arch armhf\n $ resin build --application MyApp ./source/\n $ resin build --docker \'/var/run/docker.sock\'\n $ resin build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem',
|
||||
options: dockerUtils.appendOptions([
|
||||
{
|
||||
signature: 'arch',
|
||||
parameter: 'arch',
|
||||
description: 'The architecture to build for',
|
||||
alias: 'A'
|
||||
}, {
|
||||
signature: 'devicetype',
|
||||
parameter: 'deviceType',
|
||||
description: 'The type of device this build is for',
|
||||
alias: 'd'
|
||||
}, {
|
||||
signature: 'application',
|
||||
parameter: 'application',
|
||||
description: 'The target resin.io application this build is for',
|
||||
alias: 'a'
|
||||
}, {
|
||||
signature: 'tag',
|
||||
parameter: 'tag',
|
||||
description: 'The alias to the generated image',
|
||||
alias: 't'
|
||||
}, {
|
||||
signature: 'nocache',
|
||||
description: "Don't use docker layer caching when building",
|
||||
boolean: true
|
||||
}
|
||||
]),
|
||||
action: function(params, options, done) {
|
||||
return dockerUtils.runBuild(params, options, getBundleInfo).asCallback(done);
|
||||
}
|
||||
};
|
166
build/actions/deploy.js
Normal file
166
build/actions/deploy.js
Normal file
@ -0,0 +1,166 @@
|
||||
// Generated by CoffeeScript 1.12.5
|
||||
var Promise, formatImageName, getBuilderPushEndpoint, getBundleInfo, parseInput, performUpload, pushProgress, uploadToPromise;
|
||||
|
||||
Promise = require('bluebird');
|
||||
|
||||
getBuilderPushEndpoint = function(baseUrl, owner, app) {
|
||||
var escApp, escOwner;
|
||||
escOwner = encodeURIComponent(owner);
|
||||
escApp = encodeURIComponent(app);
|
||||
return "https://builder." + baseUrl + "/v1/push?owner=" + escOwner + "&app=" + escApp;
|
||||
};
|
||||
|
||||
formatImageName = function(image) {
|
||||
return image.split('/').pop();
|
||||
};
|
||||
|
||||
parseInput = Promise.method(function(params, options) {
|
||||
var appName, context, image;
|
||||
if (params.appName == null) {
|
||||
throw new Error('Need an application to deploy to!');
|
||||
}
|
||||
appName = params.appName;
|
||||
image = void 0;
|
||||
if (params.image != null) {
|
||||
if (options.build || (options.source != null)) {
|
||||
throw new Error('Build and source parameters are not applicable when specifying an image');
|
||||
}
|
||||
options.build = false;
|
||||
image = params.image;
|
||||
} else if (options.build) {
|
||||
context = options.source || '.';
|
||||
} else {
|
||||
throw new Error('Need either an image or a build flag!');
|
||||
}
|
||||
return [appName, options.build, context, image];
|
||||
});
|
||||
|
||||
pushProgress = function(imageSize, request, timeout) {
|
||||
var progressReporter;
|
||||
if (timeout == null) {
|
||||
timeout = 250;
|
||||
}
|
||||
process.stdout.write('Initialising...');
|
||||
return progressReporter = setInterval(function() {
|
||||
var percent, sent;
|
||||
sent = request.req.connection._bytesDispatched;
|
||||
percent = (sent / imageSize) * 100;
|
||||
if (percent >= 100) {
|
||||
clearInterval(progressReporter);
|
||||
percent = 100;
|
||||
}
|
||||
process.stdout.clearLine();
|
||||
process.stdout.cursorTo(0);
|
||||
process.stdout.write("Uploaded " + (percent.toFixed(1)) + "%");
|
||||
if (percent === 100) {
|
||||
return console.log();
|
||||
}
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
getBundleInfo = function(options) {
|
||||
var helpers;
|
||||
helpers = require('../utils/helpers');
|
||||
return helpers.getAppInfo(options.appName).then(function(app) {
|
||||
return [app.arch, app.device_type];
|
||||
});
|
||||
};
|
||||
|
||||
performUpload = function(image, token, username, url, size, appName) {
|
||||
var post, request;
|
||||
request = require('request');
|
||||
url = url || process.env.RESINRC_RESIN_URL;
|
||||
post = request.post({
|
||||
url: getBuilderPushEndpoint(url, username, appName),
|
||||
auth: {
|
||||
bearer: token
|
||||
},
|
||||
body: image
|
||||
});
|
||||
return uploadToPromise(post, size);
|
||||
};
|
||||
|
||||
uploadToPromise = function(request, size) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var handleMessage;
|
||||
handleMessage = function(data) {
|
||||
var obj;
|
||||
data = data.toString();
|
||||
if (process.env.DEBUG) {
|
||||
console.log("Received data: " + data);
|
||||
}
|
||||
obj = JSON.parse(data);
|
||||
if (obj.type != null) {
|
||||
switch (obj.type) {
|
||||
case 'error':
|
||||
return reject(new Error("Remote error: " + obj.error));
|
||||
case 'success':
|
||||
return resolve(obj.image);
|
||||
case 'status':
|
||||
return console.log("Remote: " + obj.message);
|
||||
default:
|
||||
return reject(new Error("Received unexpected reply from remote: " + data));
|
||||
}
|
||||
} else {
|
||||
return reject(new Error("Received unexpected reply from remote: " + data));
|
||||
}
|
||||
};
|
||||
request.on('error', reject).on('data', handleMessage);
|
||||
return pushProgress(size, request);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
signature: 'deploy <appName> [image]',
|
||||
description: 'Deploy a container to a resin.io application',
|
||||
help: 'Use this command to deploy and optionally build an image to an application.\n\nUsage: deploy <appName> ([image] | --build [--source build-dir])\n\nNote: If building with this command, all options supported by `resin build`\nare also support with this command.\n\nExamples:\n$ resin deploy myApp --build --source myBuildDir/\n$ resin deploy myApp myApp/myImage',
|
||||
permission: 'user',
|
||||
options: [
|
||||
{
|
||||
signature: 'build',
|
||||
boolean: true,
|
||||
description: 'Build image then deploy',
|
||||
alias: 'b'
|
||||
}, {
|
||||
signature: 'source',
|
||||
parameter: 'source',
|
||||
description: 'The source directory to use when building the image',
|
||||
alias: 's'
|
||||
}
|
||||
],
|
||||
action: function(params, options, done) {
|
||||
var _, docker, dockerUtils, resin, tmp, tmpNameAsync;
|
||||
_ = require('lodash');
|
||||
tmp = require('tmp');
|
||||
tmpNameAsync = Promise.promisify(tmp.tmpName);
|
||||
resin = require('resin-sdk-preconfigured');
|
||||
dockerUtils = require('../utils/docker');
|
||||
tmp.setGracefulCleanup();
|
||||
docker = dockerUtils.getDocker(options);
|
||||
return parseInput(params, options).then(function(arg) {
|
||||
var appName, build, context, imageName;
|
||||
appName = arg[0], build = arg[1], context = arg[2], imageName = arg[3];
|
||||
return tmpNameAsync().then(function(tmpPath) {
|
||||
options = _.assign({}, options, {
|
||||
appName: appName
|
||||
});
|
||||
params = _.assign({}, params, {
|
||||
context: context
|
||||
});
|
||||
return Promise["try"](function() {
|
||||
if (build) {
|
||||
return dockerUtils.runBuild(params, options, getBundleInfo);
|
||||
} else {
|
||||
return imageName;
|
||||
}
|
||||
}).then(function(imageName) {
|
||||
return Promise.join(dockerUtils.bufferImage(docker, imageName, tmpPath), resin.auth.getToken(), resin.auth.whoami(), resin.settings.get('resinUrl'), dockerUtils.getImageSize(docker, imageName), params.appName, performUpload);
|
||||
})["finally"](function() {
|
||||
return require('fs').unlink(tmpPath);
|
||||
});
|
||||
});
|
||||
}).then(function(imageName) {
|
||||
return console.log("Successfully deployed image: " + (formatImageName(imageName)));
|
||||
}).asCallback(done);
|
||||
}
|
||||
};
|
@ -32,5 +32,7 @@ module.exports = {
|
||||
config: require('./config'),
|
||||
sync: require('./sync'),
|
||||
ssh: require('./ssh'),
|
||||
internal: require('./internal')
|
||||
internal: require('./internal'),
|
||||
build: require('./build'),
|
||||
deploy: require('./deploy')
|
||||
};
|
||||
|
@ -49,6 +49,8 @@ plugins = require('./utils/plugins');
|
||||
|
||||
update = require('./utils/update');
|
||||
|
||||
require('any-promise/register/bluebird');
|
||||
|
||||
capitano.permission('user', function(done) {
|
||||
return resin.auth.isLoggedIn().then(function(isLoggedIn) {
|
||||
if (!isLoggedIn) {
|
||||
@ -186,6 +188,10 @@ capitano.command(actions.local.stop);
|
||||
|
||||
capitano.command(actions.internal.osInit);
|
||||
|
||||
capitano.command(actions.build);
|
||||
|
||||
capitano.command(actions.deploy);
|
||||
|
||||
update.notify();
|
||||
|
||||
plugins.register(/^resin-plugin-(.+)$/).then(function() {
|
||||
|
@ -1,19 +0,0 @@
|
||||
// Generated by CoffeeScript 1.12.2
|
||||
(function() {
|
||||
var getSdk, opts, settings;
|
||||
|
||||
getSdk = require('resin-sdk');
|
||||
|
||||
settings = require('resin-settings-client');
|
||||
|
||||
opts = {
|
||||
apiUrl: settings.get('apiUrl'),
|
||||
imageMakerUrl: settings.get('imageMakerUrl'),
|
||||
dataDirectory: settings.get('dataDirectory'),
|
||||
apiVersion: 'v2',
|
||||
retries: 2
|
||||
};
|
||||
|
||||
module.exports = getSdk(opts);
|
||||
|
||||
}).call(this);
|
182
build/utils/docker.js
Normal file
182
build/utils/docker.js
Normal file
@ -0,0 +1,182 @@
|
||||
// Generated by CoffeeScript 1.12.5
|
||||
var generateConnectOpts, tarDirectory;
|
||||
|
||||
exports.appendOptions = function(opts) {
|
||||
return opts.concat([
|
||||
{
|
||||
signature: 'docker',
|
||||
parameter: 'docker',
|
||||
description: 'Path to a local docker socket',
|
||||
alias: 'P'
|
||||
}, {
|
||||
signature: 'dockerHost',
|
||||
parameter: 'dockerHost',
|
||||
description: 'The address of the host containing the docker daemon',
|
||||
alias: 'h'
|
||||
}, {
|
||||
signature: 'dockerPort',
|
||||
parameter: 'dockerPort',
|
||||
description: 'The port on which the host docker daemon is listening',
|
||||
alias: 'p'
|
||||
}, {
|
||||
signature: 'ca',
|
||||
parameter: 'ca',
|
||||
description: 'Docker host TLS certificate authority file'
|
||||
}, {
|
||||
signature: 'cert',
|
||||
parameter: 'cert',
|
||||
description: 'Docker host TLS certificate file'
|
||||
}, {
|
||||
signature: 'key',
|
||||
parameter: 'key',
|
||||
description: 'Docker host TLS key file'
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
exports.generateConnectOpts = generateConnectOpts = function(opts) {
|
||||
var connectOpts;
|
||||
connectOpts = {};
|
||||
if ((opts.docker != null) && (opts.dockerHost == null)) {
|
||||
connectOpts.socketPath = opts.docker;
|
||||
} else if ((opts.dockerHost != null) && (opts.docker == null)) {
|
||||
connectOpts.host = opts.dockerHost;
|
||||
connectOpts.port = opts.dockerPort || 2376;
|
||||
} else if ((opts.docker != null) && (opts.dockerHost != null)) {
|
||||
throw new Error("Both a local docker socket and docker host have been provided. Don't know how to continue.");
|
||||
} else {
|
||||
connectOpts.socketPath = '/var/run/docker.sock';
|
||||
}
|
||||
if ((opts.ca != null) || (opts.cert != null) || (opts.key != null)) {
|
||||
if (!((opts.ca != null) && (opts.cert != null) && (opts.key != null))) {
|
||||
throw new Error('You must provide a CA, certificate and key in order to use TLS');
|
||||
}
|
||||
connectOpts.ca = opts.ca;
|
||||
connectOpts.cert = opts.cert;
|
||||
connectOpts.key = opts.key;
|
||||
}
|
||||
return connectOpts;
|
||||
};
|
||||
|
||||
exports.tarDirectory = tarDirectory = function(dir) {
|
||||
var Promise, fs, getFiles, klaw, pack, path, streamToPromise, tar;
|
||||
Promise = require('bluebird');
|
||||
tar = require('tar-stream');
|
||||
klaw = require('klaw');
|
||||
path = require('path');
|
||||
fs = require('mz/fs');
|
||||
streamToPromise = require('stream-to-promise');
|
||||
getFiles = function() {
|
||||
return streamToPromise(klaw(dir)).filter(function(item) {
|
||||
return !item.stats.isDirectory();
|
||||
}).map(function(item) {
|
||||
return item.path;
|
||||
});
|
||||
};
|
||||
pack = tar.pack();
|
||||
return getFiles(dir).map(function(file) {
|
||||
var relPath;
|
||||
relPath = path.relative(path.resolve(dir), file);
|
||||
return Promise.join(relPath, fs.stat(file), fs.readFile(file), function(filename, stats, data) {
|
||||
return pack.entryAsync({
|
||||
name: filename,
|
||||
size: stats.size
|
||||
}, data);
|
||||
});
|
||||
}).then(function() {
|
||||
pack.finalize();
|
||||
return pack;
|
||||
});
|
||||
};
|
||||
|
||||
exports.runBuild = function(params, options, getBundleInfo) {
|
||||
var Promise, dockerBuild, resolver;
|
||||
Promise = require('bluebird');
|
||||
dockerBuild = require('resin-docker-build');
|
||||
resolver = require('resin-bundle-resolve');
|
||||
if (params.context == null) {
|
||||
params.context = '.';
|
||||
}
|
||||
return tarDirectory(params.context).then(function(tarStream) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var builder, connectOpts, hooks, opts;
|
||||
hooks = {
|
||||
buildSuccess: function(image) {
|
||||
if (options.tag != null) {
|
||||
console.log("Tagging image as " + options.tag);
|
||||
}
|
||||
return resolve(image);
|
||||
},
|
||||
buildFailure: reject,
|
||||
buildStream: function(stream) {
|
||||
getBundleInfo(options).then(function(info) {
|
||||
var arch, bundle, deviceType;
|
||||
if (info == null) {
|
||||
console.log('Warning: No architecture/device type or application information provided.\n Dockerfile/project pre-processing will not be performed.');
|
||||
return tarStream.pipe(stream);
|
||||
} else {
|
||||
arch = info[0], deviceType = info[1];
|
||||
bundle = new resolver.Bundle(tarStream, deviceType, arch);
|
||||
return resolver.resolveBundle(bundle, resolver.getDefaultResolvers()).then(function(resolved) {
|
||||
console.log("Building " + resolved.projectType + " project");
|
||||
return resolved.tarStream.pipe(stream);
|
||||
});
|
||||
}
|
||||
})["catch"](reject);
|
||||
return stream.pipe(process.stdout);
|
||||
}
|
||||
};
|
||||
connectOpts = generateConnectOpts(options);
|
||||
if (process.env.DEBUG != null) {
|
||||
console.log('Connecting with the following options:');
|
||||
console.log(JSON.stringify(connectOpts, null, ' '));
|
||||
}
|
||||
builder = new dockerBuild.Builder(connectOpts);
|
||||
opts = {};
|
||||
if (options.tag != null) {
|
||||
opts['t'] = options.tag;
|
||||
}
|
||||
if (options.nocache != null) {
|
||||
opts['nocache'] = true;
|
||||
}
|
||||
return builder.createBuildStream(opts, hooks, reject);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.bufferImage = function(docker, imageId, tmpFile) {
|
||||
var Promise, fs, image, stream;
|
||||
Promise = require('bluebird');
|
||||
fs = require('fs');
|
||||
stream = fs.createWriteStream(tmpFile);
|
||||
image = docker.getImage(imageId);
|
||||
return image.get().then(function(img) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
return img.on('error', reject).on('data', function(data) {
|
||||
return stream.write(data);
|
||||
}).on('end', function() {
|
||||
stream.close();
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}).then(function() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
return fs.createReadStream(tmpFile).on('open', function() {
|
||||
return resolve(this);
|
||||
}).on('error', reject);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.getDocker = function(options) {
|
||||
var Docker, Promise, connectOpts;
|
||||
Docker = require('dockerode');
|
||||
Promise = require('bluebird');
|
||||
connectOpts = generateConnectOpts(options);
|
||||
connectOpts['Promise'] = Promise;
|
||||
return new Docker(connectOpts);
|
||||
};
|
||||
|
||||
exports.getImageSize = function(docker, image) {
|
||||
return docker.getImage(image).inspectAsync().get('Size');
|
||||
};
|
@ -94,3 +94,19 @@ exports.osProgressHandler = function(step) {
|
||||
});
|
||||
return rindle.wait(step);
|
||||
};
|
||||
|
||||
exports.getAppInfo = function(application) {
|
||||
var _, resin;
|
||||
resin = require('resin-sdk-preconfigured');
|
||||
_ = require('lodash');
|
||||
return Promise.join(resin.models.application.get(application), resin.models.config.getDeviceTypes(), function(app, config) {
|
||||
config = _.find(config, {
|
||||
'slug': app.device_type
|
||||
});
|
||||
if (config == null) {
|
||||
throw new Error('Could not read application information!');
|
||||
}
|
||||
app.arch = config.arch;
|
||||
return app;
|
||||
});
|
||||
};
|
||||
|
74
lib/actions/build.coffee
Normal file
74
lib/actions/build.coffee
Normal file
@ -0,0 +1,74 @@
|
||||
# Imported here because it's needed for the setup
|
||||
# of this action
|
||||
Promise = require('bluebird')
|
||||
dockerUtils = require('../utils/docker')
|
||||
|
||||
getBundleInfo = Promise.method (options) ->
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
if options.application?
|
||||
# An application was provided
|
||||
return helpers.getAppInfo(options.application)
|
||||
.then (app) ->
|
||||
return [app.arch, app.device_type]
|
||||
else if options.arch? and options.deviceType?
|
||||
return [options.arch, options.deviceType]
|
||||
else
|
||||
# No information, cannot do resolution
|
||||
return undefined
|
||||
|
||||
module.exports =
|
||||
signature: 'build [source]'
|
||||
description: 'Build a container locally'
|
||||
permission: 'user'
|
||||
help: '''
|
||||
Use this command to build a container with a provided docker daemon.
|
||||
|
||||
You must provide either an application or a device-type/architecture
|
||||
pair to use the resin Dockerfile pre-processor
|
||||
(e.g. Dockerfile.template -> Dockerfile).
|
||||
|
||||
Examples:
|
||||
|
||||
$ resin build
|
||||
$ resin build ./source/
|
||||
$ resin build --deviceType raspberrypi3 --arch armhf
|
||||
$ resin build --application MyApp ./source/
|
||||
$ resin build --docker '/var/run/docker.sock'
|
||||
$ resin build --dockerHost my.docker.host --dockerPort 2376 --ca ca.pem --key key.pem --cert cert.pem
|
||||
'''
|
||||
options: dockerUtils.appendOptions [
|
||||
{
|
||||
signature: 'arch'
|
||||
parameter: 'arch'
|
||||
description: 'The architecture to build for'
|
||||
alias: 'A'
|
||||
},
|
||||
{
|
||||
signature: 'devicetype'
|
||||
parameter: 'deviceType'
|
||||
description: 'The type of device this build is for'
|
||||
alias: 'd'
|
||||
},
|
||||
{
|
||||
signature: 'application'
|
||||
parameter: 'application'
|
||||
description: 'The target resin.io application this build is for'
|
||||
alias: 'a'
|
||||
},
|
||||
{
|
||||
signature: 'tag'
|
||||
parameter: 'tag'
|
||||
description: 'The alias to the generated image'
|
||||
alias: 't'
|
||||
},
|
||||
{
|
||||
signature: 'nocache'
|
||||
description: "Don't use docker layer caching when building"
|
||||
boolean: true
|
||||
},
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
dockerUtils.runBuild(params, options, getBundleInfo)
|
||||
.asCallback(done)
|
||||
|
157
lib/actions/deploy.coffee
Normal file
157
lib/actions/deploy.coffee
Normal file
@ -0,0 +1,157 @@
|
||||
Promise = require('bluebird')
|
||||
|
||||
getBuilderPushEndpoint = (baseUrl, owner, app) ->
|
||||
escOwner = encodeURIComponent(owner)
|
||||
escApp = encodeURIComponent(app)
|
||||
"https://builder.#{baseUrl}/v1/push?owner=#{escOwner}&app=#{escApp}"
|
||||
|
||||
formatImageName = (image) ->
|
||||
image.split('/').pop()
|
||||
|
||||
parseInput = Promise.method (params, options) ->
|
||||
if not params.appName?
|
||||
throw new Error('Need an application to deploy to!')
|
||||
appName = params.appName
|
||||
image = undefined
|
||||
if params.image?
|
||||
if options.build or options.source?
|
||||
throw new Error('Build and source parameters are not applicable when specifying an image')
|
||||
options.build = false
|
||||
image = params.image
|
||||
else if options.build
|
||||
context = options.source || '.'
|
||||
else
|
||||
throw new Error('Need either an image or a build flag!')
|
||||
|
||||
return [appName, options.build, context, image]
|
||||
|
||||
pushProgress = (imageSize, request, timeout = 250) ->
|
||||
process.stdout.write('Initialising...')
|
||||
progressReporter = setInterval ->
|
||||
sent = request.req.connection._bytesDispatched
|
||||
percent = (sent / imageSize) * 100
|
||||
if percent >= 100
|
||||
clearInterval(progressReporter)
|
||||
percent = 100
|
||||
process.stdout.clearLine()
|
||||
process.stdout.cursorTo(0)
|
||||
process.stdout.write("Uploaded #{percent.toFixed(1)}%")
|
||||
console.log() if percent == 100
|
||||
, timeout
|
||||
|
||||
getBundleInfo = (options) ->
|
||||
helpers = require('../utils/helpers')
|
||||
|
||||
helpers.getAppInfo(options.appName)
|
||||
.then (app) ->
|
||||
[app.arch, app.device_type]
|
||||
|
||||
performUpload = (image, token, username, url, size, appName) ->
|
||||
request = require('request')
|
||||
url = url || process.env.RESINRC_RESIN_URL
|
||||
post = request.post
|
||||
url: getBuilderPushEndpoint(url, username, appName)
|
||||
auth:
|
||||
bearer: token
|
||||
body: image
|
||||
|
||||
uploadToPromise(post, size)
|
||||
|
||||
uploadToPromise = (request, size) ->
|
||||
new Promise (resolve, reject) ->
|
||||
|
||||
handleMessage = (data) ->
|
||||
data = data.toString()
|
||||
if process.env.DEBUG
|
||||
console.log("Received data: #{data}")
|
||||
|
||||
obj = JSON.parse(data)
|
||||
if obj.type?
|
||||
switch obj.type
|
||||
when 'error' then reject(new Error("Remote error: #{obj.error}"))
|
||||
when 'success' then resolve(obj.image)
|
||||
when 'status' then console.log("Remote: #{obj.message}")
|
||||
else reject(new Error("Received unexpected reply from remote: #{data}"))
|
||||
else
|
||||
reject(new Error("Received unexpected reply from remote: #{data}"))
|
||||
|
||||
request
|
||||
.on('error', reject)
|
||||
.on('data', handleMessage)
|
||||
|
||||
# Set up upload reporting
|
||||
pushProgress(size, request)
|
||||
|
||||
|
||||
module.exports =
|
||||
signature: 'deploy <appName> [image]'
|
||||
description: 'Deploy a container to a resin.io application'
|
||||
help: '''
|
||||
Use this command to deploy and optionally build an image to an application.
|
||||
|
||||
Usage: deploy <appName> ([image] | --build [--source build-dir])
|
||||
|
||||
Note: If building with this command, all options supported by `resin build`
|
||||
are also support with this command.
|
||||
|
||||
Examples:
|
||||
$ resin deploy myApp --build --source myBuildDir/
|
||||
$ resin deploy myApp myApp/myImage
|
||||
'''
|
||||
permission: 'user'
|
||||
options: [
|
||||
{
|
||||
signature: 'build'
|
||||
boolean: true
|
||||
description: 'Build image then deploy'
|
||||
alias: 'b'
|
||||
},
|
||||
{
|
||||
signature: 'source'
|
||||
parameter: 'source'
|
||||
description: 'The source directory to use when building the image'
|
||||
alias: 's'
|
||||
}
|
||||
]
|
||||
action: (params, options, done) ->
|
||||
_ = require('lodash')
|
||||
tmp = require('tmp')
|
||||
tmpNameAsync = Promise.promisify(tmp.tmpName)
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
|
||||
dockerUtils = require('../utils/docker')
|
||||
|
||||
# Ensure the tmp files gets deleted
|
||||
tmp.setGracefulCleanup()
|
||||
|
||||
docker = dockerUtils.getDocker(options)
|
||||
# Check input parameters
|
||||
parseInput(params, options)
|
||||
.then ([appName, build, context, imageName]) ->
|
||||
tmpNameAsync()
|
||||
.then (tmpPath) ->
|
||||
|
||||
# Setup the build args for how the build routine expects them
|
||||
options = _.assign({}, options, { appName })
|
||||
params = _.assign({}, params, { context })
|
||||
|
||||
Promise.try ->
|
||||
if build
|
||||
dockerUtils.runBuild(params, options, getBundleInfo)
|
||||
else
|
||||
imageName
|
||||
.then (imageName) ->
|
||||
Promise.join(
|
||||
dockerUtils.bufferImage(docker, imageName, tmpPath)
|
||||
resin.auth.getToken()
|
||||
resin.auth.whoami()
|
||||
resin.settings.get('resinUrl')
|
||||
dockerUtils.getImageSize(docker, imageName)
|
||||
params.appName
|
||||
performUpload
|
||||
)
|
||||
.finally ->
|
||||
require('fs').unlink(tmpPath)
|
||||
.then (imageName) ->
|
||||
console.log("Successfully deployed image: #{formatImageName(imageName)}")
|
||||
.asCallback(done)
|
@ -32,3 +32,5 @@ module.exports =
|
||||
sync: require('./sync')
|
||||
ssh: require('./ssh')
|
||||
internal: require('./internal')
|
||||
build: require('./build')
|
||||
deploy: require('./deploy')
|
||||
|
@ -34,6 +34,12 @@ events = require('./events')
|
||||
plugins = require('./utils/plugins')
|
||||
update = require('./utils/update')
|
||||
|
||||
# Assign bluebird as the global promise library
|
||||
# stream-to-promise will produce native promises if not
|
||||
# for this module, which could wreak havoc in this
|
||||
# bluebird-only codebase.
|
||||
require('any-promise/register/bluebird')
|
||||
|
||||
capitano.permission 'user', (done) ->
|
||||
resin.auth.isLoggedIn().then (isLoggedIn) ->
|
||||
if not isLoggedIn
|
||||
@ -147,6 +153,10 @@ capitano.command(actions.local.stop)
|
||||
# ---------- Internal utils ----------
|
||||
capitano.command(actions.internal.osInit)
|
||||
|
||||
#------------ Local build and deploy -------
|
||||
capitano.command(actions.build)
|
||||
capitano.command(actions.deploy)
|
||||
|
||||
update.notify()
|
||||
|
||||
plugins.register(/^resin-plugin-(.+)$/).then ->
|
||||
|
200
lib/utils/docker.coffee
Normal file
200
lib/utils/docker.coffee
Normal file
@ -0,0 +1,200 @@
|
||||
# Functions to help actions which rely on using docker
|
||||
|
||||
# Use this function to seed an action's list of capitano options
|
||||
# with the docker options. Using this interface means that
|
||||
# all functions using docker will expose the same interface
|
||||
#
|
||||
# NOTE: Care MUST be taken when using the function, so as to
|
||||
# not redefine/override options already provided.
|
||||
exports.appendOptions = (opts) ->
|
||||
opts.concat [
|
||||
{
|
||||
signature: 'docker'
|
||||
parameter: 'docker'
|
||||
description: 'Path to a local docker socket'
|
||||
alias: 'P'
|
||||
},
|
||||
{
|
||||
signature: 'dockerHost'
|
||||
parameter: 'dockerHost'
|
||||
description: 'The address of the host containing the docker daemon'
|
||||
alias: 'h'
|
||||
},
|
||||
{
|
||||
signature: 'dockerPort'
|
||||
parameter: 'dockerPort'
|
||||
description: 'The port on which the host docker daemon is listening'
|
||||
alias: 'p'
|
||||
},
|
||||
{
|
||||
signature: 'ca'
|
||||
parameter: 'ca'
|
||||
description: 'Docker host TLS certificate authority file'
|
||||
},
|
||||
{
|
||||
signature: 'cert'
|
||||
parameter: 'cert'
|
||||
description: 'Docker host TLS certificate file'
|
||||
},
|
||||
{
|
||||
signature: 'key'
|
||||
parameter: 'key'
|
||||
description: 'Docker host TLS key file'
|
||||
}
|
||||
]
|
||||
|
||||
exports.generateConnectOpts = generateConnectOpts = (opts) ->
|
||||
connectOpts = {}
|
||||
# Firsly need to decide between a local docker socket
|
||||
# and a host available over a host:port combo
|
||||
if opts.docker? and not opts.dockerHost?
|
||||
# good, local docker socket
|
||||
connectOpts.socketPath = opts.docker
|
||||
else if opts.dockerHost? and not opts.docker?
|
||||
# Good a host is provided, and local socket isn't
|
||||
connectOpts.host = opts.dockerHost
|
||||
connectOpts.port = opts.dockerPort || 2376
|
||||
else if opts.docker? and opts.dockerHost?
|
||||
# Both provided, no obvious way to continue
|
||||
throw new Error("Both a local docker socket and docker host have been provided. Don't know how to continue.")
|
||||
else
|
||||
# None provided, assume default docker local socket
|
||||
connectOpts.socketPath = '/var/run/docker.sock'
|
||||
|
||||
# Now need to check if the user wants to connect over TLS
|
||||
# to the host
|
||||
|
||||
# If any are set...
|
||||
if (opts.ca? or opts.cert? or opts.key?)
|
||||
# but not all
|
||||
if not (opts.ca? and opts.cert? and opts.key?)
|
||||
throw new Error('You must provide a CA, certificate and key in order to use TLS')
|
||||
connectOpts.ca = opts.ca
|
||||
connectOpts.cert = opts.cert
|
||||
connectOpts.key = opts.key
|
||||
|
||||
return connectOpts
|
||||
|
||||
exports.tarDirectory = tarDirectory = (dir) ->
|
||||
Promise = require('bluebird')
|
||||
tar = require('tar-stream')
|
||||
klaw = require('klaw')
|
||||
path = require('path')
|
||||
fs = require('mz/fs')
|
||||
streamToPromise = require('stream-to-promise')
|
||||
|
||||
getFiles = ->
|
||||
streamToPromise(klaw(dir))
|
||||
.filter((item) -> not item.stats.isDirectory())
|
||||
.map((item) -> item.path)
|
||||
|
||||
pack = tar.pack()
|
||||
getFiles(dir)
|
||||
.map (file) ->
|
||||
relPath = path.relative(path.resolve(dir), file)
|
||||
Promise.join relPath, fs.stat(file), fs.readFile(file),
|
||||
(filename, stats, data) ->
|
||||
pack.entryAsync({ name: filename, size: stats.size }, data)
|
||||
.then ->
|
||||
pack.finalize()
|
||||
return pack
|
||||
|
||||
# Pass in the command line parameters and options and also
|
||||
# a function which will return the information about the bundle
|
||||
exports.runBuild = (params, options, getBundleInfo) ->
|
||||
Promise = require('bluebird')
|
||||
dockerBuild = require('resin-docker-build')
|
||||
resolver = require('resin-bundle-resolve')
|
||||
|
||||
# The default build context is the current directory
|
||||
params.context ?= '.'
|
||||
|
||||
# Tar up the directory, ready for the build stream
|
||||
tarDirectory(params.context)
|
||||
.then (tarStream) ->
|
||||
new Promise (resolve, reject) ->
|
||||
hooks =
|
||||
buildSuccess: (image) ->
|
||||
if options.tag?
|
||||
console.log("Tagging image as #{options.tag}")
|
||||
resolve(image)
|
||||
buildFailure: reject
|
||||
buildStream: (stream) ->
|
||||
getBundleInfo(options)
|
||||
.then (info) ->
|
||||
if !info?
|
||||
console.log '''
|
||||
Warning: No architecture/device type or application information provided.
|
||||
Dockerfile/project pre-processing will not be performed.
|
||||
'''
|
||||
tarStream.pipe(stream)
|
||||
else
|
||||
[arch, deviceType] = info
|
||||
# Perform type resolution on the project
|
||||
bundle = new resolver.Bundle(tarStream, deviceType, arch)
|
||||
resolver.resolveBundle(bundle, resolver.getDefaultResolvers())
|
||||
.then (resolved) ->
|
||||
console.log("Building #{resolved.projectType} project")
|
||||
# Send the resolved tar stream to the docker daemon
|
||||
resolved.tarStream.pipe(stream)
|
||||
.catch(reject)
|
||||
|
||||
# And print the output
|
||||
stream.pipe(process.stdout)
|
||||
|
||||
# Create a builder
|
||||
connectOpts = generateConnectOpts(options)
|
||||
|
||||
# Allow degugging output, hidden behind an env var
|
||||
if process.env.DEBUG?
|
||||
console.log('Connecting with the following options:')
|
||||
console.log(JSON.stringify(connectOpts, null, ' '))
|
||||
|
||||
builder = new dockerBuild.Builder(connectOpts)
|
||||
opts = {}
|
||||
|
||||
if options.tag?
|
||||
opts['t'] = options.tag
|
||||
if options.nocache?
|
||||
opts['nocache'] = true
|
||||
|
||||
builder.createBuildStream(opts, hooks, reject)
|
||||
|
||||
# Given an image id or tag, export the image to a tar archive.
|
||||
# Also needs the options generated by the appendOptions()
|
||||
# function, and a tmpFile to buffer the data into.
|
||||
exports.bufferImage = (docker, imageId, tmpFile) ->
|
||||
Promise = require('bluebird')
|
||||
fs = require('fs')
|
||||
|
||||
stream = fs.createWriteStream(tmpFile)
|
||||
|
||||
image = docker.getImage(imageId)
|
||||
image.get()
|
||||
.then (img) ->
|
||||
new Promise (resolve, reject) ->
|
||||
img
|
||||
.on('error', reject)
|
||||
.on 'data', (data) ->
|
||||
stream.write(data)
|
||||
.on 'end', ->
|
||||
stream.close()
|
||||
resolve()
|
||||
.then ->
|
||||
new Promise (resolve, reject) ->
|
||||
fs.createReadStream(tmpFile)
|
||||
.on 'open', ->
|
||||
resolve(this)
|
||||
.on('error', reject)
|
||||
|
||||
exports.getDocker = (options) ->
|
||||
Docker = require('dockerode')
|
||||
Promise = require('bluebird')
|
||||
connectOpts = generateConnectOpts(options)
|
||||
# Use bluebird's promises
|
||||
connectOpts['Promise'] = Promise
|
||||
new Docker(connectOpts)
|
||||
|
||||
exports.getImageSize = (docker, image) ->
|
||||
docker.getImage(image).inspectAsync()
|
||||
.get('Size')
|
@ -91,3 +91,17 @@ exports.osProgressHandler = (step) ->
|
||||
progressBars[state.type].update(state)
|
||||
|
||||
return rindle.wait(step)
|
||||
|
||||
exports.getAppInfo = (application) ->
|
||||
resin = require('resin-sdk-preconfigured')
|
||||
_ = require('lodash')
|
||||
Promise.join(
|
||||
resin.models.application.get(application),
|
||||
resin.models.config.getDeviceTypes(),
|
||||
(app, config) ->
|
||||
config = _.find(config, 'slug': app.device_type)
|
||||
if !config?
|
||||
throw new Error('Could not read application information!')
|
||||
app.arch = config.arch
|
||||
return app
|
||||
)
|
||||
|
16
package.json
16
package.json
@ -33,6 +33,9 @@
|
||||
"gulp-shell": "^0.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"any-promise": "^1.3.0",
|
||||
"babel-preset-es2015": "^6.16.0",
|
||||
"babel-register": "^6.16.3",
|
||||
"bluebird": "^3.3.3",
|
||||
"capitano": "^1.7.0",
|
||||
"chalk": "^1.1.3",
|
||||
@ -40,32 +43,41 @@
|
||||
"columnify": "^1.5.2",
|
||||
"denymount": "^2.2.0",
|
||||
"docker-toolbelt": "^1.3.3",
|
||||
"dockerode": "^2.4.2",
|
||||
"drivelist": "^5.0.16",
|
||||
"etcher-image-write": "^9.0.3",
|
||||
"inquirer": "^3.0.6",
|
||||
"is-root": "^1.0.0",
|
||||
"js-yaml": "^3.7.0",
|
||||
"klaw": "^1.3.1",
|
||||
"lodash": "^3.10.0",
|
||||
"mixpanel": "^0.4.0",
|
||||
"moment": "^2.12.0",
|
||||
"mz": "^2.6.0",
|
||||
"nplugm": "^3.0.0",
|
||||
"president": "^2.0.1",
|
||||
"prettyjson": "^1.1.3",
|
||||
"reconfix": "^0.0.3",
|
||||
"raven": "^1.2.0",
|
||||
"resin-cli-auth": "^1.1.3",
|
||||
"reconfix": "^0.0.3",
|
||||
"request": "^2.81.0",
|
||||
"resin-bundle-resolve": "^0.0.2",
|
||||
"resin-cli-auth": "^1.0.0",
|
||||
"resin-cli-errors": "^1.2.0",
|
||||
"resin-cli-form": "^1.4.1",
|
||||
"resin-cli-visuals": "^1.3.0",
|
||||
"resin-config-json": "^1.0.0",
|
||||
"resin-device-config": "^3.0.0",
|
||||
"resin-device-init": "^2.2.1",
|
||||
"resin-docker-build": "^0.4.0",
|
||||
"resin-image-fs": "^2.1.2",
|
||||
"resin-image-manager": "^4.1.1",
|
||||
"resin-sdk-preconfigured": "^6.0.0",
|
||||
"resin-sync": "^7.0.0",
|
||||
"rimraf": "^2.4.3",
|
||||
"rindle": "^1.0.0",
|
||||
"tmp": "^0.0.31",
|
||||
"stream-to-promise": "^2.2.0",
|
||||
"tmp": "0.0.31",
|
||||
"umount": "^1.1.5",
|
||||
"underscore.string": "^3.1.1",
|
||||
"unzip2": "^0.2.5",
|
||||
|
Loading…
x
Reference in New Issue
Block a user