From a2b761ec4b3f3aaee565b7ebfa43fb4bae429455 Mon Sep 17 00:00:00 2001 From: Scott Lowe Date: Fri, 13 Mar 2020 12:32:29 +0100 Subject: [PATCH] Convert command `scan` to TypeScript, migrate to oclif Change-type: patch Signed-off-by: Scott Lowe --- automation/capitanodoc/capitanodoc.ts | 2 +- doc/cli.markdown | 9 +- lib/actions-oclif/scan.ts | 174 ++++++++++++++++++++++++++ lib/actions/index.coffee | 1 - lib/actions/local/common.d.ts | 19 +++ lib/actions/scan.coffee | 115 ----------------- lib/app-capitano.coffee | 3 +- lib/preparser.ts | 1 + lib/utils/docker-coffee.d.ts | 8 +- tests/commands/help.spec.ts | 19 ++- typings/balena-sync/index.d.ts | 12 ++ 11 files changed, 235 insertions(+), 128 deletions(-) create mode 100644 lib/actions-oclif/scan.ts create mode 100644 lib/actions/local/common.d.ts delete mode 100644 lib/actions/scan.coffee diff --git a/automation/capitanodoc/capitanodoc.ts b/automation/capitanodoc/capitanodoc.ts index 43cb8548..40d22be8 100644 --- a/automation/capitanodoc/capitanodoc.ts +++ b/automation/capitanodoc/capitanodoc.ts @@ -82,7 +82,7 @@ const capitanoDoc = { { title: 'Network', files: [ - 'build/actions/scan.js', + 'build/actions-oclif/scan.js', 'build/actions/ssh.js', 'build/actions/tunnel.js', ], diff --git a/doc/cli.markdown b/doc/cli.markdown index 1687b970..1d334b6b 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -1182,6 +1182,7 @@ Only show system logs. This can be used in combination with --service. ## scan +Scan for balenaOS devices on your local network. Examples: @@ -1191,13 +1192,13 @@ Examples: ### Options -#### --verbose, -v +#### -v, --verbose -Display full info +display full info -#### --timeout, -t <timeout> +#### -t, --timeout TIMEOUT -Scan timeout in seconds +scan timeout in seconds ## ssh <applicationOrDevice> [serviceName] diff --git a/lib/actions-oclif/scan.ts b/lib/actions-oclif/scan.ts new file mode 100644 index 00000000..a80143fc --- /dev/null +++ b/lib/actions-oclif/scan.ts @@ -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 = { + 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(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.`; +} diff --git a/lib/actions/index.coffee b/lib/actions/index.coffee index 7691d2d1..f0f65e7c 100644 --- a/lib/actions/index.coffee +++ b/lib/actions/index.coffee @@ -22,7 +22,6 @@ module.exports = tags: require('./tags') logs: require('./logs') local: require('./local') - scan: require('./scan') help: require('./help') os: require('./os') config: require('./config') diff --git a/lib/actions/local/common.d.ts b/lib/actions/local/common.d.ts new file mode 100644 index 00000000..cdc6f0c0 --- /dev/null +++ b/lib/actions/local/common.d.ts @@ -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; diff --git a/lib/actions/scan.coffee b/lib/actions/scan.coffee deleted file mode 100644 index 5a0a3a9e..00000000 --- a/lib/actions/scan.coffee +++ /dev/null @@ -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)) diff --git a/lib/app-capitano.coffee b/lib/app-capitano.coffee index 6da5ada9..521d6dc2 100644 --- a/lib/app-capitano.coffee +++ b/lib/app-capitano.coffee @@ -107,7 +107,6 @@ capitano.command(actions.ssh.ssh) # ---------- Local balenaOS Module ---------- capitano.command(actions.local.configure) capitano.command(actions.local.flash) -capitano.command(actions.scan) # ---------- Public utils ---------- capitano.command(actions.util.availableDrives) @@ -135,7 +134,7 @@ exports.run = (argv) -> # cmdSignature is literally a string like, for example: # "push " # ("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 '*' cmdSignature = command.signature.toString() events.trackCommand(cmdSignature) diff --git a/lib/preparser.ts b/lib/preparser.ts index 4a8855a5..00648a8d 100644 --- a/lib/preparser.ts +++ b/lib/preparser.ts @@ -143,6 +143,7 @@ export const convertedCommands = [ 'os:configure', 'settings', 'version', + 'scan', ]; /** diff --git a/lib/utils/docker-coffee.d.ts b/lib/utils/docker-coffee.d.ts index e46f8f89..a3491f03 100644 --- a/lib/utils/docker-coffee.d.ts +++ b/lib/utils/docker-coffee.d.ts @@ -30,8 +30,14 @@ export interface BuildDockerOptions { timeout?: number; } +export interface DockerToolbeltOpts { + host: string; + port: number; + timeout?: number; +} + export function getDocker( options: BuildDockerOptions, ): Bluebird; -export function createClient(options: BuildDockerOptions): DockerToolbelt; +export function createClient(opts: DockerToolbeltOpts): DockerToolbelt; diff --git a/tests/commands/help.spec.ts b/tests/commands/help.spec.ts index a37b2889..afa831b1 100644 --- a/tests/commands/help.spec.ts +++ b/tests/commands/help.spec.ts @@ -27,7 +27,7 @@ Primary commands: deploy [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 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() { - it('should print simple help text', async () => { + it('should list primary command summaries', async () => { 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( cleanOutput([ SIMPLE_HELP, @@ -106,7 +117,7 @@ describe('balena help', function() { 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'); expect(cleanOutput(out)).to.deep.equal( @@ -118,7 +129,7 @@ describe('balena help', function() { 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(''); expect(cleanOutput(out)).to.deep.equal( diff --git a/typings/balena-sync/index.d.ts b/typings/balena-sync/index.d.ts index 7d8af80a..f797aa2d 100644 --- a/typings/balena-sync/index.d.ts +++ b/typings/balena-sync/index.d.ts @@ -20,9 +20,21 @@ declare module 'balena-sync' { export function capitano(tool: 'balena-cli'): CommandDefinition; + export interface LocalBalenaOsDevice { + address: string; + host: string; + port: number; + } + declare namespace forms { export function selectLocalBalenaOsDevice( timeout?: number, ): Promise; } + + declare namespace discover { + export function discoverLocalBalenaOsDevices( + timeout?: number, + ): Promise; + } }