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:
Cameron Diver 2017-03-29 12:03:40 +01:00
parent 2e042499af
commit d3772386bf
No known key found for this signature in database
GPG Key ID: E76D7ACBEE436E12
15 changed files with 906 additions and 23 deletions

View File

@ -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
View 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
View 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);
}
};

View File

@ -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')
};

View File

@ -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() {

View File

@ -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
View 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');
};

View File

@ -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
View 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
View 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)

View File

@ -32,3 +32,5 @@ module.exports =
sync: require('./sync')
ssh: require('./ssh')
internal: require('./internal')
build: require('./build')
deploy: require('./deploy')

View File

@ -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
View 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')

View File

@ -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
)

View File

@ -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",