mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-01-29 15:44:26 +00:00
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:
parent
30cca93283
commit
22b3c39b2b
@ -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
|
||||
|
@ -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
217
build/actions/preload.js
Normal 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);
|
||||
}
|
||||
};
|
@ -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);
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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
223
lib/actions/preload.coffee
Normal 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)
|
@ -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)
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user