Merge pull request #1867 from balena-io/1770-major-sdk-pkg-proxy

Release CLI v12
This commit is contained in:
bulldozer-balena[bot] 2020-06-16 08:00:33 +00:00 committed by GitHub
commit d709e06f48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
105 changed files with 2098 additions and 1226 deletions

View File

@ -5,31 +5,26 @@ npm:
os: alpine os: alpine
architecture: x86_64 architecture: x86_64
node_versions: node_versions:
- "8"
- "10" - "10"
- name: linux - name: linux
os: alpine os: alpine
architecture: x86 architecture: x86
node_versions: node_versions:
- "8"
- "10" - "10"
- name: darwin - name: darwin
os: macos os: macos
architecture: x86_64 architecture: x86_64
node_versions: node_versions:
- "8"
- "10" - "10"
- name: windows - name: windows
os: windows os: windows
architecture: x86_64 architecture: x86_64
node_versions: node_versions:
- "8"
- "10" - "10"
- name: windows - name: windows
os: windows os: windows
architecture: x86 architecture: x86
node_versions: node_versions:
- "8"
- "10" - "10"
docker: docker:

View File

@ -12,7 +12,8 @@ The balena CLI is an open source project and your contribution is welcome!
In order to ease development: In order to ease development:
* `npm run build:fast` skips some of the build steps for interactive testing, or * `npm run build:fast` skips some of the build steps for interactive testing, or
* `./bin/balena-dev` uses `ts-node/register` and `coffeescript/register` to transpile on the fly. * `npm run test:source` skips testing the standalone zip packages (which is rather slow)
* `./bin/balena-dev` uses `ts-node/register` to transpile on the fly.
Before opening a PR, test your changes with `npm test`. Keep compatibility in mind, as the CLI is Before opening a PR, test your changes with `npm test`. Keep compatibility in mind, as the CLI is
meant to run on Linux, macOS and Windows. balena CI will run test code on all three platforms, but meant to run on Linux, macOS and Windows. balena CI will run test code on all three platforms, but
@ -121,22 +122,15 @@ $ npm install # "cleanly" update the npm-shrinkwrap.json file
$ git add npm-shrinkwrap.json # add it for committing (solve merge errors) $ git add npm-shrinkwrap.json # add it for committing (solve merge errors)
``` ```
## TypeScript vs CoffeeScript, and Capitano vs oclif ## TypeScript and oclif
The CLI was originally written in [CoffeeScript](https://coffeescript.org), but we decided to The CLI currently contains a mix of plain JavaScript and
migrate to [TypeScript](https://www.typescriptlang.org/) in order to take advantage of static [TypeScript](https://www.typescriptlang.org/) code. The goal is to have all code written in
typing and formal programming interfaces. The migration is taking place gradually, as part of Typescript, in order to take advantage of static typing and formal programming interfaces.
maintenance work or the implementation of new features. The recommended way of making the The migration towards Typescript is taking place gradually, as part of maintenance work or
conversion is to first generate plain Javascript, for example using the command: the implementation of new features. Historically, the CLI was originally written in
[CoffeeScript](https://coffeescript.org), but all CoffeeScript code was migrated to either
``` Javascript or Typescript.
npx decaffeinate --use-js-modules file.coffee
```
Then manually convert plain Javascript to Typescript. There is also a ["Coffeescript Preview"
Visual Studio Code
extension](https://marketplace.visualstudio.com/items?itemName=drewbarrett.vscode-coffeescript-preview)
that you may find handy.
Similarly, [Capitano](https://github.com/balena-io/capitano) was originally adopted as the CLI's Similarly, [Capitano](https://github.com/balena-io/capitano) was originally adopted as the CLI's
framework, but later we decided to take advantage of [oclif](https://oclif.io/)'s features such framework, but later we decided to take advantage of [oclif](https://oclif.io/)'s features such

View File

@ -118,7 +118,7 @@ async function buildPkg() {
console.log(`\nCopying to build-bin:\n${nativeExtensionPaths.join('\n')}`); console.log(`\nCopying to build-bin:\n${nativeExtensionPaths.join('\n')}`);
await Bluebird.map(nativeExtensionPaths, extPath => await Bluebird.map(nativeExtensionPaths, (extPath) =>
fs.copy( fs.copy(
extPath, extPath,
extPath.replace( extPath.replace(
@ -268,7 +268,7 @@ export async function buildOclifInstaller() {
} }
for (const dir of dirs) { for (const dir of dirs) {
console.log(`rimraf(${dir})`); console.log(`rimraf(${dir})`);
await Bluebird.fromCallback(cb => rimraf(dir, cb)); await Bluebird.fromCallback((cb) => rimraf(dir, cb));
} }
console.log('======================================================='); console.log('=======================================================');
console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`); console.log(`oclif-dev "${packCmd}" "${packOpts.join('" "')}"`);

View File

@ -55,7 +55,7 @@ function renderOclifCommand(command: OclifCommand): string[] {
result.push(description); result.push(description);
if (!_.isEmpty(command.examples)) { if (!_.isEmpty(command.examples)) {
result.push('Examples:', command.examples!.map(v => `\t${v}`).join('\n')); result.push('Examples:', command.examples!.map((v) => `\t${v}`).join('\n'));
} }
if (!_.isEmpty(command.args)) { if (!_.isEmpty(command.args)) {
@ -106,7 +106,7 @@ function renderToc(categories: Category[]): string[] {
result.push(`- ${category.title}`); result.push(`- ${category.title}`);
result.push( result.push(
category.commands category.commands
.map(command => { .map((command) => {
const signature = const signature =
typeof command === 'object' typeof command === 'object'
? command.signature // Capitano ? command.signature // Capitano
@ -139,10 +139,7 @@ function sortCommands(doc: Document): void {
(cmd: CapitanoCommand | OclifCommand, x: string) => (cmd: CapitanoCommand | OclifCommand, x: string) =>
typeof cmd === 'object' // Capitano vs oclif command typeof cmd === 'object' // Capitano vs oclif command
? cmd.signature.replace(/\W+/g, ' ').includes(x) ? cmd.signature.replace(/\W+/g, ' ').includes(x)
: (cmd.usage || '') : (cmd.usage || '').toString().replace(/\W+/g, ' ').includes(x),
.toString()
.replace(/\W+/g, ' ')
.includes(x),
), ),
); );
} }

View File

@ -93,7 +93,7 @@ export class MarkdownFileParser {
crlfDelay: Infinity, crlfDelay: Infinity,
}); });
rl.on('line', line => { rl.on('line', (line) => {
// try to match a line like "## Getting Started", where the number // try to match a line like "## Getting Started", where the number
// of '#' characters is the sectionLevel ('##' -> 2), and the // of '#' characters is the sectionLevel ('##' -> 2), and the
// sectionTitle is "Getting Started" // sectionTitle is "Getting Started"

View File

@ -40,13 +40,13 @@ async function checkBuildTimestamps() {
const stagedFiles = _.uniq([ const stagedFiles = _.uniq([
...gitStatus.created, ...gitStatus.created,
...gitStatus.staged, ...gitStatus.staged,
...gitStatus.renamed.map(o => o.to), ...gitStatus.renamed.map((o) => o.to),
]) ])
// select only staged files that start with lib/ or typings/ // select only staged files that start with lib/ or typings/
.filter(f => f.match(/^(lib|typings)[/\\]/)) .filter((f) => f.match(/^(lib|typings)[/\\]/))
.map(f => path.join(ROOT, f)); .map((f) => path.join(ROOT, f));
const fStats = await Promise.all(stagedFiles.map(f => fs.stat(f))); const fStats = await Promise.all(stagedFiles.map((f) => fs.stat(f)));
fStats.forEach((fStat, index) => { fStats.forEach((fStat, index) => {
if (fStat.mtimeMs > docStat.mtimeMs) { if (fStat.mtimeMs > docStat.mtimeMs) {
const fPath = stagedFiles[index]; const fPath = stagedFiles[index];

View File

@ -31,9 +31,7 @@ function semverGte(v1, v2) {
function checkNpmVersion() { function checkNpmVersion() {
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const npmVersion = execSync('npm --version') const npmVersion = execSync('npm --version').toString().trim();
.toString()
.trim();
const requiredVersion = '6.9.0'; const requiredVersion = '6.9.0';
if (!semverGte(npmVersion, requiredVersion)) { if (!semverGte(npmVersion, requiredVersion)) {
// In case you take issue with the error message below: // In case you take issue with the error message below:

View File

@ -61,7 +61,7 @@ export async function release() {
} }
/** Return a cached Octokit instance, creating a new one as needed. */ /** Return a cached Octokit instance, creating a new one as needed. */
const getOctokit = _.once(function() { const getOctokit = _.once(function () {
const Octokit = (require('@octokit/rest') as typeof import('@octokit/rest')).Octokit.plugin( const Octokit = (require('@octokit/rest') as typeof import('@octokit/rest')).Octokit.plugin(
require('@octokit/plugin-throttling'), require('@octokit/plugin-throttling'),
); );

View File

@ -87,7 +87,7 @@ async function main() {
const upstreamName = process.argv[2]; const upstreamName = process.argv[2];
const upstream = upstreams.find(v => v.repo === upstreamName); const upstream = upstreams.find((v) => v.repo === upstreamName);
if (!upstream) { if (!upstream) {
console.error( console.error(

View File

@ -50,7 +50,7 @@ export async function runUnderMsys(argv?: string[]) {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const args = ['-lc', shellEscape(newArgv)]; const args = ['-lc', shellEscape(newArgv)];
const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' }); const child = spawn(MSYS2_BASH, args, { stdio: 'inherit' });
child.on('close', code => { child.on('close', (code) => {
if (code) { if (code) {
console.log(`runUnderMsys: child process exited with code ${code}`); console.log(`runUnderMsys: child process exited with code ${code}`);
reject(code); reject(code);
@ -93,7 +93,7 @@ export async function getSubprocessStdout(
// every line provided to the stderr stream // every line provided to the stderr stream
const lines = _.filter( const lines = _.filter(
stderr.trim().split(/\r?\n/), stderr.trim().split(/\r?\n/),
line => !line.startsWith('[debug]'), (line) => !line.startsWith('[debug]'),
); );
if (lines.length > 0) { if (lines.length > 0) {
reject( reject(

View File

@ -306,7 +306,7 @@ Examples:
#### -v, --verbose #### -v, --verbose
add extra columns in the tabular output (SLUG) No-op since release v12.0.0
## app <name> ## app <name>
@ -729,24 +729,22 @@ List the environment or configuration variables of an application, device or
service, as selected by the respective command-line options. (A service is service, as selected by the respective command-line options. (A service is
an application container in a "microservices" application.) an application container in a "microservices" application.)
The results include application-wide (fleet), device-wide (multiple services on
a device) and service-specific variables that apply to the selected application,
device or service. It can be thought of as including "inherited" variables;
for example, a service inherits device-wide variables, and a device inherits
application-wide variables.
The printed output may include DEVICE and/or SERVICE columns to distinguish
between application-wide, device-specific and service-specific variables.
An asterisk in these columns indicates that the variable applies to
"all devices" or "all services".
The --config option is used to list "configuration variables" that control The --config option is used to list "configuration variables" that control
balena platform features, as opposed to custom environment variables defined balena platform features, as opposed to custom environment variables defined
by the user. The --config and the --service options are mutually exclusive by the user. The --config and the --service options are mutually exclusive
because configuration variables cannot be set for specific services. because configuration variables cannot be set for specific services.
The --all option is used to include application-wide (fleet), device-wide
(multiple services on a device) and service-specific variables that apply to
the selected application, device or service. It can be thought of as including
"inherited" variables: for example, a service inherits device-wide variables,
and a device inherits application-wide variables. Variables are still filtered
out by type with the --config option, such that configuration and non-
configuration variables are never listed together.
When the --all option is used, the printed output may include DEVICE and/or
SERVICE columns to distinguish between application-wide, device-specific and
service-specific variables. An asterisk in these columns indicates that the
variable applies to "all devices" or "all services".
The --json option is recommended when scripting the output of this command, The --json option is recommended when scripting the output of this command,
because the JSON format is less likely to change and it better represents data because the JSON format is less likely to change and it better represents data
types like lists and empty strings. The 'jq' utility may be helpful in shell types like lists and empty strings. The 'jq' utility may be helpful in shell
@ -761,21 +759,20 @@ by its owner).
Examples: Examples:
$ balena envs --application MyApp $ balena envs --application MyApp
$ balena envs --application MyApp --all --json $ balena envs --application MyApp --json
$ balena envs --application MyApp --service MyService
$ balena envs --application MyApp --service MyService $ balena envs --application MyApp --service MyService
$ balena envs --application MyApp --all --service MyService
$ balena envs --application MyApp --config $ balena envs --application MyApp --config
$ balena envs --device 7cf02a6 $ balena envs --device 7cf02a6
$ balena envs --device 7cf02a6 --all --json $ balena envs --device 7cf02a6 --json
$ balena envs --device 7cf02a6 --config --all --json $ balena envs --device 7cf02a6 --config --json
$ balena envs --device 7cf02a6 --all --service MyService $ balena envs --device 7cf02a6 --service MyService
### Options ### Options
#### --all #### --all
include app-wide, device-wide variables that apply to the selected device or service. No-op since balena CLI v12.0.0.
Variables are still filtered out by type with the --config option.
#### -a, --application APPLICATION #### -a, --application APPLICATION
@ -1868,25 +1865,16 @@ secrets.json file exists in the balena directory (usually $HOME/.balena),
this file will be used instead. this file will be used instead.
DOCKERIGNORE AND GITIGNORE FILES DOCKERIGNORE AND GITIGNORE FILES
By default, both '.dockerignore' and '.gitignore' files are taken into account The balena CLI will use a '.dockerignore' file (if any) at the source directory
in order to prevent files from being sent to the balenaCloud builder or Docker in order to decide which source files to exclude from the "build context" sent
or balenaEngine (balenaOS device). to balenaCloud, Docker or balenaEngine. Previous balena CLI releases (before
v12.0.0) also took '.gitignore' files into account, but this is no longer the
case. This allows files to be used for an image build even if they are listed
in '.gitignore'.
However, this behavior has been DEPRECATED and will change in an upcoming major A few "hardcoded" dockerignore patterns are also used and "merged" (in memory)
version release. The --nogitignore (-G) option should be used to enable the new with the patterns found in the '.dockerignore' file (if any), in the following
behavior already now. This option will cause the CLI to: order:
* Disregard all '.gitignore' files at the source directory and subdirectories,
and consider only the '.dockerignore' file (if any) at the source directory.
* Consequently, allow files to be sent to balenaCloud / Docker / balenaEngine
even if they are listed in '.gitignore' files (a longstanding feature request).
* Use a new '.dockerignore' parser and filter library that improves compatibility
with "docker build" and fixes several issues (mainly on Windows).
* Prevent a warning message from being printed.
When --nogitignore (-G) is provided, a few "hardcoded" dockerignore patterns are
also used and "merged" (in memory) with the patterns found in the '.dockerignore'
file (if any), in the following order:
**/.git **/.git
< user's patterns from the '.dockerignore' file, if any > < user's patterns from the '.dockerignore' file, if any >
@ -1982,14 +1970,15 @@ left hand side of the = character will be treated as the variable name.
#### --convert-eol, -l #### --convert-eol, -l
On Windows only, convert line endings from CRLF (Windows format) to LF (Unix format). No-op and deprecated since balena CLI v12.0.0
Source files are not modified.
#### --noconvert-eol
Don't convert line endings from CRLF (Windows format) to LF (Unix format).
#### --nogitignore, -G #### --nogitignore, -G
Disregard all .gitignore files, and consider only the .dockerignore file (if any) No-op and deprecated since balena CLI v12.0.0. See "balena help push".
at the source directory. This will be the default behavior in an upcoming major
version release. For more information, see 'balena help push'.
# Settings # Settings
@ -2076,25 +2065,16 @@ secrets.json file exists in the balena directory (usually $HOME/.balena),
this file will be used instead. this file will be used instead.
DOCKERIGNORE AND GITIGNORE FILES DOCKERIGNORE AND GITIGNORE FILES
By default, both '.dockerignore' and '.gitignore' files are taken into account The balena CLI will use a '.dockerignore' file (if any) at the source directory
in order to prevent files from being sent to the balenaCloud builder or Docker in order to decide which source files to exclude from the "build context" sent
or balenaEngine (balenaOS device). to balenaCloud, Docker or balenaEngine. Previous balena CLI releases (before
v12.0.0) also took '.gitignore' files into account, but this is no longer the
case. This allows files to be used for an image build even if they are listed
in '.gitignore'.
However, this behavior has been DEPRECATED and will change in an upcoming major A few "hardcoded" dockerignore patterns are also used and "merged" (in memory)
version release. The --nogitignore (-G) option should be used to enable the new with the patterns found in the '.dockerignore' file (if any), in the following
behavior already now. This option will cause the CLI to: order:
* Disregard all '.gitignore' files at the source directory and subdirectories,
and consider only the '.dockerignore' file (if any) at the source directory.
* Consequently, allow files to be sent to balenaCloud / Docker / balenaEngine
even if they are listed in '.gitignore' files (a longstanding feature request).
* Use a new '.dockerignore' parser and filter library that improves compatibility
with "docker build" and fixes several issues (mainly on Windows).
* Prevent a warning message from being printed.
When --nogitignore (-G) is provided, a few "hardcoded" dockerignore patterns are
also used and "merged" (in memory) with the patterns found in the '.dockerignore'
file (if any), in the following order:
**/.git **/.git
< user's patterns from the '.dockerignore' file, if any > < user's patterns from the '.dockerignore' file, if any >
@ -2148,13 +2128,15 @@ Alternative Dockerfile name/path, relative to the source folder
#### --logs #### --logs
Display full log output No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.
#### --nologs
Hide the image build log output (produce less verbose output)
#### --nogitignore, -G #### --nogitignore, -G
Disregard all .gitignore files, and consider only the .dockerignore file (if any) No-op and deprecated since balena CLI v12.0.0. See "balena help undefined".
at the source directory. This will be the default behavior in an upcoming major
version release. For more information, see 'balena help undefined'.
#### --noparent-check #### --noparent-check
@ -2166,7 +2148,11 @@ Path to a YAML or JSON file with passwords for a private Docker registry
#### --convert-eol, -l #### --convert-eol, -l
On Windows only, convert line endings from CRLF (Windows format) to LF (Unix format). Source files are not modified. No-op and deprecated since balena CLI v12.0.0
#### --noconvert-eol
Don't convert line endings from CRLF (Windows format) to LF (Unix format).
#### --docker, -P &#60;docker&#62; #### --docker, -P &#60;docker&#62;
@ -2259,25 +2245,16 @@ secrets.json file exists in the balena directory (usually $HOME/.balena),
this file will be used instead. this file will be used instead.
DOCKERIGNORE AND GITIGNORE FILES DOCKERIGNORE AND GITIGNORE FILES
By default, both '.dockerignore' and '.gitignore' files are taken into account The balena CLI will use a '.dockerignore' file (if any) at the source directory
in order to prevent files from being sent to the balenaCloud builder or Docker in order to decide which source files to exclude from the "build context" sent
or balenaEngine (balenaOS device). to balenaCloud, Docker or balenaEngine. Previous balena CLI releases (before
v12.0.0) also took '.gitignore' files into account, but this is no longer the
case. This allows files to be used for an image build even if they are listed
in '.gitignore'.
However, this behavior has been DEPRECATED and will change in an upcoming major A few "hardcoded" dockerignore patterns are also used and "merged" (in memory)
version release. The --nogitignore (-G) option should be used to enable the new with the patterns found in the '.dockerignore' file (if any), in the following
behavior already now. This option will cause the CLI to: order:
* Disregard all '.gitignore' files at the source directory and subdirectories,
and consider only the '.dockerignore' file (if any) at the source directory.
* Consequently, allow files to be sent to balenaCloud / Docker / balenaEngine
even if they are listed in '.gitignore' files (a longstanding feature request).
* Use a new '.dockerignore' parser and filter library that improves compatibility
with "docker build" and fixes several issues (mainly on Windows).
* Prevent a warning message from being printed.
When --nogitignore (-G) is provided, a few "hardcoded" dockerignore patterns are
also used and "merged" (in memory) with the patterns found in the '.dockerignore'
file (if any), in the following order:
**/.git **/.git
< user's patterns from the '.dockerignore' file, if any > < user's patterns from the '.dockerignore' file, if any >
@ -2327,13 +2304,15 @@ Alternative Dockerfile name/path, relative to the source folder
#### --logs #### --logs
Display full log output No-op and deprecated since balena CLI v12.0.0. Build logs are now shown by default.
#### --nologs
Hide the image build log output (produce less verbose output)
#### --nogitignore, -G #### --nogitignore, -G
Disregard all .gitignore files, and consider only the .dockerignore file (if any) No-op and deprecated since balena CLI v12.0.0. See "balena help undefined".
at the source directory. This will be the default behavior in an upcoming major
version release. For more information, see 'balena help undefined'.
#### --noparent-check #### --noparent-check
@ -2345,7 +2324,11 @@ Path to a YAML or JSON file with passwords for a private Docker registry
#### --convert-eol, -l #### --convert-eol, -l
On Windows only, convert line endings from CRLF (Windows format) to LF (Unix format). Source files are not modified. No-op and deprecated since balena CLI v12.0.0
#### --noconvert-eol
Don't convert line endings from CRLF (Windows format) to LF (Unix format).
#### --docker, -P &#60;docker&#62; #### --docker, -P &#60;docker&#62;

View File

@ -74,9 +74,9 @@ export default class AppsCmd extends Command {
); );
// Add extended properties // Add extended properties
applications.forEach(application => { applications.forEach((application) => {
application.device_count = _.size(application.owns__device); application.device_count = _.size(application.owns__device);
application.online_devices = _.sumBy(application.owns__device, d => application.online_devices = _.sumBy(application.owns__device, (d) =>
d.is_online === true ? 1 : 0, d.is_online === true ? 1 : 0,
); );
}); });

View File

@ -60,13 +60,13 @@ export default class DevicePublicUrlCmd extends Command {
{ {
name: 'uuid', name: 'uuid',
description: 'the uuid of the device to manage', description: 'the uuid of the device to manage',
parse: dev => tryAsInteger(dev), parse: (dev) => tryAsInteger(dev),
required: true, required: true,
}, },
{ {
// Optional hidden arg to support old command format // Optional hidden arg to support old command format
name: 'legacyUuid', name: 'legacyUuid',
parse: dev => tryAsInteger(dev), parse: (dev) => tryAsInteger(dev),
hidden: true, hidden: true,
}, },
]; ];

View File

@ -80,7 +80,7 @@ export default class DevicesSupportedCmd extends Command {
const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd); const { flags: options } = this.parse<FlagsDef, {}>(DevicesSupportedCmd);
let deviceTypes: Array<Partial<SDK.DeviceType>> = await getBalenaSdk() let deviceTypes: Array<Partial<SDK.DeviceType>> = await getBalenaSdk()
.models.config.getDeviceTypes() .models.config.getDeviceTypes()
.map(d => { .map((d) => {
if (d.aliases && d.aliases.length) { if (d.aliases && d.aliases.length) {
// remove aliases that are equal to the slug // remove aliases that are equal to the slug
d.aliases = d.aliases.filter((alias: string) => alias !== d.slug); d.aliases = d.aliases.filter((alias: string) => alias !== d.slug);
@ -95,7 +95,7 @@ export default class DevicesSupportedCmd extends Command {
return d; return d;
}); });
if (!options.discontinued) { if (!options.discontinued) {
deviceTypes = deviceTypes.filter(dt => dt.state !== 'DISCONTINUED'); deviceTypes = deviceTypes.filter((dt) => dt.state !== 'DISCONTINUED');
} }
const fields = options.verbose const fields = options.verbose
? ['slug', 'aliases', 'arch', 'state', 'name'] ? ['slug', 'aliases', 'arch', 'state', 'name']
@ -103,7 +103,7 @@ export default class DevicesSupportedCmd extends Command {
? ['slug', 'aliases', 'arch', 'name'] ? ['slug', 'aliases', 'arch', 'name']
: ['slug', 'name']; : ['slug', 'name'];
deviceTypes = _.sortBy( deviceTypes = _.sortBy(
deviceTypes.map(d => _.pick(d, fields) as Partial<SDK.DeviceType>), deviceTypes.map((d) => _.pick(d, fields) as Partial<SDK.DeviceType>),
fields, fields,
); );
if (options.json) { if (options.json) {

View File

@ -130,7 +130,7 @@ export default class EnvAddCmd extends Command {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const reservedPrefixes = await getReservedPrefixes(balena); const reservedPrefixes = await getReservedPrefixes(balena);
const isConfigVar = _.some(reservedPrefixes, prefix => const isConfigVar = _.some(reservedPrefixes, (prefix) =>
_.startsWith(params.name, prefix), _.startsWith(params.name, prefix),
); );

View File

@ -61,7 +61,7 @@ export default class EnvRenameCmd extends Command {
name: 'id', name: 'id',
required: true, required: true,
description: "variable's numeric database ID", description: "variable's numeric database ID",
parse: input => parseAsInteger(input, 'id'), parse: (input) => parseAsInteger(input, 'id'),
}, },
{ {
name: 'value', name: 'value',

View File

@ -64,7 +64,7 @@ export default class EnvRmCmd extends Command {
name: 'id', name: 'id',
required: true, required: true,
description: "variable's numeric database ID", description: "variable's numeric database ID",
parse: input => parseAsInteger(input, 'id'), parse: (input) => parseAsInteger(input, 'id'),
}, },
]; ];

View File

@ -45,7 +45,7 @@ export default class KeyCmd extends Command {
{ {
name: 'id', name: 'id',
description: 'balenaCloud ID for the SSH key', description: 'balenaCloud ID for the SSH key',
parse: x => parseAsInteger(x, 'id'), parse: (x) => parseAsInteger(x, 'id'),
required: true, required: true,
}, },
]; ];

View File

@ -48,7 +48,7 @@ export default class KeyRmCmd extends Command {
{ {
name: 'id', name: 'id',
description: 'balenaCloud ID for the SSH key', description: 'balenaCloud ID for the SSH key',
parse: x => parseAsInteger(x, 'id'), parse: (x) => parseAsInteger(x, 'id'),
required: true, required: true,
}, },
]; ];

View File

@ -47,7 +47,7 @@ export default class KeysCmd extends Command {
const keys = await getBalenaSdk().models.key.getAll(); const keys = await getBalenaSdk().models.key.getAll();
// Use 'name' instead of 'title' to match dashboard. // Use 'name' instead of 'title' to match dashboard.
const displayKeys: Array<{ id: number; name: string }> = keys.map(k => { const displayKeys: Array<{ id: number; name: string }> = keys.map((k) => {
return { id: k.id, name: k.title }; return { id: k.id, name: k.title };
}); });

View File

@ -261,7 +261,7 @@ export default class OsConfigureCmd extends Command {
if (options['system-connection']) { if (options['system-connection']) {
const files = await Bluebird.map( const files = await Bluebird.map(
options['system-connection'], options['system-connection'],
async filePath => { async (filePath) => {
const content = await fs.readFile(filePath, 'utf8'); const content = await fs.readFile(filePath, 'utf8');
const name = path.basename(filePath); const name = path.basename(filePath);
@ -485,7 +485,7 @@ function camelifyConfigOptions(options: FlagsDef): { [key: string]: any } {
if (key.startsWith('config-')) { if (key.startsWith('config-')) {
return key return key
.substring('config-'.length) .substring('config-'.length)
.replace(/-[a-z]/g, match => match.substring(1).toUpperCase()); .replace(/-[a-z]/g, (match) => match.substring(1).toUpperCase());
} }
return key; return key;
}); });

View File

@ -33,10 +33,10 @@ Opts must be an object with the following keys:
buildEmulated buildEmulated
buildOpts: arguments to forward to docker build command buildOpts: arguments to forward to docker build command
*/ */
const buildProject = function(docker, logger, composeOpts, opts) { const buildProject = function (docker, logger, composeOpts, opts) {
const { loadProject } = require('../utils/compose_ts'); const { loadProject } = require('../utils/compose_ts');
return Promise.resolve(loadProject(logger, composeOpts)) return Promise.resolve(loadProject(logger, composeOpts))
.then(function(project) { .then(function (project) {
const appType = opts.app?.application_type?.[0]; const appType = opts.app?.application_type?.[0];
if ( if (
appType != null && appType != null &&
@ -65,7 +65,7 @@ const buildProject = function(docker, logger, composeOpts, opts) {
composeOpts.nogitignore, composeOpts.nogitignore,
); );
}) })
.then(function() { .then(function () {
logger.outputDeferredMessages(); logger.outputDeferredMessages();
logger.logSuccess('Build succeeded!'); logger.logSuccess('Build succeeded!');
}) })
@ -154,7 +154,7 @@ Examples:
const { application, arch, deviceType } = options; const { application, arch, deviceType } = options;
return Promise.try(function() { return Promise.try(function () {
if ( if (
(application == null && (arch == null || deviceType == null)) || (application == null && (arch == null || deviceType == null)) ||
(application != null && (arch != null || deviceType != null)) (application != null && (arch != null || deviceType != null))
@ -175,7 +175,7 @@ Examples:
registrySecretsPath: options['registry-secrets'], registrySecretsPath: options['registry-secrets'],
}), }),
) )
.then(function({ dockerfilePath, registrySecrets }) { .then(function ({ dockerfilePath, registrySecrets }) {
options.dockerfile = dockerfilePath; options.dockerfile = dockerfilePath;
options['registry-secrets'] = registrySecrets; options['registry-secrets'] = registrySecrets;
@ -184,11 +184,11 @@ Examples:
} else { } else {
return helpers return helpers
.getAppWithArch(application) .getAppWithArch(application)
.then(app => [app, app.arch, app.device_type]); .then((app) => [app, app.arch, app.device_type]);
} }
}) })
.then(function([app, resolvedArch, resolvedDeviceType]) { .then(function ([app, resolvedArch, resolvedDeviceType]) {
return Promise.join( return Promise.join(
dockerUtils.getDocker(options), dockerUtils.getDocker(options),
dockerUtils.generateBuildOpts(options), dockerUtils.generateBuildOpts(options),

View File

@ -58,8 +58,8 @@ Examples:
() => options.drive || getVisuals().drive('Select the device drive'), () => options.drive || getVisuals().drive('Select the device drive'),
) )
.tap(umountAsync) .tap(umountAsync)
.then(drive => config.read(drive, options.type)) .then((drive) => config.read(drive, options.type))
.tap(configJSON => { .tap((configJSON) => {
console.info(prettyjson.render(configJSON)); console.info(prettyjson.render(configJSON));
}); });
}, },
@ -105,16 +105,16 @@ Examples:
() => options.drive || getVisuals().drive('Select the device drive'), () => options.drive || getVisuals().drive('Select the device drive'),
) )
.tap(umountAsync) .tap(umountAsync)
.then(drive => .then((drive) =>
config config
.read(drive, options.type) .read(drive, options.type)
.then(function(configJSON) { .then(function (configJSON) {
console.info(`Setting ${params.key} to ${params.value}`); console.info(`Setting ${params.key} to ${params.value}`);
_.set(configJSON, params.key, params.value); _.set(configJSON, params.key, params.value);
return configJSON; return configJSON;
}) })
.tap(() => umountAsync(drive)) .tap(() => umountAsync(drive))
.then(configJSON => config.write(drive, options.type, configJSON)), .then((configJSON) => config.write(drive, options.type, configJSON)),
) )
.tap(() => { .tap(() => {
console.info('Done'); console.info('Done');
@ -163,10 +163,10 @@ Examples:
() => options.drive || getVisuals().drive('Select the device drive'), () => options.drive || getVisuals().drive('Select the device drive'),
) )
.tap(umountAsync) .tap(umountAsync)
.then(drive => .then((drive) =>
readFileAsync(params.file, 'utf8') readFileAsync(params.file, 'utf8')
.then(JSON.parse) .then(JSON.parse)
.then(configJSON => config.write(drive, options.type, configJSON)), .then((configJSON) => config.write(drive, options.type, configJSON)),
) )
.tap(() => { .tap(() => {
console.info('Done'); console.info('Done');
@ -220,12 +220,12 @@ Examples:
() => options.drive || getVisuals().drive('Select the device drive'), () => options.drive || getVisuals().drive('Select the device drive'),
) )
.tap(umountAsync) .tap(umountAsync)
.then(drive => .then((drive) =>
config config
.read(drive, options.type) .read(drive, options.type)
.get('uuid') .get('uuid')
.tap(() => umountAsync(drive)) .tap(() => umountAsync(drive))
.then(function(uuid) { .then(function (uuid) {
let configureCommand = `os configure ${drive} --device ${uuid}`; let configureCommand = `os configure ${drive} --device ${uuid}`;
if (options.advanced) { if (options.advanced) {
configureCommand += ' --advanced'; configureCommand += ' --advanced';
@ -349,14 +349,14 @@ See the help page for examples:
} }
return Promise.try( return Promise.try(
/** @returns {Promise<any>} */ function() { /** @returns {Promise<any>} */ function () {
if (options.device != null) { if (options.device != null) {
return balena.models.device.get(options.device); return balena.models.device.get(options.device);
} }
return balena.models.application.get(options.application); return balena.models.application.get(options.application);
}, },
) )
.then(function(resource) { .then(function (resource) {
const deviceType = options.deviceType || resource.device_type; const deviceType = options.deviceType || resource.device_type;
let manifestPromise = balena.models.device.getManifestBySlug( let manifestPromise = balena.models.device.getManifestBySlug(
deviceType, deviceType,
@ -367,8 +367,8 @@ See the help page for examples:
const appManifestPromise = balena.models.device.getManifestBySlug( const appManifestPromise = balena.models.device.getManifestBySlug(
app.device_type, app.device_type,
); );
manifestPromise = manifestPromise.tap(paramDeviceType => manifestPromise = manifestPromise.tap((paramDeviceType) =>
appManifestPromise.then(function(appDeviceType) { appManifestPromise.then(function (appDeviceType) {
if ( if (
!helpers.areDeviceTypesCompatible( !helpers.areDeviceTypesCompatible(
appDeviceType, appDeviceType,
@ -391,7 +391,7 @@ See the help page for examples:
// required option, that value is used (and the corresponding question is not asked) // required option, that value is used (and the corresponding question is not asked)
form.run(formOptions, { override: options }), form.run(formOptions, { override: options }),
) )
.then(function(answers) { .then(function (answers) {
answers.version = options.version; answers.version = options.version;
if (resource.uuid != null) { if (resource.uuid != null) {
@ -406,7 +406,7 @@ See the help page for examples:
} }
}); });
}) })
.then(function(config) { .then(function (config) {
if (options.output != null) { if (options.output != null) {
return writeFileAsync(options.output, JSON.stringify(config)); return writeFileAsync(options.output, JSON.stringify(config));
} }

View File

@ -36,7 +36,7 @@ Opts must be an object with the following keys:
buildEmulated buildEmulated
buildOpts: arguments to forward to docker build command buildOpts: arguments to forward to docker build command
*/ */
const deployProject = function(docker, logger, composeOpts, opts) { const deployProject = function (docker, logger, composeOpts, opts) {
const _ = require('lodash'); const _ = require('lodash');
const doodles = require('resin-doodles'); const doodles = require('resin-doodles');
const sdk = getBalenaSdk(); const sdk = getBalenaSdk();
@ -46,7 +46,7 @@ const deployProject = function(docker, logger, composeOpts, opts) {
} = require('../utils/compose_ts'); } = require('../utils/compose_ts');
return Promise.resolve(loadProject(logger, composeOpts, opts.image)) return Promise.resolve(loadProject(logger, composeOpts, opts.image))
.then(function(project) { .then(function (project) {
if ( if (
project.descriptors.length > 1 && project.descriptors.length > 1 &&
!opts.app.application_type?.[0]?.supports_multicontainer !opts.app.application_type?.[0]?.supports_multicontainer
@ -58,7 +58,7 @@ const deployProject = function(docker, logger, composeOpts, opts) {
// find which services use images that already exist locally // find which services use images that already exist locally
return ( return (
Promise.map(project.descriptors, function(d) { Promise.map(project.descriptors, function (d) {
// unconditionally build (or pull) if explicitly requested // unconditionally build (or pull) if explicitly requested
if (opts.shouldPerformBuild) { if (opts.shouldPerformBuild) {
return d; return d;
@ -69,8 +69,8 @@ const deployProject = function(docker, logger, composeOpts, opts) {
.return(d.serviceName) .return(d.serviceName)
.catchReturn(); .catchReturn();
}) })
.filter(d => !!d) .filter((d) => !!d)
.then(function(servicesToSkip) { .then(function (servicesToSkip) {
// multibuild takes in a composition and always attempts to // multibuild takes in a composition and always attempts to
// build or pull all services. we workaround that here by // build or pull all services. we workaround that here by
// passing a modified composition. // passing a modified composition.
@ -101,11 +101,11 @@ const deployProject = function(docker, logger, composeOpts, opts) {
composeOpts.dockerfilePath, composeOpts.dockerfilePath,
composeOpts.nogitignore, composeOpts.nogitignore,
) )
.then(builtImages => _.keyBy(builtImages, 'serviceName')); .then((builtImages) => _.keyBy(builtImages, 'serviceName'));
}) })
.then(builtImages => .then((builtImages) =>
project.descriptors.map( project.descriptors.map(
d => (d) =>
builtImages[d.serviceName] ?? { builtImages[d.serviceName] ?? {
serviceName: d.serviceName, serviceName: d.serviceName,
name: typeof d.image === 'string' ? d.image : d.image.tag, name: typeof d.image === 'string' ? d.image : d.image.tag,
@ -115,7 +115,7 @@ const deployProject = function(docker, logger, composeOpts, opts) {
), ),
) )
// @ts-ignore slightly different return types of partial vs non-partial release // @ts-ignore slightly different return types of partial vs non-partial release
.then(function(images) { .then(function (images) {
if (opts.app.application_type?.[0]?.is_legacy) { if (opts.app.application_type?.[0]?.is_legacy) {
const { deployLegacy } = require('../utils/deploy-legacy'); const { deployLegacy } = require('../utils/deploy-legacy');
@ -138,7 +138,7 @@ const deployProject = function(docker, logger, composeOpts, opts) {
shouldUploadLogs: opts.shouldUploadLogs, shouldUploadLogs: opts.shouldUploadLogs,
}, },
deployLegacy, deployLegacy,
).then(releaseId => ).then((releaseId) =>
// @ts-ignore releaseId should be inferred as a number because that's what deployLegacy is // @ts-ignore releaseId should be inferred as a number because that's what deployLegacy is
// typed as returning but the .js type-checking doesn't manage to infer it correctly due to // typed as returning but the .js type-checking doesn't manage to infer it correctly due to
// Promise.join typings // Promise.join typings
@ -165,7 +165,7 @@ const deployProject = function(docker, logger, composeOpts, opts) {
}) })
); );
}) })
.then(function(release) { .then(function (release) {
logger.outputDeferredMessages(); logger.outputDeferredMessages();
logger.logSuccess('Deploy succeeded!'); logger.logSuccess('Deploy succeeded!');
logger.logSuccess(`Release: ${release.commit}`); logger.logSuccess(`Release: ${release.commit}`);
@ -263,7 +263,7 @@ Examples:
appName = appName_raw || appName || options.application; appName = appName_raw || appName || options.application;
delete options.application; delete options.application;
return Promise.try(function() { return Promise.try(function () {
if (appName == null) { if (appName == null) {
throw new ExpectedError( throw new ExpectedError(
'Please specify the name of the application to deploy', 'Please specify the name of the application to deploy',
@ -276,10 +276,10 @@ Examples:
); );
} }
}) })
.then(function() { .then(function () {
if (image) { if (image) {
return getRegistrySecrets(sdk, options['registry-secrets']).then( return getRegistrySecrets(sdk, options['registry-secrets']).then(
registrySecrets => { (registrySecrets) => {
options['registry-secrets'] = registrySecrets; options['registry-secrets'] = registrySecrets;
}, },
); );
@ -289,14 +289,14 @@ Examples:
noParentCheck: options['noparent-check'] || false, noParentCheck: options['noparent-check'] || false,
projectPath: options.source || '.', projectPath: options.source || '.',
registrySecretsPath: options['registry-secrets'], registrySecretsPath: options['registry-secrets'],
}).then(function({ dockerfilePath, registrySecrets }) { }).then(function ({ dockerfilePath, registrySecrets }) {
options.dockerfile = dockerfilePath; options.dockerfile = dockerfilePath;
options['registry-secrets'] = registrySecrets; options['registry-secrets'] = registrySecrets;
}); });
} }
}) })
.then(() => helpers.getAppWithArch(appName)) .then(() => helpers.getAppWithArch(appName))
.then(function(app) { .then(function (app) {
return Promise.join( return Promise.join(
dockerUtils.getDocker(options), dockerUtils.getDocker(options),
dockerUtils.generateBuildOpts(options), dockerUtils.generateBuildOpts(options),

View File

@ -47,7 +47,7 @@ Examples:
const Promise = require('bluebird'); const Promise = require('bluebird');
const balena = getBalenaSdk(); const balena = getBalenaSdk();
return Promise.try(function() { return Promise.try(function () {
if (options.application != null) { if (options.application != null) {
return balena.models.device.getAllByApplication( return balena.models.device.getAllByApplication(
options.application, options.application,
@ -55,8 +55,8 @@ Examples:
); );
} }
return balena.models.device.getAll(expandForAppName); return balena.models.device.getAll(expandForAppName);
}).tap(function(devices) { }).tap(function (devices) {
devices = _.map(devices, function(device) { devices = _.map(devices, function (device) {
// @ts-ignore extending the device object with extra props // @ts-ignore extending the device object with extra props
device.dashboard_url = balena.models.device.getDashboardUrl( device.dashboard_url = balena.models.device.getDashboardUrl(
device.uuid, device.uuid,
@ -105,10 +105,10 @@ Examples:
return balena.models.device return balena.models.device
.get(params.uuid, expandForAppName) .get(params.uuid, expandForAppName)
.then(device => .then((device) =>
// @ts-ignore `device.getStatus` requires a device with service info, but // @ts-ignore `device.getStatus` requires a device with service info, but
// this device isn't typed with them, possibly needs fixing? // this device isn't typed with them, possibly needs fixing?
balena.models.device.getStatus(device).then(function(status) { balena.models.device.getStatus(params.uuid).then(function (status) {
device.status = status; device.status = status;
// @ts-ignore extending the device object with extra props // @ts-ignore extending the device object with extra props
device.dashboard_url = balena.models.device.getDashboardUrl( device.dashboard_url = balena.models.device.getDashboardUrl(
@ -173,7 +173,7 @@ Examples:
return Promise.join( return Promise.join(
balena.models.application.get(params.application), balena.models.application.get(params.application),
options.uuid ?? balena.models.device.generateUniqueKey(), options.uuid ?? balena.models.device.generateUniqueKey(),
function(application, uuid) { function (application, uuid) {
console.info(`Registering to ${application.app_name}: ${uuid}`); console.info(`Registering to ${application.app_name}: ${uuid}`);
return balena.models.device.register(application.id, uuid); return balena.models.device.register(application.id, uuid);
}, },
@ -286,7 +286,7 @@ Examples:
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const form = require('resin-cli-form'); const form = require('resin-cli-form');
return Promise.try(function() { return Promise.try(function () {
if (!_.isEmpty(params.newName)) { if (!_.isEmpty(params.newName)) {
return params.newName; return params.newName;
} }
@ -321,7 +321,7 @@ Examples:
return balena.models.device return balena.models.device
.get(params.uuid, expandForAppName) .get(params.uuid, expandForAppName)
.then(function(device) { .then(function (device) {
// @ts-ignore extending the device object with extra props // @ts-ignore extending the device object with extra props
device.application_name = device.belongs_to__application?.[0] device.application_name = device.belongs_to__application?.[0]
? device.belongs_to__application[0].app_name ? device.belongs_to__application[0].app_name
@ -333,9 +333,9 @@ Examples:
return Promise.all([ return Promise.all([
balena.models.device.getManifestBySlug(device.device_type), balena.models.device.getManifestBySlug(device.device_type),
balena.models.config.getDeviceTypes(), balena.models.config.getDeviceTypes(),
]).then(function([deviceDeviceType, deviceTypes]) { ]).then(function ([deviceDeviceType, deviceTypes]) {
const compatibleDeviceTypes = deviceTypes.filter( const compatibleDeviceTypes = deviceTypes.filter(
dt => (dt) =>
balena.models.os.isArchitectureCompatibleWith( balena.models.os.isArchitectureCompatibleWith(
deviceDeviceType.arch, deviceDeviceType.arch,
dt.arch, dt.arch,
@ -344,11 +344,11 @@ Examples:
dt.state !== 'DISCONTINUED', dt.state !== 'DISCONTINUED',
); );
return patterns.selectApplication(application => return patterns.selectApplication((application) =>
_.every([ _.every([
_.some( _.some(
compatibleDeviceTypes, compatibleDeviceTypes,
dt => dt.slug === application.device_type, (dt) => dt.slug === application.device_type,
), ),
// @ts-ignore using the extended device object prop // @ts-ignore using the extended device object prop
device.application_name !== application.app_name, device.application_name !== application.app_name,
@ -356,8 +356,8 @@ Examples:
); );
}); });
}) })
.tap(application => balena.models.device.move(params.uuid, application)) .tap((application) => balena.models.device.move(params.uuid, application))
.then(application => { .then((application) => {
console.info(`${params.uuid} was moved to ${application}`); console.info(`${params.uuid} was moved to ${application}`);
}); });
}, },
@ -404,28 +404,28 @@ Examples:
const patterns = require('../utils/patterns'); const patterns = require('../utils/patterns');
const { runCommand } = require('../utils/helpers'); const { runCommand } = require('../utils/helpers');
return Promise.try(function() { return Promise.try(function () {
if (options.application != null) { if (options.application != null) {
return options.application; return options.application;
} }
return patterns.selectApplication(); return patterns.selectApplication();
}) })
.then(balena.models.application.get) .then(balena.models.application.get)
.then(function(application) { .then(function (application) {
const download = () => const download = () =>
tmpNameAsync() tmpNameAsync()
.then(function(tempPath) { .then(function (tempPath) {
const osVersion = options['os-version'] || 'default'; const osVersion = options['os-version'] || 'default';
return runCommand( return runCommand(
`os download ${application.device_type} --output '${tempPath}' --version ${osVersion}`, `os download ${application.device_type} --output '${tempPath}' --version ${osVersion}`,
); );
}) })
.disposer(tempPath => rimraf(tempPath)); .disposer((tempPath) => rimraf(tempPath));
return Promise.using(download(), tempPath => return Promise.using(download(), (tempPath) =>
runCommand(`device register ${application.app_name}`) runCommand(`device register ${application.app_name}`)
.then(balena.models.device.get) .then(balena.models.device.get)
.tap(function(device) { .tap(function (device) {
let configureCommand = `os configure '${tempPath}' --device ${device.uuid}`; let configureCommand = `os configure '${tempPath}' --device ${device.uuid}`;
if (options.config) { if (options.config) {
configureCommand += ` --config '${options.config}'`; configureCommand += ` --config '${options.config}'`;
@ -433,7 +433,7 @@ Examples:
configureCommand += ' --advanced'; configureCommand += ' --advanced';
} }
return runCommand(configureCommand) return runCommand(configureCommand)
.then(function() { .then(function () {
let osInitCommand = `os initialize '${tempPath}' --type ${application.device_type}`; let osInitCommand = `os initialize '${tempPath}' --type ${application.device_type}`;
if (options.yes) { if (options.yes) {
osInitCommand += ' --yes'; osInitCommand += ' --yes';
@ -443,13 +443,13 @@ Examples:
} }
return runCommand(osInitCommand); return runCommand(osInitCommand);
}) })
.catch(error => .catch((error) =>
balena.models.device.remove(device.uuid).finally(function() { balena.models.device.remove(device.uuid).finally(function () {
throw error; throw error;
}), }),
); );
}), }),
).then(function(device) { ).then(function (device) {
console.log('Done'); console.log('Done');
return device.uuid; return device.uuid;
}); });

View File

@ -100,7 +100,7 @@ export const osUpdate: CommandDefinition<OsUpdate.Args, OsUpdate.Options> = {
targetOsVersion = await form.ask({ targetOsVersion = await form.ask({
message: 'Target OS version', message: 'Target OS version',
type: 'list', type: 'list',
choices: hupVersionInfo.versions.map(version => ({ choices: hupVersionInfo.versions.map((version) => ({
name: name:
hupVersionInfo.recommended === version hupVersionInfo.recommended === version
? `${version} (recommended)` ? `${version} (recommended)`

View File

@ -23,8 +23,8 @@ import { getManualSortCompareFunction } from '../utils/helpers';
import { exitWithExpectedError } from '../errors'; import { exitWithExpectedError } from '../errors';
import { getOclifHelpLinePairs } from './help_ts'; import { getOclifHelpLinePairs } from './help_ts';
const parse = object => const parse = (object) =>
_.map(object, function(item) { _.map(object, function (item) {
// Hacky way to determine if an object is // Hacky way to determine if an object is
// a function or a command // a function or a command
let signature; let signature;
@ -37,12 +37,12 @@ const parse = object =>
return [signature, item.description]; return [signature, item.description];
}); });
const indent = function(text) { const indent = function (text) {
text = _.map(text.split('\n'), line => ' ' + line); text = _.map(text.split('\n'), (line) => ' ' + line);
return text.join('\n'); return text.join('\n');
}; };
const print = usageDescriptionPairs => const print = (usageDescriptionPairs) =>
console.log( console.log(
indent( indent(
columnify(_.fromPairs(usageDescriptionPairs), { columnify(_.fromPairs(usageDescriptionPairs), {
@ -71,7 +71,7 @@ const manuallySortedPrimaryCommands = [
'local scan', 'local scan',
]; ];
const general = function(_params, options, done) { const general = function (_params, options, done) {
console.log('Usage: balena [COMMAND] [OPTIONS]\n'); console.log('Usage: balena [COMMAND] [OPTIONS]\n');
console.log('Primary commands:\n'); console.log('Primary commands:\n');
@ -79,10 +79,10 @@ const general = function(_params, options, done) {
// We do not want the wildcard command // We do not want the wildcard command
// to be printed in the help screen. // to be printed in the help screen.
const commands = capitano.state.commands.filter( const commands = capitano.state.commands.filter(
command => !command.hidden && !command.isWildcard(), (command) => !command.hidden && !command.isWildcard(),
); );
const capitanoCommands = _.groupBy(commands, function(command) { const capitanoCommands = _.groupBy(commands, function (command) {
if (command.primary) { if (command.primary) {
return 'primary'; return 'primary';
} }
@ -90,11 +90,11 @@ const general = function(_params, options, done) {
}); });
return getOclifHelpLinePairs() return getOclifHelpLinePairs()
.then(function(oclifHelpLinePairs) { .then(function (oclifHelpLinePairs) {
const primaryHelpLinePairs = parse(capitanoCommands.primary) const primaryHelpLinePairs = parse(capitanoCommands.primary)
.concat(oclifHelpLinePairs.primary) .concat(oclifHelpLinePairs.primary)
.sort( .sort(
getManualSortCompareFunction(manuallySortedPrimaryCommands, function( getManualSortCompareFunction(manuallySortedPrimaryCommands, function (
[signature], [signature],
manualItem, manualItem,
) { ) {
@ -133,7 +133,7 @@ const general = function(_params, options, done) {
}; };
const commandHelp = (params, _options, done) => const commandHelp = (params, _options, done) =>
capitano.state.getMatchCommand(params.command, function(error, command) { capitano.state.getMatchCommand(params.command, function (error, command) {
if (error != null) { if (error != null) {
return done(error); return done(error);
} }

View File

@ -7,7 +7,7 @@ import { getChalk } from '../../utils/lazy';
export const dockerPort = 2375; export const dockerPort = 2375;
export const dockerTimeout = 2000; export const dockerTimeout = 2000;
export const filterOutSupervisorContainer = function(container) { export const filterOutSupervisorContainer = function (container) {
for (const name of container.Names) { for (const name of container.Names) {
if ( if (
name.includes('resin_supervisor') || name.includes('resin_supervisor') ||
@ -19,7 +19,7 @@ export const filterOutSupervisorContainer = function(container) {
return true; return true;
}; };
export const selectContainerFromDevice = Promise.method(function( export const selectContainerFromDevice = Promise.method(function (
deviceIp, deviceIp,
filterSupervisor, filterSupervisor,
) { ) {
@ -34,8 +34,8 @@ export const selectContainerFromDevice = Promise.method(function(
}); });
// List all containers, including those not running // List all containers, including those not running
return docker.listContainers({ all: true }).then(function(containers) { return docker.listContainers({ all: true }).then(function (containers) {
containers = containers.filter(function(container) { containers = containers.filter(function (container) {
if (!filterSupervisor) { if (!filterSupervisor) {
return true; return true;
} }
@ -48,7 +48,7 @@ export const selectContainerFromDevice = Promise.method(function(
return form.ask({ return form.ask({
message: 'Select a container', message: 'Select a container',
type: 'list', type: 'list',
choices: _.map(containers, function(container) { choices: _.map(containers, function (container) {
const containerName = container.Names?.[0] || 'Untitled'; const containerName = container.Names?.[0] || 'Untitled';
const shortContainerId = ('' + container.Id).substr(0, 11); const shortContainerId = ('' + container.Id).substr(0, 11);
@ -61,7 +61,7 @@ export const selectContainerFromDevice = Promise.method(function(
}); });
}); });
export const pipeContainerStream = Promise.method(function({ export const pipeContainerStream = Promise.method(function ({
deviceIp, deviceIp,
name, name,
outStream, outStream,
@ -75,8 +75,8 @@ export const pipeContainerStream = Promise.method(function({
const container = docker.getContainer(name); const container = docker.getContainer(name);
return container return container
.inspect() .inspect()
.then(containerInfo => containerInfo?.State?.Running) .then((containerInfo) => containerInfo?.State?.Running)
.then(isRunning => .then((isRunning) =>
container.attach({ container.attach({
logs: !follow || !isRunning, logs: !follow || !isRunning,
stream: follow && isRunning, stream: follow && isRunning,
@ -84,8 +84,8 @@ export const pipeContainerStream = Promise.method(function({
stderr: true, stderr: true,
}), }),
) )
.then(containerStream => containerStream.pipe(outStream)) .then((containerStream) => containerStream.pipe(outStream))
.catch(function(err) { .catch(function (err) {
err = '' + err.statusCode; err = '' + err.statusCode;
if (err === '404') { if (err === '404') {
return console.log( return console.log(

View File

@ -17,7 +17,7 @@ limitations under the License.
const BOOT_PARTITION = 1; const BOOT_PARTITION = 1;
const CONNECTIONS_FOLDER = '/system-connections'; const CONNECTIONS_FOLDER = '/system-connections';
const getConfigurationSchema = function(connnectionFileName) { const getConfigurationSchema = function (connnectionFileName) {
if (connnectionFileName == null) { if (connnectionFileName == null) {
connnectionFileName = 'resin-wifi'; connnectionFileName = 'resin-wifi';
} }
@ -72,7 +72,7 @@ const getConfigurationSchema = function(connnectionFileName) {
}; };
}; };
const inquirerOptions = data => [ const inquirerOptions = (data) => [
{ {
message: 'Network SSID', message: 'Network SSID',
type: 'input', type: 'input',
@ -111,7 +111,7 @@ const inquirerOptions = data => [
}, },
]; ];
const getConfiguration = function(data) { const getConfiguration = function (data) {
const _ = require('lodash'); const _ = require('lodash');
const inquirer = require('inquirer'); const inquirer = require('inquirer');
@ -121,7 +121,7 @@ const getConfiguration = function(data) {
return inquirer return inquirer
.prompt(inquirerOptions(data)) .prompt(inquirerOptions(data))
.then(answers => _.merge(data, answers)); .then((answers) => _.merge(data, answers));
}; };
// Taken from https://goo.gl/kr1kCt // Taken from https://goo.gl/kr1kCt
@ -154,7 +154,7 @@ method=auto\
* if the `resin-sample` exists it's reconfigured (legacy mode, will be removed eventually) * if the `resin-sample` exists it's reconfigured (legacy mode, will be removed eventually)
* otherwise, the new file is created * otherwise, the new file is created
*/ */
const prepareConnectionFile = function(target) { const prepareConnectionFile = function (target) {
const _ = require('lodash'); const _ = require('lodash');
const imagefs = require('resin-image-fs'); const imagefs = require('resin-image-fs');
@ -164,7 +164,7 @@ const prepareConnectionFile = function(target) {
partition: BOOT_PARTITION, partition: BOOT_PARTITION,
path: CONNECTIONS_FOLDER, path: CONNECTIONS_FOLDER,
}) })
.then(function(files) { .then(function (files) {
// The required file already exists // The required file already exists
if (_.includes(files, 'resin-wifi')) { if (_.includes(files, 'resin-wifi')) {
return null; return null;
@ -211,12 +211,12 @@ const prepareConnectionFile = function(target) {
) )
.thenReturn(null); .thenReturn(null);
}) })
.then(connectionFileName => getConfigurationSchema(connectionFileName)); .then((connectionFileName) => getConfigurationSchema(connectionFileName));
}; };
const removeHostname = function(schema) { const removeHostname = function (schema) {
const _ = require('lodash'); const _ = require('lodash');
schema.mapper = _.reject(schema.mapper, mapper => schema.mapper = _.reject(schema.mapper, (mapper) =>
_.isEqual(Object.keys(mapper.template), ['hostname']), _.isEqual(Object.keys(mapper.template), ['hostname']),
); );
}; };
@ -244,14 +244,14 @@ Examples:
return prepareConnectionFile(params.target) return prepareConnectionFile(params.target)
.tap(() => .tap(() =>
isMountedAsync(params.target).then(function(isMounted) { isMountedAsync(params.target).then(function (isMounted) {
if (!isMounted) { if (!isMounted) {
return; return;
} }
return umountAsync(params.target); return umountAsync(params.target);
}), }),
) )
.then(function(configurationSchema) { .then(function (configurationSchema) {
const dmOpts = {}; const dmOpts = {};
if (process.pkg) { if (process.pkg) {
// when running in a standalone pkg install, the 'denymount' // when running in a standalone pkg install, the 'denymount'
@ -261,11 +261,11 @@ Examples:
'denymount', 'denymount',
); );
} }
const dmHandler = cb => const dmHandler = (cb) =>
reconfix reconfix
.readConfiguration(configurationSchema, params.target) .readConfiguration(configurationSchema, params.target)
.then(getConfiguration) .then(getConfiguration)
.then(function(answers) { .then(function (answers) {
if (!answers.hostname) { if (!answers.hostname) {
removeHostname(configurationSchema); removeHostname(configurationSchema);
} }

View File

@ -83,16 +83,16 @@ export const logs: CommandDefinition<
}, },
], ],
primary: true, primary: true,
async action(params, options, done) { async action(params, options) {
normalizeUuidProp(params); normalizeUuidProp(params);
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const { ExpectedError } = await import('../errors');
const { serviceIdToName } = await import('../utils/cloud'); const { serviceIdToName } = await import('../utils/cloud');
const { displayDeviceLogs, displayLogObject } = await import( const { displayDeviceLogs, displayLogObject } = await import(
'../utils/device/logs' '../utils/device/logs'
); );
const { validateIPAddress } = await import('../utils/validation'); const { validateIPAddress } = await import('../utils/validation');
const { checkLoggedIn } = await import('../utils/patterns'); const { checkLoggedIn } = await import('../utils/patterns');
const { exitWithExpectedError } = await import('../errors');
const Logger = await import('../utils/logger'); const Logger = await import('../utils/logger');
const logger = Logger.getLogger(); const logger = Logger.getLogger();
@ -136,15 +136,12 @@ export const logs: CommandDefinition<
try { try {
await deviceApi.ping(); await deviceApi.ping();
} catch (e) { } catch (e) {
exitWithExpectedError( throw new ExpectedError(
new Error( `Cannot access local mode device at address ${params.uuidOrDevice}`,
`Cannot access local mode device at address ${params.uuidOrDevice}`,
),
); );
} }
const logStream = await deviceApi.getLogStream(); const logStream = await deviceApi.getLogStream();
displayDeviceLogs( await displayDeviceLogs(
logStream, logStream,
logger, logger,
options.system || false, options.system || false,
@ -153,18 +150,19 @@ export const logs: CommandDefinition<
} else { } else {
await checkLoggedIn(); await checkLoggedIn();
if (options.tail) { if (options.tail) {
return balena.logs const logStream = await balena.logs.subscribe(params.uuidOrDevice, {
.subscribe(params.uuidOrDevice, { count: 100 }) count: 100,
.then(function(logStream) { });
logStream.on('line', displayCloudLog); // Never resolve (quit with CTRL-C), but reject on a broken connection
logStream.on('error', done); await new Promise((_resolve, reject) => {
}) logStream.on('line', displayCloudLog);
.catch(done); logStream.on('error', reject);
});
} else { } else {
return balena.logs const logMessages = await balena.logs.history(params.uuidOrDevice);
.history(params.uuidOrDevice) for (const logMessage of logMessages) {
.each(displayCloudLog) await displayCloudLog(logMessage);
.catch(done); }
} }
} }
}, },

View File

@ -19,7 +19,7 @@ import * as commandOptions from './command-options';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { getBalenaSdk, getVisuals } from '../utils/lazy'; import { getBalenaSdk, getVisuals } from '../utils/lazy';
const formatVersion = function(v, isRecommended) { const formatVersion = function (v, isRecommended) {
let result = `v${v}`; let result = `v${v}`;
if (isRecommended) { if (isRecommended) {
result += ' (recommended)'; result += ' (recommended)';
@ -27,7 +27,7 @@ const formatVersion = function(v, isRecommended) {
return result; return result;
}; };
const resolveVersion = function(deviceType, version) { const resolveVersion = function (deviceType, version) {
if (version !== 'menu') { if (version !== 'menu') {
if (version[0] === 'v') { if (version[0] === 'v') {
version = version.slice(1); version = version.slice(1);
@ -40,8 +40,8 @@ const resolveVersion = function(deviceType, version) {
return balena.models.os return balena.models.os
.getSupportedVersions(deviceType) .getSupportedVersions(deviceType)
.then(function({ versions: vs, recommended }) { .then(function ({ versions: vs, recommended }) {
const choices = vs.map(v => ({ const choices = vs.map((v) => ({
value: v, value: v,
name: formatVersion(v, v === recommended), name: formatVersion(v, v === recommended),
})); }));
@ -72,7 +72,7 @@ Example:
return balena.models.os return balena.models.os
.getSupportedVersions(params.type) .getSupportedVersions(params.type)
.then(({ versions: vs, recommended }) => { .then(({ versions: vs, recommended }) => {
vs.forEach(v => { vs.forEach((v) => {
console.log(formatVersion(v, v === recommended)); console.log(formatVersion(v, v === recommended));
}); });
}); });
@ -123,7 +123,7 @@ Examples:
console.info(`Getting device operating system for ${params.type}`); console.info(`Getting device operating system for ${params.type}`);
let displayVersion = ''; let displayVersion = '';
return Promise.try(function() { return Promise.try(function () {
if (!options.version) { if (!options.version) {
console.warn(`OS version is not specified, using the default version: \ console.warn(`OS version is not specified, using the default version: \
the newest stable (non-pre-release) version if available, \ the newest stable (non-pre-release) version if available, \
@ -133,13 +133,13 @@ versions for the given device type are pre-release).`);
} }
return resolveVersion(params.type, options.version); return resolveVersion(params.type, options.version);
}) })
.then(function(version) { .then(function (version) {
if (version !== 'default') { if (version !== 'default') {
displayVersion = ` ${version}`; displayVersion = ` ${version}`;
} }
return manager.get(params.type, version); return manager.get(params.type, version);
}) })
.then(function(stream) { .then(function (stream) {
const visuals = getVisuals(); const visuals = getVisuals();
const bar = new visuals.Progress( const bar = new visuals.Progress(
`Downloading Device OS${displayVersion}`, `Downloading Device OS${displayVersion}`,
@ -148,7 +148,7 @@ versions for the given device type are pre-release).`);
`Downloading Device OS${displayVersion} (size unknown)`, `Downloading Device OS${displayVersion} (size unknown)`,
); );
stream.on('progress', function(state) { stream.on('progress', function (state) {
if (state != null) { if (state != null) {
return bar.update(state); return bar.update(state);
} else { } else {
@ -178,7 +178,7 @@ versions for the given device type are pre-release).`);
}, },
}; };
const buildConfigForDeviceType = function(deviceType, advanced) { const buildConfigForDeviceType = function (deviceType, advanced) {
if (advanced == null) { if (advanced == null) {
advanced = false; advanced = false;
} }
@ -201,7 +201,7 @@ const buildConfigForDeviceType = function(deviceType, advanced) {
return form.run(questions, { override }); return form.run(questions, { override });
}; };
const $buildConfig = function(image, deviceTypeSlug, advanced) { const $buildConfig = function (image, deviceTypeSlug, advanced) {
if (advanced == null) { if (advanced == null) {
advanced = false; advanced = false;
} }
@ -210,7 +210,7 @@ const $buildConfig = function(image, deviceTypeSlug, advanced) {
return Promise.resolve( return Promise.resolve(
helpers.getManifest(image, deviceTypeSlug), helpers.getManifest(image, deviceTypeSlug),
).then(deviceTypeManifest => ).then((deviceTypeManifest) =>
buildConfigForDeviceType(deviceTypeManifest, advanced), buildConfigForDeviceType(deviceTypeManifest, advanced),
); );
}; };
@ -246,7 +246,7 @@ Example:
params.image, params.image,
params['device-type'], params['device-type'],
options.advanced, options.advanced,
).then(answers => ).then((answers) =>
writeFileAsync(options.output, JSON.stringify(answers, null, 4)), writeFileAsync(options.output, JSON.stringify(answers, null, 4)),
); );
}, },
@ -295,14 +295,14 @@ Initializing device
${INIT_WARNING_MESSAGE}\ ${INIT_WARNING_MESSAGE}\
`); `);
return Promise.resolve(helpers.getManifest(params.image, options.type)) return Promise.resolve(helpers.getManifest(params.image, options.type))
.then(manifest => .then((manifest) =>
form.run(manifest.initialization?.options, { form.run(manifest.initialization?.options, {
override: { override: {
drive: options.drive, drive: options.drive,
}, },
}), }),
) )
.tap(function(answers) { .tap(function (answers) {
if (answers.drive == null) { if (answers.drive == null) {
return; return;
} }
@ -316,7 +316,7 @@ ${INIT_WARNING_MESSAGE}\
.return(answers.drive) .return(answers.drive)
.then(umountAsync); .then(umountAsync);
}) })
.tap(answers => .tap((answers) =>
helpers.sudo([ helpers.sudo([
'internal', 'internal',
'osinit', 'osinit',
@ -325,7 +325,7 @@ ${INIT_WARNING_MESSAGE}\
JSON.stringify(answers), JSON.stringify(answers),
]), ]),
) )
.then(function(answers) { .then(function (answers) {
if (answers.drive == null) { if (answers.drive == null) {
return; return;
} }

View File

@ -18,10 +18,10 @@ import * as _ from 'lodash';
import { getBalenaSdk, getVisuals } from '../utils/lazy'; import { getBalenaSdk, getVisuals } from '../utils/lazy';
import * as dockerUtils from '../utils/docker'; import * as dockerUtils from '../utils/docker';
const isCurrent = commit => commit === 'latest' || commit === 'current'; const isCurrent = (commit) => commit === 'latest' || commit === 'current';
let allDeviceTypes; let allDeviceTypes;
const getDeviceTypes = function() { const getDeviceTypes = function () {
const Bluebird = require('bluebird'); const Bluebird = require('bluebird');
if (allDeviceTypes !== undefined) { if (allDeviceTypes !== undefined) {
return Bluebird.resolve(allDeviceTypes); return Bluebird.resolve(allDeviceTypes);
@ -29,27 +29,24 @@ const getDeviceTypes = function() {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
return balena.models.config return balena.models.config
.getDeviceTypes() .getDeviceTypes()
.then(deviceTypes => _.sortBy(deviceTypes, 'name')) .then((deviceTypes) => _.sortBy(deviceTypes, 'name'))
.tap(dt => { .tap((dt) => {
allDeviceTypes = dt; allDeviceTypes = dt;
}); });
}; };
const getDeviceTypesWithSameArch = function(deviceTypeSlug) { const getDeviceTypesWithSameArch = function (deviceTypeSlug) {
return getDeviceTypes().then(function(deviceTypes) { return getDeviceTypes().then(function (deviceTypes) {
const deviceType = _.find(deviceTypes, { slug: deviceTypeSlug }); const deviceType = _.find(deviceTypes, { slug: deviceTypeSlug });
return _(deviceTypes) return _(deviceTypes).filter({ arch: deviceType.arch }).map('slug').value();
.filter({ arch: deviceType.arch })
.map('slug')
.value();
}); });
}; };
const getApplicationsWithSuccessfulBuilds = function(deviceType) { const getApplicationsWithSuccessfulBuilds = function (deviceType) {
const balenaPreload = require('balena-preload'); const balenaPreload = require('balena-preload');
const balena = getBalenaSdk(); const balena = getBalenaSdk();
return getDeviceTypesWithSameArch(deviceType).then(deviceTypes => { return getDeviceTypesWithSameArch(deviceType).then((deviceTypes) => {
/** @type {import('balena-sdk').PineOptionsFor<import('balena-sdk').Application>} */ /** @type {import('balena-sdk').PineOptionsFor<import('balena-sdk').Application>} */
const options = { const options = {
$filter: { $filter: {
@ -84,7 +81,7 @@ const getApplicationsWithSuccessfulBuilds = function(deviceType) {
}); });
}; };
const selectApplication = function(deviceType) { const selectApplication = function (deviceType) {
const visuals = getVisuals(); const visuals = getVisuals();
const form = require('resin-cli-form'); const form = require('resin-cli-form');
const { exitWithExpectedError } = require('../errors'); const { exitWithExpectedError } = require('../errors');
@ -94,7 +91,7 @@ const selectApplication = function(deviceType) {
); );
applicationInfoSpinner.start(); applicationInfoSpinner.start();
return getApplicationsWithSuccessfulBuilds(deviceType).then(function( return getApplicationsWithSuccessfulBuilds(deviceType).then(function (
applications, applications,
) { ) {
applicationInfoSpinner.stop(); applicationInfoSpinner.stop();
@ -106,7 +103,7 @@ const selectApplication = function(deviceType) {
return form.ask({ return form.ask({
message: 'Select an application', message: 'Select an application',
type: 'list', type: 'list',
choices: applications.map(app => ({ choices: applications.map((app) => ({
name: app.app_name, name: app.app_name,
value: app, value: app,
})), })),
@ -114,7 +111,7 @@ const selectApplication = function(deviceType) {
}); });
}; };
const selectApplicationCommit = function(releases) { const selectApplicationCommit = function (releases) {
const form = require('resin-cli-form'); const form = require('resin-cli-form');
const { exitWithExpectedError } = require('../errors'); const { exitWithExpectedError } = require('../errors');
@ -123,7 +120,7 @@ const selectApplicationCommit = function(releases) {
} }
const DEFAULT_CHOICE = { name: 'current', value: 'current' }; const DEFAULT_CHOICE = { name: 'current', value: 'current' };
const choices = [DEFAULT_CHOICE].concat( const choices = [DEFAULT_CHOICE].concat(
releases.map(release => ({ releases.map((release) => ({
name: `${release.end_timestamp} - ${release.commit}`, name: `${release.end_timestamp} - ${release.commit}`,
value: release.commit, value: release.commit,
})), })),
@ -136,7 +133,7 @@ const selectApplicationCommit = function(releases) {
}); });
}; };
const offerToDisableAutomaticUpdates = function( const offerToDisableAutomaticUpdates = function (
application, application,
commit, commit,
pinDevice, pinDevice,
@ -170,7 +167,7 @@ Alternatively you can pass the --pin-device-to-release flag to pin only this dev
message, message,
type: 'confirm', type: 'confirm',
}) })
.then(function(update) { .then(function (update) {
if (!update) { if (!update) {
return; return;
} }
@ -268,7 +265,7 @@ Examples:
const progressBars = {}; const progressBars = {};
const progressHandler = function(event) { const progressHandler = function (event) {
let progressBar = progressBars[event.name]; let progressBar = progressBars[event.name];
if (!progressBar) { if (!progressBar) {
progressBar = progressBars[event.name] = new visuals.Progress( progressBar = progressBars[event.name] = new visuals.Progress(
@ -280,7 +277,7 @@ Examples:
const spinners = {}; const spinners = {};
const spinnerHandler = function(event) { const spinnerHandler = function (event) {
let spinner = spinners[event.name]; let spinner = spinners[event.name];
if (!spinner) { if (!spinner) {
spinner = spinners[event.name] = new visuals.Spinner(event.name); spinner = spinners[event.name] = new visuals.Spinner(event.name);
@ -326,7 +323,7 @@ Examples:
} }
// Get a configured dockerode instance // Get a configured dockerode instance
return dockerUtils.getDocker(options).then(function(docker) { return dockerUtils.getDocker(options).then(function (docker) {
const preloader = new balenaPreload.Preloader( const preloader = new balenaPreload.Preloader(
balena, balena,
docker, docker,
@ -342,7 +339,7 @@ Examples:
let gotSignal = false; let gotSignal = false;
nodeCleanup(function(_exitCode, signal) { nodeCleanup(function (_exitCode, signal) {
if (signal) { if (signal) {
gotSignal = true; gotSignal = true;
nodeCleanup.uninstall(); // don't call cleanup handler again nodeCleanup.uninstall(); // don't call cleanup handler again
@ -361,7 +358,7 @@ Examples:
preloader.on('progress', progressHandler); preloader.on('progress', progressHandler);
preloader.on('spinner', spinnerHandler); preloader.on('spinner', spinnerHandler);
return new Promise(function(resolve, reject) { return new Promise(function (resolve, reject) {
preloader.on('error', reject); preloader.on('error', reject);
return preloader return preloader
@ -371,7 +368,7 @@ Examples:
if (!preloader.appId) { if (!preloader.appId) {
return selectApplication( return selectApplication(
preloader.config.deviceType, preloader.config.deviceType,
).then(application => preloader.setApplication(application)); ).then((application) => preloader.setApplication(application));
} }
}) })
.then(() => { .then(() => {
@ -381,7 +378,7 @@ Examples:
// handle `--commit current` (and its `--commit latest` synonym) // handle `--commit current` (and its `--commit latest` synonym)
return 'latest'; return 'latest';
} }
const release = _.find(preloader.application.owns__release, r => const release = _.find(preloader.application.owns__release, (r) =>
r.commit.startsWith(options.commit), r.commit.startsWith(options.commit),
); );
if (!release) { if (!release) {
@ -393,7 +390,7 @@ Examples:
} }
return selectApplicationCommit(preloader.application.owns__release); return selectApplicationCommit(preloader.application.owns__release);
}) })
.then(function(commit) { .then(function (commit) {
if (isCurrent(commit)) { if (isCurrent(commit)) {
preloader.commit = preloader.application.commit; preloader.commit = preloader.application.commit;
} else { } else {
@ -416,7 +413,7 @@ Examples:
.catch(reject); .catch(reject);
}) })
.then(done) .then(done)
.finally(function() { .finally(function () {
if (!gotSignal) { if (!gotSignal) {
return preloader.cleanup(); return preloader.cleanup();
} }

View File

@ -80,7 +80,7 @@ async function getAppOwner(sdk: BalenaSDK, appName: string) {
// user has access to a collab app with the same name as a personal app. We // user has access to a collab app with the same name as a personal app. We
// present a list to the user which shows the fully qualified application // present a list to the user which shows the fully qualified application
// name (user/appname) and allows them to select // name (user/appname) and allows them to select
const entries = _.map(applications, app => { const entries = _.map(applications, (app) => {
const username = _.get(app, 'user[0].username'); const username = _.get(app, 'user[0].username');
return { return {
name: `${username}/${appName}`, name: `${username}/${appName}`,
@ -406,7 +406,7 @@ export const push: CommandDefinition<
: options.env || [], : options.env || [],
convertEol, convertEol,
}), }),
).catch(BuildError, e => { ).catch(BuildError, (e) => {
throw new ExpectedError(e.toString()); throw new ExpectedError(e.toString());
}); });
break; break;

View File

@ -101,7 +101,7 @@ async function getContainerId(
}); });
const containers = await new Promise<string>((resolve, reject) => { const containers = await new Promise<string>((resolve, reject) => {
const output: string[] = []; const output: string[] = [];
subprocess.stdout.on('data', chunk => output.push(chunk.toString())); subprocess.stdout.on('data', (chunk) => output.push(chunk.toString()));
subprocess.on('close', (code: number) => { subprocess.on('close', (code: number) => {
if (code !== 0) { if (code !== 0) {
reject( reject(

View File

@ -130,7 +130,7 @@ export const tunnel: CommandDefinition<Args, Options> = {
logger.logInfo(`Opening a tunnel to ${device.uuid}...`); logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
const localListeners = _.chain(ports) const localListeners = _.chain(ports)
.map(mapping => { .map((mapping) => {
const regexResult = /^([0-9]+)(?:$|\:(?:([\w\:\.]+)\:|)([0-9]+))$/.exec( const regexResult = /^([0-9]+)(?:$|\:(?:([\w\:\.]+)\:|)([0-9]+))$/.exec(
mapping, mapping,
); );
@ -168,7 +168,7 @@ export const tunnel: CommandDefinition<Args, Options> = {
}) })
.map(({ localPort, localAddress, remotePort }) => { .map(({ localPort, localAddress, remotePort }) => {
return tunnelConnectionToDevice(device.uuid, remotePort, sdk) return tunnelConnectionToDevice(device.uuid, remotePort, sdk)
.then(handler => .then((handler) =>
createServer((client: Socket) => { createServer((client: Socket) => {
return handler(client) return handler(client)
.then(() => { .then(() => {
@ -181,7 +181,7 @@ export const tunnel: CommandDefinition<Args, Options> = {
remotePort, remotePort,
); );
}) })
.catch(err => .catch((err) =>
logConnection( logConnection(
client.remoteAddress || '', client.remoteAddress || '',
client.remotePort || 0, client.remotePort || 0,
@ -195,7 +195,7 @@ export const tunnel: CommandDefinition<Args, Options> = {
}), }),
) )
.then( .then(
server => (server) =>
new Bluebird.Promise<Server>((resolve, reject) => { new Bluebird.Promise<Server>((resolve, reject) => {
server.on('error', reject); server.on('error', reject);
server.listen(localPort, localAddress, () => { server.listen(localPort, localAddress, () => {

View File

@ -20,10 +20,8 @@ import * as capitano from 'capitano';
import * as actions from './actions'; import * as actions from './actions';
import * as events from './events'; import * as events from './events';
capitano.permission('user', done => capitano.permission('user', (done) =>
require('./utils/patterns') require('./utils/patterns').checkLoggedIn().then(done, done),
.checkLoggedIn()
.then(done, done),
); );
capitano.command({ capitano.command({
@ -108,7 +106,7 @@ capitano.command(actions.push.push);
export function run(argv) { export function run(argv) {
const cli = capitano.parse(argv.slice(2)); const cli = capitano.parse(argv.slice(2));
const runCommand = function() { const runCommand = function () {
const capitanoExecuteAsync = Promise.promisify(capitano.execute); const capitanoExecuteAsync = Promise.promisify(capitano.execute);
if (cli.global?.help) { if (cli.global?.help) {
return capitanoExecuteAsync({ return capitanoExecuteAsync({
@ -119,11 +117,11 @@ export function run(argv) {
} }
}; };
const trackCommand = function() { const trackCommand = function () {
const getMatchCommandAsync = Promise.promisify( const getMatchCommandAsync = Promise.promisify(
capitano.state.getMatchCommand, capitano.state.getMatchCommand,
); );
return getMatchCommandAsync(cli.command).then(function(command) { return getMatchCommandAsync(cli.command).then(function (command) {
// cmdSignature is literally a string like, for example: // cmdSignature is literally a string like, for example:
// "push <applicationOrDevice>" // "push <applicationOrDevice>"
// ("applicationOrDevice" is NOT replaced with its actual value) // ("applicationOrDevice" is NOT replaced with its actual value)

View File

@ -54,7 +54,7 @@ export const setupSentry = onceAsync(async () => {
dsn: config.sentryDsn, dsn: config.sentryDsn,
release: packageJSON.version, release: packageJSON.version,
}); });
Sentry.configureScope(scope => { Sentry.configureScope((scope) => {
scope.setExtras({ scope.setExtras({
is_pkg: !!(process as any).pkg, is_pkg: !!(process as any).pkg,
node_version: process.version, node_version: process.version,
@ -162,12 +162,21 @@ async function setupGlobalAgentProxy(
privateNoProxy.push(`172.${i}.*`); privateNoProxy.push(`172.${i}.*`);
} }
// BALENARC_DO_PROXY is a list of entries to exclude from BALENARC_NO_PROXY
// (so "do proxy" takes precedence over "no proxy"). It is an undocumented
// feature/hack added to facilitate testing of the CLI's standalone executable
// through a local proxy server, by setting BALENARC_DO_PROXY="localhost,127.0.0.1"
// See also runCommandInSubprocess() function in `tests/helpers.ts`.
const doProxy = (process.env.BALENARC_DO_PROXY || '').split(',');
const env = process.env; const env = process.env;
env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE = ''; env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE = '';
env.NO_PROXY = [ env.NO_PROXY = [
...requiredNoProxy, ...requiredNoProxy,
...(noProxy ? noProxy.split(',').filter(v => v) : privateNoProxy), ...(noProxy ? noProxy.split(',').filter((v) => v) : privateNoProxy),
].join(','); ]
.filter((i) => !doProxy.includes(i))
.join(',');
if (proxy) { if (proxy) {
const proxyUrl: string = const proxyUrl: string =

View File

@ -42,7 +42,7 @@ export async function run(command: string[], options: AppOptions) {
return require('@oclif/command/flush'); return require('@oclif/command/flush');
} }
}, },
error => { (error) => {
// oclif sometimes exits with ExitError code 0 (not an error) // oclif sometimes exits with ExitError code 0 (not an error)
// (Avoid `error instanceof ExitError` here for the reasons explained // (Avoid `error instanceof ExitError` here for the reasons explained
// in the CONTRIBUTING.md file regarding the `instanceof` operator.) // in the CONTRIBUTING.md file regarding the `instanceof` operator.)

View File

@ -36,7 +36,7 @@ const createServer = ({ port }: { port: number }) => {
app.set('views', path.join(__dirname, 'pages')); app.set('views', path.join(__dirname, 'pages'));
const server = app.listen(port); const server = app.listen(port);
server.on('connection', socket => serverSockets.push(socket)); server.on('connection', (socket) => serverSockets.push(socket));
return { app, server }; return { app, server };
}; };
@ -55,7 +55,7 @@ const createServer = ({ port }: { port: number }) => {
* https://github.com/nodejs/node-v0.x-archive/issues/9066 * https://github.com/nodejs/node-v0.x-archive/issues/9066
*/ */
export function shutdownServer() { export function shutdownServer() {
serverSockets.forEach(s => s.unref()); serverSockets.forEach((s) => s.unref());
serverSockets.splice(0); serverSockets.splice(0);
} }

View File

@ -39,7 +39,7 @@ export const getDashboardLoginURL = (callbackUrl: string) => {
return getBalenaSdk() return getBalenaSdk()
.settings.get('dashboardUrl') .settings.get('dashboardUrl')
.then(dashboardUrl => .then((dashboardUrl) =>
url.resolve(dashboardUrl, `/login/cli/${callbackUrl}`), url.resolve(dashboardUrl, `/login/cli/${callbackUrl}`),
); );
}; };
@ -73,12 +73,12 @@ export const loginIfTokenValid = (token: string) => {
return balena.auth return balena.auth
.getToken() .getToken()
.catchReturn(undefined) .catchReturn(undefined)
.then(currentToken => .then((currentToken) =>
balena.auth balena.auth
.loginWithToken(token) .loginWithToken(token)
.return(token) .return(token)
.then(balena.auth.isLoggedIn) .then(balena.auth.isLoggedIn)
.tap(isLoggedIn => { .tap((isLoggedIn) => {
if (isLoggedIn) { if (isLoggedIn) {
return; return;
} }

View File

@ -94,9 +94,9 @@ function interpret(error: Error): string {
const messages: { const messages: {
[key: string]: (error: Error & { path?: string }) => string; [key: string]: (error: Error & { path?: string }) => string;
} = { } = {
EISDIR: error => `File is a directory: ${error.path}`, EISDIR: (error) => `File is a directory: ${error.path}`,
ENOENT: error => `No such file or directory: ${error.path}`, ENOENT: (error) => `No such file or directory: ${error.path}`,
ENOGIT: () => stripIndent` ENOGIT: () => stripIndent`
Git is not installed on this system. Git is not installed on this system.
@ -112,7 +112,7 @@ const messages: {
If this is not the case, and you're trying to burn an SDCard, check that the write lock is not set.`, If this is not the case, and you're trying to burn an SDCard, check that the write lock is not set.`,
EACCES: e => messages.EPERM(e), EACCES: (e) => messages.EPERM(e),
ETIMEDOUT: () => ETIMEDOUT: () =>
'Oops something went wrong, please check your connection and try again.', 'Oops something went wrong, please check your connection and try again.',
@ -130,9 +130,8 @@ const messages: {
`, `,
BalenaExpiredToken: () => stripIndent` BalenaExpiredToken: () => stripIndent`
Looks like your session token has expired. Looks like the session token has expired.
Please try logging in again with: Try logging in again with the "balena login" command.`,
$ balena login`,
}; };
const EXPECTED_ERROR_REGEXES = [ const EXPECTED_ERROR_REGEXES = [
@ -167,8 +166,8 @@ export async function handleError(error: Error) {
// Expected? // Expected?
const isExpectedError = const isExpectedError =
error instanceof ExpectedError || error instanceof ExpectedError ||
EXPECTED_ERROR_REGEXES.some(re => re.test(message[0])) || EXPECTED_ERROR_REGEXES.some((re) => re.test(message[0])) ||
EXPECTED_ERROR_REGEXES.some(re => re.test((error as BalenaError).code)); EXPECTED_ERROR_REGEXES.some((re) => re.test((error as BalenaError).code));
// Output/report error // Output/report error
if (isExpectedError) { if (isExpectedError) {
@ -199,7 +198,7 @@ export function printErrorMessage(message: string) {
const messageLines = message.split('\n'); const messageLines = message.split('\n');
console.error(chalk.red(messageLines.shift())); console.error(chalk.red(messageLines.shift()));
messageLines.forEach(line => { messageLines.forEach((line) => {
console.error(line); console.error(line);
}); });

View File

@ -45,7 +45,7 @@ const getMixpanel = _.once((balenaUrl: string) => {
*/ */
export async function trackCommand(commandSignature: string) { export async function trackCommand(commandSignature: string) {
const Sentry = await import('@sentry/node'); const Sentry = await import('@sentry/node');
Sentry.configureScope(scope => { Sentry.configureScope((scope) => {
scope.setExtra('command', commandSignature); scope.setExtra('command', commandSignature);
}); });
const balena = getBalenaSdk(); const balena = getBalenaSdk();
@ -56,7 +56,7 @@ export async function trackCommand(commandSignature: string) {
mixpanel: balenaUrlPromise.then(getMixpanel), mixpanel: balenaUrlPromise.then(getMixpanel),
}) })
.then(({ username, balenaUrl, mixpanel }) => { .then(({ username, balenaUrl, mixpanel }) => {
Sentry.configureScope(scope => { Sentry.configureScope((scope) => {
scope.setUser({ scope.setUser({
id: username, id: username,
username, username,

View File

@ -19,7 +19,7 @@ import { Hook } from '@oclif/config';
let trackResolve: (result: Promise<any>) => void; let trackResolve: (result: Promise<any>) => void;
// note: trackPromise is subject to a Bluebird.timeout, defined in events.ts // note: trackPromise is subject to a Bluebird.timeout, defined in events.ts
export const trackPromise = new Promise(resolve => { export const trackPromise = new Promise((resolve) => {
trackResolve = resolve; trackResolve = resolve;
}); });
@ -34,7 +34,7 @@ export const trackPromise = new Promise(resolve => {
* A command signature is something like "env add NAME [VALUE]". That's * A command signature is something like "env add NAME [VALUE]". That's
* literally so: 'NAME' and 'VALUE' are NOT replaced with actual values. * literally so: 'NAME' and 'VALUE' are NOT replaced with actual values.
*/ */
const hook: Hook<'prerun'> = async function(options) { const hook: Hook<'prerun'> = async function (options) {
const events = await import('../../events'); const events = await import('../../events');
const usage: string | string[] | undefined = options.Command.usage; const usage: string | string[] | undefined = options.Command.usage;
const cmdSignature = const cmdSignature =

View File

@ -75,11 +75,9 @@ export async function routeCliFramework(argv: string[], options: AppOptions) {
} }
if (process.env.DEBUG) { if (process.env.DEBUG) {
console.log( console.log(
`[debug] new argv=[${[ `[debug] new argv=[${[argv[0], argv[1], ...oclifArgs]}] length=${
argv[0], oclifArgs.length + 2
argv[1], }`,
...oclifArgs,
]}] length=${oclifArgs.length + 2}`,
); );
} }
await (await import('./app-oclif')).run(oclifArgs, options); await (await import('./app-oclif')).run(oclifArgs, options);

View File

@ -21,7 +21,7 @@ import * as path from 'path';
import { getChalk } from './lazy'; import { getChalk } from './lazy';
export const appendProjectOptions = opts => export const appendProjectOptions = (opts) =>
opts.concat([ opts.concat([
{ {
signature: 'projectName', signature: 'projectName',
@ -119,7 +119,7 @@ Source files are not modified.`,
export function generateOpts(options) { export function generateOpts(options) {
const fs = require('mz/fs'); const fs = require('mz/fs');
const { isV12 } = require('./version'); const { isV12 } = require('./version');
return fs.realpath(options.source || '.').then(projectPath => ({ return fs.realpath(options.source || '.').then((projectPath) => ({
projectName: options.projectName, projectName: options.projectName,
projectPath, projectPath,
inlineLogs: !options.nologs && (!!options.logs || isV12()), inlineLogs: !options.nologs && (!!options.logs || isV12()),
@ -152,7 +152,7 @@ export function createProject(composePath, composeStr, projectName = null) {
if (projectName == null) { if (projectName == null) {
projectName = path.basename(composePath); projectName = path.basename(composePath);
} }
const descriptors = compose.parse(composition).map(function(descr) { const descriptors = compose.parse(composition).map(function (descr) {
// generate an image name based on the project and service names // generate an image name based on the project and service names
// if one is not given and the service requires a build // if one is not given and the service requires a build
if ( if (
@ -216,7 +216,7 @@ function originalTarDirectory(dir, param) {
let readFile; let readFile;
if (process.platform === 'win32') { if (process.platform === 'win32') {
const { readFileWithEolConversion } = require('./eol-conversion'); const { readFileWithEolConversion } = require('./eol-conversion');
readFile = file => readFileWithEolConversion(file, convertEol); readFile = (file) => readFileWithEolConversion(file, convertEol);
} else { } else {
({ readFile } = fs); ({ readFile } = fs);
} }
@ -224,14 +224,14 @@ function originalTarDirectory(dir, param) {
const getFiles = () => const getFiles = () =>
// @ts-ignore `klaw` returns a `Walker` which is close enough to a stream to work but ts complains // @ts-ignore `klaw` returns a `Walker` which is close enough to a stream to work but ts complains
Promise.resolve(streamToPromise(klaw(dir))) Promise.resolve(streamToPromise(klaw(dir)))
.filter(item => !item.stats.isDirectory()) .filter((item) => !item.stats.isDirectory())
.map(item => item.path); .map((item) => item.path);
const ignore = new FileIgnorer(dir); const ignore = new FileIgnorer(dir);
const pack = tar.pack(); const pack = tar.pack();
const ignoreFiles = {}; const ignoreFiles = {};
return getFiles() return getFiles()
.each(function(file) { .each(function (file) {
const type = ignore.getIgnoreFileType(path.relative(dir, file)); const type = ignore.getIgnoreFileType(path.relative(dir, file));
if (type != null) { if (type != null) {
ignoreFiles[type] = ignoreFiles[type] || []; ignoreFiles[type] = ignoreFiles[type] || [];
@ -248,7 +248,7 @@ function originalTarDirectory(dir, param) {
} }
}) })
.filter(ignore.filter) .filter(ignore.filter)
.map(function(file) { .map(function (file) {
const relPath = path.relative(path.resolve(dir), file); const relPath = path.relative(path.resolve(dir), file);
return Promise.join( return Promise.join(
relPath, relPath,
@ -267,7 +267,7 @@ function originalTarDirectory(dir, param) {
); );
}) })
.then(() => preFinalizeCallback?.(pack)) .then(() => preFinalizeCallback?.(pack))
.then(function() { .then(function () {
pack.finalize(); pack.finalize();
return pack; return pack;
}); });
@ -278,7 +278,7 @@ function originalTarDirectory(dir, param) {
* @param {number} len * @param {number} len
* @returns {string} * @returns {string}
*/ */
const truncateString = function(str, len) { const truncateString = function (str, len) {
if (str.length < len) { if (str.length < len) {
return str; return str;
} }
@ -341,13 +341,13 @@ export function buildProject(
return Promise.resolve(checkBuildSecretsRequirements(docker, projectPath)) return Promise.resolve(checkBuildSecretsRequirements(docker, projectPath))
.then(() => qemu.installQemuIfNeeded(emulated, logger, arch, docker)) .then(() => qemu.installQemuIfNeeded(emulated, logger, arch, docker))
.tap(function(needsQemu) { .tap(function (needsQemu) {
if (!needsQemu) { if (!needsQemu) {
return; return;
} }
logger.logInfo('Emulation is enabled'); logger.logInfo('Emulation is enabled');
// Copy qemu into all build contexts // Copy qemu into all build contexts
return Promise.map(imageDescriptors, function(d) { return Promise.map(imageDescriptors, function (d) {
if (typeof d.image === 'string' || d.image.context == null) { if (typeof d.image === 'string' || d.image.context == null) {
return; return;
} }
@ -359,7 +359,7 @@ export function buildProject(
needsQemu, // Tar up the directory, ready for the build stream needsQemu, // Tar up the directory, ready for the build stream
) => ) =>
tarDirectory(projectPath, { convertEol, nogitignore }) tarDirectory(projectPath, { convertEol, nogitignore })
.then(tarStream => .then((tarStream) =>
makeBuildTasks( makeBuildTasks(
composition, composition,
tarStream, tarStream,
@ -368,7 +368,7 @@ export function buildProject(
projectName, projectName,
), ),
) )
.map(function(/** @type {any} */ task) { .map(function (/** @type {any} */ task) {
const d = imageDescriptorsByServiceName[task.serviceName]; const d = imageDescriptorsByServiceName[task.serviceName];
// multibuild parses the composition internally so any tags we've // multibuild parses the composition internally so any tags we've
@ -428,7 +428,7 @@ export function buildProject(
.return([task, binPath]); .return([task, binPath]);
}), }),
) )
.map(function([task, qemuPath]) { .map(function ([task, qemuPath]) {
const captureStream = buildLogCapture(task.external, task.logBuffer); const captureStream = buildLogCapture(task.external, task.logBuffer);
if (task.external) { if (task.external) {
@ -437,7 +437,7 @@ export function buildProject(
captureStream.pipe(task.logStream); captureStream.pipe(task.logStream);
task.progressHook = pullProgressAdapter(captureStream); task.progressHook = pullProgressAdapter(captureStream);
} else { } else {
task.streamHook = function(stream) { task.streamHook = function (stream) {
let rawStream; let rawStream;
stream = createLogStream(stream); stream = createLogStream(stream);
if (qemuPath != null) { if (qemuPath != null) {
@ -461,11 +461,11 @@ export function buildProject(
} }
return task; return task;
}) })
.then(function(tasks) { .then(function (tasks) {
logger.logDebug('Prepared tasks; building...'); logger.logDebug('Prepared tasks; building...');
return Promise.map( return Promise.map(
builder.performBuilds(tasks, docker, BALENA_ENGINE_TMP_PATH), builder.performBuilds(tasks, docker, BALENA_ENGINE_TMP_PATH),
function(builtImage) { function (builtImage) {
if (!builtImage.successful) { if (!builtImage.successful) {
/** @type {Error & {serviceName?: string}} */ /** @type {Error & {serviceName?: string}} */
const error = builtImage.error ?? new Error(); const error = builtImage.error ?? new Error();
@ -499,12 +499,12 @@ export function buildProject(
.getImage(image.name) .getImage(image.name)
.inspect() .inspect()
.get('Size') .get('Size')
.then(size => { .then((size) => {
image.props.size = size; image.props.size = size;
}) })
.return(image); .return(image);
}, },
).tap(function(images) { ).tap(function (images) {
const summary = _(images) const summary = _(images)
.map(({ serviceName, props }) => [ .map(({ serviceName, props }) => [
serviceName, serviceName,
@ -526,7 +526,7 @@ export function buildProject(
* @param {import('resin-compose-parse').Composition} composition * @param {import('resin-compose-parse').Composition} composition
* @returns {Promise<import('./compose-types').Release>} * @returns {Promise<import('./compose-types').Release>}
*/ */
export const createRelease = function( export const createRelease = function (
apiEndpoint, apiEndpoint,
auth, auth,
userId, userId,
@ -546,12 +546,9 @@ export const createRelease = function(
application: appId, application: appId,
composition, composition,
source: 'local', source: 'local',
commit: crypto commit: crypto.pseudoRandomBytes(16).toString('hex').toLowerCase(),
.pseudoRandomBytes(16)
.toString('hex')
.toLowerCase(),
}) })
.then(function({ release, serviceImages }) { .then(function ({ release, serviceImages }) {
return { return {
client, client,
release: _.omit(release, [ release: _.omit(release, [
@ -560,7 +557,7 @@ export const createRelease = function(
'is_created_by__user', 'is_created_by__user',
'__metadata', '__metadata',
]), ]),
serviceImages: _.mapValues(serviceImages, serviceImage => serviceImages: _.mapValues(serviceImages, (serviceImage) =>
_.omit(serviceImage, [ _.omit(serviceImage, [
'created_at', 'created_at',
'is_a_build_of__service', 'is_a_build_of__service',
@ -579,7 +576,7 @@ export const createRelease = function(
* @returns {Promise<Array<import('./compose-types').TaggedImage>>} * @returns {Promise<Array<import('./compose-types').TaggedImage>>}
*/ */
export const tagServiceImages = (docker, images, serviceImages) => export const tagServiceImages = (docker, images, serviceImages) =>
Promise.map(images, function(d) { Promise.map(images, function (d) {
const serviceImage = serviceImages[d.serviceName]; const serviceImage = serviceImages[d.serviceName];
const imageName = serviceImage.is_stored_at__image_location; const imageName = serviceImage.is_stored_at__image_location;
const match = /(.*?)\/(.*?)(?::([^/]*))?$/.exec(imageName); const match = /(.*?)\/(.*?)(?::([^/]*))?$/.exec(imageName);
@ -592,7 +589,7 @@ export const tagServiceImages = (docker, images, serviceImages) =>
.getImage(d.name) .getImage(d.name)
.tag({ repo: name, tag, force: true }) .tag({ repo: name, tag, force: true })
.then(() => docker.getImage(`${name}:${tag}`)) .then(() => docker.getImage(`${name}:${tag}`))
.then(localImage => ({ .then((localImage) => ({
serviceName: d.serviceName, serviceName: d.serviceName,
serviceImage, serviceImage,
localImage, localImage,
@ -629,13 +626,13 @@ export const getPreviousRepos = (sdk, docker, logger, appID) =>
$top: 1, $top: 1,
}, },
}) })
.then(function(release) { .then(function (release) {
// grab all images from the latest release, return all image locations in the registry // grab all images from the latest release, return all image locations in the registry
if (release.length > 0) { if (release.length > 0) {
const images = release[0].contains__image; const images = release[0].contains__image;
return Promise.map(images, function(d) { return Promise.map(images, function (d) {
const imageName = d.image[0].is_stored_at__image_location; const imageName = d.image[0].is_stored_at__image_location;
return docker.getRegistryAndName(imageName).then(function(registry) { return docker.getRegistryAndName(imageName).then(function (registry) {
logger.logDebug( logger.logDebug(
`Requesting access to previously pushed image repo (${registry.imageName})`, `Requesting access to previously pushed image repo (${registry.imageName})`,
); );
@ -646,7 +643,7 @@ export const getPreviousRepos = (sdk, docker, logger, appID) =>
return []; return [];
} }
}) })
.catch(e => { .catch((e) => {
logger.logDebug(`Failed to access previously pushed image repo: ${e}`); logger.logDebug(`Failed to access previously pushed image repo: ${e}`);
return []; return [];
}); });
@ -659,7 +656,7 @@ export const getPreviousRepos = (sdk, docker, logger, appID) =>
* @param {string[]} previousRepos * @param {string[]} previousRepos
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
export const authorizePush = function( export const authorizePush = function (
sdk, sdk,
tokenAuthEndpoint, tokenAuthEndpoint,
registry, registry,
@ -677,7 +674,7 @@ export const authorizePush = function(
url: '/auth/v1/token', url: '/auth/v1/token',
qs: { qs: {
service: registry, service: registry,
scope: images.map(repo => `repository:${repo}:pull,push`), scope: images.map((repo) => `repository:${repo}:pull,push`),
}, },
}) })
.get('body') .get('body')
@ -691,7 +688,7 @@ export const authorizePush = function(
* @param {Array<import('./compose-types').TaggedImage>} images * @param {Array<import('./compose-types').TaggedImage>} images
* @param {(serviceImage: import('balena-release/build/models').ImageModel, props: object) => void} afterEach * @param {(serviceImage: import('balena-release/build/models').ImageModel, props: object) => void} afterEach
*/ */
export const pushAndUpdateServiceImages = function( export const pushAndUpdateServiceImages = function (
docker, docker,
token, token,
images, images,
@ -725,7 +722,7 @@ export const pushAndUpdateServiceImages = function(
1.4, // `backoffScaler` - wait multiplier for each retry 1.4, // `backoffScaler` - wait multiplier for each retry
).finally(renderer.end), ).finally(renderer.end),
/** @type {(size: number, digest: string) => void} */ /** @type {(size: number, digest: string) => void} */
function(size, digest) { function (size, digest) {
serviceImage.image_size = size; serviceImage.image_size = size;
serviceImage.content_hash = digest; serviceImage.content_hash = digest;
serviceImage.build_log = logs; serviceImage.build_log = logs;
@ -741,7 +738,7 @@ export const pushAndUpdateServiceImages = function(
serviceImage.status = 'success'; serviceImage.status = 'success';
}, },
) )
.tapCatch(function(e) { .tapCatch(function (e) {
serviceImage.error_message = '' + e; serviceImage.error_message = '' + e;
serviceImage.status = 'failed'; serviceImage.status = 'failed';
}) })
@ -752,7 +749,7 @@ export const pushAndUpdateServiceImages = function(
// utilities // utilities
const renderProgressBar = function(percentage, stepCount) { const renderProgressBar = function (percentage, stepCount) {
const _ = require('lodash'); const _ = require('lodash');
percentage = _.clamp(percentage, 0, 100); percentage = _.clamp(percentage, 0, 100);
const barCount = Math.floor((stepCount * percentage) / 100); const barCount = Math.floor((stepCount * percentage) / 100);
@ -761,8 +758,8 @@ const renderProgressBar = function(percentage, stepCount) {
return `${bar} ${_.padStart(percentage, 3)}%`; return `${bar} ${_.padStart(percentage, 3)}%`;
}; };
var pushProgressRenderer = function(tty, prefix) { var pushProgressRenderer = function (tty, prefix) {
const fn = function(e) { const fn = function (e) {
const { error, percentage } = e; const { error, percentage } = e;
if (error != null) { if (error != null) {
throw new Error(error); throw new Error(error);
@ -776,15 +773,15 @@ var pushProgressRenderer = function(tty, prefix) {
return fn; return fn;
}; };
var createLogStream = function(input) { var createLogStream = function (input) {
const split = require('split'); const split = require('split');
const stripAnsi = require('strip-ansi-stream'); const stripAnsi = require('strip-ansi-stream');
return input.pipe(stripAnsi()).pipe(split()); return input.pipe(stripAnsi()).pipe(split());
}; };
var dropEmptyLinesStream = function() { var dropEmptyLinesStream = function () {
const through = require('through2'); const through = require('through2');
return through(function(data, _enc, cb) { return through(function (data, _enc, cb) {
const str = data.toString('utf-8'); const str = data.toString('utf-8');
if (str.trim()) { if (str.trim()) {
this.push(str); this.push(str);
@ -793,10 +790,10 @@ var dropEmptyLinesStream = function() {
}); });
}; };
var buildLogCapture = function(objectMode, buffer) { var buildLogCapture = function (objectMode, buffer) {
const through = require('through2'); const through = require('through2');
return through({ objectMode }, function(data, _enc, cb) { return through({ objectMode }, function (data, _enc, cb) {
// data from pull stream // data from pull stream
if (data.error) { if (data.error) {
buffer.push(`${data.error}`); buffer.push(`${data.error}`);
@ -814,7 +811,7 @@ var buildLogCapture = function(objectMode, buffer) {
}); });
}; };
var buildProgressAdapter = function(inline) { var buildProgressAdapter = function (inline) {
const through = require('through2'); const through = require('through2');
const stepRegex = /^\s*Step\s+(\d+)\/(\d+)\s*: (.+)$/; const stepRegex = /^\s*Step\s+(\d+)\/(\d+)\s*: (.+)$/;
@ -823,7 +820,7 @@ var buildProgressAdapter = function(inline) {
let numSteps = null; let numSteps = null;
let progress; let progress;
return through({ objectMode: true }, function(str, _enc, cb) { return through({ objectMode: true }, function (str, _enc, cb) {
if (str == null) { if (str == null) {
return cb(null, str); return cb(null, str);
} }
@ -855,8 +852,8 @@ var buildProgressAdapter = function(inline) {
}); });
}; };
var pullProgressAdapter = outStream => var pullProgressAdapter = (outStream) =>
function({ status, id, percentage, error, errorDetail }) { function ({ status, id, percentage, error, errorDetail }) {
if (status != null) { if (status != null) {
status = status.replace(/^Status: /, ''); status = status.replace(/^Status: /, '');
} }
@ -887,8 +884,8 @@ class BuildProgressUI {
const services = _.map(descriptors, 'serviceName'); const services = _.map(descriptors, 'serviceName');
const streams = _(services) const streams = _(services)
.map(function(service) { .map(function (service) {
const stream = through.obj(function(event, _enc, cb) { const stream = through.obj(function (event, _enc, cb) {
eventHandler(service, event); eventHandler(service, event);
return cb(); return cb();
}); });
@ -937,7 +934,7 @@ class BuildProgressUI {
start() { start() {
process.on('SIGINT', this._handleInterrupt); process.on('SIGINT', this._handleInterrupt);
this._tty.hideCursor(); this._tty.hideCursor();
this._services.forEach(service => { this._services.forEach((service) => {
this.streams[service].write({ status: 'Preparing...' }); this.streams[service].write({ status: 'Preparing...' });
}); });
this._runloop = require('./compose_ts').createRunLoop(this._display); this._runloop = require('./compose_ts').createRunLoop(this._display);
@ -978,7 +975,7 @@ class BuildProgressUI {
const serviceToDataMap = this._serviceToDataMap; const serviceToDataMap = this._serviceToDataMap;
return _(services) return _(services)
.map(function(service) { .map(function (service) {
const { status, progress, error } = serviceToDataMap[service] ?? {}; const { status, progress, error } = serviceToDataMap[service] ?? {};
if (error) { if (error) {
return `${error}`; return `${error}`;
@ -1060,8 +1057,8 @@ class BuildProgressInline {
const services = _.map(descriptors, 'serviceName'); const services = _.map(descriptors, 'serviceName');
const eventHandler = this._renderEvent; const eventHandler = this._renderEvent;
const streams = _(services) const streams = _(services)
.map(function(service) { .map(function (service) {
const stream = through.obj(function(event, _enc, cb) { const stream = through.obj(function (event, _enc, cb) {
eventHandler(service, event); eventHandler(service, event);
return cb(); return cb();
}); });
@ -1083,7 +1080,7 @@ class BuildProgressInline {
start() { start() {
this._outStream.write('Building services...\n'); this._outStream.write('Building services...\n');
this._services.forEach(service => { this._services.forEach((service) => {
this.streams[service].write({ status: 'Preparing...' }); this.streams[service].write({ status: 'Preparing...' });
}); });
this._startTime = Date.now(); this._startTime = Date.now();
@ -1099,7 +1096,7 @@ class BuildProgressInline {
this._ended = true; this._ended = true;
if (summary != null) { if (summary != null) {
this._services.forEach(service => { this._services.forEach((service) => {
this._renderEvent(service, summary[service]); this._renderEvent(service, summary[service]);
}); });
} }
@ -1122,7 +1119,7 @@ class BuildProgressInline {
_renderEvent(service, event) { _renderEvent(service, event) {
const _ = require('lodash'); const _ = require('lodash');
const str = (function() { const str = (function () {
const { status, error } = event; const { status, error } = event;
if (error) { if (error) {
return `${error}`; return `${error}`;

View File

@ -188,7 +188,7 @@ export async function tarDirectory(
let readFile: (file: string) => Promise<Buffer>; let readFile: (file: string) => Promise<Buffer>;
if (process.platform === 'win32') { if (process.platform === 'win32') {
const { readFileWithEolConversion } = require('./eol-conversion'); const { readFileWithEolConversion } = require('./eol-conversion');
readFile = file => readFileWithEolConversion(file, convertEol); readFile = (file) => readFileWithEolConversion(file, convertEol);
} else { } else {
readFile = fs.readFile; readFile = fs.readFile;
} }
@ -222,7 +222,7 @@ export function printGitignoreWarn(
dockerignoreFile: string, dockerignoreFile: string,
gitignoreFiles: string[], gitignoreFiles: string[],
) { ) {
const ignoreFiles = [dockerignoreFile, ...gitignoreFiles].filter(e => e); const ignoreFiles = [dockerignoreFile, ...gitignoreFiles].filter((e) => e);
if (ignoreFiles.length === 0) { if (ignoreFiles.length === 0) {
return; return;
} }
@ -346,7 +346,7 @@ export async function makeBuildTasks(
const buildTasks = await MultiBuild.splitBuildStream(composition, tarStream); const buildTasks = await MultiBuild.splitBuildStream(composition, tarStream);
logger.logDebug('Found build tasks:'); logger.logDebug('Found build tasks:');
_.each(buildTasks, task => { _.each(buildTasks, (task) => {
let infoStr: string; let infoStr: string;
if (task.external) { if (task.external) {
infoStr = `image pull [${task.imageName}]`; infoStr = `image pull [${task.imageName}]`;
@ -369,7 +369,7 @@ export async function makeBuildTasks(
); );
logger.logDebug('Found project types:'); logger.logDebug('Found project types:');
_.each(buildTasks, task => { _.each(buildTasks, (task) => {
if (task.external) { if (task.external) {
logger.logDebug(` ${task.serviceName}: External image`); logger.logDebug(` ${task.serviceName}: External image`);
} else { } else {
@ -403,7 +403,7 @@ async function performResolution(
); );
// Do one task at a time (Bluebird.each instead of Bluebird.all) // Do one task at a time (Bluebird.each instead of Bluebird.all)
// in order to reduce peak memory usage. Resolves to buildTasks. // in order to reduce peak memory usage. Resolves to buildTasks.
Bluebird.each(buildTasks, buildTask => { Bluebird.each(buildTasks, (buildTask) => {
// buildStream is falsy for "external" tasks (image pull) // buildStream is falsy for "external" tasks (image pull)
if (!buildTask.buildStream) { if (!buildTask.buildStream) {
return buildTask; return buildTask;
@ -551,7 +551,7 @@ export async function validateProjectDirectory(
const checkCompose = async (folder: string) => { const checkCompose = async (folder: string) => {
return _.some( return _.some(
await Promise.all( await Promise.all(
compositionFileNames.map(filename => compositionFileNames.map((filename) =>
fs.exists(path.join(folder, filename)), fs.exists(path.join(folder, filename)),
), ),
), ),
@ -615,7 +615,7 @@ async function pushServiceImages(
const { pushAndUpdateServiceImages } = await import('./compose'); const { pushAndUpdateServiceImages } = await import('./compose');
const releaseMod = await import('balena-release'); const releaseMod = await import('balena-release');
logger.logInfo('Pushing images to registry...'); logger.logInfo('Pushing images to registry...');
await pushAndUpdateServiceImages(docker, token, taggedImages, async function( await pushAndUpdateServiceImages(docker, token, taggedImages, async function (
serviceImage, serviceImage,
) { ) {
logger.logDebug( logger.logDebug(
@ -713,12 +713,12 @@ function runSpinner(
spinner: () => string, spinner: () => string,
msg: string, msg: string,
) { ) {
const runloop = createRunLoop(function() { const runloop = createRunLoop(function () {
tty.clearLine(); tty.clearLine();
tty.writeLine(`${msg} ${spinner()}`); tty.writeLine(`${msg} ${spinner()}`);
return tty.cursorUp(); return tty.cursorUp();
}); });
runloop.onEnd = function() { runloop.onEnd = function () {
tty.clearLine(); tty.clearLine();
return tty.writeLine(msg); return tty.writeLine(msg);
}; };

View File

@ -72,7 +72,7 @@ export function generateBaseConfig(
application.app_name, application.app_name,
options, options,
) as Promise<ImgConfig & { apiKey?: string }>; ) as Promise<ImgConfig & { apiKey?: string }>;
return promise.tap(config => { return promise.tap((config) => {
// os.getConfig always returns a config for an app // os.getConfig always returns a config for an app
delete config.apiKey; delete config.apiKey;
@ -91,7 +91,7 @@ export function generateApplicationConfig(
application: BalenaSdk.Application, application: BalenaSdk.Application,
options: { version: string; deviceType?: string }, options: { version: string; deviceType?: string },
) { ) {
return generateBaseConfig(application, options).tap(config => { return generateBaseConfig(application, options).tap((config) => {
if (semver.satisfies(options.version, '<2.7.8')) { if (semver.satisfies(options.version, '<2.7.8')) {
return addApplicationKey(config, application.id); return addApplicationKey(config, application.id);
} }
@ -108,12 +108,12 @@ export function generateDeviceConfig(
) { ) {
return getBalenaSdk() return getBalenaSdk()
.models.application.get(device.belongs_to__application.__id) .models.application.get(device.belongs_to__application.__id)
.then(application => { .then((application) => {
const baseConfigOpts = { const baseConfigOpts = {
...options, ...options,
deviceType: device.device_type, deviceType: device.device_type,
}; };
return generateBaseConfig(application, baseConfigOpts).tap(config => { return generateBaseConfig(application, baseConfigOpts).tap((config) => {
if ( if (
deviceApiKey == null && deviceApiKey == null &&
semver.satisfies(options.version, '<2.0.3') semver.satisfies(options.version, '<2.0.3')
@ -123,7 +123,7 @@ export function generateDeviceConfig(
return addDeviceKey(config, device.uuid, deviceApiKey || true); return addDeviceKey(config, device.uuid, deviceApiKey || true);
}); });
}) })
.then(config => { .then((config) => {
// Associate a device, to prevent the supervisor // Associate a device, to prevent the supervisor
// from creating another one on its own. // from creating another one on its own.
config.registered_at = Math.floor(Date.now() / 1000); config.registered_at = Math.floor(Date.now() / 1000);
@ -137,7 +137,7 @@ export function generateDeviceConfig(
function addApplicationKey(config: any, applicationNameOrId: string | number) { function addApplicationKey(config: any, applicationNameOrId: string | number) {
return getBalenaSdk() return getBalenaSdk()
.models.application.generateApiKey(applicationNameOrId) .models.application.generateApiKey(applicationNameOrId)
.tap(apiKey => { .tap((apiKey) => {
config.apiKey = apiKey; config.apiKey = apiKey;
}); });
} }
@ -145,7 +145,7 @@ function addApplicationKey(config: any, applicationNameOrId: string | number) {
function addProvisioningKey(config: any, applicationNameOrId: string | number) { function addProvisioningKey(config: any, applicationNameOrId: string | number) {
return getBalenaSdk() return getBalenaSdk()
.models.application.generateProvisioningKey(applicationNameOrId) .models.application.generateProvisioningKey(applicationNameOrId)
.tap(apiKey => { .tap((apiKey) => {
config.apiKey = apiKey; config.apiKey = apiKey;
}); });
} }
@ -161,7 +161,7 @@ function addDeviceKey(
} else { } else {
return customDeviceApiKey; return customDeviceApiKey;
} }
}).tap(deviceApiKey => { }).tap((deviceApiKey) => {
config.deviceApiKey = deviceApiKey; config.deviceApiKey = deviceApiKey;
}); });
} }

View File

@ -18,19 +18,19 @@
import * as Promise from 'bluebird'; import * as Promise from 'bluebird';
import { getVisuals } from './lazy'; import { getVisuals } from './lazy';
const getBuilderPushEndpoint = function(baseUrl, owner, app) { const getBuilderPushEndpoint = function (baseUrl, owner, app) {
const querystring = require('querystring'); const querystring = require('querystring');
const args = querystring.stringify({ owner, app }); const args = querystring.stringify({ owner, app });
return `https://builder.${baseUrl}/v1/push?${args}`; return `https://builder.${baseUrl}/v1/push?${args}`;
}; };
const getBuilderLogPushEndpoint = function(baseUrl, buildId, owner, app) { const getBuilderLogPushEndpoint = function (baseUrl, buildId, owner, app) {
const querystring = require('querystring'); const querystring = require('querystring');
const args = querystring.stringify({ owner, app, buildId }); const args = querystring.stringify({ owner, app, buildId });
return `https://builder.${baseUrl}/v1/pushLogs?${args}`; return `https://builder.${baseUrl}/v1/pushLogs?${args}`;
}; };
const bufferImage = function(docker, imageId, bufferFile) { const bufferImage = function (docker, imageId, bufferFile) {
const streamUtils = require('./streams'); const streamUtils = require('./streams');
const image = docker.getImage(imageId); const image = docker.getImage(imageId);
@ -40,14 +40,14 @@ const bufferImage = function(docker, imageId, bufferFile) {
image.get(), image.get(),
imageMetadata.get('Size'), imageMetadata.get('Size'),
(imageStream, imageSize) => (imageStream, imageSize) =>
streamUtils.buffer(imageStream, bufferFile).tap(bufferedStream => { streamUtils.buffer(imageStream, bufferFile).tap((bufferedStream) => {
// @ts-ignore adding an extra property // @ts-ignore adding an extra property
bufferedStream.length = imageSize; bufferedStream.length = imageSize;
}), }),
); );
}; };
const showPushProgress = function(message) { const showPushProgress = function (message) {
const visuals = getVisuals(); const visuals = getVisuals();
const progressBar = new visuals.Progress(message); const progressBar = new visuals.Progress(message);
progressBar.update({ percentage: 0 }); progressBar.update({ percentage: 0 });
@ -55,8 +55,8 @@ const showPushProgress = function(message) {
}; };
const uploadToPromise = (uploadRequest, logger) => const uploadToPromise = (uploadRequest, logger) =>
new Promise(function(resolve, reject) { new Promise(function (resolve, reject) {
const handleMessage = function(data) { const handleMessage = function (data) {
let obj; let obj;
data = data.toString(); data = data.toString();
logger.logDebug(`Received data: ${data}`); logger.logDebug(`Received data: ${data}`);
@ -90,7 +90,7 @@ const uploadToPromise = (uploadRequest, logger) =>
/** /**
* @returns {Promise<{ buildId: number }>} * @returns {Promise<{ buildId: number }>}
*/ */
const uploadImage = function( const uploadImage = function (
imageStream, imageStream,
token, token,
username, username,
@ -139,7 +139,7 @@ const uploadImage = function(
return uploadToPromise(uploadRequest, logger); return uploadToPromise(uploadRequest, logger);
}; };
const uploadLogs = function(logs, token, url, buildId, username, appName) { const uploadLogs = function (logs, token, url, buildId, username, appName) {
const request = require('request'); const request = require('request');
return request.post({ return request.post({
json: true, json: true,
@ -159,7 +159,7 @@ opts must be a hash with the following keys:
- buildLogs: a string with build output - buildLogs: a string with build output
- shouldUploadLogs - shouldUploadLogs
*/ */
export const deployLegacy = function( export const deployLegacy = function (
docker, docker,
logger, logger,
token, token,
@ -177,10 +177,10 @@ export const deployLegacy = function(
const logs = buildLogs; const logs = buildLogs;
return tmpNameAsync() return tmpNameAsync()
.then(function(bufferFile) { .then(function (bufferFile) {
logger.logInfo('Initializing deploy...'); logger.logInfo('Initializing deploy...');
return bufferImage(docker, imageName, bufferFile) return bufferImage(docker, imageName, bufferFile)
.then(stream => .then((stream) =>
uploadImage(stream, token, username, url, appName, logger), uploadImage(stream, token, username, url, appName, logger),
) )
.finally(() => .finally(() =>
@ -192,7 +192,7 @@ export const deployLegacy = function(
), ),
); );
}) })
.tap(function({ buildId }) { .tap(function ({ buildId }) {
if (!shouldUploadLogs) { if (!shouldUploadLogs) {
return; return;
} }

View File

@ -105,7 +105,7 @@ export class DeviceAPI {
json: true, json: true,
}, },
this.logger, this.logger,
).then(body => { ).then((body) => {
return body.state; return body.state;
}); });
} }
@ -120,7 +120,7 @@ export class DeviceAPI {
json: true, json: true,
}, },
this.logger, this.logger,
).then(body => { ).then((body) => {
return body.info; return body.info;
}); });
} }
@ -166,7 +166,7 @@ export class DeviceAPI {
return DeviceAPI.promisifiedRequest(request.get, { return DeviceAPI.promisifiedRequest(request.get, {
url, url,
json: true, json: true,
}).then(body => { }).then((body) => {
if (body.status !== 'success') { if (body.status !== 'success') {
throw new ApiErrors.DeviceAPIError( throw new ApiErrors.DeviceAPIError(
'Non-successful response from supervisor version endpoint', 'Non-successful response from supervisor version endpoint',
@ -183,7 +183,7 @@ export class DeviceAPI {
return DeviceAPI.promisifiedRequest(request.get, { return DeviceAPI.promisifiedRequest(request.get, {
url, url,
json: true, json: true,
}).then(body => { }).then((body) => {
if (body.status !== 'success') { if (body.status !== 'success') {
throw new ApiErrors.DeviceAPIError( throw new ApiErrors.DeviceAPIError(
'Non-successful response from supervisor status endpoint', 'Non-successful response from supervisor status endpoint',
@ -201,13 +201,14 @@ export class DeviceAPI {
return new Bluebird((resolve, reject) => { return new Bluebird((resolve, reject) => {
const req = request.get(url); const req = request.get(url);
req.on('error', reject).on('response', async res => { req.on('error', reject).on('response', async (res) => {
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
reject( reject(
new ApiErrors.DeviceAPIError( new ApiErrors.DeviceAPIError(
'Non-200 response from log streaming endpoint', 'Non-200 response from log streaming endpoint',
), ),
); );
return;
} }
res.socket.setKeepAlive(true, 1000); res.socket.setKeepAlive(true, 1000);
if (os.platform() !== 'win32') { if (os.platform() !== 'win32') {
@ -260,7 +261,7 @@ export class DeviceAPI {
} }
return Bluebird.fromCallback<[request.Response, { message: string }]>( return Bluebird.fromCallback<[request.Response, { message: string }]>(
cb => { (cb) => {
return requestMethod(opts, cb); return requestMethod(opts, cb);
}, },
{ multiArgs: true }, { multiArgs: true },

View File

@ -315,7 +315,7 @@ export async function performBuilds(
logger, logger,
LOCAL_APPNAME, LOCAL_APPNAME,
LOCAL_RELEASEHASH, LOCAL_RELEASEHASH,
content => { (content) => {
if (!opts.nolive) { if (!opts.nolive) {
return LivepushManager.preprocessDockerfile(content); return LivepushManager.preprocessDockerfile(content);
} else { } else {
@ -356,7 +356,7 @@ export async function performBuilds(
// Now tag any external images with the correct name that they should be, // Now tag any external images with the correct name that they should be,
// as this won't be done by resin-multibuild // as this won't be done by resin-multibuild
await Bluebird.map(localImages, async localImage => { await Bluebird.map(localImages, async (localImage) => {
if (localImage.external) { if (localImage.external) {
// We can be sure that localImage.name is set here, because of the failure code above // We can be sure that localImage.name is set here, because of the failure code above
const image = docker.getImage(localImage.name!); const image = docker.getImage(localImage.name!);
@ -368,7 +368,7 @@ export async function performBuilds(
} }
}); });
await Bluebird.map(_.uniq(imagesToRemove), image => await Bluebird.map(_.uniq(imagesToRemove), (image) =>
docker.getImage(image).remove({ force: true }), docker.getImage(image).remove({ force: true }),
); );
@ -419,7 +419,7 @@ export async function rebuildSingleTask(
logger, logger,
LOCAL_APPNAME, LOCAL_APPNAME,
LOCAL_RELEASEHASH, LOCAL_RELEASEHASH,
content => { (content) => {
if (!opts.nolive) { if (!opts.nolive) {
return LivepushManager.preprocessDockerfile(content); return LivepushManager.preprocessDockerfile(content);
} else { } else {
@ -460,16 +460,16 @@ function assignOutputHandlers(
logger: Logger, logger: Logger,
logCb?: (serviceName: string, line: string) => void, logCb?: (serviceName: string, line: string) => void,
) { ) {
_.each(buildTasks, task => { _.each(buildTasks, (task) => {
if (task.external) { if (task.external) {
task.progressHook = progressObj => { task.progressHook = (progressObj) => {
displayBuildLog( displayBuildLog(
{ serviceName: task.serviceName, message: progressObj.progress }, { serviceName: task.serviceName, message: progressObj.progress },
logger, logger,
); );
}; };
} else { } else {
task.streamHook = stream => { task.streamHook = (stream) => {
stream.on('data', (buf: Buffer) => { stream.on('data', (buf: Buffer) => {
const str = _.trimEnd(buf.toString()); const str = _.trimEnd(buf.toString());
if (str !== '') { if (str !== '') {
@ -601,7 +601,7 @@ async function inspectBuildResults(images: LocalImage[]): Promise<void> {
const failures: LocalPushErrors.BuildFailure[] = []; const failures: LocalPushErrors.BuildFailure[] = [];
_.each(images, image => { _.each(images, (image) => {
if (!image.successful) { if (!image.successful) {
failures.push({ failures.push({
error: image.error!, error: image.error!,

View File

@ -17,14 +17,14 @@ export class BuildError extends TypedError {
public toString(): string { public toString(): string {
let str = 'Some services failed to build:\n'; let str = 'Some services failed to build:\n';
_.each(this.failures, failure => { _.each(this.failures, (failure) => {
str += `\t${failure.serviceName}: ${failure.error.message}\n`; str += `\t${failure.serviceName}: ${failure.error.message}\n`;
}); });
return str; return str;
} }
public getServiceError(serviceName: string): string { public getServiceError(serviceName: string): string {
const failure = _.find(this.failures, f => f.serviceName === serviceName); const failure = _.find(this.failures, (f) => f.serviceName === serviceName);
if (failure == null) { if (failure == null) {
return 'Unknown build failure'; return 'Unknown build failure';
} }

View File

@ -199,7 +199,7 @@ export class LivepushManager {
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
this.logger.logLivepush('Cleaning up device...'); this.logger.logLivepush('Cleaning up device...');
await Promise.all( await Promise.all(
_.map(this.containers, container => { _.map(this.containers, (container) => {
container.livepush.cleanupIntermediateContainers(); container.livepush.cleanupIntermediateContainers();
}), }),
); );
@ -263,8 +263,8 @@ export class LivepushManager {
// First we detect if the file changed is the Dockerfile // First we detect if the file changed is the Dockerfile
// used to build the service // used to build the service
if ( if (
_.some(this.dockerfilePaths[serviceName], name => _.some(this.dockerfilePaths[serviceName], (name) =>
_.some(updated, changed => name === changed), _.some(updated, (changed) => name === changed),
) )
) { ) {
this.logger.logLivepush( this.logger.logLivepush(
@ -330,7 +330,7 @@ export class LivepushManager {
this.composition, this.composition,
this.buildContext, this.buildContext,
this.deployOpts, this.deployOpts,
id => { (id) => {
this.rebuildRunningIds[serviceName] = id; this.rebuildRunningIds[serviceName] = id;
}, },
); );
@ -430,10 +430,10 @@ export class LivepushManager {
const error = (msg: string) => this.logger.logError(msgString(msg)); const error = (msg: string) => this.logger.logError(msgString(msg));
const debugLog = (msg: string) => this.logger.logDebug(msgString(msg)); const debugLog = (msg: string) => this.logger.logDebug(msgString(msg));
livepush.on('commandExecute', command => livepush.on('commandExecute', (command) =>
log(`Executing command: \`${command.command}\``), log(`Executing command: \`${command.command}\``),
); );
livepush.on('commandOutput', output => livepush.on('commandOutput', (output) =>
log(` ${output.output.data.toString()}`), log(` ${output.output.data.toString()}`),
); );
livepush.on('commandReturn', ({ returnCode, command }) => { livepush.on('commandReturn', ({ returnCode, command }) => {

View File

@ -40,7 +40,7 @@ export function displayDeviceLogs(
filterServices?: string[], filterServices?: string[],
): Bluebird<void> { ): Bluebird<void> {
return new Bluebird((resolve, reject) => { return new Bluebird((resolve, reject) => {
logs.on('data', log => { logs.on('data', (log) => {
displayLogLine(log, logger, system, filterServices); displayLogLine(log, logger, system, filterServices);
}); });

View File

@ -63,7 +63,7 @@ export async function performLocalDeviceSSH(
const serviceNames: string[] = []; const serviceNames: string[] = [];
const containers = allContainers const containers = allContainers
.map(container => { .map((container) => {
for (const name of container.Names) { for (const name of container.Names) {
if (regex.test(name)) { if (regex.test(name)) {
return { id: container.Id, name }; return { id: container.Id, name };
@ -75,7 +75,7 @@ export async function performLocalDeviceSSH(
} }
return; return;
}) })
.filter(c => c != null); .filter((c) => c != null);
if (containers.length === 0) { if (containers.length === 0) {
throw new ExpectedError( throw new ExpectedError(

View File

@ -26,7 +26,7 @@ import * as _ from 'lodash';
// //
// NOTE: Care MUST be taken when using the function, so as to // NOTE: Care MUST be taken when using the function, so as to
// not redefine/override options already provided. // not redefine/override options already provided.
export const appendConnectionOptions = opts => export const appendConnectionOptions = (opts) =>
opts.concat([ opts.concat([
{ {
signature: 'docker', signature: 'docker',
@ -106,10 +106,10 @@ Implements the same feature as the "docker build --cache-from" option.`,
]); ]);
} }
const generateConnectOpts = function(opts) { const generateConnectOpts = function (opts) {
const fs = require('mz/fs'); const fs = require('mz/fs');
return Promise.try(function() { return Promise.try(function () {
const connectOpts = {}; const connectOpts = {};
// Firsly need to decide between a local docker socket // Firsly need to decide between a local docker socket
// and a host available over a host:port combo // and a host available over a host:port combo
@ -152,7 +152,7 @@ const generateConnectOpts = function(opts) {
cert: fs.readFile(opts.cert, 'utf-8'), cert: fs.readFile(opts.cert, 'utf-8'),
key: fs.readFile(opts.key, 'utf-8'), key: fs.readFile(opts.key, 'utf-8'),
}; };
return Promise.props(certBodies).then(toMerge => return Promise.props(certBodies).then((toMerge) =>
_.merge(connectOpts, toMerge), _.merge(connectOpts, toMerge),
); );
} }
@ -161,12 +161,12 @@ const generateConnectOpts = function(opts) {
}); });
}; };
const parseBuildArgs = function(args) { const parseBuildArgs = function (args) {
if (!Array.isArray(args)) { if (!Array.isArray(args)) {
args = [args]; args = [args];
} }
const buildArgs = {}; const buildArgs = {};
args.forEach(function(arg) { args.forEach(function (arg) {
// note: [^] matches any character, including line breaks // note: [^] matches any character, including line breaks
const pair = /^([^\s]+?)=([^]*)$/.exec(arg); const pair = /^([^\s]+?)=([^]*)$/.exec(arg);
if (pair != null) { if (pair != null) {
@ -187,7 +187,7 @@ export function generateBuildOpts(options) {
opts.nocache = true; opts.nocache = true;
} }
if (options['cache-from']?.trim()) { if (options['cache-from']?.trim()) {
opts.cachefrom = options['cache-from'].split(',').filter(i => !!i.trim()); opts.cachefrom = options['cache-from'].split(',').filter((i) => !!i.trim());
} }
if (options.squash != null) { if (options.squash != null) {
opts.squash = true; opts.squash = true;
@ -220,7 +220,7 @@ export function getDocker(options) {
.tap(ensureDockerSeemsAccessible); .tap(ensureDockerSeemsAccessible);
} }
const getDockerToolbelt = _.once(function() { const getDockerToolbelt = _.once(function () {
const Docker = require('docker-toolbelt'); const Docker = require('docker-toolbelt');
Promise.promisifyAll(Docker.prototype, { Promise.promisifyAll(Docker.prototype, {
filter(name) { filter(name) {
@ -252,7 +252,7 @@ const getDockerToolbelt = _.once(function() {
* }} opts * }} opts
* @returns {import('docker-toolbelt')} * @returns {import('docker-toolbelt')}
*/ */
export const createClient = function(opts) { export const createClient = function (opts) {
const Docker = getDockerToolbelt(); const Docker = getDockerToolbelt();
const docker = new Docker(opts); const docker = new Docker(opts);
const { modem } = docker; const { modem } = docker;
@ -269,7 +269,7 @@ export const createClient = function(opts) {
return docker; return docker;
}; };
var ensureDockerSeemsAccessible = function(docker) { var ensureDockerSeemsAccessible = function (docker) {
const { exitWithExpectedError } = require('../errors'); const { exitWithExpectedError } = require('../errors');
return docker return docker
.ping() .ping()

View File

@ -30,7 +30,7 @@ export function getGroupDefaults(group: {
}): { [name: string]: string | number | undefined } { }): { [name: string]: string | number | undefined } {
return _.chain(group) return _.chain(group)
.get('options') .get('options')
.map(question => [question.name, question.default]) .map((question) => [question.name, question.default])
.fromPairs() .fromPairs()
.value(); .value();
} }
@ -96,7 +96,7 @@ export async function sudo(
export function runCommand<T>(command: string): Bluebird<T> { export function runCommand<T>(command: string): Bluebird<T> {
const capitano = require('capitano'); const capitano = require('capitano');
return Bluebird.fromCallback(resolver => capitano.run(command, resolver)); return Bluebird.fromCallback((resolver) => capitano.run(command, resolver));
} }
export async function getManifest( export async function getManifest(
@ -122,7 +122,7 @@ export async function osProgressHandler(step: InitializeEmitter) {
step.on('stdout', process.stdout.write.bind(process.stdout)); step.on('stdout', process.stdout.write.bind(process.stdout));
step.on('stderr', process.stderr.write.bind(process.stderr)); step.on('stderr', process.stderr.write.bind(process.stderr));
step.on('state', function(state) { step.on('state', function (state) {
if (state.operation.command === 'burn') { if (state.operation.command === 'burn') {
return; return;
} }
@ -135,7 +135,7 @@ export async function osProgressHandler(step: InitializeEmitter) {
check: new visuals.Progress('Validating Device OS'), check: new visuals.Progress('Validating Device OS'),
}; };
step.on('burn', state => progressBars[state.type].update(state)); step.on('burn', (state) => progressBars[state.type].update(state));
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
step.on('error', reject); step.on('error', reject);
@ -149,7 +149,7 @@ export function getAppWithArch(
return Bluebird.join( return Bluebird.join(
getApplication(applicationName), getApplication(applicationName),
getBalenaSdk().models.config.getDeviceTypes(), getBalenaSdk().models.config.getDeviceTypes(),
function(app, deviceTypes) { function (app, deviceTypes) {
const config = _.find<BalenaSdk.DeviceType>(deviceTypes, { const config = _.find<BalenaSdk.DeviceType>(deviceTypes, {
slug: app.device_type, slug: app.device_type,
}); });
@ -214,8 +214,9 @@ export function retry<T>(
promise = promise.catch((err: Error) => { promise = promise.catch((err: Error) => {
const delay = backoffScaler ** count * delayMs; const delay = backoffScaler ** count * delayMs;
console.log( console.log(
`Retrying "${label}" after ${(delay / 1000).toFixed(2)}s (${count + `Retrying "${label}" after ${(delay / 1000).toFixed(2)}s (${
1} of ${times}) due to: ${err}`, count + 1
} of ${times}) due to: ${err}`,
); );
return Bluebird.delay(delay).then(() => return Bluebird.delay(delay).then(() =>
retry(func, times, label, delayMs, backoffScaler, count + 1), retry(func, times, label, delayMs, backoffScaler, count + 1),
@ -255,7 +256,7 @@ export function getManualSortCompareFunction<T, U = T>(
manuallySortedArray: U[], manuallySortedArray: U[],
equalityFunc: (a: T, x: U, index: number, array: U[]) => boolean, equalityFunc: (a: T, x: U, index: number, array: U[]) => boolean,
): (a: T, b: T) => number { ): (a: T, b: T) => number {
return function(a: T, b: T): number { return function (a: T, b: T): number {
const indexA = manuallySortedArray.findIndex((x, index, array) => const indexA = manuallySortedArray.findIndex((x, index, array) =>
equalityFunc(a, x, index, array), equalityFunc(a, x, index, array),
); );
@ -302,10 +303,10 @@ export function shellEscape(args: string[], detectShell = false): string[] {
? isWindowsComExeShell() ? isWindowsComExeShell()
: process.platform === 'win32'; : process.platform === 'win32';
if (isCmdExe) { if (isCmdExe) {
return args.map(v => windowsCmdExeEscapeArg(v)); return args.map((v) => windowsCmdExeEscapeArg(v));
} else { } else {
const shellEscapeFunc: typeof ShellEscape = require('shell-escape'); const shellEscapeFunc: typeof ShellEscape = require('shell-escape');
return args.map(v => shellEscapeFunc([v])); return args.map((v) => shellEscapeFunc([v]));
} }
} }

View File

@ -93,7 +93,7 @@ export class FileIgnorer {
): Promise<void> { ): Promise<void> {
const contents = await fs.readFile(fullPath, 'utf8'); const contents = await fs.readFile(fullPath, 'utf8');
contents.split('\n').forEach(line => { contents.split('\n').forEach((line) => {
// ignore empty lines and comments // ignore empty lines and comments
if (/\s*#/.test(line) || _.isEmpty(line)) { if (/\s*#/.test(line) || _.isEmpty(line)) {
return; return;
@ -205,7 +205,7 @@ async function listFiles(
const files: FileStats[] = []; const files: FileStats[] = [];
const dirEntries = await fs.readdir(dir); const dirEntries = await fs.readdir(dir);
await Promise.all( await Promise.all(
dirEntries.map(async entry => { dirEntries.map(async (entry) => {
const filePath = path.join(dir, entry); const filePath = path.join(dir, entry);
const stats = await fs.stat(filePath); const stats = await fs.stat(filePath);
if (stats.isDirectory()) { if (stats.isDirectory()) {

View File

@ -80,7 +80,7 @@ class Logger {
livepush: logger.createLogStream('live'), livepush: logger.createLogStream('live'),
}; };
_.forEach(this.streams, function(stream, key) { _.forEach(this.streams, function (stream, key) {
if (key !== 'debug') { if (key !== 'debug') {
stream.pipe(process.stdout); stream.pipe(process.stdout);
} else if (process.env.DEBUG) { } else if (process.env.DEBUG) {
@ -139,14 +139,14 @@ class Logger {
* Log a message for output later, ignore duplicates. * Log a message for output later, ignore duplicates.
*/ */
public deferredLog(msg: string, level: Level) { public deferredLog(msg: string, level: Level) {
if (!this.deferredLogMessages.find(entry => entry[0] === msg)) { if (!this.deferredLogMessages.find((entry) => entry[0] === msg)) {
this.deferredLogMessages.push([msg, level]); this.deferredLogMessages.push([msg, level]);
} }
} }
/** Output any messages that have been queued for deferred output */ /** Output any messages that have been queued for deferred output */
public outputDeferredMessages() { public outputDeferredMessages() {
this.deferredLogMessages.forEach(m => { this.deferredLogMessages.forEach((m) => {
this.streams[m[1]].write(m[0] + eol); this.streams[m[1]].write(m[0] + eol);
}); });
this.deferredLogMessages = []; this.deferredLogMessages = [];

View File

@ -37,8 +37,8 @@ export class CommandHelp {
return CommandHelp.compact([ return CommandHelp.compact([
// this.command.id, // this.command.id,
(this.command.args || []) (this.command.args || [])
.filter(a => !a.hidden) .filter((a) => !a.hidden)
.map(a => this.arg(a)) .map((a) => this.arg(a))
.join(' '), .join(' '),
]).join(' '); ]).join(' ');
} }
@ -54,6 +54,6 @@ export function capitanoizeOclifUsage(
): string { ): string {
return (oclifUsage || '') return (oclifUsage || '')
.toString() .toString()
.replace(/(?<=\s)[A-Z]+(?=(\s|$))/g, match => `<${match}>`) .replace(/(?<=\s)[A-Z]+(?=(\s|$))/g, (match) => `<${match}>`)
.toLowerCase(); .toLowerCase();
} }

View File

@ -123,9 +123,9 @@ export function askLoginType() {
export function selectDeviceType() { export function selectDeviceType() {
return getBalenaSdk() return getBalenaSdk()
.models.config.getDeviceTypes() .models.config.getDeviceTypes()
.then(deviceTypes => { .then((deviceTypes) => {
deviceTypes = _.sortBy(deviceTypes, 'name').filter( deviceTypes = _.sortBy(deviceTypes, 'name').filter(
dt => dt.state !== 'DISCONTINUED', (dt) => dt.state !== 'DISCONTINUED',
); );
return getForm().ask({ return getForm().ask({
message: 'Device Type', message: 'Device Type',
@ -144,7 +144,7 @@ export function confirm(
yesMessage?: string, yesMessage?: string,
exitIfDeclined = false, exitIfDeclined = false,
) { ) {
return Bluebird.try(function() { return Bluebird.try(function () {
if (yesOption) { if (yesOption) {
if (yesMessage) { if (yesMessage) {
console.log(yesMessage); console.log(yesMessage);
@ -157,7 +157,7 @@ export function confirm(
type: 'confirm', type: 'confirm',
default: false, default: false,
}); });
}).then(function(confirmed) { }).then(function (confirmed) {
if (!confirmed) { if (!confirmed) {
const err = new Error('Aborted'); const err = new Error('Aborted');
if (exitIfDeclined) { if (exitIfDeclined) {
@ -174,7 +174,7 @@ export function selectApplication(
const balena = getBalenaSdk(); const balena = getBalenaSdk();
return balena.models.application return balena.models.application
.hasAny() .hasAny()
.then(function(hasAnyApplications) { .then(function (hasAnyApplications) {
if (!hasAnyApplications) { if (!hasAnyApplications) {
throw new Error("You don't have any applications"); throw new Error("You don't have any applications");
} }
@ -182,11 +182,11 @@ export function selectApplication(
return balena.models.application.getAll(); return balena.models.application.getAll();
}) })
.filter(filter || _.constant(true)) .filter(filter || _.constant(true))
.then(applications => { .then((applications) => {
return getForm().ask({ return getForm().ask({
message: 'Select an application', message: 'Select an application',
type: 'list', type: 'list',
choices: _.map(applications, application => ({ choices: _.map(applications, (application) => ({
name: `${application.app_name} (${application.device_type})`, name: `${application.app_name} (${application.device_type})`,
value: application.app_name, value: application.app_name,
})), })),
@ -198,17 +198,17 @@ export function selectOrCreateApplication() {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
return balena.models.application return balena.models.application
.hasAny() .hasAny()
.then(hasAnyApplications => { .then((hasAnyApplications) => {
if (!hasAnyApplications) { if (!hasAnyApplications) {
// Just to make TS happy // Just to make TS happy
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }
return balena.models.application.getAll().then(applications => { return balena.models.application.getAll().then((applications) => {
const appOptions = _.map< const appOptions = _.map<
BalenaSdk.Application, BalenaSdk.Application,
{ name: string; value: string | null } { name: string; value: string | null }
>(applications, application => ({ >(applications, (application) => ({
name: `${application.app_name} (${application.device_type})`, name: `${application.app_name} (${application.device_type})`,
value: application.app_name, value: application.app_name,
})); }));
@ -225,7 +225,7 @@ export function selectOrCreateApplication() {
}); });
}); });
}) })
.then(application => { .then((application) => {
if (application) { if (application) {
return application; return application;
} }
@ -240,14 +240,14 @@ export function selectOrCreateApplication() {
export function awaitDevice(uuid: string) { export function awaitDevice(uuid: string) {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
return balena.models.device.getName(uuid).then(deviceName => { return balena.models.device.getName(uuid).then((deviceName) => {
const visuals = getVisuals(); 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`,
); );
const poll = (): Bluebird<void> => { const poll = (): Bluebird<void> => {
return balena.models.device.isOnline(uuid).then(function(isOnline) { return balena.models.device.isOnline(uuid).then(function (isOnline) {
if (isOnline) { if (isOnline) {
spinner.stop(); spinner.stop();
console.info(`The device **${deviceName}** is online!`); console.info(`The device **${deviceName}** is online!`);
@ -270,7 +270,7 @@ export function awaitDevice(uuid: string) {
export function awaitDeviceOsUpdate(uuid: string, targetOsVersion: string) { export function awaitDeviceOsUpdate(uuid: string, targetOsVersion: string) {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
return balena.models.device.getName(uuid).then(deviceName => { return balena.models.device.getName(uuid).then((deviceName) => {
const visuals = getVisuals(); const visuals = getVisuals();
const progressBar = new visuals.Progress( const progressBar = new visuals.Progress(
`Updating the OS of ${deviceName} to v${targetOsVersion}`, `Updating the OS of ${deviceName} to v${targetOsVersion}`,
@ -314,15 +314,13 @@ export function inferOrSelectDevice(preferredUuid: string) {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
return balena.models.device return balena.models.device
.getAll() .getAll()
.filter<BalenaSdk.Device>(device => device.is_online) .filter<BalenaSdk.Device>((device) => device.is_online)
.then(onlineDevices => { .then((onlineDevices) => {
if (_.isEmpty(onlineDevices)) { if (_.isEmpty(onlineDevices)) {
throw new Error("You don't have any devices online"); throw new Error("You don't have any devices online");
} }
const defaultUuid = _(onlineDevices) const defaultUuid = _(onlineDevices).map('uuid').includes(preferredUuid)
.map('uuid')
.includes(preferredUuid)
? preferredUuid ? preferredUuid
: onlineDevices[0].uuid; : onlineDevices[0].uuid;
@ -330,7 +328,7 @@ export function inferOrSelectDevice(preferredUuid: string) {
message: 'Select a device', message: 'Select a device',
type: 'list', type: 'list',
default: defaultUuid, default: defaultUuid,
choices: _.map(onlineDevices, device => ({ choices: _.map(onlineDevices, (device) => ({
name: `${device.device_name || 'Untitled'} (${device.uuid.slice( name: `${device.device_name || 'Untitled'} (${device.uuid.slice(
0, 0,
7, 7,
@ -385,7 +383,7 @@ export async function getOnlineTargetUuid(
message: 'Select a device', message: 'Select a device',
type: 'list', type: 'list',
default: devices[0].uuid, default: devices[0].uuid,
choices: _.map(devices, device => ({ choices: _.map(devices, (device) => ({
name: `${device.device_name || 'Untitled'} (${device.uuid.slice( name: `${device.device_name || 'Untitled'} (${device.uuid.slice(
0, 0,
7, 7,
@ -419,7 +417,7 @@ export function selectFromList<T>(
return getForm().ask<T>({ return getForm().ask<T>({
message, message,
type: 'list', type: 'list',
choices: _.map(choices, s => ({ choices: _.map(choices, (s) => ({
name: s.name, name: s.name,
value: s, value: s,
})), })),

View File

@ -93,7 +93,7 @@ async function execCommand(
const spinner = new visuals.Spinner(`[${deviceIp}] Connecting...`); const spinner = new visuals.Spinner(`[${deviceIp}] Connecting...`);
const innerSpinner = spinner.spinner; const innerSpinner = spinner.spinner;
const stream = through(function(data, _enc, cb) { const stream = through(function (data, _enc, cb) {
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`); innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
cb(null, data); cb(null, data);
}); });
@ -160,7 +160,7 @@ async function getOrSelectLocalDevice(deviceIp?: string): Promise<string> {
const through = await import('through2'); const through = await import('through2');
let ip: string | null = null; let ip: string | null = null;
const stream = through(function(data, _enc, cb) { const stream = through(function (data, _enc, cb) {
const match = /^==> Selected device: (.*)$/m.exec(data.toString()); const match = /^==> Selected device: (.*)$/m.exec(data.toString());
if (match) { if (match) {
ip = match[1]; ip = match[1];
@ -195,7 +195,7 @@ async function selectAppFromList(applications: BalenaSdk.Application[]) {
// name (user/appname) and allows them to select. // name (user/appname) and allows them to select.
return selectFromList( return selectFromList(
'Select application', 'Select application',
_.map(applications, app => { _.map(applications, (app) => {
return { name: app.slug, ...app }; return { name: app.slug, ...app };
}), }),
); );
@ -216,7 +216,7 @@ async function getOrSelectApplication(
} }
const compatibleDeviceTypes = _(allDeviceTypes) const compatibleDeviceTypes = _(allDeviceTypes)
.filter( .filter(
dt => (dt) =>
sdk.models.os.isArchitectureCompatibleWith( sdk.models.os.isArchitectureCompatibleWith(
deviceTypeManifest.arch, deviceTypeManifest.arch,
dt.arch, dt.arch,
@ -224,7 +224,7 @@ async function getOrSelectApplication(
!!dt.isDependent === !!deviceTypeManifest.isDependent && !!dt.isDependent === !!deviceTypeManifest.isDependent &&
dt.state !== 'DISCONTINUED', dt.state !== 'DISCONTINUED',
) )
.map(type => type.slug) .map((type) => type.slug)
.value(); .value();
if (!appName) { if (!appName) {
@ -270,7 +270,7 @@ async function getOrSelectApplication(
// We've found at least one app with the given name. // We've found at least one app with the given name.
// Filter out apps for non-matching device types and see what we're left with. // Filter out apps for non-matching device types and see what we're left with.
const validApplications = applications.filter(app => const validApplications = applications.filter((app) =>
_.includes(compatibleDeviceTypes, app.device_type), _.includes(compatibleDeviceTypes, app.device_type),
); );
@ -382,7 +382,8 @@ async function generateApplicationConfig(
const manifest = await sdk.models.device.getManifestBySlug(app.device_type); const manifest = await sdk.models.device.getManifestBySlug(app.device_type);
const opts = const opts =
manifest.options && manifest.options.filter(opt => opt.name !== 'network'); manifest.options &&
manifest.options.filter((opt) => opt.name !== 'network');
const values = { const values = {
...(opts ? await form.run(opts) : {}), ...(opts ? await form.run(opts) : {}),
...options, ...options,

View File

@ -39,13 +39,13 @@ export function copyQemu(context: string, arch: string) {
const binPath = path.join(binDir, QEMU_BIN_NAME); const binPath = path.join(binDir, QEMU_BIN_NAME);
return Bluebird.resolve(fs.mkdir(binDir)) return Bluebird.resolve(fs.mkdir(binDir))
.catch({ code: 'EEXIST' }, function() { .catch({ code: 'EEXIST' }, function () {
// noop // noop
}) })
.then(() => getQemuPath(arch)) .then(() => getQemuPath(arch))
.then( .then(
qemu => (qemu) =>
new Bluebird(function(resolve, reject) { new Bluebird(function (resolve, reject) {
const read = fs.createReadStream(qemu); const read = fs.createReadStream(qemu);
const write = fs.createWriteStream(binPath); const write = fs.createWriteStream(binPath);
@ -60,14 +60,14 @@ export function copyQemu(context: string, arch: string) {
.then(() => path.relative(context, binPath)); .then(() => path.relative(context, binPath));
} }
export const getQemuPath = function(arch: string) { export const getQemuPath = function (arch: string) {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
const path = require('path') as typeof import('path'); const path = require('path') as typeof import('path');
const fs = require('mz/fs') as typeof import('mz/fs'); const fs = require('mz/fs') as typeof import('mz/fs');
return balena.settings.get('binDirectory').then(binDir => return balena.settings.get('binDirectory').then((binDir) =>
Bluebird.resolve(fs.mkdir(binDir)) Bluebird.resolve(fs.mkdir(binDir))
.catch({ code: 'EEXIST' }, function() { .catch({ code: 'EEXIST' }, function () {
// noop // noop
}) })
.then(() => .then(() =>
@ -83,8 +83,8 @@ export function installQemu(arch: string) {
const tar = require('tar-stream') as typeof import('tar-stream'); const tar = require('tar-stream') as typeof import('tar-stream');
return getQemuPath(arch).then( return getQemuPath(arch).then(
qemuPath => (qemuPath) =>
new Bluebird(function(resolve, reject) { new Bluebird(function (resolve, reject) {
const installStream = fs.createWriteStream(qemuPath); const installStream = fs.createWriteStream(qemuPath);
const qemuArch = balenaArchToQemuArch(arch); const qemuArch = balenaArchToQemuArch(arch);
@ -96,7 +96,7 @@ export function installQemu(arch: string) {
const qemuUrl = `https://github.com/balena-io/qemu/releases/download/${urlVersion}/${urlFile}`; const qemuUrl = `https://github.com/balena-io/qemu/releases/download/${urlVersion}/${urlFile}`;
const extract = tar.extract(); const extract = tar.extract();
extract.on('entry', function(header, stream, next) { extract.on('entry', function (header, stream, next) {
stream.on('end', next); stream.on('end', next);
if (header.name.includes(`qemu-${qemuArch}-static`)) { if (header.name.includes(`qemu-${qemuArch}-static`)) {
stream.pipe(installStream); stream.pipe(installStream);
@ -111,7 +111,7 @@ export function installQemu(arch: string) {
.on('error', reject) .on('error', reject)
.pipe(extract) .pipe(extract)
.on('error', reject) .on('error', reject)
.on('finish', function() { .on('finish', function () {
fs.chmodSync(qemuPath, '755'); fs.chmodSync(qemuPath, '755');
resolve(); resolve();
}); });
@ -119,7 +119,7 @@ export function installQemu(arch: string) {
); );
} }
const balenaArchToQemuArch = function(arch: string) { const balenaArchToQemuArch = function (arch: string) {
switch (arch) { switch (arch) {
case 'armv7hf': case 'armv7hf':
case 'rpi': case 'rpi':

View File

@ -101,7 +101,12 @@ async function getBuilderEndpoint(
}); });
// Note that using https (rather than http) is a requirement when using the // Note that using https (rather than http) is a requirement when using the
// --registry-secrets feature, as the secrets are not otherwise encrypted. // --registry-secrets feature, as the secrets are not otherwise encrypted.
return `https://builder.${baseUrl}/v3/build?${args}`; let builderUrl =
process.env.BALENARC_BUILDER_URL || `https://builder.${baseUrl}`;
if (builderUrl.endsWith('/')) {
builderUrl = builderUrl.slice(0, -1);
}
return `${builderUrl}/v3/build?${args}`;
} }
export async function startRemoteBuild(build: RemoteBuild): Promise<void> { export async function startRemoteBuild(build: RemoteBuild): Promise<void> {

View File

@ -95,7 +95,7 @@ export async function execBuffered(
await exec( await exec(
deviceIp, deviceIp,
cmd, cmd,
through(function(data, _enc, cb) { through(function (data, _enc, cb) {
buffer.push(data.toString(enc)); buffer.push(data.toString(enc));
cb(); cb();
}), }),

View File

@ -23,14 +23,11 @@ export function buffer(
): Promise<NodeJS.ReadableStream> { ): Promise<NodeJS.ReadableStream> {
const fileWriteStream = fs.createWriteStream(bufferFile); const fileWriteStream = fs.createWriteStream(bufferFile);
return new Promise(function(resolve, reject) { return new Promise(function (resolve, reject) {
stream stream.on('error', reject).on('end', resolve).pipe(fileWriteStream);
.on('error', reject)
.on('end', resolve)
.pipe(fileWriteStream);
}).then( }).then(
() => () =>
new Promise(function(resolve, reject) { new Promise(function (resolve, reject) {
const fstream = fs.createReadStream(bufferFile); const fstream = fs.createReadStream(bufferFile);
fstream.on('open', () => resolve(fstream)).on('error', reject); fstream.on('open', () => resolve(fstream)).on('error', reject);

View File

@ -86,7 +86,7 @@ async function spawnAndPipe(
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const ps: ChildProcess = spawn(spawnCmd, spawnArgs, spawnOpts); const ps: ChildProcess = spawn(spawnCmd, spawnArgs, spawnOpts);
ps.on('error', reject); ps.on('error', reject);
ps.on('exit', codeOrSignal => { ps.on('exit', (codeOrSignal) => {
if (codeOrSignal !== 0) { if (codeOrSignal !== 0) {
const errMsgCmd = `[${[spawnCmd, ...spawnArgs].join()}]`; const errMsgCmd = `[${[spawnCmd, ...spawnArgs].join()}]`;
reject( reject(

View File

@ -54,14 +54,14 @@ export const tunnelConnectionToDevice = (
return (client: Socket): Bluebird<void> => return (client: Socket): Bluebird<void> =>
openPortThroughProxy(vpnUrl, 3128, auth, uuid, port) openPortThroughProxy(vpnUrl, 3128, auth, uuid, port)
.then(remote => { .then((remote) => {
client.pipe(remote); client.pipe(remote);
remote.pipe(client); remote.pipe(client);
remote.on('error', err => { remote.on('error', (err) => {
console.error('Remote: ' + err); console.error('Remote: ' + err);
client.end(); client.end();
}); });
client.on('error', err => { client.on('error', (err) => {
console.error('Client: ' + err); console.error('Client: ' + err);
remote.end(); remote.end();
}); });

View File

@ -26,7 +26,9 @@ let v12: boolean;
export function isV12(): boolean { export function isV12(): boolean {
if (v12 === undefined) { if (v12 === undefined) {
v12 = isVersionGTE('12.0.0'); // This is the `Change-type: major` PR that will produce v12.0.0.
// Enable the v12 feature switches and run all v12 tests.
v12 = true; // v12 = isVersionGTE('12.0.0');
} }
return v12; return v12;
} }

1307
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,9 @@
"build/auth/pages/*.ejs", "build/auth/pages/*.ejs",
"build/hooks", "build/hooks",
"node_modules/resin-discoverable-services/services/**/*", "node_modules/resin-discoverable-services/services/**/*",
"node_modules/balena-sdk/node_modules/balena-pine/**/*",
"node_modules/balena-pine/**/*",
"node_modules/pinejs-client-core/**/*",
"node_modules/opn/xdg-open", "node_modules/opn/xdg-open",
"node_modules/open/xdg-open", "node_modules/open/xdg-open",
"node_modules/windosu/*.bat", "node_modules/windosu/*.bat",
@ -53,7 +56,10 @@
"package": "npm run build:fast && npm run build:standalone && npm run build:installer", "package": "npm run build:fast && npm run build:standalone && npm run build:installer",
"release": "ts-node --transpile-only automation/run.ts release", "release": "ts-node --transpile-only automation/run.ts release",
"pretest": "npm run build", "pretest": "npm run build",
"test": "mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/*.spec.ts\"", "test": "npm run test:source && npm run test:standalone",
"test:source": "mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/*.spec.ts\"",
"test:standalone": "npm run build:standalone && cross-env BALENA_CLI_TEST_TYPE=standalone npm run test:source",
"test:standalone:fast": "cross-env BALENA_CLI_TEST_TYPE=standalone npm run test:source",
"test:fast": "npm run build:fast && mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/*.spec.ts\"", "test:fast": "npm run build:fast && mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/*.spec.ts\"",
"test:only": "npm run build:fast && mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/${npm_config_test}.spec.ts\"", "test:only": "npm run build:fast && mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/${npm_config_test}.spec.ts\"",
"catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted", "catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted",
@ -75,7 +81,7 @@
"author": "Juan Cruz Viotti <juan@balena.io>", "author": "Juan Cruz Viotti <juan@balena.io>",
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=8.0" "node": ">=10.0.0"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@ -94,7 +100,7 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@balena/lint": "^4.1.1", "@balena/lint": "^5.1.0",
"@oclif/config": "^1.14.0", "@oclif/config": "^1.14.0",
"@oclif/dev-cli": "1.22.0", "@oclif/dev-cli": "1.22.0",
"@oclif/parser": "^3.8.4", "@oclif/parser": "^3.8.4",
@ -114,6 +120,7 @@
"@types/fs-extra": "^8.1.0", "@types/fs-extra": "^8.1.0",
"@types/global-agent": "^2.1.0", "@types/global-agent": "^2.1.0",
"@types/global-tunnel-ng": "^2.1.0", "@types/global-tunnel-ng": "^2.1.0",
"@types/http-proxy": "^1.17.4",
"@types/intercept-stdout": "^0.1.0", "@types/intercept-stdout": "^0.1.0",
"@types/is-root": "^2.1.2", "@types/is-root": "^2.1.2",
"@types/js-yaml": "^3.12.3", "@types/js-yaml": "^3.12.3",
@ -145,18 +152,20 @@
"catch-uncommitted": "^1.5.0", "catch-uncommitted": "^1.5.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"cross-env": "^7.0.2",
"ent": "^2.2.0", "ent": "^2.2.0",
"filehound": "^1.17.4", "filehound": "^1.17.4",
"fs-extra": "^8.0.1", "fs-extra": "^8.0.1",
"gulp": "^4.0.1", "gulp": "^4.0.1",
"gulp-inline-source": "^2.1.0", "gulp-inline-source": "^2.1.0",
"http-proxy": "^1.18.1",
"husky": "^4.2.5", "husky": "^4.2.5",
"intercept-stdout": "^0.1.2", "intercept-stdout": "^0.1.2",
"mocha": "^6.2.3", "mocha": "^6.2.3",
"mock-require": "^3.0.3", "mock-require": "^3.0.3",
"nock": "^12.0.3", "nock": "^12.0.3",
"parse-link-header": "~1.0.1", "parse-link-header": "~1.0.1",
"pkg": "^4.4.2", "pkg": "^4.4.8",
"publish-release": "^1.6.1", "publish-release": "^1.6.1",
"rewire": "^4.0.1", "rewire": "^4.0.1",
"simple-git": "^1.131.0", "simple-git": "^1.131.0",
@ -181,7 +190,7 @@
"balena-image-manager": "^6.1.2", "balena-image-manager": "^6.1.2",
"balena-preload": "^8.4.0", "balena-preload": "^8.4.0",
"balena-release": "^2.1.0", "balena-release": "^2.1.0",
"balena-sdk": "^12.33.0", "balena-sdk": "^13.6.0",
"balena-semver": "^2.2.0", "balena-semver": "^2.2.0",
"balena-settings-client": "^4.0.5", "balena-settings-client": "^4.0.5",
"balena-sync": "^10.2.0", "balena-sync": "^10.2.0",
@ -206,7 +215,7 @@
"express": "^4.13.3", "express": "^4.13.3",
"fast-boot2": "^1.0.9", "fast-boot2": "^1.0.9",
"get-stdin": "^7.0.0", "get-stdin": "^7.0.0",
"global-agent": "^2.1.8", "global-agent": "^2.1.12",
"global-tunnel-ng": "^2.1.1", "global-tunnel-ng": "^2.1.1",
"humanize": "0.0.9", "humanize": "0.0.9",
"ignore": "^5.1.4", "ignore": "^5.1.4",

View File

@ -1,5 +1,5 @@
diff --git a/node_modules/pkg/lib-es5/packer.js b/node_modules/pkg/lib-es5/packer.js diff --git a/node_modules/pkg/lib-es5/packer.js b/node_modules/pkg/lib-es5/packer.js
index 607c847..4e3fb55 100644 index 7295bb6..76805a3 100644
--- a/node_modules/pkg/lib-es5/packer.js --- a/node_modules/pkg/lib-es5/packer.js
+++ b/node_modules/pkg/lib-es5/packer.js +++ b/node_modules/pkg/lib-es5/packer.js
@@ -128,6 +128,7 @@ function _default({ @@ -128,6 +128,7 @@ function _default({
@ -11,10 +11,10 @@ index 607c847..4e3fb55 100644
stripes.push({ stripes.push({
snap, snap,
diff --git a/node_modules/pkg/prelude/bootstrap.js b/node_modules/pkg/prelude/bootstrap.js diff --git a/node_modules/pkg/prelude/bootstrap.js b/node_modules/pkg/prelude/bootstrap.js
index 216579e..5cff8a8 100644 index 0d19f1d..db69015 100644
--- a/node_modules/pkg/prelude/bootstrap.js --- a/node_modules/pkg/prelude/bootstrap.js
+++ b/node_modules/pkg/prelude/bootstrap.js +++ b/node_modules/pkg/prelude/bootstrap.js
@@ -866,8 +866,10 @@ function payloadFileSync (pointer) { @@ -925,8 +925,10 @@ function payloadFileSync (pointer) {
var isFileValue = s.isFileValue; var isFileValue = s.isFileValue;
var isDirectoryValue = s.isDirectoryValue; var isDirectoryValue = s.isDirectoryValue;
@ -25,7 +25,7 @@ index 216579e..5cff8a8 100644
s.isFile = function () { s.isFile = function () {
return isFileValue; return isFileValue;
@@ -875,6 +877,9 @@ function payloadFileSync (pointer) { @@ -934,6 +936,9 @@ function payloadFileSync (pointer) {
s.isDirectory = function () { s.isDirectory = function () {
return isDirectoryValue; return isDirectoryValue;
}; };

View File

@ -22,7 +22,7 @@ import {
makeUrlFromTunnelNgConfig, makeUrlFromTunnelNgConfig,
} from '../build/app-common'; } from '../build/app-common';
describe('makeUrlFromTunnelNgConfig() function', function() { describe('makeUrlFromTunnelNgConfig() function', function () {
it('should return a URL given a GlobalTunnelNgConfig object', () => { it('should return a URL given a GlobalTunnelNgConfig object', () => {
const tunnelNgConfig: GlobalTunnelNgConfig = { const tunnelNgConfig: GlobalTunnelNgConfig = {
host: 'proxy.company.com', host: 'proxy.company.com',

View File

@ -51,8 +51,8 @@ async function getPage(name: string): Promise<string> {
return compiledTpl(); return compiledTpl();
} }
describe('Server:', function() { describe('Server:', function () {
it('should get 404 if posting to an unknown path', function(done) { it('should get 404 if posting to an unknown path', function (done) {
const promise = server.awaitForToken(options); const promise = server.awaitForToken(options);
expect(promise).to.be.rejectedWith('Unknown path or verb'); expect(promise).to.be.rejectedWith('Unknown path or verb');
@ -63,7 +63,7 @@ describe('Server:', function() {
token: tokens.johndoe.token, token: tokens.johndoe.token,
}, },
}, },
function(error, response, body) { function (error, response, body) {
expect(error).to.not.exist; expect(error).to.not.exist;
expect(response.statusCode).to.equal(404); expect(response.statusCode).to.equal(404);
expect(body).to.equal('Not found'); expect(body).to.equal('Not found');
@ -72,7 +72,7 @@ describe('Server:', function() {
); );
}); });
it('should get 404 if not using the correct verb', function(done) { it('should get 404 if not using the correct verb', function (done) {
const promise = server.awaitForToken(options); const promise = server.awaitForToken(options);
expect(promise).to.be.rejectedWith('Unknown path or verb'); expect(promise).to.be.rejectedWith('Unknown path or verb');
@ -83,7 +83,7 @@ describe('Server:', function() {
token: tokens.johndoe.token, token: tokens.johndoe.token,
}, },
}, },
function(error, response, body) { function (error, response, body) {
expect(error).to.not.exist; expect(error).to.not.exist;
expect(response.statusCode).to.equal(404); expect(response.statusCode).to.equal(404);
expect(body).to.equal('Not found'); expect(body).to.equal('Not found');
@ -92,17 +92,17 @@ describe('Server:', function() {
); );
}); });
describe('given the token authenticates with the server', function() { describe('given the token authenticates with the server', function () {
beforeEach(function() { beforeEach(function () {
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid'); this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
return this.loginIfTokenValidStub.returns(Bluebird.resolve(true)); return this.loginIfTokenValidStub.returns(Bluebird.resolve(true));
}); });
afterEach(function() { afterEach(function () {
return this.loginIfTokenValidStub.restore(); return this.loginIfTokenValidStub.restore();
}); });
return it('should eventually be the token', function(done) { return it('should eventually be the token', function (done) {
const promise = server.awaitForToken(options); const promise = server.awaitForToken(options);
expect(promise).to.eventually.equal(tokens.johndoe.token); expect(promise).to.eventually.equal(tokens.johndoe.token);
@ -113,10 +113,10 @@ describe('Server:', function() {
token: tokens.johndoe.token, token: tokens.johndoe.token,
}, },
}, },
function(error, response, body) { function (error, response, body) {
expect(error).to.not.exist; expect(error).to.not.exist;
expect(response.statusCode).to.equal(200); expect(response.statusCode).to.equal(200);
return getPage('success').then(function(expectedBody) { return getPage('success').then(function (expectedBody) {
expect(body).to.equal(expectedBody); expect(body).to.equal(expectedBody);
return done(); return done();
}); });
@ -125,17 +125,17 @@ describe('Server:', function() {
}); });
}); });
return describe('given the token does not authenticate with the server', function() { return describe('given the token does not authenticate with the server', function () {
beforeEach(function() { beforeEach(function () {
this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid'); this.loginIfTokenValidStub = sinon.stub(utils, 'loginIfTokenValid');
return this.loginIfTokenValidStub.returns(Bluebird.resolve(false)); return this.loginIfTokenValidStub.returns(Bluebird.resolve(false));
}); });
afterEach(function() { afterEach(function () {
return this.loginIfTokenValidStub.restore(); return this.loginIfTokenValidStub.restore();
}); });
it('should be rejected', function(done) { it('should be rejected', function (done) {
const promise = server.awaitForToken(options); const promise = server.awaitForToken(options);
expect(promise).to.be.rejectedWith('Invalid token'); expect(promise).to.be.rejectedWith('Invalid token');
@ -146,10 +146,10 @@ describe('Server:', function() {
token: tokens.johndoe.token, token: tokens.johndoe.token,
}, },
}, },
function(error, response, body) { function (error, response, body) {
expect(error).to.not.exist; expect(error).to.not.exist;
expect(response.statusCode).to.equal(401); expect(response.statusCode).to.equal(401);
return getPage('error').then(function(expectedBody) { return getPage('error').then(function (expectedBody) {
expect(body).to.equal(expectedBody); expect(body).to.equal(expectedBody);
return done(); return done();
}); });
@ -157,7 +157,7 @@ describe('Server:', function() {
); );
}); });
it('should be rejected if no token', function(done) { it('should be rejected if no token', function (done) {
const promise = server.awaitForToken(options); const promise = server.awaitForToken(options);
expect(promise).to.be.rejectedWith('No token'); expect(promise).to.be.rejectedWith('No token');
@ -168,10 +168,10 @@ describe('Server:', function() {
token: '', token: '',
}, },
}, },
function(error, response, body) { function (error, response, body) {
expect(error).to.not.exist; expect(error).to.not.exist;
expect(response.statusCode).to.equal(401); expect(response.statusCode).to.equal(401);
return getPage('error').then(function(expectedBody) { return getPage('error').then(function (expectedBody) {
expect(body).to.equal(expectedBody); expect(body).to.equal(expectedBody);
return done(); return done();
}); });
@ -179,7 +179,7 @@ describe('Server:', function() {
); );
}); });
return it('should be rejected if token is malformed', function(done) { return it('should be rejected if token is malformed', function (done) {
const promise = server.awaitForToken(options); const promise = server.awaitForToken(options);
expect(promise).to.be.rejectedWith('Invalid token'); expect(promise).to.be.rejectedWith('Invalid token');
@ -190,10 +190,10 @@ describe('Server:', function() {
token: 'asdf', token: 'asdf',
}, },
}, },
function(error, response, body) { function (error, response, body) {
expect(error).to.not.exist; expect(error).to.not.exist;
expect(response.statusCode).to.equal(401); expect(response.statusCode).to.equal(401);
return getPage('error').then(function(expectedBody) { return getPage('error').then(function (expectedBody) {
expect(body).to.equal(expectedBody); expect(body).to.equal(expectedBody);
return done(); return done();
}); });

View File

@ -9,8 +9,8 @@ import tokens from './tokens';
const utils = rewire('../../build/auth/utils'); const utils = rewire('../../build/auth/utils');
const balena = getBalenaSdk(); const balena = getBalenaSdk();
describe('Utils:', function() { describe('Utils:', function () {
describe('.getDashboardLoginURL()', function() { describe('.getDashboardLoginURL()', function () {
it('should eventually be a valid url', () => it('should eventually be a valid url', () =>
utils utils
.getDashboardLoginURL('https://127.0.0.1:3000/callback') .getDashboardLoginURL('https://127.0.0.1:3000/callback')
@ -22,7 +22,7 @@ describe('Utils:', function() {
Promise.props({ Promise.props({
dashboardUrl: balena.settings.get('dashboardUrl'), dashboardUrl: balena.settings.get('dashboardUrl'),
loginUrl: utils.getDashboardLoginURL('https://127.0.0.1:3000/callback'), loginUrl: utils.getDashboardLoginURL('https://127.0.0.1:3000/callback'),
}).then(function({ dashboardUrl, loginUrl }) { }).then(function ({ dashboardUrl, loginUrl }) {
const { protocol } = url.parse(loginUrl); const { protocol } = url.parse(loginUrl);
return expect(protocol).to.equal(url.parse(dashboardUrl).protocol); return expect(protocol).to.equal(url.parse(dashboardUrl).protocol);
})); }));
@ -31,7 +31,7 @@ describe('Utils:', function() {
Promise.props({ Promise.props({
dashboardUrl: balena.settings.get('dashboardUrl'), dashboardUrl: balena.settings.get('dashboardUrl'),
loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000'), loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000'),
}).then(function({ dashboardUrl, loginUrl }) { }).then(function ({ dashboardUrl, loginUrl }) {
const expectedUrl = `${dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000`; const expectedUrl = `${dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000`;
return expect(loginUrl).to.equal(expectedUrl); return expect(loginUrl).to.equal(expectedUrl);
})); }));
@ -40,55 +40,55 @@ describe('Utils:', function() {
Promise.props({ Promise.props({
dashboardUrl: balena.settings.get('dashboardUrl'), dashboardUrl: balena.settings.get('dashboardUrl'),
loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000/callback'), loginUrl: utils.getDashboardLoginURL('http://127.0.0.1:3000/callback'),
}).then(function({ dashboardUrl, loginUrl }) { }).then(function ({ dashboardUrl, loginUrl }) {
const expectedUrl = `${dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000%252Fcallback`; const expectedUrl = `${dashboardUrl}/login/cli/http%253A%252F%252F127.0.0.1%253A3000%252Fcallback`;
return expect(loginUrl).to.equal(expectedUrl); return expect(loginUrl).to.equal(expectedUrl);
})); }));
}); });
return describe('.loginIfTokenValid()', function() { return describe('.loginIfTokenValid()', function () {
it('should eventually be false if token is undefined', function() { it('should eventually be false if token is undefined', function () {
const promise = utils.loginIfTokenValid(undefined); const promise = utils.loginIfTokenValid(undefined);
return expect(promise).to.eventually.be.false; return expect(promise).to.eventually.be.false;
}); });
it('should eventually be false if token is null', function() { it('should eventually be false if token is null', function () {
const promise = utils.loginIfTokenValid(null); const promise = utils.loginIfTokenValid(null);
return expect(promise).to.eventually.be.false; return expect(promise).to.eventually.be.false;
}); });
it('should eventually be false if token is an empty string', function() { it('should eventually be false if token is an empty string', function () {
const promise = utils.loginIfTokenValid(''); const promise = utils.loginIfTokenValid('');
return expect(promise).to.eventually.be.false; return expect(promise).to.eventually.be.false;
}); });
it('should eventually be false if token is a string containing only spaces', function() { it('should eventually be false if token is a string containing only spaces', function () {
const promise = utils.loginIfTokenValid(' '); const promise = utils.loginIfTokenValid(' ');
return expect(promise).to.eventually.be.false; return expect(promise).to.eventually.be.false;
}); });
describe('given the token does not authenticate with the server', function() { describe('given the token does not authenticate with the server', function () {
beforeEach(function() { beforeEach(function () {
this.balenaAuthIsLoggedInStub = sinon.stub(balena.auth, 'isLoggedIn'); this.balenaAuthIsLoggedInStub = sinon.stub(balena.auth, 'isLoggedIn');
return this.balenaAuthIsLoggedInStub.returns(Promise.resolve(false)); return this.balenaAuthIsLoggedInStub.returns(Promise.resolve(false));
}); });
afterEach(function() { afterEach(function () {
return this.balenaAuthIsLoggedInStub.restore(); return this.balenaAuthIsLoggedInStub.restore();
}); });
it('should eventually be false', function() { it('should eventually be false', function () {
const promise = utils.loginIfTokenValid(tokens.johndoe.token); const promise = utils.loginIfTokenValid(tokens.johndoe.token);
return expect(promise).to.eventually.be.false; return expect(promise).to.eventually.be.false;
}); });
describe('given there was a token already', function() { describe('given there was a token already', function () {
beforeEach(() => balena.auth.loginWithToken(tokens.janedoe.token)); beforeEach(() => balena.auth.loginWithToken(tokens.janedoe.token));
return it('should preserve the old token', () => return it('should preserve the old token', () =>
balena.auth balena.auth
.getToken() .getToken()
.then(function(originalToken: string) { .then(function (originalToken: string) {
expect(originalToken).to.equal(tokens.janedoe.token); expect(originalToken).to.equal(tokens.janedoe.token);
return utils.loginIfTokenValid(tokens.johndoe.token); return utils.loginIfTokenValid(tokens.johndoe.token);
}) })
@ -98,7 +98,7 @@ describe('Utils:', function() {
)); ));
}); });
return describe('given there was no token', function() { return describe('given there was no token', function () {
beforeEach(() => balena.auth.logout()); beforeEach(() => balena.auth.logout());
return it('should stay without a token', () => return it('should stay without a token', () =>
@ -109,17 +109,17 @@ describe('Utils:', function() {
}); });
}); });
return describe('given the token does authenticate with the server', function() { return describe('given the token does authenticate with the server', function () {
beforeEach(function() { beforeEach(function () {
this.balenaAuthIsLoggedInStub = sinon.stub(balena.auth, 'isLoggedIn'); this.balenaAuthIsLoggedInStub = sinon.stub(balena.auth, 'isLoggedIn');
return this.balenaAuthIsLoggedInStub.returns(Promise.resolve(true)); return this.balenaAuthIsLoggedInStub.returns(Promise.resolve(true));
}); });
afterEach(function() { afterEach(function () {
return this.balenaAuthIsLoggedInStub.restore(); return this.balenaAuthIsLoggedInStub.restore();
}); });
return it('should eventually be true', function() { return it('should eventually be true', function () {
const promise = utils.loginIfTokenValid(tokens.johndoe.token); const promise = utils.loginIfTokenValid(tokens.johndoe.token);
return expect(promise).to.eventually.be.true; return expect(promise).to.eventually.be.true;
}); });

View File

@ -28,7 +28,7 @@ const jHeader = { 'Content-Type': 'application/json' };
export class BalenaAPIMock extends NockMock { export class BalenaAPIMock extends NockMock {
constructor() { constructor() {
super('https://api.balena-cloud.com'); super(/api\.balena-cloud\.com/);
} }
public expectGetApplication(opts: ScopeOpts = {}) { public expectGetApplication(opts: ScopeOpts = {}) {
@ -172,6 +172,17 @@ export class BalenaAPIMock extends NockMock {
}); });
} }
public expectGetDeviceStatus(opts: ScopeOpts = {}) {
this.optGet(
/^\/v\d+\/device\?.+&\$select=overall_status$/,
opts,
).replyWithFile(
200,
path.join(apiResponsePath, 'device-status.json'),
jHeader,
);
}
public expectGetAppEnvVars(opts: ScopeOpts = {}) { public expectGetAppEnvVars(opts: ScopeOpts = {}) {
this.optGet(/^\/v\d+\/application_environment_variable($|\?)/, opts).reply( this.optGet(/^\/v\d+\/application_environment_variable($|\?)/, opts).reply(
200, 200,
@ -206,7 +217,7 @@ export class BalenaAPIMock extends NockMock {
public expectGetAppServiceVars(opts: ScopeOpts = {}) { public expectGetAppServiceVars(opts: ScopeOpts = {}) {
this.optGet(/^\/v\d+\/service_environment_variable($|\?)/, opts).reply( this.optGet(/^\/v\d+\/service_environment_variable($|\?)/, opts).reply(
function(uri, _requestBody) { function (uri, _requestBody) {
const match = uri.match(/service_name%20eq%20%27(.+?)%27/); const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
const serviceName = (match && match[1]) || undefined; const serviceName = (match && match[1]) || undefined;
let varArray: any[]; let varArray: any[];
@ -214,7 +225,7 @@ export class BalenaAPIMock extends NockMock {
const varObj = appServiceVarsByService[serviceName]; const varObj = appServiceVarsByService[serviceName];
varArray = varObj ? [varObj] : []; varArray = varObj ? [varObj] : [];
} else { } else {
varArray = _.map(appServiceVarsByService, value => value); varArray = _.map(appServiceVarsByService, (value) => value);
} }
return [200, { d: varArray }]; return [200, { d: varArray }];
}, },
@ -254,7 +265,7 @@ export class BalenaAPIMock extends NockMock {
this.optGet( this.optGet(
/^\/v\d+\/device_service_environment_variable($|\?)/, /^\/v\d+\/device_service_environment_variable($|\?)/,
opts, opts,
).reply(function(uri, _requestBody) { ).reply(function (uri, _requestBody) {
const match = uri.match(/service_name%20eq%20%27(.+?)%27/); const match = uri.match(/service_name%20eq%20%27(.+?)%27/);
const serviceName = (match && match[1]) || undefined; const serviceName = (match && match[1]) || undefined;
let varArray: any[]; let varArray: any[];
@ -262,7 +273,7 @@ export class BalenaAPIMock extends NockMock {
const varObj = deviceServiceVarsByService[serviceName]; const varObj = deviceServiceVarsByService[serviceName];
varArray = varObj ? [varObj] : []; varArray = varObj ? [varObj] : [];
} else { } else {
varArray = _.map(deviceServiceVarsByService, value => value); varArray = _.map(deviceServiceVarsByService, (value) => value);
} }
return [200, { d: varArray }]; return [200, { d: varArray }];
}); });

View File

@ -27,7 +27,7 @@ export const builderResponsePath = path.normalize(
export class BuilderMock extends NockMock { export class BuilderMock extends NockMock {
constructor() { constructor() {
super('https://builder.balena-cloud.com'); super(/builder\.balena-cloud\.com/);
} }
public expectPostBuild(opts: { public expectPostBuild(opts: {
@ -38,7 +38,7 @@ export class BuilderMock extends NockMock {
checkURI: (uri: string) => Promise<void>; checkURI: (uri: string) => Promise<void>;
checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>; checkBuildRequestBody: (requestBody: string | Buffer) => Promise<void>;
}) { }) {
this.optPost(/^\/v3\/build($|[(?])/, opts).reply(async function( this.optPost(/^\/v3\/build($|[(?])/, opts).reply(async function (
uri, uri,
requestBody, requestBody,
callback, callback,
@ -48,7 +48,7 @@ export class BuilderMock extends NockMock {
await opts.checkURI(uri); await opts.checkURI(uri);
if (typeof requestBody === 'string') { if (typeof requestBody === 'string') {
const gzipped = Buffer.from(requestBody, 'hex'); const gzipped = Buffer.from(requestBody, 'hex');
const gunzipped = await Bluebird.fromCallback<Buffer>(cb => { const gunzipped = await Bluebird.fromCallback<Buffer>((cb) => {
zlib.gunzip(gzipped, cb); zlib.gunzip(gzipped, cb);
}); });
await opts.checkBuildRequestBody(gunzipped); await opts.checkBuildRequestBody(gunzipped);

View File

@ -41,7 +41,7 @@ Options:
--type, -t <type> application device type (Check available types with \`balena devices supported\`) --type, -t <type> application device type (Check available types with \`balena devices supported\`)
`; `;
describe('balena app create', function() { describe('balena app create', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
beforeEach(() => { beforeEach(() => {

View File

@ -62,7 +62,10 @@ const commonComposeQueryParams = [
['labels', ''], ['labels', ''],
]; ];
describe('balena build', function() { // "itSS" means "it() Skip Standalone"
const itSS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it.skip : it;
describe('balena build', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
let docker: DockerMock; let docker: DockerMock;
const isWindows = process.platform === 'win32'; const isWindows = process.platform === 'win32';
@ -135,7 +138,7 @@ describe('balena build', function() {
}); });
}); });
it('should create the expected tar stream (--emulated)', async () => { itSS('should create the expected tar stream (--emulated)', async () => {
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic'); const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
const isV12W = isWindows && isV12(); const isV12W = isWindows && isV12();
const transposedDockerfile = const transposedDockerfile =
@ -357,7 +360,7 @@ describe('balena build', function() {
}); });
}); });
describe('balena build: project validation', function() { describe('balena build: project validation', function () {
it('should raise ExpectedError if a Dockerfile cannot be found', async () => { it('should raise ExpectedError if a Dockerfile cannot be found', async () => {
const projectPath = path.join( const projectPath = path.join(
projectsPath, projectsPath,
@ -370,10 +373,10 @@ describe('balena build: project validation', function() {
`found in source folder "${projectPath}"`, `found in source folder "${projectPath}"`,
]; ];
const { out, err } = await runCommand(`build ${projectPath} -a testApp`); const { out, err } = await runCommand(
expect( `build ${projectPath} -A amd64 -d nuc`,
cleanOutput(err).map(line => line.replace(/\s{2,}/g, ' ')), );
).to.include.members(expectedErrorLines); expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
expect(out).to.be.empty; expect(out).to.be.empty;
}); });
}); });

View File

@ -56,7 +56,7 @@ const commonQueryParams = [
['labels', ''], ['labels', ''],
]; ];
describe('balena deploy', function() { describe('balena deploy', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
let docker: DockerMock; let docker: DockerMock;
let sentryStatus: boolean | undefined; let sentryStatus: boolean | undefined;
@ -173,6 +173,9 @@ describe('balena deploy', function() {
const expectedResponseLines = ['[Error] Deploy failed']; const expectedResponseLines = ['[Error] Deploy failed'];
const errMsg = 'Patch Image Error'; const errMsg = 'Patch Image Error';
const expectedErrorLines = [errMsg]; const expectedErrorLines = [errMsg];
// The SDK should produce an "unexpected" BalenaRequestError, which
// causes the CLI to call process.exit() with process.exitCode = 1
const expectedExitCode = 1;
// Mock this patch HTTP request to return status code 500, in which case // Mock this patch HTTP request to return status code 500, in which case
// the release status should be saved as "failed" rather than "success" // the release status should be saved as "failed" rather than "success"
@ -202,21 +205,29 @@ describe('balena deploy', function() {
expectedFilesByService: { main: expectedFiles }, expectedFilesByService: { main: expectedFiles },
expectedQueryParamsByService: { main: commonQueryParams }, expectedQueryParamsByService: { main: commonQueryParams },
expectedErrorLines, expectedErrorLines,
expectedExitCode,
expectedResponseLines, expectedResponseLines,
projectPath, projectPath,
responseBody, responseBody,
responseCode: 200, responseCode: 200,
services: ['main'], services: ['main'],
}); });
// The SDK should produce an "unexpected" BalenaRequestError, which
// causes the CLI to call process.exit() with process.exitCode = 1
// @ts-ignore
sinon.assert.calledWith(process.exit);
expect(process.exitCode).to.equal(1);
}); });
}); });
describe('balena deploy: project validation', function() { describe('balena deploy: project validation', function () {
let api: BalenaAPIMock;
this.beforeEach(() => {
api = new BalenaAPIMock();
api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true });
});
this.afterEach(() => {
// Check all expected api calls have been made and clean up.
api.done();
});
it('should raise ExpectedError if a Dockerfile cannot be found', async () => { it('should raise ExpectedError if a Dockerfile cannot be found', async () => {
const projectPath = path.join( const projectPath = path.join(
projectsPath, projectsPath,
@ -232,9 +243,7 @@ describe('balena deploy: project validation', function() {
const { out, err } = await runCommand( const { out, err } = await runCommand(
`deploy testApp --source ${projectPath}`, `deploy testApp --source ${projectPath}`,
); );
expect( expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
cleanOutput(err).map(line => line.replace(/\s{2,}/g, ' ')),
).to.include.members(expectedErrorLines);
expect(out).to.be.empty; expect(out).to.be.empty;
}); });
}); });

View File

@ -36,7 +36,7 @@ Options:
--application, -a, --app <application> application name --application, -a, --app <application> application name
`; `;
describe('balena device move', function() { describe('balena device move', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
beforeEach(() => { beforeEach(() => {

View File

@ -31,7 +31,7 @@ Examples:
\t$ balena device 7cf02a6 \t$ balena device 7cf02a6
`; `;
describe('balena device', function() { describe('balena device', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
beforeEach(() => { beforeEach(() => {
@ -68,11 +68,13 @@ describe('balena device', function() {
}); });
it('should list device details for provided uuid', async () => { it('should list device details for provided uuid', async () => {
api.expectGetWhoAmI({ optional: true }); api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true }); api.expectGetMixpanel({ optional: true });
api.expectGetDeviceStatus();
api.scope api.scope
.get(/^\/v5\/device/) .get(
/^\/v5\/device\?.+&\$expand=belongs_to__application\(\$select=app_name\)/,
)
.replyWithFile(200, path.join(apiResponsePath, 'device.json'), { .replyWithFile(200, path.join(apiResponsePath, 'device.json'), {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}); });
@ -91,11 +93,13 @@ describe('balena device', function() {
it('correctly handles devices with missing application', async () => { it('correctly handles devices with missing application', async () => {
// Devices with missing applications will have application name set to `N/a`. // Devices with missing applications will have application name set to `N/a`.
// e.g. When user has a device associated with app that user is no longer a collaborator of. // e.g. When user has a device associated with app that user is no longer a collaborator of.
api.expectGetWhoAmI({ optional: true }); api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true }); api.expectGetMixpanel({ optional: true });
api.expectGetDeviceStatus();
api.scope api.scope
.get(/^\/v5\/device/) .get(
/^\/v5\/device\?.+&\$expand=belongs_to__application\(\$select=app_name\)/,
)
.replyWithFile( .replyWithFile(
200, 200,
path.join(apiResponsePath, 'device-missing-app.json'), path.join(apiResponsePath, 'device-missing-app.json'),

View File

@ -40,7 +40,7 @@ Options:
--application, -a, --app <application> application name --application, -a, --app <application> application name
`; `;
describe('balena devices', function() { describe('balena devices', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
beforeEach(() => { beforeEach(() => {
@ -64,7 +64,7 @@ describe('balena devices', function() {
}); });
it('should list devices from own and collaborator apps', async () => { it('should list devices from own and collaborator apps', async () => {
api.expectGetWhoAmI({ optional: true }); api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true }); api.expectGetMixpanel({ optional: true });
api.scope api.scope
@ -85,11 +85,11 @@ describe('balena devices', function() {
); );
expect(lines).to.have.lengthOf.at.least(2); expect(lines).to.have.lengthOf.at.least(2);
expect(lines.some(l => l.includes('test app'))).to.be.true; expect(lines.some((l) => l.includes('test app'))).to.be.true;
// Devices with missing applications will have application name set to `N/a`. // Devices with missing applications will have application name set to `N/a`.
// e.g. When user has a device associated with app that user is no longer a collaborator of. // e.g. When user has a device associated with app that user is no longer a collaborator of.
expect(lines.some(l => l.includes('N/a'))).to.be.true; expect(lines.some((l) => l.includes('N/a'))).to.be.true;
expect(err).to.eql([]); expect(err).to.eql([]);
}); });

View File

@ -21,7 +21,7 @@ import { isV12 } from '../../../build/utils/version';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../balena-api-mock';
import { cleanOutput, runCommand } from '../../helpers'; import { cleanOutput, runCommand } from '../../helpers';
describe('balena devices supported', function() { describe('balena devices supported', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
beforeEach(() => { beforeEach(() => {
@ -59,11 +59,11 @@ describe('balena devices supported', function() {
expect(lines).to.have.lengthOf.at.least(2); expect(lines).to.have.lengthOf.at.least(2);
// Discontinued devices should be filtered out from results // Discontinued devices should be filtered out from results
expect(lines.some(l => l.includes('DISCONTINUED'))).to.be.false; expect(lines.some((l) => l.includes('DISCONTINUED'))).to.be.false;
// Experimental devices should be listed as beta // Experimental devices should be listed as beta
expect(lines.some(l => l.includes('EXPERIMENTAL'))).to.be.false; expect(lines.some((l) => l.includes('EXPERIMENTAL'))).to.be.false;
expect(lines.some(l => l.includes('NEW'))).to.be.true; expect(lines.some((l) => l.includes('NEW'))).to.be.true;
expect(err).to.eql([]); expect(err).to.eql([]);
}); });

View File

@ -20,7 +20,7 @@ import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../balena-api-mock';
import { runCommand } from '../../helpers'; import { runCommand } from '../../helpers';
describe('balena env add', function() { describe('balena env add', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
beforeEach(() => { beforeEach(() => {

View File

@ -22,7 +22,7 @@ import { isV12 } from '../../../lib/utils/version';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../balena-api-mock';
import { runCommand } from '../../helpers'; import { runCommand } from '../../helpers';
describe('balena envs', function() { describe('balena envs', function () {
const appName = 'test'; const appName = 'test';
let fullUUID: string; let fullUUID: string;
let shortUUID: string; let shortUUID: string;
@ -33,9 +33,7 @@ describe('balena envs', function() {
api.expectGetWhoAmI({ optional: true, persist: true }); api.expectGetWhoAmI({ optional: true, persist: true });
api.expectGetMixpanel({ optional: true }); api.expectGetMixpanel({ optional: true });
// Random device UUID used to frustrate _.memoize() in utils/cloud.ts // Random device UUID used to frustrate _.memoize() in utils/cloud.ts
fullUUID = require('crypto') fullUUID = require('crypto').randomBytes(16).toString('hex');
.randomBytes(16)
.toString('hex');
shortUUID = fullUUID.substring(0, 7); shortUUID = fullUUID.substring(0, 7);
}); });

View File

@ -20,7 +20,7 @@ import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../balena-api-mock';
import { runCommand } from '../../helpers'; import { runCommand } from '../../helpers';
describe('balena env rename', function() { describe('balena env rename', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
beforeEach(() => { beforeEach(() => {

View File

@ -20,7 +20,7 @@ import { expect } from 'chai';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../balena-api-mock';
import { runCommand } from '../../helpers'; import { runCommand } from '../../helpers';
describe('balena env rm', function() { describe('balena env rm', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
beforeEach(() => { beforeEach(() => {

View File

@ -92,7 +92,7 @@ const ONLINE_RESOURCES = `
For bug reports or feature requests, see: https://github.com/balena-io/balena-cli/issues/ For bug reports or feature requests, see: https://github.com/balena-io/balena-cli/issues/
`; `;
describe('balena help', function() { describe('balena help', function () {
it('should list primary command summaries', async () => { it('should list primary command summaries', async () => {
const { out, err } = await runCommand('help'); const { out, err } = await runCommand('help');

View File

@ -0,0 +1,65 @@
/**
* @license
* Copyright 2020 Balena Ltd.
*
* 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 { expect } from 'chai';
import { BalenaAPIMock } from '../balena-api-mock';
import { cleanOutput, runCommand } from '../helpers';
import { SupervisorMock } from '../supervisor-mock';
const itS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it : it.skip;
describe('balena logs', function () {
let api: BalenaAPIMock;
let supervisor: SupervisorMock;
this.beforeEach(() => {
api = new BalenaAPIMock();
supervisor = new SupervisorMock();
api.expectGetWhoAmI();
api.expectGetMixpanel({ optional: true });
});
this.afterEach(() => {
// Check all expected api calls have been made and clean up.
api.done();
supervisor.done();
});
// skip non-standalone tests because nock's mock socket causes the error:
// "setKeepAliveInterval expects an instance of socket as its first argument"
// in utils/device/api.ts: NetKeepalive.setKeepAliveInterval(sock, 5000);
itS('should reach the expected endpoints on a local device', async () => {
supervisor.expectGetPing();
supervisor.expectGetLogs();
const { err, out } = await runCommand('logs 1.2.3.4');
expect(err).to.be.empty;
const removeTimestamps = (logLine: string) =>
logLine.replace(/(?<=\[Logs\]) \[.+?\]/, '');
const cleanedOut = cleanOutput(out, true).map((l) => removeTimestamps(l));
expect(cleanedOut).to.deep.equal([
'[Logs] Streaming logs',
'[Logs] [bar] bar 8 (332) Linux 4e3f81149d71 4.19.75 #1 SMP PREEMPT Mon Mar 23 11:50:49 UTC 2020 aarch64 GNU/Linux',
'[Logs] [foo] foo 8 (200) Linux cc5df60d89ee 4.19.75 #1 SMP PREEMPT Mon Mar 23 11:50:49 UTC 2020 aarch64 GNU/Linux',
'[Error] Connection to device lost',
]);
});
});

View File

@ -6,7 +6,7 @@ import { runCommand } from '../../helpers';
import { BalenaAPIMock } from '../../balena-api-mock'; import { BalenaAPIMock } from '../../balena-api-mock';
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
describe('balena os configure', function() { describe('balena os configure', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
beforeEach(async () => { beforeEach(async () => {

View File

@ -80,7 +80,7 @@ const commonQueryParams = [
const itSkipWindows = process.platform === 'win32' ? it.skip : it; const itSkipWindows = process.platform === 'win32' ? it.skip : it;
describe('balena push', function() { describe('balena push', function () {
let api: BalenaAPIMock; let api: BalenaAPIMock;
let builder: BuilderMock; let builder: BuilderMock;
const isWindows = process.platform === 'win32'; const isWindows = process.platform === 'win32';
@ -167,7 +167,7 @@ describe('balena push', function() {
path.join(builderResponsePath, responseFilename), path.join(builderResponsePath, responseFilename),
'utf8', 'utf8',
); );
const expectedQueryParams = commonQueryParams.map(i => const expectedQueryParams = commonQueryParams.map((i) =>
i[0] === 'dockerfilePath' ? ['dockerfilePath', 'Dockerfile-alt'] : i, i[0] === 'dockerfilePath' ? ['dockerfilePath', 'Dockerfile-alt'] : i,
); );
@ -471,7 +471,7 @@ describe('balena push', function() {
}); });
}); });
describe('balena push: project validation', function() { describe('balena push: project validation', function () {
it('should raise ExpectedError if the project folder is not a directory', async () => { it('should raise ExpectedError if the project folder is not a directory', async () => {
const projectPath = path.join( const projectPath = path.join(
projectsPath, projectsPath,
@ -486,9 +486,7 @@ describe('balena push: project validation', function() {
const { out, err } = await runCommand( const { out, err } = await runCommand(
`push testApp --source ${projectPath} --nogitignore`, `push testApp --source ${projectPath} --nogitignore`,
); );
expect( expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
cleanOutput(err).map(line => line.replace(/\s{2,}/g, ' ')),
).to.include.members(expectedErrorLines);
expect(out).to.be.empty; expect(out).to.be.empty;
}); });
@ -507,9 +505,7 @@ describe('balena push: project validation', function() {
const { out, err } = await runCommand( const { out, err } = await runCommand(
`push testApp --source ${projectPath}`, `push testApp --source ${projectPath}`,
); );
expect( expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
cleanOutput(err).map(line => line.replace(/\s{2,}/g, ' ')),
).to.include.members(expectedErrorLines);
expect(out).to.be.empty; expect(out).to.be.empty;
}); });
@ -536,12 +532,8 @@ describe('balena push: project validation', function() {
const { out, err } = await runCommand( const { out, err } = await runCommand(
`push testApp --source ${projectPath} --nolive`, `push testApp --source ${projectPath} --nolive`,
); );
expect( expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
cleanOutput(err).map(line => line.replace(/\s{2,}/g, ' ')), expect(cleanOutput(out, true)).to.include.members(expectedOutputLines);
).to.include.members(expectedErrorLines);
expect(
cleanOutput(out).map(line => line.replace(/\s{2,}/g, ' ')),
).to.include.members(expectedOutputLines);
}); });
it('should suppress a parent folder check with --noparent-check', async () => { it('should suppress a parent folder check with --noparent-check', async () => {
@ -558,9 +550,7 @@ describe('balena push: project validation', function() {
const { out, err } = await runCommand( const { out, err } = await runCommand(
`push testApp --source ${projectPath} --nolive --noparent-check`, `push testApp --source ${projectPath} --nolive --noparent-check`,
); );
expect( expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
cleanOutput(err).map(line => line.replace(/\s{2,}/g, ' ')),
).to.include.members(expectedErrorLines);
expect(out).to.be.empty; expect(out).to.be.empty;
}); });
}); });

View File

@ -1,3 +1,19 @@
/**
* @license
* Copyright 2019-2020 Balena Ltd.
*
* 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 { expect } from 'chai'; import { expect } from 'chai';
import * as fs from 'fs'; import * as fs from 'fs';
import { runCommand } from '../helpers'; import { runCommand } from '../helpers';
@ -7,31 +23,37 @@ const nodeVersion = process.version.startsWith('v')
? process.version.slice(1) ? process.version.slice(1)
: process.version; : process.version;
describe('balena version', function() { describe('balena version', function () {
it('should print the installed version of the CLI', async () => { it('should print the installed version of the CLI', async () => {
const { out } = await runCommand('version'); const { err, out } = await runCommand('version');
expect(err).to.be.empty;
expect(out.join('')).to.equal(`${packageJSON.version}\n`); expect(out.join('')).to.equal(`${packageJSON.version}\n`);
}); });
it('should print additional version information with the -a flag', async () => { it('should print additional version information with the -a flag', async () => {
const { out } = await runCommand('version -a'); const { err, out } = await runCommand('version -a');
expect(err).to.be.empty;
expect(out.join('')).to.equal( expect(out[0].trim()).to.equal(
`balena-cli version "${packageJSON.version}" `balena-cli version "${packageJSON.version}"`,
Node.js version "${nodeVersion}"
`,
); );
if (process.env.BALENA_CLI_TEST_TYPE === 'standalone') {
expect(out[1]).to.match(/Node.js version "\d+\.\d+.\d+"/);
} else {
expect(out[1].trim()).to.equal(`Node.js version "${nodeVersion}"`);
}
}); });
it('should print version information as JSON with the the -j flag', async () => { it('should print version information as JSON with the the -j flag', async () => {
const { out } = await runCommand('version -j'); const { err, out } = await runCommand('version -j');
expect(err).to.be.empty;
const json = JSON.parse(out.join('')); const json = JSON.parse(out.join(''));
expect(json['balena-cli']).to.equal(packageJSON.version);
expect(json).to.deep.equal({ if (process.env.BALENA_CLI_TEST_TYPE === 'standalone') {
'balena-cli': packageJSON.version, expect(json['Node.js']).to.match(/\d+\.\d+.\d+/);
'Node.js': nodeVersion, } else {
}); expect(json['Node.js']).to.equal(nodeVersion);
}
}); });
}); });

View File

@ -21,6 +21,7 @@ import * as _ from 'lodash';
import { fs } from 'mz'; import { fs } from 'mz';
import * as path from 'path'; import * as path from 'path';
import { PathUtils } from 'resin-multibuild'; import { PathUtils } from 'resin-multibuild';
import * as sinon from 'sinon';
import { Readable } from 'stream'; import { Readable } from 'stream';
import * as tar from 'tar-stream'; import * as tar from 'tar-stream';
import { streamToBuffer } from 'tar-utils'; import { streamToBuffer } from 'tar-utils';
@ -88,7 +89,7 @@ export async function inspectTarStream(
}); });
expect(found).to.deep.equal( expect(found).to.deep.equal(
_.mapValues(expectedFiles, v => _.omit(v, 'testStream', 'contents')), _.mapValues(expectedFiles, (v) => _.omit(v, 'testStream', 'contents')),
); );
} }
@ -139,6 +140,7 @@ export async function testDockerBuildStream(o: {
expectedFilesByService: ExpectedTarStreamFilesByService; expectedFilesByService: ExpectedTarStreamFilesByService;
expectedQueryParamsByService: { [service: string]: string[][] }; expectedQueryParamsByService: { [service: string]: string[][] };
expectedErrorLines?: string[]; expectedErrorLines?: string[];
expectedExitCode?: number;
expectedResponseLines: string[]; expectedResponseLines: string[];
projectPath: string; projectPath: string;
responseCode: number; responseCode: number;
@ -174,21 +176,25 @@ export async function testDockerBuildStream(o: {
o.dockerMock.expectGetImages(); o.dockerMock.expectGetImages();
} }
const { out, err } = await runCommand(o.commandLine); const { exitCode, out, err } = await runCommand(o.commandLine);
const cleanLines = (lines: string[]) =>
cleanOutput(lines).map(line => line.replace(/\s{2,}/g, ' '));
if (expectedErrorLines.length) { if (expectedErrorLines.length) {
expect(cleanLines(err)).to.include.members(expectedErrorLines); expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
} else { } else {
expect(err).to.be.empty; expect(err).to.be.empty;
} }
if (expectedResponseLines.length) { if (expectedResponseLines.length) {
expect(cleanLines(out)).to.include.members(expectedResponseLines); expect(cleanOutput(out, true)).to.include.members(expectedResponseLines);
} else { } else {
expect(out).to.be.empty; expect(out).to.be.empty;
} }
if (o.expectedExitCode != null) {
if (process.env.BALENA_CLI_TEST_TYPE !== 'standalone') {
// @ts-ignore
sinon.assert.calledWith(process.exit);
}
expect(o.expectedExitCode).to.equal(exitCode);
}
} }
/** /**
@ -214,14 +220,12 @@ export async function testPushBuildStream(o: {
const queryParams = Array.from(url.searchParams.entries()); const queryParams = Array.from(url.searchParams.entries());
expect(queryParams).to.have.deep.members(expectedQueryParams); expect(queryParams).to.have.deep.members(expectedQueryParams);
}, },
checkBuildRequestBody: buildRequestBody => checkBuildRequestBody: (buildRequestBody) =>
inspectTarStream(buildRequestBody, o.expectedFiles, o.projectPath), inspectTarStream(buildRequestBody, o.expectedFiles, o.projectPath),
}); });
const { out, err } = await runCommand(o.commandLine); const { out, err } = await runCommand(o.commandLine);
expect(err).to.be.empty; expect(err).to.be.empty;
expect( expect(cleanOutput(out, true)).to.include.members(expectedResponseLines);
cleanOutput(out).map(line => line.replace(/\s{2,}/g, ' ')),
).to.include.members(expectedResponseLines);
} }

View File

@ -80,7 +80,7 @@ export class DockerMock extends NockMock {
this.optPost( this.optPost(
new RegExp(`^/build\\?t=${_.escapeRegExp(opts.tag)}&`), new RegExp(`^/build\\?t=${_.escapeRegExp(opts.tag)}&`),
opts, opts,
).reply(async function(uri, requestBody, cb) { ).reply(async function (uri, requestBody, cb) {
let error: Error | null = null; let error: Error | null = null;
try { try {
await opts.checkURI(uri); await opts.checkURI(uri);

View File

@ -124,7 +124,7 @@ describe('handleError() function', () => {
'to be one of', 'to be one of',
]; ];
messagesToMatch.forEach(message => { messagesToMatch.forEach((message) => {
it(`should match as expected: "${message}"`, async () => { it(`should match as expected: "${message}"`, async () => {
await ErrorsModule.handleError(new Error(message)); await ErrorsModule.handleError(new Error(message));
@ -146,7 +146,7 @@ describe('handleError() function', () => {
new BalenaExpiredToken('test'), new BalenaExpiredToken('test'),
]; ];
typedErrorsToMatch.forEach(typedError => { typedErrorsToMatch.forEach((typedError) => {
it(`should treat typedError ${typedError.name} as expected`, async () => { it(`should treat typedError ${typedError.name} as expected`, async () => {
await ErrorsModule.handleError(typedError); await ErrorsModule.handleError(typedError);

View File

@ -18,37 +18,63 @@
// tslint:disable-next-line:no-var-requires // tslint:disable-next-line:no-var-requires
require('./config-tests'); // required for side effects require('./config-tests'); // required for side effects
import { execFile } from 'child_process';
import intercept = require('intercept-stdout'); import intercept = require('intercept-stdout');
import * as _ from 'lodash'; import * as _ from 'lodash';
import { fs } from 'mz';
import * as nock from 'nock'; import * as nock from 'nock';
import * as path from 'path'; import * as path from 'path';
import * as balenaCLI from '../build/app'; import * as balenaCLI from '../build/app';
import { setupSentry } from '../build/app-common'; import { setupSentry } from '../build/app-common';
export const runCommand = async (cmd: string) => { const balenaExe = process.platform === 'win32' ? 'balena.exe' : 'balena';
const standalonePath = path.resolve(__dirname, '..', 'build-bin', balenaExe);
interface TestOutput {
err: string[]; // stderr
out: string[]; // stdout
exitCode?: number; // process.exitCode
}
/**
* Filter stdout / stderr lines to remove lines that start with `[debug]` and
* other lines that can be ignored for testing purposes.
* @param testOutput
*/
function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
return {
exitCode: testOutput.exitCode,
err: testOutput.err.filter(
(line: string) =>
!line.match(/\[debug\]/i) &&
// TODO stop this warning message from appearing when running
// sdk.setSharedOptions multiple times in the same process
!line.startsWith('Shared SDK options') &&
// Node 12: '[DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated'
!line.includes('[DEP0066]'),
),
out: testOutput.out.filter((line: string) => !line.match(/\[debug\]/i)),
};
}
/**
* Run the CLI in this same process, by calling the run() function in `app.ts`.
* @param cmd Command to execute, e.g. `push myApp` (without 'balena' prefix)
*/
async function runCommanInProcess(cmd: string): Promise<TestOutput> {
const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')]; const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')];
const err: string[] = []; const err: string[] = [];
const out: string[] = []; const out: string[] = [];
const stdoutHook = (log: string | Buffer) => { const stdoutHook = (log: string | Buffer) => {
// Skip over debug messages if (typeof log === 'string') {
if (typeof log === 'string' && !log.startsWith('[debug]')) {
out.push(log); out.push(log);
} }
}; };
const stderrHook = (log: string | Buffer) => { const stderrHook = (log: string | Buffer) => {
// Skip over debug messages if (typeof log === 'string') {
if (
typeof log === 'string' &&
!log.match(/\[debug\]/i) &&
// TODO stop this warning message from appearing when running
// sdk.setSharedOptions multiple times in the same process
!log.startsWith('Shared SDK options') &&
// Node 12: '[DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated'
!log.includes('[DEP0066]')
) {
err.push(log); err.push(log);
} }
}; };
@ -58,40 +84,150 @@ export const runCommand = async (cmd: string) => {
await balenaCLI.run(preArgs.concat(cmd.split(' ')), { await balenaCLI.run(preArgs.concat(cmd.split(' ')), {
noFlush: true, noFlush: true,
}); });
return {
err,
out,
};
} finally { } finally {
unhookIntercept(); unhookIntercept();
} }
}; return filterCliOutputForTests({
err,
out,
// this makes sense if `process.exit()` was stubbed with sinon
exitCode: process.exitCode,
});
}
/**
* Run the command (e.g. `balena xxx args`) in a child process, instead of
* the same process as mocha. This is slow and does not allow mocking the
* source code, but it is useful for testing the standalone zip package binary.
* (Every now and then, bugs surface because of missing entries in the
* `pkg.assets` section of `package.json`, usually because of updated
* dependencies that don't clearly declare the have compatibility issues
* with `pkg`.)
*
* `mocha` runs on the parent process, and many of the tests inspect network
* traffic intercepted with `nock`. But this interception only works in the
* parent process itself. To get around this, we run a HTTP proxy server on
* the parent process, and get the child process to use it (the CLI already had
* support for proxy servers as a product feature, and this testing arrangement
* also exercises the proxy capabilities).
*
* @param cmd Command to execute, e.g. `push myApp` (without 'balena' prefix)
* @param proxyPort TCP port number for the HTTP proxy server running on the
* parent process
*/
async function runCommandInSubprocess(
cmd: string,
proxyPort: number,
): Promise<TestOutput> {
let exitCode = 0;
let stdout = '';
let stderr = '';
const addedEnvs = {
// Use http instead of https, so we can intercept and test the data,
// for example the contents of tar streams sent by the CLI to Docker
BALENARC_API_URL: 'http://api.balena-cloud.com',
BALENARC_BUILDER_URL: 'http://builder.balena-cloud.com',
BALENARC_PROXY: `http://127.0.0.1:${proxyPort}`,
// override default proxy exclusion to allow proxying of requests to 127.0.0.1
BALENARC_DO_PROXY: '127.0.0.1,localhost',
};
await new Promise((resolve) => {
const child = execFile(
standalonePath,
cmd.split(' '),
{ env: { ...process.env, ...addedEnvs } },
($error, $stdout, $stderr) => {
stderr = $stderr || '';
stdout = $stdout || '';
// $error will be set if the CLI child process exits with a
// non-zero exit code. Usually this is harmless/expected, as
// the CLI child process is tested for error conditions.
if ($error && process.env.DEBUG) {
console.error(`
[debug] Error (possibly expected) executing child CLI process "${standalonePath}"
------------------------------------------------------------------
${$error}
------------------------------------------------------------------`);
}
resolve();
},
);
child.on('exit', (code: number, signal: string) => {
if (process.env.DEBUG) {
console.error(
`CLI child process exited with code=${code} signal=${signal}`,
);
}
exitCode = code;
});
});
const splitLines = (lines: string) =>
lines
.split(/[\r\n]/) // includes '\r' in isolation, used in progress bars
.filter((l) => l)
.map((l) => l + '\n');
return filterCliOutputForTests({
exitCode,
err: splitLines(stderr),
out: splitLines(stdout),
});
}
/**
* Run a CLI command and capture its stdout, stderr and exit code for testing.
* If the BALENA_CLI_TEST_TYPE env var is set to 'standalone', then the command
* will be executed in a separate child process, and a proxy server will be
* started in order to intercept and test HTTP requests.
* Otherwise, simply call the CLI's run() entry point in this same process.
* @param cmd Command to execute, e.g. `push myApp` (without 'balena' prefix)
*/
export async function runCommand(cmd: string): Promise<TestOutput> {
if (process.env.BALENA_CLI_TEST_TYPE === 'standalone') {
const semver = await import('semver');
if (semver.lt(process.version, '10.16.0')) {
throw new Error(
`The standalone tests require Node.js >= v10.16.0 because of net/proxy features ('global-agent' npm package)`,
);
}
if (!(await fs.exists(standalonePath))) {
throw new Error(`Standalone executable not found: "${standalonePath}"`);
}
const proxy = await import('./proxy-server');
const [proxyPort] = await proxy.createProxyServerOnce();
return runCommandInSubprocess(cmd, proxyPort);
} else {
return runCommanInProcess(cmd);
}
}
export const balenaAPIMock = () => { export const balenaAPIMock = () => {
if (!nock.isActive()) { if (!nock.isActive()) {
nock.activate(); nock.activate();
} }
return nock(/./) return nock(/./).get('/config/vars').reply(200, {
.get('/config/vars') reservedNames: [],
.reply(200, { reservedNamespaces: [],
reservedNames: [], invalidRegex: '/^d|W/',
reservedNamespaces: [], whiteListedNames: [],
invalidRegex: '/^d|W/', whiteListedNamespaces: [],
whiteListedNames: [], blackListedNames: [],
whiteListedNamespaces: [], configVarSchema: [],
blackListedNames: [], });
configVarSchema: [],
});
}; };
export function cleanOutput(output: string[] | string): string[] { export function cleanOutput(
output: string[] | string,
collapseBlank = false,
): string[] {
const cleanLine = collapseBlank
? (line: string) => monochrome(line.trim()).replace(/\s{2,}/g, ' ')
: (line: string) => monochrome(line.trim());
return _(_.castArray(output)) return _(_.castArray(output))
.map((log: string) => { .map((log: string) => log.split('\n').map(cleanLine))
return log.split('\n').map(line => {
return monochrome(line.trim());
});
})
.flatten() .flatten()
.compact() .compact()
.value(); .value();
@ -106,7 +242,7 @@ export function cleanOutput(output: string[] | string): string[] {
* coded from observation of a few samples only, and may not cover all cases. * coded from observation of a few samples only, and may not cover all cases.
*/ */
export function monochrome(text: string): string { export function monochrome(text: string): string {
return text.replace(/\u001b\[\??\d+?[a-zA-Z]\r?/g, ''); return text.replace(/\u001b\[\??(\d+;)*\d+[a-zA-Z]\r?/g, '');
} }
/** /**
@ -142,7 +278,7 @@ export function fillTemplateArray(
templateStringArray: Array<string | string[]>, templateStringArray: Array<string | string[]>,
templateVars: object, templateVars: object,
): Array<string | string[]> { ): Array<string | string[]> {
return templateStringArray.map(i => return templateStringArray.map((i) =>
Array.isArray(i) Array.isArray(i)
? fillTemplateArray(i, templateVars) ? fillTemplateArray(i, templateVars)
: fillTemplate(i, templateVars), : fillTemplate(i, templateVars),

View File

@ -91,7 +91,7 @@ export class NockMock {
inspectRequest: (uri: string, requestBody: nock.Body) => void, inspectRequest: (uri: string, requestBody: nock.Body) => void,
replyBody: nock.ReplyBody, replyBody: nock.ReplyBody,
) { ) {
return function( return function (
this: nock.ReplyFnContext, this: nock.ReplyFnContext,
uri: string, uri: string,
requestBody: nock.Body, requestBody: nock.Body,
@ -133,11 +133,22 @@ export class NockMock {
} }
protected handleUnexpectedRequest(req: any) { protected handleUnexpectedRequest(req: any) {
const {
interceptorServerPort,
} = require('./proxy-server') as typeof import('./proxy-server');
const o = req.options || {}; const o = req.options || {};
const u = o.uri || {}; const u = o.uri || {};
const method = req.method;
const proto = req.protocol || req.proto || o.proto || u.protocol;
const host = req.host || req.headers?.host || o.host || u.host;
const path = req.path || o.path || u.path;
// Requests made by the local proxy/interceptor server are OK
if (host === `127.0.0.1:${interceptorServerPort}`) {
return;
}
console.error( console.error(
`Unexpected http request!: ${req.method} ${o.proto || `NockMock: Unexpected HTTP request: ${method} ${proto}//${host}${path}`,
u.protocol}//${o.host || u.host}${req.path || o.path || u.path}`,
); );
// Errors thrown here are not causing the tests to fail for some reason. // Errors thrown here are not causing the tests to fail for some reason.
// Possibly due to CLI global error handlers? (error.js) // Possibly due to CLI global error handlers? (error.js)
@ -155,13 +166,13 @@ export class NockMock {
let mocks = scope.pendingMocks(); let mocks = scope.pendingMocks();
console.error(`pending mocks ${mocks.length}: ${mocks}`); console.error(`pending mocks ${mocks.length}: ${mocks}`);
this.scope.on('request', function(_req, _interceptor, _body) { this.scope.on('request', function (_req, _interceptor, _body) {
console.log(`>> REQUEST:` + _req.path); console.log(`>> REQUEST:` + _req.path);
mocks = scope.pendingMocks(); mocks = scope.pendingMocks();
console.error(`pending mocks ${mocks.length}: ${mocks}`); console.error(`pending mocks ${mocks.length}: ${mocks}`);
}); });
this.scope.on('replied', function(_req) { this.scope.on('replied', function (_req) {
console.log(`<< REPLIED:` + _req.path); console.log(`<< REPLIED:` + _req.path);
}); });
} }

217
tests/proxy-server.ts Normal file
View File

@ -0,0 +1,217 @@
/**
* @license
* Copyright 2020 Balena Ltd.
*
* 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.
*/
/**
* This module creates two HTTP servers listening on the local machine:
* * The "proxy server" which is a standard HTTP proxy server that handles the
* CONNECT HTTP verb, using the `http-proxy` dependency.
* * The "interceptor server" which actually handles the proxied requests.
*
* The proxy server proxies the client request to the interceptor server. (This
* two-server approach (proxy + interceptor) is mainly a result of accommodating
* the typical setup documented by the `http-proxy` dependency.)
*
* The use case for these servers is to test the standalone executable (CLI's
* standalone zip package) in a child process. Most of the CLI's automated tests
* currently test HTTP requests using `nock`, but `nock` can only mock/test the
* same process (Node's built-in `http` library). However, the CLI has support
* for proxy servers as a product feature, so the idea was to proxy the child
* process requests to the parent process, where the proxy / interceptor servers
* run. The interceptor server then forwards the request (mostly unchanged) with
* the expectation that `nock` will intercept the requests for testing (in the
* parent process) as usual.
*
* 1. A `mocha` test case calls `runCommand('push test-rpi')`, with `nock` setup
* to intercept HTTP requests (in the same process that runs `mocha`).
* 2. The proxy and interceptor servers are started in the parent process (only
* once: singleton) at free TCP port numbers randomly allocated by the OS.
* 3. A CLI child process gets spawned to run the command (`balena push test-rpi`)
* with environment variables including BALENARC_PROXY (set to
* 'http://127.0.0.1:${proxyPort}'). (Additional env vars instruct the
* child process to use HTTP instead of HTTPS for the balena API and builder.)
* 4. The child process sends the HTTP requests to the proxy server.
* 5. The proxy server forwards the request to the interceptor server.
* 6. The interceptor server simply re-issues the HTTP request (unchange), with
* the expectation that `nock` will intercept it.
* 7. `nock` (running on the parent process, same process that runs `mocha`)
* intercepts the HTTP request, test it and replies with a mocked response.
* 8. `nocks` response is returned to the interceptor server, which returns it
* to the proxy server, which returns it to the child process, which continues
* CLI command execution.
*/
import * as http from 'http';
const proxyServers: http.Server[] = [];
after(function () {
if (proxyServers.length) {
if (process.env.DEBUG) {
console.error(
`[debug] Closing proxy servers (count=${proxyServers.length})`,
);
}
proxyServers.forEach((s) => s.close());
proxyServers.splice(0);
}
});
export let proxyServerPort = 0;
export let interceptorServerPort = 0;
export async function createProxyServerOnce(): Promise<[number, number]> {
if (proxyServerPort === 0) {
[proxyServerPort, interceptorServerPort] = await createProxyServer();
}
return [proxyServerPort, interceptorServerPort];
}
async function createProxyServer(): Promise<[number, number]> {
const httpProxy = require('http-proxy') as typeof import('http-proxy');
const interceptorPort = await createInterceptorServer();
const proxy = httpProxy.createProxyServer();
proxy.on('error', function (
err: Error,
_req: http.IncomingMessage,
res: http.ServerResponse,
) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
const msg = `Proxy server error: ${err}`;
console.error(msg);
res.end(msg);
});
const server = http.createServer(function (
req: http.IncomingMessage,
res: http.ServerResponse,
) {
if (process.env.DEBUG) {
console.error(`[debug] Proxy forwarding for ${req.url}`);
}
proxy.web(req, res, { target: `http://127.0.0.1:${interceptorPort}` });
});
proxyServers.push(server);
server.on('error', (err: Error) => {
console.error(`Proxy server error (http.createServer):\n${err}`);
});
let proxyPort = 0; // TCP port number, 0 means automatic allocation
await new Promise((resolve, reject) => {
const listener = server.listen(0, '127.0.0.1', (err: Error) => {
if (err) {
console.error(`Error starting proxy server:\n${err}`);
reject(err);
} else {
const info: any = listener.address();
proxyPort = info.port;
console.error(
`[Info] Proxy server listening on ${info.address}:${proxyPort}`,
);
resolve();
}
});
});
return [proxyPort, interceptorPort];
}
async function createInterceptorServer(): Promise<number> {
const url = await import('url');
const server = http.createServer();
proxyServers.push(server);
server
.on('error', (err: Error) => {
console.error(`Interceptor server error: ${err}`);
})
.on(
'request',
(cliReq: http.IncomingMessage, cliRes: http.ServerResponse) => {
const proxiedFor = `http://${cliReq.headers.host}${cliReq.url}`;
if (process.env.DEBUG) {
console.error(`[debug] Interceptor forwarding for ${proxiedFor}`);
}
// tslint:disable-next-line:prefer-const
let { protocol, hostname, port, path: urlPath, hash } = url.parse(
proxiedFor,
);
protocol = (protocol || 'http:').toLowerCase();
port = port || (protocol === 'https:' ? '443' : '80');
const reqOpts = {
protocol,
port,
host: hostname,
path: `${urlPath || ''}${hash || ''}`,
method: cliReq.method,
headers: cliReq.headers,
};
const srvReq = http.request(reqOpts);
srvReq
.on('error', (err) => {
console.error(
`Interceptor server error in onward request:\n${err}`,
);
})
.on('response', (srvRes: http.IncomingMessage) => {
// Copy headers, status code and status message from interceptor to client
for (const [key, val] of Object.entries(srvRes.headers)) {
if (key && val) {
cliRes.setHeader(key, val);
}
}
cliRes.statusCode = srvRes.statusCode || cliRes.statusCode;
cliRes.statusMessage = srvRes.statusMessage || cliRes.statusMessage;
srvRes.pipe(cliRes).on('error', (err: Error) => {
console.error(
`Interceptor server error piping response to proxy server:\n${err}`,
);
cliRes.end();
});
});
cliReq.pipe(srvReq).on('error', (err: Error) => {
console.error(
`Proxy server error piping client request onward:\n${err}`,
);
});
},
);
let interceptorPort = 0;
await new Promise((resolve, reject) => {
const listener = server.listen(0, '127.0.0.1', (err: Error) => {
if (err) {
console.error(`Error starting interceptor server:\n${err}`);
reject(err);
} else {
const info: any = listener.address();
interceptorPort = info.port;
console.error(
`[Info] Interceptor server listening on ${info.address}:${interceptorPort}`,
);
resolve();
}
});
});
return interceptorPort;
}

56
tests/supervisor-mock.ts Normal file
View File

@ -0,0 +1,56 @@
/**
* @license
* Copyright 2020 Balena Ltd.
*
* 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 * as _ from 'lodash';
import * as path from 'path';
import { Readable } from 'stream';
import { NockMock, ScopeOpts } from './nock-mock';
export const dockerResponsePath = path.normalize(
path.join(__dirname, 'test-data', 'docker-response'),
);
export class SupervisorMock extends NockMock {
constructor() {
super(/1\.2\.3\.4:48484/);
}
public expectGetPing(opts: ScopeOpts = {}) {
this.optGet('/ping', opts).reply(200, 'OK');
}
public expectGetLogs(opts: ScopeOpts = {}) {
const chunks = [
'\n',
'{"message":"Streaming logs","isSystem":true}\n',
'{"serviceName":"bar","serviceId":1,"imageId":1,"isStdout":true,"timestamp":1591991625223,"message":"bar 8 (332) Linux 4e3f81149d71 4.19.75 #1 SMP PREEMPT Mon Mar 23 11:50:49 UTC 2020 aarch64 GNU/Linux"}\n',
'{"serviceName":"foo","serviceId":2,"imageId":2,"isStdout":true,"timestamp":1591991628757,"message":"foo 8 (200) Linux cc5df60d89ee 4.19.75 #1 SMP PREEMPT Mon Mar 23 11:50:49 UTC 2020 aarch64 GNU/Linux"}\n',
];
let chunkCount = 0;
const chunkedStream = new Readable({
read(_size) {
setTimeout(() => {
this.push(chunkCount === chunks.length ? null : chunks[chunkCount++]);
}, 10);
},
});
this.optGet('/v2/local/logs', opts).reply((_uri, _reqBody, cb) => {
cb(null, [200, chunkedStream]);
});
}
}

View File

@ -0,0 +1,8 @@
{
"d": [
{
"overall_status": "offline",
"__metadata": {}
}
]
}

Some files were not shown because too many files have changed in this diff Show More