mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-22 06:57:48 +00:00
Merge branch 'master' into typo
This commit is contained in:
commit
9d5949e9d1
19
CHANGELOG.md
19
CHANGELOG.md
@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file
|
|||||||
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## 9.9.2 - 2019-01-11
|
||||||
|
|
||||||
|
* Lazy-load etcher-sdk to speed up startup [Pagan Gazzard]
|
||||||
|
* Lazy-load resin-cli-form and resin-cli-visuals to speed up startup [Pagan Gazzard]
|
||||||
|
|
||||||
|
## 9.9.1 - 2019-01-11
|
||||||
|
|
||||||
|
* Update util available-drives action [Alexis Svinartchouk]
|
||||||
|
* Update lib/actions/local/flash.coffee [Alexis Svinartchouk]
|
||||||
|
|
||||||
|
## 9.9.0 - 2019-01-10
|
||||||
|
|
||||||
|
* Request access to previously pushed release via `balena deploy` [Matthew McGinn]
|
||||||
|
|
||||||
|
## 9.8.0 - 2019-01-01
|
||||||
|
|
||||||
|
* Escape backticks in JS template literal [Trevor Sullivan]
|
||||||
|
* Moving docs from PR #1055 [Trevor Sullivan]
|
||||||
|
|
||||||
## 9.7.0 - 2018-12-28
|
## 9.7.0 - 2018-12-28
|
||||||
|
|
||||||
* Added documentation about the dependencies required to build balena-cli [Trevor Sullivan]
|
* Added documentation about the dependencies required to build balena-cli [Trevor Sullivan]
|
||||||
|
@ -7,6 +7,23 @@ Please make sure your system meets the requirements as specified in the [README]
|
|||||||
|
|
||||||
## Install the CLI
|
## Install the CLI
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
Before installing the Balena CLI from npm, make sure you have the following dependencies installed:
|
||||||
|
|
||||||
|
* make
|
||||||
|
* g++ compiler
|
||||||
|
* Python 2.7
|
||||||
|
* git
|
||||||
|
|
||||||
|
For example, to install these packages on a Debian-based Linux operating systems:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
$ sudo apt-get install g++ make python git --yes
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**NOTE**: If you are installing the stand-alone binary CLI, you will not need to install these dependencies.
|
||||||
|
|
||||||
### Npm install
|
### Npm install
|
||||||
|
|
||||||
The best supported way to install the CLI is from npm:
|
The best supported way to install the CLI is from npm:
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
Promise = require('bluebird')
|
Promise = require('bluebird')
|
||||||
_ = require('lodash')
|
_ = require('lodash')
|
||||||
form = require('resin-cli-form')
|
|
||||||
chalk = require('chalk')
|
chalk = require('chalk')
|
||||||
|
|
||||||
dockerUtils = require('../../utils/docker')
|
dockerUtils = require('../../utils/docker')
|
||||||
@ -15,6 +14,7 @@ exports.filterOutSupervisorContainer = filterOutSupervisorContainer = (container
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
exports.selectContainerFromDevice = Promise.method (deviceIp, filterSupervisor = false) ->
|
exports.selectContainerFromDevice = Promise.method (deviceIp, filterSupervisor = false) ->
|
||||||
|
form = require('resin-cli-form')
|
||||||
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort, timeout: dockerTimeout)
|
docker = dockerUtils.createClient(host: deviceIp, port: dockerPort, timeout: dockerTimeout)
|
||||||
|
|
||||||
# List all containers, including those not running
|
# List all containers, including those not running
|
||||||
|
@ -1,120 +0,0 @@
|
|||||||
###
|
|
||||||
Copyright 2017 Balena
|
|
||||||
|
|
||||||
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.
|
|
||||||
###
|
|
||||||
|
|
||||||
module.exports =
|
|
||||||
signature: 'local flash <image>'
|
|
||||||
description: 'Flash an image to a drive'
|
|
||||||
help: '''
|
|
||||||
Use this command to flash a balenaOS image to a drive.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
$ balena local flash path/to/balenaos.img
|
|
||||||
$ balena local flash path/to/balenaos.img --drive /dev/disk2
|
|
||||||
$ balena local flash path/to/balenaos.img --drive /dev/disk2 --yes
|
|
||||||
'''
|
|
||||||
options: [
|
|
||||||
signature: 'yes'
|
|
||||||
boolean: true
|
|
||||||
description: 'confirm non-interactively'
|
|
||||||
alias: 'y'
|
|
||||||
,
|
|
||||||
signature: 'drive'
|
|
||||||
parameter: 'drive'
|
|
||||||
description: 'drive'
|
|
||||||
alias: 'd'
|
|
||||||
]
|
|
||||||
root: true
|
|
||||||
action: (params, options, done) ->
|
|
||||||
|
|
||||||
_ = require('lodash')
|
|
||||||
os = require('os')
|
|
||||||
Promise = require('bluebird')
|
|
||||||
umountAsync = Promise.promisify(require('umount').umount)
|
|
||||||
fs = Promise.promisifyAll(require('fs'))
|
|
||||||
driveListAsync = Promise.promisify(require('drivelist').list)
|
|
||||||
chalk = require('chalk')
|
|
||||||
visuals = require('resin-cli-visuals')
|
|
||||||
form = require('resin-cli-form')
|
|
||||||
imageWrite = require('etcher-image-write')
|
|
||||||
|
|
||||||
form.run [
|
|
||||||
{
|
|
||||||
message: 'Select drive'
|
|
||||||
type: 'drive'
|
|
||||||
name: 'drive'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: 'This will erase the selected drive. Are you sure?'
|
|
||||||
type: 'confirm'
|
|
||||||
name: 'yes'
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
override:
|
|
||||||
drive: options.drive
|
|
||||||
|
|
||||||
# If `options.yes` is `false`, pass `undefined`,
|
|
||||||
# otherwise the question will not be asked because
|
|
||||||
# `false` is a defined value.
|
|
||||||
yes: options.yes || undefined
|
|
||||||
|
|
||||||
# TODO: dedupe with the resin-device-operations
|
|
||||||
.then (answers) ->
|
|
||||||
if answers.yes isnt true
|
|
||||||
console.log(chalk.red.bold('Aborted image flash'))
|
|
||||||
process.exit(0)
|
|
||||||
|
|
||||||
driveListAsync().then (drives) ->
|
|
||||||
selectedDrive = _.find(drives, device: answers.drive)
|
|
||||||
|
|
||||||
if not selectedDrive?
|
|
||||||
throw new Error("Drive not found: #{answers.drive}")
|
|
||||||
|
|
||||||
return selectedDrive
|
|
||||||
.then (selectedDrive) ->
|
|
||||||
progressBars =
|
|
||||||
write: new visuals.Progress('Flashing')
|
|
||||||
check: new visuals.Progress('Validating')
|
|
||||||
|
|
||||||
umountAsync(selectedDrive.device).then ->
|
|
||||||
Promise.props
|
|
||||||
imageSize: fs.statAsync(params.image).get('size'),
|
|
||||||
imageStream: Promise.resolve(fs.createReadStream(params.image))
|
|
||||||
driveFileDescriptor: fs.openAsync(selectedDrive.raw, 'rs+')
|
|
||||||
.then (results) ->
|
|
||||||
imageWrite.write
|
|
||||||
fd: results.driveFileDescriptor
|
|
||||||
device: selectedDrive.raw
|
|
||||||
size: selectedDrive.size
|
|
||||||
,
|
|
||||||
stream: results.imageStream,
|
|
||||||
size: results.imageSize
|
|
||||||
,
|
|
||||||
check: true
|
|
||||||
.then (writer) ->
|
|
||||||
new Promise (resolve, reject) ->
|
|
||||||
writer.on 'progress', (state) ->
|
|
||||||
progressBars[state.type].update(state)
|
|
||||||
writer.on('error', reject)
|
|
||||||
writer.on('done', resolve)
|
|
||||||
.then ->
|
|
||||||
if (os.platform() is 'win32') and selectedDrive.mountpoint?
|
|
||||||
ejectAsync = Promise.promisify(require('removedrive').eject)
|
|
||||||
return ejectAsync(selectedDrive.mountpoint)
|
|
||||||
|
|
||||||
return umountAsync(selectedDrive.device)
|
|
||||||
.asCallback(done)
|
|
120
lib/actions/local/flash.ts
Normal file
120
lib/actions/local/flash.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 Balena
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CommandDefinition } from 'capitano';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { stripIndent } from 'common-tags';
|
||||||
|
import * as sdk from 'etcher-sdk';
|
||||||
|
|
||||||
|
async function getDrive(options: {
|
||||||
|
drive?: string;
|
||||||
|
}): Promise<sdk.sourceDestination.BlockDevice> {
|
||||||
|
const sdk = await import('etcher-sdk');
|
||||||
|
|
||||||
|
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
|
||||||
|
const scanner = new sdk.scanner.Scanner([adapter]);
|
||||||
|
await scanner.start();
|
||||||
|
let drive: sdk.sourceDestination.BlockDevice;
|
||||||
|
if (options.drive !== undefined) {
|
||||||
|
const d = scanner.getBy('device', options.drive);
|
||||||
|
if (d === undefined || !(d instanceof sdk.sourceDestination.BlockDevice)) {
|
||||||
|
throw new Error(`Drive not found: ${options.drive}`);
|
||||||
|
}
|
||||||
|
drive = d;
|
||||||
|
} else {
|
||||||
|
const { DriveList } = await import('../../utils/visuals/drive-list');
|
||||||
|
const driveList = new DriveList(scanner);
|
||||||
|
drive = await driveList.run();
|
||||||
|
}
|
||||||
|
scanner.stop();
|
||||||
|
return drive;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const flash: CommandDefinition<
|
||||||
|
{ image: string },
|
||||||
|
{ drive: string; yes: boolean }
|
||||||
|
> = {
|
||||||
|
signature: 'local flash <image>',
|
||||||
|
description: 'Flash an image to a drive',
|
||||||
|
//root: true,
|
||||||
|
help: stripIndent`
|
||||||
|
Use this command to flash a balenaOS image to a drive.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
$ balena local flash path/to/balenaos.img
|
||||||
|
$ balena local flash path/to/balenaos.img --drive /dev/disk2
|
||||||
|
$ balena local flash path/to/balenaos.img --drive /dev/disk2 --yes
|
||||||
|
`,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
signature: 'yes',
|
||||||
|
boolean: true,
|
||||||
|
description: 'confirm non-interactively',
|
||||||
|
alias: 'y',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
signature: 'drive',
|
||||||
|
parameter: 'drive',
|
||||||
|
description: 'drive',
|
||||||
|
alias: 'd',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
async action(params, options) {
|
||||||
|
const visuals = await import('resin-cli-visuals');
|
||||||
|
const form = await import('resin-cli-form');
|
||||||
|
const { sourceDestination, multiWrite } = await import('etcher-sdk');
|
||||||
|
|
||||||
|
const drive = await getDrive(options);
|
||||||
|
|
||||||
|
const yes =
|
||||||
|
options.yes ||
|
||||||
|
(await form.ask({
|
||||||
|
message: 'This will erase the selected drive. Are you sure?',
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'yes',
|
||||||
|
default: false,
|
||||||
|
}));
|
||||||
|
if (yes !== true) {
|
||||||
|
console.log(chalk.red.bold('Aborted image flash'));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = new sourceDestination.File(
|
||||||
|
params.image,
|
||||||
|
sourceDestination.File.OpenFlags.Read,
|
||||||
|
);
|
||||||
|
|
||||||
|
const progressBars: { [key: string]: any } = {
|
||||||
|
flashing: new visuals.Progress('Flashing'),
|
||||||
|
verifying: new visuals.Progress('Validating'),
|
||||||
|
};
|
||||||
|
|
||||||
|
await multiWrite.pipeSourceToDestinations(
|
||||||
|
source,
|
||||||
|
[drive],
|
||||||
|
(_, error) => {
|
||||||
|
// onFail
|
||||||
|
console.log(chalk.red.bold(error.message));
|
||||||
|
},
|
||||||
|
(progress: sdk.multiWrite.MultiDestinationProgress) => {
|
||||||
|
// onProgress
|
||||||
|
progressBars[progress.type].update(progress);
|
||||||
|
},
|
||||||
|
true, // verify
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||||||
###
|
###
|
||||||
|
|
||||||
exports.configure = require('./configure')
|
exports.configure = require('./configure')
|
||||||
exports.flash = require('./flash')
|
exports.flash = require('./flash').flash
|
||||||
exports.logs = require('./logs')
|
exports.logs = require('./logs')
|
||||||
exports.scan = require('./scan')
|
exports.scan = require('./scan')
|
||||||
exports.ssh = require('./ssh')
|
exports.ssh = require('./ssh')
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
###
|
|
||||||
Copyright 2016-2017 Balena
|
|
||||||
|
|
||||||
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.
|
|
||||||
###
|
|
||||||
|
|
||||||
_ = require('lodash')
|
|
||||||
|
|
||||||
exports.availableDrives =
|
|
||||||
# TODO: dedupe with https://github.com/balena-io-modules/resin-cli-visuals/blob/master/lib/widgets/drive/index.coffee
|
|
||||||
signature: 'util available-drives'
|
|
||||||
description: 'list available drives'
|
|
||||||
help: """
|
|
||||||
Use this command to list your machine's drives usable for writing the OS image to.
|
|
||||||
Skips the system drives.
|
|
||||||
"""
|
|
||||||
action: ->
|
|
||||||
Promise = require('bluebird')
|
|
||||||
drivelist = require('drivelist')
|
|
||||||
driveListAsync = Promise.promisify(drivelist.list)
|
|
||||||
chalk = require('chalk')
|
|
||||||
visuals = require('resin-cli-visuals')
|
|
||||||
|
|
||||||
formatDrive = (drive) ->
|
|
||||||
size = drive.size / 1000000000
|
|
||||||
return {
|
|
||||||
device: drive.device
|
|
||||||
size: "#{size.toFixed(1)} GB"
|
|
||||||
description: drive.description
|
|
||||||
}
|
|
||||||
|
|
||||||
getDrives = ->
|
|
||||||
driveListAsync().then (drives) ->
|
|
||||||
return _.reject(drives, system: true)
|
|
||||||
|
|
||||||
getDrives()
|
|
||||||
.then (drives) ->
|
|
||||||
if not drives.length
|
|
||||||
console.error("#{chalk.red('x')} No available drives were detected, plug one in!")
|
|
||||||
return
|
|
||||||
|
|
||||||
console.log visuals.table.horizontal drives.map(formatDrive), [
|
|
||||||
'device'
|
|
||||||
'size'
|
|
||||||
'description'
|
|
||||||
]
|
|
65
lib/actions/util.ts
Normal file
65
lib/actions/util.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* decaffeinate suggestions:
|
||||||
|
* DS102: Remove unnecessary code created because of implicit returns
|
||||||
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
Copyright 2016-2017 Balena
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CommandDefinition } from 'capitano';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import { stripIndent } from 'common-tags';
|
||||||
|
|
||||||
|
export const availableDrives: CommandDefinition<{}, {}> = {
|
||||||
|
signature: 'util available-drives',
|
||||||
|
description: 'list available drives',
|
||||||
|
help: stripIndent`
|
||||||
|
Use this command to list your machine's drives usable for writing the OS image to.
|
||||||
|
Skips the system drives.
|
||||||
|
`,
|
||||||
|
async action() {
|
||||||
|
const sdk = await import('etcher-sdk');
|
||||||
|
const visuals = await import('resin-cli-visuals');
|
||||||
|
|
||||||
|
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
|
||||||
|
const scanner = new sdk.scanner.Scanner([adapter]);
|
||||||
|
await scanner.start();
|
||||||
|
|
||||||
|
function formatDrive(drive: any) {
|
||||||
|
const size = drive.size / 1000000000;
|
||||||
|
return {
|
||||||
|
device: drive.device,
|
||||||
|
size: `${size.toFixed(1)} GB`,
|
||||||
|
description: drive.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanner.drives.size === 0) {
|
||||||
|
console.error(
|
||||||
|
`${chalk.red('x')} No available drives were detected, plug one in!`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
visuals.table.horizontal(Array.from(scanner.drives).map(formatDrive), [
|
||||||
|
'device',
|
||||||
|
'size',
|
||||||
|
'description',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
scanner.stop();
|
||||||
|
},
|
||||||
|
};
|
@ -349,13 +349,54 @@ tagServiceImages = (docker, images, serviceImages) ->
|
|||||||
logs: d.logs
|
logs: d.logs
|
||||||
props: d.props
|
props: d.props
|
||||||
|
|
||||||
authorizePush = (tokenAuthEndpoint, registry, images) ->
|
|
||||||
|
getPreviousRepos = (sdk, docker, logger, appID) ->
|
||||||
|
sdk.pine.get(
|
||||||
|
resource: 'release'
|
||||||
|
options:
|
||||||
|
$filter:
|
||||||
|
belongs_to__application: appID
|
||||||
|
status: 'success'
|
||||||
|
$select:
|
||||||
|
[ 'id' ]
|
||||||
|
$expand:
|
||||||
|
contains__image:
|
||||||
|
$expand: 'image'
|
||||||
|
$orderby: 'id desc'
|
||||||
|
$top: 1
|
||||||
|
)
|
||||||
|
.then (release) ->
|
||||||
|
# grab all images from the latest release, return all image locations in the registry
|
||||||
|
if release?.length > 0
|
||||||
|
images = release[0].contains__image
|
||||||
|
Promise.map images, (d) ->
|
||||||
|
imageName = d.image[0].is_stored_at__image_location
|
||||||
|
docker.getRegistryAndName(imageName)
|
||||||
|
.then ( registry ) ->
|
||||||
|
logger.logDebug("Requesting access to previously pushed image repo (#{registry.imageName})")
|
||||||
|
return registry.imageName
|
||||||
|
.catch (e) ->
|
||||||
|
logger.logDebug("Failed to access previously pushed image repo: #{e}")
|
||||||
|
|
||||||
|
authorizePush = (sdk, logger, tokenAuthEndpoint, registry, images, previousRepos) ->
|
||||||
_ = require('lodash')
|
_ = require('lodash')
|
||||||
sdk = require('balena-sdk').fromSharedOptions()
|
|
||||||
|
# TODO: https://github.com/balena-io/balena-cli/issues/1070
|
||||||
|
maxRepos = 20
|
||||||
|
|
||||||
if not _.isArray(images)
|
if not _.isArray(images)
|
||||||
images = [ images ]
|
images = [ images ]
|
||||||
|
|
||||||
|
if images.length > maxRepos
|
||||||
|
throw new Error (
|
||||||
|
"More than #{maxRepos} containers is currently not supported, see " +
|
||||||
|
'https://github.com/balena-io/balena-cli/issues/1070 for more information'
|
||||||
|
)
|
||||||
|
images.push previousRepos...
|
||||||
|
if images.length + previousRepos?.length > maxRepos
|
||||||
|
logger.logDebug("Truncating requested repositories to #{maxRepos} by limiting previously pushed repo access")
|
||||||
|
# at this point, we know we're only truncating access to previously pushed repos
|
||||||
|
images = images[0...maxRepos]
|
||||||
sdk.request.send
|
sdk.request.send
|
||||||
baseUrl: tokenAuthEndpoint
|
baseUrl: tokenAuthEndpoint
|
||||||
url: '/auth/v1/token'
|
url: '/auth/v1/token'
|
||||||
@ -423,7 +464,10 @@ exports.deployProject = (
|
|||||||
tagServiceImages(docker, images, serviceImages)
|
tagServiceImages(docker, images, serviceImages)
|
||||||
.tap (images) ->
|
.tap (images) ->
|
||||||
logger.logDebug('Authorizing push...')
|
logger.logDebug('Authorizing push...')
|
||||||
authorizePush(apiEndpoint, images[0].registry, _.map(images, 'repo'))
|
sdk = require('balena-sdk').fromSharedOptions()
|
||||||
|
getPreviousRepos(sdk, docker, logger, appId)
|
||||||
|
.then (previousRepos) ->
|
||||||
|
authorizePush(sdk, logger, apiEndpoint, images[0].registry, _.map(images, 'repo'), previousRepos)
|
||||||
.then (token) ->
|
.then (token) ->
|
||||||
logger.logInfo('Pushing images to registry...')
|
logger.logInfo('Pushing images to registry...')
|
||||||
pushAndUpdateServiceImages docker, token, images, (serviceImage) ->
|
pushAndUpdateServiceImages docker, token, images, (serviceImage) ->
|
||||||
|
@ -13,11 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
import _form = require('resin-cli-form');
|
||||||
|
import _visuals = require('resin-cli-visuals');
|
||||||
|
|
||||||
import _ = require('lodash');
|
import _ = require('lodash');
|
||||||
import Promise = require('bluebird');
|
import Promise = require('bluebird');
|
||||||
import form = require('resin-cli-form');
|
|
||||||
import visuals = require('resin-cli-visuals');
|
|
||||||
import BalenaSdk = require('balena-sdk');
|
import BalenaSdk = require('balena-sdk');
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import validation = require('./validation');
|
import validation = require('./validation');
|
||||||
@ -25,8 +25,11 @@ import messages = require('./messages');
|
|||||||
|
|
||||||
const balena = BalenaSdk.fromSharedOptions();
|
const balena = BalenaSdk.fromSharedOptions();
|
||||||
|
|
||||||
|
const getForm = _.once((): typeof _form => require('resin-cli-form'));
|
||||||
|
const getVisuals = _.once((): typeof _visuals => require('resin-cli-visuals'));
|
||||||
|
|
||||||
export function authenticate(options: {}): Promise<void> {
|
export function authenticate(options: {}): Promise<void> {
|
||||||
return form
|
return getForm()
|
||||||
.run(
|
.run(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@ -50,7 +53,7 @@ export function authenticate(options: {}): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return form
|
return getForm()
|
||||||
.ask({
|
.ask({
|
||||||
message: 'Two factor auth challenge:',
|
message: 'Two factor auth challenge:',
|
||||||
name: 'code',
|
name: 'code',
|
||||||
@ -72,7 +75,7 @@ export function authenticate(options: {}): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function askLoginType() {
|
export function askLoginType() {
|
||||||
return form.ask({
|
return getForm().ask({
|
||||||
message: 'How would you like to login?',
|
message: 'How would you like to login?',
|
||||||
name: 'loginType',
|
name: 'loginType',
|
||||||
type: 'list',
|
type: 'list',
|
||||||
@ -100,7 +103,7 @@ export function askLoginType() {
|
|||||||
export function selectDeviceType() {
|
export function selectDeviceType() {
|
||||||
return balena.models.config.getDeviceTypes().then(deviceTypes => {
|
return balena.models.config.getDeviceTypes().then(deviceTypes => {
|
||||||
deviceTypes = _.sortBy(deviceTypes, 'name');
|
deviceTypes = _.sortBy(deviceTypes, 'name');
|
||||||
return form.ask({
|
return getForm().ask({
|
||||||
message: 'Device Type',
|
message: 'Device Type',
|
||||||
type: 'list',
|
type: 'list',
|
||||||
choices: _.map(deviceTypes, ({ slug: value, name }) => ({
|
choices: _.map(deviceTypes, ({ slug: value, name }) => ({
|
||||||
@ -124,7 +127,7 @@ export function confirm(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return form.ask({
|
return getForm().ask({
|
||||||
message,
|
message,
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
default: false,
|
default: false,
|
||||||
@ -150,7 +153,7 @@ export function selectApplication(
|
|||||||
})
|
})
|
||||||
.filter(filter || _.constant(true))
|
.filter(filter || _.constant(true))
|
||||||
.then(applications => {
|
.then(applications => {
|
||||||
return form.ask({
|
return getForm().ask({
|
||||||
message: 'Select an application',
|
message: 'Select an application',
|
||||||
type: 'list',
|
type: 'list',
|
||||||
choices: _.map(applications, application => ({
|
choices: _.map(applications, application => ({
|
||||||
@ -181,7 +184,7 @@ export function selectOrCreateApplication() {
|
|||||||
value: null,
|
value: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return form.ask({
|
return getForm().ask({
|
||||||
message: 'Select an application',
|
message: 'Select an application',
|
||||||
type: 'list',
|
type: 'list',
|
||||||
choices: appOptions,
|
choices: appOptions,
|
||||||
@ -193,7 +196,7 @@ export function selectOrCreateApplication() {
|
|||||||
return application;
|
return application;
|
||||||
}
|
}
|
||||||
|
|
||||||
return form.ask({
|
return getForm().ask({
|
||||||
message: 'Choose a Name for your new application',
|
message: 'Choose a Name for your new application',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
validate: validation.validateApplicationName,
|
validate: validation.validateApplicationName,
|
||||||
@ -203,6 +206,7 @@ export function selectOrCreateApplication() {
|
|||||||
|
|
||||||
export function awaitDevice(uuid: string) {
|
export function awaitDevice(uuid: string) {
|
||||||
return balena.models.device.getName(uuid).then(deviceName => {
|
return balena.models.device.getName(uuid).then(deviceName => {
|
||||||
|
const visuals = getVisuals();
|
||||||
const spinner = new visuals.Spinner(
|
const spinner = new visuals.Spinner(
|
||||||
`Waiting for ${deviceName} to come online`,
|
`Waiting for ${deviceName} to come online`,
|
||||||
);
|
);
|
||||||
@ -243,7 +247,7 @@ export function inferOrSelectDevice(preferredUuid: string) {
|
|||||||
? preferredUuid
|
? preferredUuid
|
||||||
: onlineDevices[0].uuid;
|
: onlineDevices[0].uuid;
|
||||||
|
|
||||||
return form.ask({
|
return getForm().ask({
|
||||||
message: 'Select a device',
|
message: 'Select a device',
|
||||||
type: 'list',
|
type: 'list',
|
||||||
default: defaultUuid,
|
default: defaultUuid,
|
||||||
@ -262,7 +266,7 @@ export function selectFromList<T>(
|
|||||||
message: string,
|
message: string,
|
||||||
choices: Array<T & { name: string }>,
|
choices: Array<T & { name: string }>,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
return form.ask({
|
return getForm().ask({
|
||||||
message,
|
message,
|
||||||
type: 'list',
|
type: 'list',
|
||||||
choices: _.map(choices, s => ({
|
choices: _.map(choices, s => ({
|
||||||
|
25
lib/utils/visuals/custom-dynamic-list.ts
Normal file
25
lib/utils/visuals/custom-dynamic-list.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import DynamicList = require('inquirer-dynamic-list');
|
||||||
|
|
||||||
|
export abstract class CustomDynamicList<T> extends DynamicList {
|
||||||
|
constructor(message: string, emptyMessage: string) {
|
||||||
|
super({ message, emptyMessage, choices: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract getThings(): Iterable<T>;
|
||||||
|
|
||||||
|
protected abstract format(thing: T): string;
|
||||||
|
|
||||||
|
refresh(): void {
|
||||||
|
this.opt.choices.choices = [];
|
||||||
|
this.opt.choices.realChoices = [];
|
||||||
|
for (const thing of this.getThings()) {
|
||||||
|
this.addChoice({ name: this.format(thing), value: thing });
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(): Promise<T> {
|
||||||
|
this.refresh();
|
||||||
|
return await super.run();
|
||||||
|
}
|
||||||
|
}
|
32
lib/utils/visuals/drive-list.ts
Normal file
32
lib/utils/visuals/drive-list.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import chalk from 'chalk';
|
||||||
|
import * as _sdk from 'etcher-sdk';
|
||||||
|
|
||||||
|
import { CustomDynamicList } from './custom-dynamic-list';
|
||||||
|
|
||||||
|
export class DriveList extends CustomDynamicList<
|
||||||
|
_sdk.sourceDestination.BlockDevice
|
||||||
|
> {
|
||||||
|
constructor(private scanner: _sdk.scanner.Scanner) {
|
||||||
|
super(
|
||||||
|
'Select a drive',
|
||||||
|
`${chalk.red('x')} No available drives were detected, plug one in!`,
|
||||||
|
);
|
||||||
|
const refresh = this.refresh.bind(this);
|
||||||
|
scanner.on('attach', refresh);
|
||||||
|
scanner.on('detach', refresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected *getThings() {
|
||||||
|
const sdk: typeof _sdk = require('etcher-sdk')
|
||||||
|
for (const drive of this.scanner.drives) {
|
||||||
|
if (drive instanceof sdk.sourceDestination.BlockDevice) {
|
||||||
|
yield drive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected format(drive: _sdk.sourceDestination.BlockDevice) {
|
||||||
|
const size = drive.size / 1e9;
|
||||||
|
return `${drive.device} (${size.toFixed(1)} GB) - ${drive.description}`;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "balena-cli",
|
"name": "balena-cli",
|
||||||
"version": "9.7.0",
|
"version": "9.9.2",
|
||||||
"description": "The official balena CLI tool",
|
"description": "The official balena CLI tool",
|
||||||
"main": "./build/actions/index.js",
|
"main": "./build/actions/index.js",
|
||||||
"homepage": "https://github.com/balena-io/balena-cli",
|
"homepage": "https://github.com/balena-io/balena-cli",
|
||||||
@ -126,9 +126,8 @@
|
|||||||
"docker-toolbelt": "^3.3.5",
|
"docker-toolbelt": "^3.3.5",
|
||||||
"dockerode": "^2.5.5",
|
"dockerode": "^2.5.5",
|
||||||
"dockerode-options": "^0.2.1",
|
"dockerode-options": "^0.2.1",
|
||||||
"drivelist": "^5.0.22",
|
|
||||||
"ejs": "^2.5.7",
|
"ejs": "^2.5.7",
|
||||||
"etcher-image-write": "^9.0.3",
|
"etcher-sdk": "^0.2.0",
|
||||||
"event-stream": "3.3.4",
|
"event-stream": "3.3.4",
|
||||||
"express": "^4.13.3",
|
"express": "^4.13.3",
|
||||||
"global-tunnel-ng": "^2.1.1",
|
"global-tunnel-ng": "^2.1.1",
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./typings/*.d.ts",
|
"./typings/*.d.ts",
|
||||||
|
"./node_modules/etcher-sdk/typings/**/*.d.ts",
|
||||||
"./lib/**/*.ts"
|
"./lib/**/*.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
26
typings/inquire-dynamic-list.d.ts
vendored
Normal file
26
typings/inquire-dynamic-list.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
declare module 'inquirer-dynamic-list' {
|
||||||
|
interface Choice {
|
||||||
|
name: string;
|
||||||
|
value: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DynamicList {
|
||||||
|
opt: {
|
||||||
|
choices: {
|
||||||
|
choices: Choice[];
|
||||||
|
realChoices: Choice[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(options: {
|
||||||
|
message?: string;
|
||||||
|
emptyMessage?: string;
|
||||||
|
choices: Choice[];
|
||||||
|
});
|
||||||
|
addChoice(choice: Choice): void;
|
||||||
|
render(): void;
|
||||||
|
run(): Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export = DynamicList;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user