Integrate resin-preload

* split docker connection options from lib.utils.docker.appendOptions

Connects to #609
Connects to https://github.com/resin-io/resin-preload/pull/81

Change-Type: minor
This commit is contained in:
Alexis Svinartchouk 2017-08-04 14:53:31 +02:00
parent 30cca93283
commit 22b3c39b2b
12 changed files with 487 additions and 5 deletions

View File

@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
### Added
- Preload support
## [6.4.0] - 2017-08-11
### Changed

View File

@ -35,5 +35,6 @@ module.exports = {
internal: require('./internal'),
build: require('./build'),
deploy: require('./deploy'),
util: require('./util')
util: require('./util'),
preload: require('./preload')
};

217
build/actions/preload.js Normal file
View File

@ -0,0 +1,217 @@
// Generated by CoffeeScript 1.12.7
/*
Copyright 2016-2017 Resin.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var LATEST, dockerUtils, getApplicationsWithSuccessfulBuilds, offerToDisableAutomaticUpdates, selectApplication, selectApplicationCommit;
dockerUtils = require('../utils/docker');
LATEST = 'latest';
getApplicationsWithSuccessfulBuilds = function(resin, deviceType) {
return resin.pine.get({
resource: 'my_application',
options: {
filter: {
device_type: deviceType,
build: {
$any: {
$alias: 'b',
$expr: {
b: {
status: 'success'
}
}
}
}
},
expand: {
environment_variable: {
$select: ['name', 'value']
},
build: {
$select: ['id', 'commit_hash', 'push_timestamp', 'status'],
$orderby: 'push_timestamp desc'
}
},
select: ['id', 'app_name', 'device_type', 'commit'],
orderby: 'app_name asc'
}
}).then(function(applications) {
applications.forEach(function(application) {
return application.build = application.build.filter(function(build) {
return build.status === 'success';
});
});
return applications;
});
};
selectApplication = function(expectedError, resin, form, deviceType) {
return getApplicationsWithSuccessfulBuilds(resin, deviceType).then(function(applications) {
if (applications.length === 0) {
expectedError("You have no apps with successful builds for a '" + deviceType + "' device type.");
}
return form.ask({
message: 'Select an application',
type: 'list',
choices: applications.map(function(app) {
return {
name: app.app_name,
value: app
};
})
});
});
};
selectApplicationCommit = function(expectedError, resin, form, builds) {
var DEFAULT_CHOICE, choices;
if (builds.length === 0) {
expectedError('This application has no successful builds.');
}
DEFAULT_CHOICE = {
'name': LATEST,
'value': LATEST
};
choices = [DEFAULT_CHOICE].concat(builds.map(function(build) {
return {
name: build.push_timestamp + " - " + build.commit_hash,
value: build.commit_hash
};
}));
return form.ask({
message: 'Select a build',
type: 'list',
"default": LATEST,
choices: choices
});
};
offerToDisableAutomaticUpdates = function(Promise, form, resin, application, commit) {
var message;
if (commit === LATEST || !application.should_track_latest_release) {
return Promise.resolve();
}
message = '\nThis application is set to automatically update all devices to the latest available version.\nThis might be unexpected behaviour: with this enabled, the preloaded device will still\ndownload and install the latest build once it is online.\n\nDo you want to disable automatic updates for this application?';
return form.ask({
message: message,
type: 'confirm'
}).then(function(update) {
if (!update) {
return;
}
return resin.pine.patch({
resource: 'application',
id: application.id,
body: {
should_track_latest_release: false
}
});
});
};
module.exports = {
signature: 'preload <image>',
description: '(beta) preload an app on a disk image',
help: 'Warning: "resin preload" requires Docker to be correctly installed in\nyour shell environment. For more information (including Windows support)\nplease check the README here: https://github.com/resin-io/resin-cli .\n\nUse this command to preload an application to a local disk image with a\nbuilt commit from Resin.io.\nThis can be used with cloud builds, or images deployed with resin deploy.\n\nExamples:\n $ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png\n $ resin preload resin.img',
permission: 'user',
primary: true,
options: dockerUtils.appendConnectionOptions([
{
signature: 'app',
parameter: 'appId',
description: 'id of the application to preload',
alias: 'a'
}, {
signature: 'commit',
parameter: 'hash',
description: 'a specific application commit to preload (ignored if no appId is given)',
alias: 'c'
}, {
signature: 'splash-image',
parameter: 'splashImage.png',
description: 'path to a png image to replace the splash screen',
alias: 's'
}, {
signature: 'dont-detect-flasher-type-images',
boolean: true,
description: 'Disables the flasher type images detection: treats all images as non flasher types'
}
]),
action: function(params, options, done) {
var Promise, _, errors, expectedError, form, preload, resin, streamToPromise;
_ = require('lodash');
Promise = require('bluebird');
resin = require('resin-sdk-preconfigured');
streamToPromise = require('stream-to-promise');
form = require('resin-cli-form');
preload = require('resin-preload');
errors = require('resin-errors');
expectedError = require('../utils/patterns').expectedError;
options.image = params.image;
options.appId = options.app;
delete options.app;
options.dontDetectFlasherTypeImages = options['dont-detect-flasher-type-images'];
delete options['dont-detect-flasher-type-images'];
return dockerUtils.getDocker(options).then(function(docker) {
var buildOutputStream;
buildOutputStream = preload.build(docker);
buildOutputStream.pipe(process.stdout);
return streamToPromise(buildOutputStream).then(resin.settings.getAll).then(function(settings) {
options.proxy = settings.proxy;
options.apiHost = settings.apiUrl;
return preload.getDeviceTypeSlug(docker, options)["catch"](preload.errors.ResinError, expectedError);
}).then(function(deviceType) {
return Promise["try"](function() {
if (options.appId) {
return preload.getApplication(resin, options.appId)["catch"](errors.ResinApplicationNotFound, expectedError);
}
return selectApplication(expectedError, resin, form, deviceType);
}).then(function(application) {
options.application = application;
if (deviceType !== application.device_type) {
expectedError("Image device type (" + application.device_type + ") and application device type (" + deviceType + ") do not match");
}
return Promise["try"](function() {
if (options.commit) {
if (_.map(builds, 'commit_hash').indexOf(options.commit) === -1) {
expectedError('There is no build matching this commit');
}
return options.commit;
}
return selectApplicationCommit(expectedError, resin, form, application.build);
}).then(function(commit) {
if (commit !== LATEST) {
options.commit = commit;
}
return offerToDisableAutomaticUpdates(Promise, form, resin, application, commit);
});
});
}).then(function() {
return preload.run(resin, docker, options)["catch"](preload.errors.ResinError, expectedError);
});
}).then(function(info) {
info.stdout.pipe(process.stdout);
info.stderr.pipe(process.stderr);
return info.statusCodePromise;
}).then(function(statusCode) {
if (statusCode !== 0) {
return process.exit(statusCode);
}
}).then(done);
}
};

View File

@ -199,6 +199,8 @@ capitano.command(actions.logs);
capitano.command(actions.sync);
capitano.command(actions.preload);
capitano.command(actions.ssh);
capitano.command(actions.local.configure);

View File

@ -1,11 +1,11 @@
// Generated by CoffeeScript 1.12.7
var QEMU_BIN_NAME, QEMU_VERSION, cacheHighlightStream, copyQemu, ensureDockerSeemsAccessible, generateConnectOpts, getQemuPath, hasQemu, installQemu, parseBuildArgs, platformNeedsQemu, tarDirectory;
var QEMU_BIN_NAME, QEMU_VERSION, appendConnectionOptions, cacheHighlightStream, copyQemu, ensureDockerSeemsAccessible, generateConnectOpts, getQemuPath, hasQemu, installQemu, parseBuildArgs, platformNeedsQemu, tarDirectory;
QEMU_VERSION = 'v2.5.50-resin-execve';
QEMU_BIN_NAME = 'qemu-execve';
exports.appendOptions = function(opts) {
exports.appendConnectionOptions = appendConnectionOptions = function(opts) {
return opts.concat([
{
signature: 'docker',
@ -34,7 +34,13 @@ exports.appendOptions = function(opts) {
signature: 'key',
parameter: 'key',
description: 'Docker host TLS key file'
}, {
}
]);
};
exports.appendOptions = function(opts) {
return appendConnectionOptions(opts).concat([
{
signature: 'tag',
parameter: 'tag',
description: 'The alias to the generated image',

View File

@ -219,3 +219,11 @@ exports.printErrorMessage = function(message) {
console.error(chalk.red(message));
return console.error(chalk.red("\n" + messages.getHelp + "\n"));
};
exports.expectedError = function(message) {
if (message instanceof Error) {
message = message.message;
}
console.error(chalk.red(message));
return process.exit(1);
};

View File

@ -35,3 +35,4 @@ module.exports =
build: require('./build')
deploy: require('./deploy')
util: require('./util')
preload: require('./preload')

223
lib/actions/preload.coffee Normal file
View File

@ -0,0 +1,223 @@
###
Copyright 2016-2017 Resin.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
dockerUtils = require('../utils/docker')
LATEST = 'latest'
getApplicationsWithSuccessfulBuilds = (resin, deviceType) ->
resin.pine.get
resource: 'my_application'
options:
filter:
device_type: deviceType
build:
$any:
$alias: 'b'
$expr:
b:
status: 'success'
expand:
environment_variable:
$select: ['name', 'value']
build:
$select: [ 'id', 'commit_hash', 'push_timestamp', 'status' ]
$orderby: 'push_timestamp desc'
# FIXME: The filter is commented because it causes an api error.
# We manually filter out successful builds below.
# We should move that here once this API error is resolved.
#$filter:
# status: 'success'
select: [ 'id', 'app_name', 'device_type', 'commit' ]
orderby: 'app_name asc'
# manual filtering
.then (applications) ->
applications.forEach (application) ->
application.build = application.build.filter (build) ->
build.status == 'success'
applications
selectApplication = (expectedError, resin, form, deviceType) ->
getApplicationsWithSuccessfulBuilds(resin, deviceType)
.then (applications) ->
if applications.length == 0
expectedError("You have no apps with successful builds for a '#{deviceType}' device type.")
form.ask
message: 'Select an application'
type: 'list'
choices: applications.map (app) ->
name: app.app_name
value: app
selectApplicationCommit = (expectedError, resin, form, builds) ->
if builds.length == 0
expectedError('This application has no successful builds.')
DEFAULT_CHOICE = {'name': LATEST, 'value': LATEST}
choices = [ DEFAULT_CHOICE ].concat builds.map (build) ->
name: "#{build.push_timestamp} - #{build.commit_hash}"
value: build.commit_hash
return form.ask
message: 'Select a build'
type: 'list'
default: LATEST
choices: choices
offerToDisableAutomaticUpdates = (Promise, form, resin, application, commit) ->
if commit == LATEST or not application.should_track_latest_release
return Promise.resolve()
message = '''
This application is set to automatically update all devices to the latest available version.
This might be unexpected behaviour: with this enabled, the preloaded device will still
download and install the latest build once it is online.
Do you want to disable automatic updates for this application?
'''
form.ask
message: message,
type: 'confirm'
.then (update) ->
if not update
return
resin.pine.patch
resource: 'application'
id: application.id
body:
should_track_latest_release: false
module.exports =
signature: 'preload <image>'
description: '(beta) preload an app on a disk image'
help: '''
Warning: "resin preload" requires Docker to be correctly installed in
your shell environment. For more information (including Windows support)
please check the README here: https://github.com/resin-io/resin-cli .
Use this command to preload an application to a local disk image with a
built commit from Resin.io.
This can be used with cloud builds, or images deployed with resin deploy.
Examples:
$ resin preload resin.img --app 1234 --commit e1f2592fc6ee949e68756d4f4a48e49bff8d72a0 --splash-image some-image.png
$ resin preload resin.img
'''
permission: 'user'
primary: true
options: dockerUtils.appendConnectionOptions [
{
signature: 'app'
parameter: 'appId'
description: 'id of the application to preload'
alias: 'a'
}
{
signature: 'commit'
parameter: 'hash'
description: 'a specific application commit to preload (ignored if no appId is given)'
alias: 'c'
}
{
signature: 'splash-image'
parameter: 'splashImage.png'
description: 'path to a png image to replace the splash screen'
alias: 's'
}
{
signature: 'dont-detect-flasher-type-images'
boolean: true
description: 'Disables the flasher type images detection: treats all images as non flasher types'
}
]
action: (params, options, done) ->
_ = require('lodash')
Promise = require('bluebird')
resin = require('resin-sdk-preconfigured')
streamToPromise = require('stream-to-promise')
form = require('resin-cli-form')
preload = require('resin-preload')
errors = require('resin-errors')
{ expectedError } = require('../utils/patterns')
options.image = params.image
options.appId = options.app
delete options.app
options.dontDetectFlasherTypeImages = options['dont-detect-flasher-type-images']
delete options['dont-detect-flasher-type-images']
# Get a configured dockerode instance
dockerUtils.getDocker(options)
.then (docker) ->
# Build the preloader image
buildOutputStream = preload.build(docker)
buildOutputStream.pipe(process.stdout)
streamToPromise(buildOutputStream)
# Get resin sdk settings so we can pass them to the preloader
.then(resin.settings.getAll)
.then (settings) ->
options.proxy = settings.proxy
options.apiHost = settings.apiUrl
# Use the preloader docker image to extract the deviceType of the image
preload.getDeviceTypeSlug(docker, options)
.catch(preload.errors.ResinError, expectedError)
.then (deviceType) ->
# Use the appId given as --app or show an interactive app selection menu
Promise.try ->
if options.appId
return preload.getApplication(resin, options.appId)
.catch(errors.ResinApplicationNotFound, expectedError)
selectApplication(expectedError, resin, form, deviceType)
.then (application) ->
options.application = application
# Check that the app device type and the image device type match
if deviceType != application.device_type
expectedError(
"Image device type (#{application.device_type}) and application device type (#{deviceType}) do not match"
)
# Use the commit given as --commit or show an interactive commit selection menu
Promise.try ->
if options.commit
if _.map(builds, 'commit_hash').indexOf(options.commit) == -1
expectedError('There is no build matching this commit')
return options.commit
selectApplicationCommit(expectedError, resin, form, application.build)
.then (commit) ->
# No commit specified => use the latest commit
if commit != LATEST
options.commit = commit
# Propose to disable automatic app updates if the commit is not the latest
offerToDisableAutomaticUpdates(Promise, form, resin, application, commit)
.then ->
# All options are ready: preload the image.
preload.run(resin, docker, options)
.catch(preload.errors.ResinError, expectedError)
.then (info) ->
info.stdout.pipe(process.stdout)
info.stderr.pipe(process.stderr)
info.statusCodePromise
.then (statusCode) ->
if statusCode != 0
process.exit(statusCode)
.then(done)

View File

@ -172,6 +172,9 @@ capitano.command(actions.logs)
# ---------- Sync Module ----------
capitano.command(actions.sync)
# ---------- Preload Module ----------
capitano.command(actions.preload)
# ---------- SSH Module ----------
capitano.command(actions.ssh)

View File

@ -9,7 +9,7 @@ QEMU_BIN_NAME = 'qemu-execve'
#
# NOTE: Care MUST be taken when using the function, so as to
# not redefine/override options already provided.
exports.appendOptions = (opts) ->
exports.appendConnectionOptions = appendConnectionOptions = (opts) ->
opts.concat [
{
signature: 'docker'
@ -44,6 +44,16 @@ exports.appendOptions = (opts) ->
parameter: 'key'
description: 'Docker host TLS key file'
},
]
# 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) ->
appendConnectionOptions(opts).concat [
{
signature: 'tag'
parameter: 'tag'

View File

@ -173,3 +173,9 @@ exports.inferOrSelectDevice = (preferredUuid) ->
exports.printErrorMessage = (message) ->
console.error(chalk.red(message))
console.error(chalk.red("\n#{messages.getHelp}\n"))
exports.expectedError = (message) ->
if message instanceof Error
message = message.message
console.error(chalk.red(message))
process.exit(1)

View File

@ -82,6 +82,7 @@
"resin-doodles": "0.0.1",
"resin-image-fs": "^2.3.0",
"resin-image-manager": "^4.1.1",
"resin-preload": "^2.0.0",
"resin-sdk-preconfigured": "^6.4.1",
"resin-settings-client": "^3.6.1",
"resin-stream-logger": "^0.0.4",