Convert command scan to TypeScript, migrate to oclif

Change-type: patch
Signed-off-by: Scott Lowe <scott@balena.io>
This commit is contained in:
Scott Lowe 2020-03-13 12:32:29 +01:00 committed by Balena CI
parent e3672bc655
commit a2b761ec4b
11 changed files with 235 additions and 128 deletions

View File

@ -82,7 +82,7 @@ const capitanoDoc = {
{ {
title: 'Network', title: 'Network',
files: [ files: [
'build/actions/scan.js', 'build/actions-oclif/scan.js',
'build/actions/ssh.js', 'build/actions/ssh.js',
'build/actions/tunnel.js', 'build/actions/tunnel.js',
], ],

View File

@ -1182,6 +1182,7 @@ Only show system logs. This can be used in combination with --service.
## scan ## scan
Scan for balenaOS devices on your local network.
Examples: Examples:
@ -1191,13 +1192,13 @@ Examples:
### Options ### Options
#### --verbose, -v #### -v, --verbose
Display full info display full info
#### --timeout, -t &#60;timeout&#62; #### -t, --timeout TIMEOUT
Scan timeout in seconds scan timeout in seconds
## ssh &#60;applicationOrDevice&#62; [serviceName] ## ssh &#60;applicationOrDevice&#62; [serviceName]

174
lib/actions-oclif/scan.ts Normal file
View File

@ -0,0 +1,174 @@
/**
* @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 { flags } from '@oclif/command';
import { LocalBalenaOsDevice } from 'balena-sync';
import { stripIndent } from 'common-tags';
import Command from '../command';
import * as cf from '../utils/common-flags';
import { getVisuals } from '../utils/lazy';
interface FlagsDef {
verbose: boolean;
timeout?: number;
help: void;
}
export default class ScanCmd extends Command {
public static description = stripIndent`
Scan for balenaOS devices on your local network.
Scan for balenaOS devices on your local network.
`;
public static examples = [
'$ balena scan',
'$ balena scan --timeout 120',
'$ balena scan --verbose',
];
public static usage = 'scan';
public static flags: flags.Input<FlagsDef> = {
verbose: flags.boolean({
char: 'v',
default: false,
description: 'display full info',
}),
timeout: flags.integer({
char: 't',
description: 'scan timeout in seconds',
}),
help: cf.help,
};
public static primary = true;
public static root = true;
public async run() {
const Bluebird = await import('bluebird');
const _ = await import('lodash');
const { SpinnerPromise } = getVisuals();
const { discover } = await import('balena-sync');
const prettyjson = await import('prettyjson');
const { ExpectedError } = await import('../errors');
const { dockerPort, dockerTimeout } = await import(
'../actions/local/common'
);
const dockerUtils = await import('../utils/docker');
const { flags: options } = this.parse<FlagsDef, {}>(ScanCmd);
const discoverTimeout =
options.timeout != null ? options.timeout * 1000 : undefined;
// Find active local devices
const activeLocalDevices: LocalBalenaOsDevice[] = await new SpinnerPromise({
promise: discover.discoverLocalBalenaOsDevices(discoverTimeout),
startMessage: 'Scanning for local balenaOS devices..',
stopMessage: 'Reporting scan results',
}).filter(async ({ address }: { address: string }) => {
const docker = dockerUtils.createClient({
host: address,
port: dockerPort,
timeout: dockerTimeout,
}) as any;
try {
await docker.pingAsync();
return true;
} catch (err) {
return false;
}
});
// Exit with message if no devices found
if (_.isEmpty(activeLocalDevices)) {
// TODO: Consider whether this should really be an error
throw new ExpectedError(
process.platform === 'win32'
? ScanCmd.noDevicesFoundMessage + ScanCmd.windowsTipMessage
: ScanCmd.noDevicesFoundMessage,
);
}
// Query devices for info
const devicesInfo = await Bluebird.map(
activeLocalDevices,
({ host, address }) => {
const docker = dockerUtils.createClient({
host: address,
port: dockerPort,
timeout: dockerTimeout,
}) as any;
return Bluebird.props({
host,
address,
dockerInfo: docker
.infoAsync()
.catchReturn('Could not get Docker info'),
dockerVersion: docker
.versionAsync()
.catchReturn('Could not get Docker version'),
});
},
);
// Reduce properties if not --verbose
if (!options.verbose) {
devicesInfo.forEach((d: any) => {
d.dockerInfo = _.isObject(d.dockerInfo)
? _.pick(d.dockerInfo, ScanCmd.dockerInfoProperties)
: d.dockerInfo;
d.dockerVersion = _.isObject(d.dockerVersion)
? _.pick(d.dockerVersion, ScanCmd.dockerVersionProperties)
: d.dockerVersion;
});
}
// Output results
console.log(prettyjson.render(devicesInfo, { noColor: true }));
}
protected static dockerInfoProperties = [
'Containers',
'ContainersRunning',
'ContainersPaused',
'ContainersStopped',
'Images',
'Driver',
'SystemTime',
'KernelVersion',
'OperatingSystem',
'Architecture',
];
protected static dockerVersionProperties = ['Version', 'ApiVersion'];
protected static noDevicesFoundMessage =
'Could not find any balenaOS devices on the local network.';
protected static windowsTipMessage = `
Note for Windows users:
The 'scan' command relies on the Bonjour service. Check whether Bonjour is
installed (Control Panel > Programs and Features). If not, you can download
Bonjour for Windows (included with Bonjour Print Services) from here:
https://support.apple.com/kb/DL999
After installing Bonjour, restart your PC and run the 'balena scan' command
again.`;
}

View File

@ -22,7 +22,6 @@ module.exports =
tags: require('./tags') tags: require('./tags')
logs: require('./logs') logs: require('./logs')
local: require('./local') local: require('./local')
scan: require('./scan')
help: require('./help') help: require('./help')
os: require('./os') os: require('./os')
config: require('./config') config: require('./config')

19
lib/actions/local/common.d.ts vendored Normal file
View File

@ -0,0 +1,19 @@
/**
* @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.
*/
export const dockerPort: number;
export const dockerTimeout: number;

View File

@ -1,115 +0,0 @@
###
Copyright 2017 Balena
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
###
{ getVisuals } = require('../utils/lazy')
dockerInfoProperties = [
'Containers'
'ContainersRunning'
'ContainersPaused'
'ContainersStopped'
'Images'
'Driver'
'SystemTime'
'KernelVersion'
'OperatingSystem'
'Architecture'
]
dockerVersionProperties = [
'Version'
'ApiVersion'
]
scanErrorMessage = 'Could not find any balenaOS devices in the local network.'
winScanErrorMessage = scanErrorMessage + """
\n
Note for Windows users:
The 'scan' command relies on the Bonjour service. Check whether Bonjour is
installed (Control Panel > Programs and Features). If not, you can download
Bonjour for Windows (included with Bonjour Print Services) from here:
https://support.apple.com/kb/DL999
After installing Bonjour, restart your PC and run the 'balena scan' command
again.
"""
module.exports =
signature: 'scan'
description: 'Scan for balenaOS devices in your local network'
help: '''
Examples:
$ balena scan
$ balena scan --timeout 120
$ balena scan --verbose
'''
options: [
signature: 'verbose'
boolean: true
description: 'Display full info'
alias: 'v'
,
signature: 'timeout'
parameter: 'timeout'
description: 'Scan timeout in seconds'
alias: 't'
]
primary: true
root: true
action: (params, options) ->
Promise = require('bluebird')
_ = require('lodash')
prettyjson = require('prettyjson')
{ discover } = require('balena-sync')
{ SpinnerPromise } = getVisuals()
{ dockerPort, dockerTimeout } = require('./local/common')
dockerUtils = require('../utils/docker')
{ exitWithExpectedError } = require('../utils/patterns')
if options.timeout?
options.timeout *= 1000
Promise.try ->
new SpinnerPromise
promise: discover.discoverLocalBalenaOsDevices(options.timeout)
startMessage: 'Scanning for local balenaOS devices..'
stopMessage: 'Reporting scan results'
.filter ({ address }) ->
Promise.try ->
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
docker.pingAsync()
.return(true)
.catchReturn(false)
.tap (devices) ->
if _.isEmpty(devices)
exitWithExpectedError(if process.platform == 'win32' then winScanErrorMessage else scanErrorMessage)
.map ({ host, address }) ->
docker = dockerUtils.createClient(host: address, port: dockerPort, timeout: dockerTimeout)
Promise.props
dockerInfo: docker.infoAsync().catchReturn('Could not get Docker info')
dockerVersion: docker.versionAsync().catchReturn('Could not get Docker version')
.then ({ dockerInfo, dockerVersion }) ->
if not options.verbose
dockerInfo = _.pick(dockerInfo, dockerInfoProperties) if _.isObject(dockerInfo)
dockerVersion = _.pick(dockerVersion, dockerVersionProperties) if _.isObject(dockerVersion)
return { host, address, dockerInfo, dockerVersion }
.then (devicesInfo) ->
console.log(prettyjson.render(devicesInfo, noColor: true))

View File

@ -107,7 +107,6 @@ capitano.command(actions.ssh.ssh)
# ---------- Local balenaOS Module ---------- # ---------- Local balenaOS Module ----------
capitano.command(actions.local.configure) capitano.command(actions.local.configure)
capitano.command(actions.local.flash) capitano.command(actions.local.flash)
capitano.command(actions.scan)
# ---------- Public utils ---------- # ---------- Public utils ----------
capitano.command(actions.util.availableDrives) capitano.command(actions.util.availableDrives)
@ -135,7 +134,7 @@ exports.run = (argv) ->
# 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)
# In case of failures like an inexistent or invalid command, # In case of failures like an nonexistent or invalid command,
# command.signature.toString() returns '*' # command.signature.toString() returns '*'
cmdSignature = command.signature.toString() cmdSignature = command.signature.toString()
events.trackCommand(cmdSignature) events.trackCommand(cmdSignature)

View File

@ -143,6 +143,7 @@ export const convertedCommands = [
'os:configure', 'os:configure',
'settings', 'settings',
'version', 'version',
'scan',
]; ];
/** /**

View File

@ -30,8 +30,14 @@ export interface BuildDockerOptions {
timeout?: number; timeout?: number;
} }
export interface DockerToolbeltOpts {
host: string;
port: number;
timeout?: number;
}
export function getDocker( export function getDocker(
options: BuildDockerOptions, options: BuildDockerOptions,
): Bluebird<DockerToolbelt>; ): Bluebird<DockerToolbelt>;
export function createClient(options: BuildDockerOptions): DockerToolbelt; export function createClient(opts: DockerToolbeltOpts): DockerToolbelt;

View File

@ -27,7 +27,7 @@ Primary commands:
deploy <appName> [image] Deploy a single image or a multicontainer project to a balena application deploy <appName> [image] Deploy a single image or a multicontainer project to a balena application
join [deviceiporhostname] move a local device to an application on another balena server join [deviceiporhostname] move a local device to an application on another balena server
leave [deviceiporhostname] remove a local device from its balena application leave [deviceiporhostname] remove a local device from its balena application
scan Scan for balenaOS devices in your local network scan scan for balenaOS devices on your local network
`; `;
@ -92,9 +92,20 @@ const GLOBAL_OPTIONS = `
`; `;
describe('balena help', function() { describe('balena help', function() {
it('should print simple help text', async () => { it('should list primary command summaries', async () => {
const { out, err } = await runCommand('help'); const { out, err } = await runCommand('help');
console.log('ONE');
console.log(cleanOutput(out));
console.log(
cleanOutput([
SIMPLE_HELP,
'Run `balena help --verbose` to list additional commands',
GLOBAL_OPTIONS,
]),
);
console.log();
expect(cleanOutput(out)).to.deep.equal( expect(cleanOutput(out)).to.deep.equal(
cleanOutput([ cleanOutput([
SIMPLE_HELP, SIMPLE_HELP,
@ -106,7 +117,7 @@ describe('balena help', function() {
expect(err.join('')).to.equal(''); expect(err.join('')).to.equal('');
}); });
it('should print additional commands with the -v flag', async () => { it('should list all command summaries with the -v flag', async () => {
const { out, err } = await runCommand('help -v'); const { out, err } = await runCommand('help -v');
expect(cleanOutput(out)).to.deep.equal( expect(cleanOutput(out)).to.deep.equal(
@ -118,7 +129,7 @@ describe('balena help', function() {
expect(err.join('')).to.equal(''); expect(err.join('')).to.equal('');
}); });
it('should print simple help text when no arguments present', async () => { it('should list primary command summaries', async () => {
const { out, err } = await runCommand(''); const { out, err } = await runCommand('');
expect(cleanOutput(out)).to.deep.equal( expect(cleanOutput(out)).to.deep.equal(

View File

@ -20,9 +20,21 @@ declare module 'balena-sync' {
export function capitano(tool: 'balena-cli'): CommandDefinition; export function capitano(tool: 'balena-cli'): CommandDefinition;
export interface LocalBalenaOsDevice {
address: string;
host: string;
port: number;
}
declare namespace forms { declare namespace forms {
export function selectLocalBalenaOsDevice( export function selectLocalBalenaOsDevice(
timeout?: number, timeout?: number,
): Promise<string>; ): Promise<string>;
} }
declare namespace discover {
export function discoverLocalBalenaOsDevices(
timeout?: number,
): Promise<LocalBalenaOsDevice[]>;
}
} }