From b97565d2e7732ae636fdc6ccd035448dbc8b466a Mon Sep 17 00:00:00 2001 From: Cameron Diver Date: Wed, 24 Apr 2019 10:06:53 +0100 Subject: [PATCH 1/6] refactor: Create and use validation functions for input This includes IP address, application name and dotlocal url parsing. Change-type: patch Signed-off-by: Cameron Diver --- lib/actions/push.ts | 13 ++++++------- lib/utils/validation.ts | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/actions/push.ts b/lib/actions/push.ts index 81e09bb4..5c71e963 100644 --- a/lib/actions/push.ts +++ b/lib/actions/push.ts @@ -19,11 +19,10 @@ import { CommandDefinition } from 'capitano'; import { stripIndent } from 'common-tags'; import { registrySecretsHelp } from '../utils/messages'; - -// An regex to detect an IP address, from https://www.regular-expressions.info/ip.html -const IP_REGEX = new RegExp( - /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/, -); +import { + validateApplicationName, + validateIPAddress, +} from '../utils/validation'; enum BuildTarget { Cloud, @@ -32,11 +31,11 @@ enum BuildTarget { function getBuildTarget(appOrDevice: string): BuildTarget | null { // First try the application regex from the api - if (/^[a-zA-Z0-9_-]+$/.test(appOrDevice)) { + if (validateApplicationName(appOrDevice)) { return BuildTarget.Cloud; } - if (IP_REGEX.test(appOrDevice)) { + if (validateIPAddress(appOrDevice)) { return BuildTarget.Device; } diff --git a/lib/utils/validation.ts b/lib/utils/validation.ts index 194571b8..7b4a42eb 100644 --- a/lib/utils/validation.ts +++ b/lib/utils/validation.ts @@ -16,6 +16,13 @@ limitations under the License. import validEmail = require('@resin.io/valid-email'); +const APPNAME_REGEX = new RegExp(/^[a-zA-Z0-9_-]+$/); +// An regex to detect an IP address, from https://www.regular-expressions.info/ip.html +const IP_REGEX = new RegExp( + /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/, +); +const DOTLOCAL_REGEX = new RegExp(/^[a-zA-Z0-9-]+\.local$/); + export function validateEmail(input: string) { if (!validEmail(input)) { return 'Email is not valid'; @@ -37,5 +44,13 @@ export function validateApplicationName(input: string) { return 'The application name should be at least 4 characters'; } - return true; + return APPNAME_REGEX.test(input); +} + +export function validateIPAddress(input: string): boolean { + return IP_REGEX.test(input); +} + +export function validateDotLocalUrl(input: string): boolean { + return DOTLOCAL_REGEX.test(input); } From 4676396b5f402dfaf974707eba070637537747fe Mon Sep 17 00:00:00 2001 From: Cameron Diver Date: Wed, 24 Apr 2019 11:31:45 +0100 Subject: [PATCH 2/6] logs: Make device logs consistent across the CLI Change-type: patch Signed-off-by: Cameron Diver --- lib/actions/logs.coffee | 13 +++++++++++-- lib/utils/cloud.ts | 21 +++++++++++++++++++++ lib/utils/device/logs.ts | 37 ++++++++++++++++++++----------------- 3 files changed, 52 insertions(+), 19 deletions(-) create mode 100644 lib/utils/cloud.ts diff --git a/lib/actions/logs.coffee b/lib/actions/logs.coffee index 02fa4642..4ca1aaff 100644 --- a/lib/actions/logs.coffee +++ b/lib/actions/logs.coffee @@ -45,10 +45,19 @@ module.exports = normalizeUuidProp(params) balena = require('balena-sdk').fromSharedOptions() moment = require('moment') + { serviceIdToName } = require('../utils/cloud') + { displayLogObject } = require('../utils/device/logs') + Logger = require('../utils/logger') + + logger = new Logger() printLine = (line) -> - timestamp = moment(line.timestamp).format('DD.MM.YY HH:mm:ss (ZZ)') - console.log("#{timestamp} #{line.message}") + if not line.isSystem + serviceIdToName(balena, line.serviceId).then (serviceName) -> + line.serviceName = serviceName + displayLogObject(line, logger) + else + displayLogObject(line, logger) if options.tail balena.logs.subscribe(params.uuid, { count: 100 }).then (logs) -> diff --git a/lib/utils/cloud.ts b/lib/utils/cloud.ts new file mode 100644 index 00000000..f10dd5c7 --- /dev/null +++ b/lib/utils/cloud.ts @@ -0,0 +1,21 @@ +import { BalenaSDK } from 'balena-sdk'; +import memoize = require('lodash/memoize'); + +export const serviceIdToName = memoize( + async (sdk: BalenaSDK, serviceId: number): Promise => { + const serviceName = await sdk.pine.get({ + resource: 'service', + id: serviceId, + options: { + $select: 'service_name', + }, + }); + + if (serviceName != null) { + return serviceName.service_name; + } + return; + }, + // Memoize the call based on service id + (_sdk, id) => id.toString(), +); diff --git a/lib/utils/device/logs.ts b/lib/utils/device/logs.ts index 5f38789b..792b7b96 100644 --- a/lib/utils/device/logs.ts +++ b/lib/utils/device/logs.ts @@ -51,28 +51,31 @@ export function displayBuildLog(log: BuildLog, logger: Logger): void { function displayLogLine(log: string | Buffer, logger: Logger): void { try { const obj: Log = JSON.parse(log.toString()); - - let toPrint: string; - if (obj.timestamp != null) { - toPrint = `[${new Date(obj.timestamp).toLocaleString()}]`; - } else { - toPrint = `[${new Date().toLocaleString()}]`; - } - - if (obj.serviceName != null) { - const colourFn = getServiceColourFn(obj.serviceName); - - toPrint += ` ${colourFn(`[${obj.serviceName}]`)}`; - } - - toPrint += ` ${obj.message}`; - - logger.logLogs(toPrint); + displayLogObject(obj, logger); } catch (e) { logger.logDebug(`Dropping device log due to failed parsing: ${e}`); } } +export function displayLogObject(obj: Log, logger: Logger): void { + let toPrint: string; + if (obj.timestamp != null) { + toPrint = `[${new Date(obj.timestamp).toLocaleString()}]`; + } else { + toPrint = `[${new Date().toLocaleString()}]`; + } + + if (obj.serviceName != null) { + const colourFn = getServiceColourFn(obj.serviceName); + + toPrint += ` ${colourFn(`[${obj.serviceName}]`)}`; + } + + toPrint += ` ${obj.message}`; + + logger.logLogs(toPrint); +} + const getServiceColourFn = _.memoize(_getServiceColourFn); const colorHash = new ColorHash(); From d41fb72ded929bcbe42a309c89545ecd5b306022 Mon Sep 17 00:00:00 2001 From: Cameron Diver Date: Wed, 24 Apr 2019 11:59:30 +0100 Subject: [PATCH 3/6] refactor: Convert logs action to typescript Change-type: patch Signed-off-by: Cameron Diver --- lib/actions/logs.coffee | 70 ---------------------------- lib/actions/logs.ts | 101 ++++++++++++++++++++++++++++++++++++++++ lib/app.coffee | 2 +- 3 files changed, 102 insertions(+), 71 deletions(-) delete mode 100644 lib/actions/logs.coffee create mode 100644 lib/actions/logs.ts diff --git a/lib/actions/logs.coffee b/lib/actions/logs.coffee deleted file mode 100644 index 4ca1aaff..00000000 --- a/lib/actions/logs.coffee +++ /dev/null @@ -1,70 +0,0 @@ -### -Copyright 2016-2017 Balena - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -### - -{ normalizeUuidProp } = require('../utils/normalization') - -module.exports = - signature: 'logs ' - description: 'show device logs' - help: ''' - Use this command to show logs for a specific device. - - By default, the command prints all log messages and exit. - - To continuously stream output, and see new logs in real time, use the `--tail` option. - - Examples: - - $ balena logs 23c73a1 - $ balena logs 23c73a1 - ''' - options: [ - { - signature: 'tail' - description: 'continuously stream output' - boolean: true - alias: 't' - } - ] - permission: 'user' - primary: true - action: (params, options, done) -> - normalizeUuidProp(params) - balena = require('balena-sdk').fromSharedOptions() - moment = require('moment') - { serviceIdToName } = require('../utils/cloud') - { displayLogObject } = require('../utils/device/logs') - Logger = require('../utils/logger') - - logger = new Logger() - - printLine = (line) -> - if not line.isSystem - serviceIdToName(balena, line.serviceId).then (serviceName) -> - line.serviceName = serviceName - displayLogObject(line, logger) - else - displayLogObject(line, logger) - - if options.tail - balena.logs.subscribe(params.uuid, { count: 100 }).then (logs) -> - logs.on('line', printLine) - logs.on('error', done) - .catch(done) - else - balena.logs.history(params.uuid) - .each(printLine) - .catch(done) diff --git a/lib/actions/logs.ts b/lib/actions/logs.ts new file mode 100644 index 00000000..416f0ec7 --- /dev/null +++ b/lib/actions/logs.ts @@ -0,0 +1,101 @@ +/* +Copyright 2016-2019 Balena + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { BalenaSDK } from 'balena-sdk'; +import { CommandDefinition } from 'capitano'; +import { stripIndent } from 'common-tags'; + +import { normalizeUuidProp } from '../utils/normalization'; + +type CloudLog = + | { + isSystem: false; + serviceId: number; + timestamp: number; + message: string; + } + | { + isSystem: true; + timestamp: number; + message: string; + }; + +export const logs: CommandDefinition< + { + uuid: string; + }, + { tail: boolean } +> = { + signature: 'logs ', + description: 'show device logs', + help: stripIndent` + Use this command to show logs for a specific device. + + By default, the command prints all log messages and exit. + + To continuously stream output, and see new logs in real time, use the \`--tail\` option. + + Examples: + + $ balena logs 23c73a1 + $ balena logs 23c73a1`, + options: [ + { + signature: 'tail', + description: 'continuously stream output', + boolean: true, + alias: 't', + }, + ], + permission: 'user', + primary: true, + async action(params, options, done) { + normalizeUuidProp(params); + const balena = (await import('balena-sdk')).fromSharedOptions(); + const { serviceIdToName } = await import('../utils/cloud'); + const { displayLogObject } = await import('../utils/device/logs'); + const Logger = await import('../utils/logger'); + + const logger = new Logger(); + + const displayCloudLog = async (line: CloudLog) => { + if (!line.isSystem) { + let serviceName = await serviceIdToName(balena, line.serviceId); + if (serviceName == null) { + serviceName = 'Unknown service'; + } + displayLogObject({ serviceName, ...line }, logger); + } else { + displayLogObject(line, logger); + } + }; + + if (options.tail) { + return balena.logs + .subscribe(params.uuid, { count: 100 }) + .then(function(logStream) { + logStream.on('line', displayCloudLog); + logStream.on('error', done); + }) + .catch(done); + } else { + return balena.logs + .history(params.uuid) + .each(displayCloudLog) + .catch(done); + } + }, +}; diff --git a/lib/app.coffee b/lib/app.coffee index 62f4f596..99083be8 100644 --- a/lib/app.coffee +++ b/lib/app.coffee @@ -174,7 +174,7 @@ capitano.command(actions.config.generate) capitano.command(actions.settings.list) # ---------- Logs Module ---------- -capitano.command(actions.logs) +capitano.command(actions.logs.logs) # ---------- Sync Module ---------- capitano.command(actions.sync) From d412d39164f51d4504d60a1d7ba7594d067284b1 Mon Sep 17 00:00:00 2001 From: Cameron Diver Date: Wed, 24 Apr 2019 12:08:26 +0100 Subject: [PATCH 4/6] Add ability to use balena logs with a local mode device Change-type: patch Signed-off-by: Cameron Diver --- doc/cli.markdown | 9 ++++--- lib/actions/logs.ts | 58 ++++++++++++++++++++++++++++------------ lib/utils/device/logs.ts | 2 +- 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/doc/cli.markdown b/doc/cli.markdown index 9f523de2..b4d5ebc2 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -146,7 +146,7 @@ environment variable (in the same standard URL format). - Logs - - [logs <uuid>](#logs-uuid) + - [logs <uuidOrDevice>](#logs-uuidordevice) - Sync @@ -853,7 +853,7 @@ Examples: # Logs -## logs <uuid> +## logs <uuidOrDevice> Use this command to show logs for a specific device. @@ -861,10 +861,13 @@ By default, the command prints all log messages and exit. To continuously stream output, and see new logs in real time, use the `--tail` option. +If an IP address is passed to this command, the logs to a local mode device with +that address. Note that --tail is implied when this command is provided an IP address. + Examples: $ balena logs 23c73a1 - $ balena logs 23c73a1 + $ balena logs 192.168.0.31 ### Options diff --git a/lib/actions/logs.ts b/lib/actions/logs.ts index 416f0ec7..5d5157ff 100644 --- a/lib/actions/logs.ts +++ b/lib/actions/logs.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { BalenaSDK } from 'balena-sdk'; import { CommandDefinition } from 'capitano'; import { stripIndent } from 'common-tags'; @@ -35,11 +34,11 @@ type CloudLog = export const logs: CommandDefinition< { - uuid: string; + uuidOrDevice: string; }, { tail: boolean } > = { - signature: 'logs ', + signature: 'logs ', description: 'show device logs', help: stripIndent` Use this command to show logs for a specific device. @@ -48,10 +47,13 @@ export const logs: CommandDefinition< To continuously stream output, and see new logs in real time, use the \`--tail\` option. + If an IP address is passed to this command, the logs to a local mode device with + that address. Note that --tail is implied when this command is provided an IP address. + Examples: $ balena logs 23c73a1 - $ balena logs 23c73a1`, + $ balena logs 192.168.0.31`, options: [ { signature: 'tail', @@ -66,7 +68,11 @@ export const logs: CommandDefinition< normalizeUuidProp(params); const balena = (await import('balena-sdk')).fromSharedOptions(); const { serviceIdToName } = await import('../utils/cloud'); - const { displayLogObject } = await import('../utils/device/logs'); + const { displayDeviceLogs, displayLogObject } = await import( + '../utils/device/logs' + ); + const { validateIPAddress } = await import('../utils/validation'); + const { exitWithExpectedError } = await import('../utils/patterns'); const Logger = await import('../utils/logger'); const logger = new Logger(); @@ -83,19 +89,37 @@ export const logs: CommandDefinition< } }; - if (options.tail) { - return balena.logs - .subscribe(params.uuid, { count: 100 }) - .then(function(logStream) { - logStream.on('line', displayCloudLog); - logStream.on('error', done); - }) - .catch(done); + if (validateIPAddress(params.uuidOrDevice)) { + const { DeviceAPI } = await import('../utils/device/api'); + const deviceApi = new DeviceAPI(logger, params.uuidOrDevice); + logger.logDebug('Checking we can access device'); + try { + await deviceApi.ping(); + } catch (e) { + exitWithExpectedError( + new Error( + `Cannot access local mode device at address ${params.uuidOrDevice}`, + ), + ); + } + + const logStream = await deviceApi.getLogStream(); + displayDeviceLogs(logStream, logger); } else { - return balena.logs - .history(params.uuid) - .each(displayCloudLog) - .catch(done); + if (options.tail) { + return balena.logs + .subscribe(params.uuidOrDevice, { count: 100 }) + .then(function(logStream) { + logStream.on('line', displayCloudLog); + logStream.on('error', done); + }) + .catch(done); + } else { + return balena.logs + .history(params.uuidOrDevice) + .each(displayCloudLog) + .catch(done); + } } }, }; diff --git a/lib/utils/device/logs.ts b/lib/utils/device/logs.ts index 792b7b96..aac24917 100644 --- a/lib/utils/device/logs.ts +++ b/lib/utils/device/logs.ts @@ -57,7 +57,7 @@ function displayLogLine(log: string | Buffer, logger: Logger): void { } } -export function displayLogObject(obj: Log, logger: Logger): void { +export function displayLogObject(obj: T, logger: Logger): void { let toPrint: string; if (obj.timestamp != null) { toPrint = `[${new Date(obj.timestamp).toLocaleString()}]`; From 7b5272e92652028be2a1c6d542849222cd3d0667 Mon Sep 17 00:00:00 2001 From: Cameron Diver Date: Wed, 24 Apr 2019 12:47:46 +0100 Subject: [PATCH 5/6] Add tslint config to enable consistent lint process The lint configuration used seems to vary between build machines, and this is a bug in resin-lint. Until that's fixed, we provide another tslint which points to the resin-lint configuration. Signed-off-by: Cameron Diver --- .gitignore | 1 - tslint.json | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 tslint.json diff --git a/.gitignore b/.gitignore index 3184a91b..6b0791b8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,6 @@ package-lock.json .balenaconf resinrc.yml balenarc.yml -tslint.json .DS_Store .idea diff --git a/tslint.json b/tslint.json new file mode 100644 index 00000000..0be8c926 --- /dev/null +++ b/tslint.json @@ -0,0 +1,6 @@ +{ + "extends": "./node_modules/resin-lint/config/tslint-prettier.json", + "rules": { + "ignoreDefinitionFiles": false + } +} From f816cb4ce85992bc9147ab25990ded368b5ec00c Mon Sep 17 00:00:00 2001 From: Cameron Diver Date: Wed, 24 Apr 2019 13:04:49 +0100 Subject: [PATCH 6/6] Fix and update log documentation Signed-off-by: Cameron Diver --- doc/cli.markdown | 9 ++++++--- lib/actions/logs.ts | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/doc/cli.markdown b/doc/cli.markdown index b4d5ebc2..33336e7b 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -857,16 +857,19 @@ Examples: Use this command to show logs for a specific device. -By default, the command prints all log messages and exit. +By default, the command prints all log messages and exits. To continuously stream output, and see new logs in real time, use the `--tail` option. -If an IP address is passed to this command, the logs to a local mode device with -that address. Note that --tail is implied when this command is provided an IP address. +If an IP address is passed to this command, logs are displayed from +a local mode device with that address. Note that --tail is implied +when this command is provided an IP address. Examples: $ balena logs 23c73a1 + $ balena logs 23c73a1 --tail + $ balena logs 192.168.0.31 ### Options diff --git a/lib/actions/logs.ts b/lib/actions/logs.ts index 5d5157ff..1089a53b 100644 --- a/lib/actions/logs.ts +++ b/lib/actions/logs.ts @@ -43,16 +43,19 @@ export const logs: CommandDefinition< help: stripIndent` Use this command to show logs for a specific device. - By default, the command prints all log messages and exit. + By default, the command prints all log messages and exits. To continuously stream output, and see new logs in real time, use the \`--tail\` option. - If an IP address is passed to this command, the logs to a local mode device with - that address. Note that --tail is implied when this command is provided an IP address. + If an IP address is passed to this command, logs are displayed from + a local mode device with that address. Note that --tail is implied + when this command is provided an IP address. Examples: $ balena logs 23c73a1 + $ balena logs 23c73a1 --tail + $ balena logs 192.168.0.31`, options: [ {