diff --git a/automation/capitanodoc/capitanodoc.ts b/automation/capitanodoc/capitanodoc.ts index 37d53143..6854bf6d 100644 --- a/automation/capitanodoc/capitanodoc.ts +++ b/automation/capitanodoc/capitanodoc.ts @@ -107,7 +107,7 @@ const capitanoDoc = { title: 'Network', files: [ 'build/actions-oclif/scan.js', - 'build/actions/ssh.js', + 'build/actions-oclif/ssh.js', 'build/actions/tunnel.js', ], }, diff --git a/doc/cli.markdown b/doc/cli.markdown index 1196c9b1..c9169a54 100644 --- a/doc/cli.markdown +++ b/doc/cli.markdown @@ -213,7 +213,7 @@ Users are encouraged to regularly update the balena CLI to the latest version. - Network - [scan](#scan) - - [ssh <applicationOrDevice> [serviceName]](#ssh-applicationordevice-servicename) + - [ssh <applicationordevice> [servicename]](#ssh-applicationordevice-servicename) - [tunnel <deviceOrApplication>](#tunnel-deviceorapplication) - Notes @@ -1368,9 +1368,8 @@ scan timeout in seconds ## ssh <applicationOrDevice> [serviceName] -This command can be used to start a shell on a local or remote device. - -If a service name is not provided, a shell will be opened on the host OS. +Start a shell on a local or remote device. If a service name is not provided, +a shell will be opened on the host OS. If an application name is provided, an interactive menu will be presented for the selection of an online device. A shell will then be opened for the @@ -1382,33 +1381,49 @@ is initiated directly to balenaOS on port `22222` via an openssh-compatible client. Otherwise, any connection initiated remotely traverses the balenaCloud VPN. -Examples: - balena ssh MyApp +Commands may be piped to the standard input for remote execution (see examples). +Note however that remote command execution on service containers (as opposed to +the host OS) is not currently possible when a device UUID is used (instead of +an IP address) because of a balenaCloud backend limitation. - balena ssh f49cefd - balena ssh f49cefd my-service - balena ssh f49cefd --port - - balena ssh 192.168.0.1 --verbose - balena ssh f49cefd.local my-service - -Warning: `balena ssh` requires an openssh-compatible client to be correctly +Note: `balena ssh` requires an openssh-compatible client to be correctly installed in your shell environment. For more information (including Windows support) please check: - https://github.com/balena-io/balena-cli/blob/master/INSTALL.md#additional-dependencies + https://github.com/balena-io/balena-cli/blob/master/INSTALL.md#additional-dependencies, + +Examples: + + $ balena ssh MyApp + $ balena ssh f49cefd + $ balena ssh f49cefd my-service + $ balena ssh f49cefd --port + $ balena ssh 192.168.0.1 --verbose + $ balena ssh f49cefd.local my-service + $ echo "uptime; exit;" | balena ssh f49cefd + $ echo "uptime; exit;" | balena ssh 192.168.0.1 myService + +### Arguments + +#### APPLICATIONORDEVICE + +application name, device uuid, or address of local device + +#### SERVICENAME + +service name, if connecting to a container ### Options -#### --port, -p <port> +#### -p, --port PORT SSH server port number (default 22222) if the target is an IP address or .local hostname. Otherwise, port number for the balenaCloud gateway (default 22). -#### --tty, -t +#### -t, --tty Force pseudo-terminal allocation (bypass TTY autodetection for stdin) -#### --verbose, -v +#### -v, --verbose Increase verbosity diff --git a/lib/actions-oclif/ssh.ts b/lib/actions-oclif/ssh.ts new file mode 100644 index 00000000..f9250ee2 --- /dev/null +++ b/lib/actions-oclif/ssh.ts @@ -0,0 +1,393 @@ +/** + * @license + * Copyright 2016-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 Command from '../command'; +import * as cf from '../utils/common-flags'; +import { getBalenaSdk, stripIndent } from '../utils/lazy'; +import { + parseAsInteger, + validateDotLocalUrl, + validateIPAddress, +} from '../utils/validation'; +import * as BalenaSdk from 'balena-sdk'; + +interface FlagsDef { + port?: number; + tty: boolean; + verbose: boolean; + noproxy: boolean; + help: void; +} + +interface ArgsDef { + applicationOrDevice: string; + serviceName?: string; +} + +export default class NoteCmd extends Command { + public static description = stripIndent` + SSH into the host or application container of a device. + + Start a shell on a local or remote device. If a service name is not provided, + a shell will be opened on the host OS. + + If an application name is provided, an interactive menu will be presented + for the selection of an online device. A shell will then be opened for the + host OS or service container of the chosen device. + + For local devices, the IP address and .local domain name are supported. + If the device is referenced by IP or \`.local\` address, the connection + is initiated directly to balenaOS on port \`22222\` via an + openssh-compatible client. Otherwise, any connection initiated remotely + traverses the balenaCloud VPN. + + Commands may be piped to the standard input for remote execution (see examples). + Note however that remote command execution on service containers (as opposed to + the host OS) is not currently possible when a device UUID is used (instead of + an IP address) because of a balenaCloud backend limitation. + + Note: \`balena ssh\` requires an openssh-compatible client to be correctly + installed in your shell environment. For more information (including Windows + support) please check: + https://github.com/balena-io/balena-cli/blob/master/INSTALL.md#additional-dependencies, + `; + + public static examples = [ + '$ balena ssh MyApp', + '$ balena ssh f49cefd', + '$ balena ssh f49cefd my-service', + '$ balena ssh f49cefd --port ', + '$ balena ssh 192.168.0.1 --verbose', + '$ balena ssh f49cefd.local my-service', + '$ echo "uptime; exit;" | balena ssh f49cefd', + '$ echo "uptime; exit;" | balena ssh 192.168.0.1 myService', + ]; + + public static args = [ + { + name: 'applicationOrDevice', + description: 'application name, device uuid, or address of local device', + required: true, + }, + { + name: 'serviceName', + description: 'service name, if connecting to a container', + required: false, + }, + ]; + + public static usage = 'ssh [serviceName]'; + + public static flags: flags.Input = { + port: flags.integer({ + description: stripIndent` + SSH server port number (default 22222) if the target is an IP address or .local + hostname. Otherwise, port number for the balenaCloud gateway (default 22).`, + char: 'p', + parse: (p) => parseAsInteger(p, 'port'), + }), + tty: flags.boolean({ + description: + 'Force pseudo-terminal allocation (bypass TTY autodetection for stdin)', + char: 't', + }), + verbose: flags.boolean({ + description: 'Increase verbosity', + char: 'v', + }), + noproxy: flags.boolean({ + description: 'Bypass global proxy configuration for the ssh connection', + }), + help: cf.help, + }; + + public static primary = true; + + public async run() { + const { args: params, flags: options } = this.parse( + NoteCmd, + ); + + const { ExpectedError } = await import('../errors'); + const { getProxyConfig, which } = await import('../utils/helpers'); + const { checkLoggedIn, getOnlineTargetUuid } = await import( + '../utils/patterns' + ); + const { spawnSshAndExitOnError } = await import('../utils/ssh'); + const sdk = getBalenaSdk(); + + const proxyConfig = getProxyConfig(); + const useProxy = !!proxyConfig && !options.noproxy; + + // if we're doing a direct SSH connection locally... + if ( + validateDotLocalUrl(params.applicationOrDevice) || + validateIPAddress(params.applicationOrDevice) + ) { + const { performLocalDeviceSSH } = await import('../utils/device/ssh'); + return await performLocalDeviceSSH({ + address: params.applicationOrDevice, + port: options.port, + forceTTY: options.tty, + verbose: options.verbose, + service: params.serviceName, + }); + } + + // this will be a tunnelled SSH connection... + await checkLoggedIn(); + const uuid = await getOnlineTargetUuid(sdk, params.applicationOrDevice); + let version: string | undefined; + let id: number | undefined; + + const device = await sdk.models.device.get(uuid, { + $select: ['id', 'supervisor_version', 'is_online'], + }); + id = device.id; + version = device.supervisor_version; + + const [whichProxytunnel, username, proxyUrl] = await Promise.all([ + useProxy ? which('proxytunnel', false) : undefined, + sdk.auth.whoami(), + // note that `proxyUrl` refers to the balenaCloud "resin-proxy" + // service, currently "balena-devices.com", rather than some + // local proxy server URL + sdk.settings.get('proxyUrl'), + ]); + + const getSshProxyCommand = () => { + if (!proxyConfig) { + return; + } + if (!whichProxytunnel) { + console.warn(stripIndent` + Proxy is enabled but the \`proxytunnel\` binary cannot be found. + Please install it if you want to route the \`balena ssh\` requests through the proxy. + Alternatively you can pass \`--noproxy\` param to the \`balena ssh\` command to ignore the proxy config + for the \`ssh\` requests. + + Attempting the unproxied request for now.`); + return; + } + + const p = proxyConfig; + if (p.username && p.password) { + // proxytunnel understands these variables for proxy authentication. + // Setting the variables instead of command-line options avoids the + // need for shell-specific escaping of special characters like '$'. + process.env.PROXYUSER = p.username; + process.env.PROXYPASS = p.password; + } + + return [ + 'proxytunnel', + `--proxy=${p.host}:${p.port}`, + // ssh replaces these %h:%p variables in the ProxyCommand option + // https://linux.die.net/man/5/ssh_config + '--dest=%h:%p', + ...(options.verbose ? ['--verbose'] : []), + ]; + }; + + const proxyCommand = useProxy ? getSshProxyCommand() : undefined; + + if (username == null) { + throw new ExpectedError( + `Opening an SSH connection to a remote device requires you to be logged in.`, + ); + } + + // At this point, we have a long uuid with a device + // that we know exists and is accessible + let containerId: string | undefined; + if (params.serviceName != null) { + containerId = await this.getContainerId( + sdk, + uuid, + params.serviceName, + { + port: options.port, + proxyCommand, + proxyUrl: proxyUrl || '', + username: username!, + }, + version, + id, + ); + } + + let accessCommand: string; + if (containerId != null) { + accessCommand = `enter ${uuid} ${containerId}`; + } else { + accessCommand = `host ${uuid}`; + } + + const command = this.generateVpnSshCommand({ + uuid, + command: accessCommand, + verbose: options.verbose, + port: options.port, + proxyCommand, + proxyUrl: proxyUrl || '', + username: username!, + }); + + return spawnSshAndExitOnError(command); + } + + async getContainerId( + sdk: BalenaSdk.BalenaSDK, + uuid: string, + serviceName: string, + sshOpts: { + port?: number; + proxyCommand?: string[]; + proxyUrl: string; + username: string; + }, + version?: string, + id?: number, + ): Promise { + const semver = await import('balena-semver'); + + if (version == null || id == null) { + const device = await sdk.models.device.get(uuid, { + $select: ['id', 'supervisor_version'], + }); + version = device.supervisor_version; + id = device.id; + } + + let containerId: string | undefined; + if (semver.gte(version, '8.6.0')) { + const apiUrl = await sdk.settings.get('apiUrl'); + // TODO: Move this into the SDKs device model + const request = await sdk.request.send({ + method: 'POST', + url: '/supervisor/v2/containerId', + baseUrl: apiUrl, + body: { + method: 'GET', + deviceId: id, + }, + }); + if (request.status !== 200) { + throw new Error( + `There was an error connecting to device ${uuid}, HTTP response code: ${request.status}.`, + ); + } + const body = request.body; + if (body.status !== 'success') { + throw new Error( + `There was an error communicating with device ${uuid}.\n\tError: ${body.message}`, + ); + } + containerId = body.services[serviceName]; + } else { + console.error(stripIndent` + Using legacy method to detect container ID. This will be slow. + To speed up this process, please update your device to an OS + which has a supervisor version of at least v8.6.0. + `); + // We need to execute a balena ps command on the device, + // and parse the output, looking for a specific + // container + const childProcess = await import('child_process'); + const escapeRegex = await import('lodash/escapeRegExp'); + const { which } = await import('../utils/helpers'); + const { deviceContainerEngineBinary } = await import( + '../utils/device/ssh' + ); + + const sshBinary = await which('ssh'); + const sshArgs = this.generateVpnSshCommand({ + uuid, + verbose: false, + port: sshOpts.port, + command: `host ${uuid} "${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`, + proxyCommand: sshOpts.proxyCommand, + proxyUrl: sshOpts.proxyUrl, + username: sshOpts.username, + }); + + if (process.env.DEBUG) { + console.error(`[debug] [${sshBinary}, ${sshArgs.join(', ')}]`); + } + const subProcess = childProcess.spawn(sshBinary, sshArgs, { + stdio: [null, 'pipe', null], + }); + const containers = await new Promise((resolve, reject) => { + const output: string[] = []; + subProcess.stdout.on('data', (chunk) => output.push(chunk.toString())); + subProcess.on('close', (code: number) => { + if (code !== 0) { + reject( + new Error( + `Non-zero error code when looking for service container: ${code}`, + ), + ); + } else { + resolve(output.join('')); + } + }); + }); + + const lines = containers.split('\n'); + const regex = new RegExp(`\\/?${escapeRegex(serviceName)}_\\d+_\\d+`); + for (const container of lines) { + const [cId, name] = container.split(' '); + if (regex.test(name)) { + containerId = cId; + break; + } + } + } + + if (containerId == null) { + throw new Error( + `Could not find a service ${serviceName} on device ${uuid}.`, + ); + } + return containerId; + } + + generateVpnSshCommand(opts: { + uuid: string; + command: string; + verbose: boolean; + port?: number; + username: string; + proxyUrl: string; + proxyCommand?: string[]; + }) { + return [ + ...(opts.verbose ? ['-vvv'] : []), + '-t', + ...['-o', 'LogLevel=ERROR'], + ...['-o', 'StrictHostKeyChecking=no'], + ...['-o', 'UserKnownHostsFile=/dev/null'], + ...(opts.proxyCommand && opts.proxyCommand.length + ? ['-o', `ProxyCommand=${opts.proxyCommand.join(' ')}`] + : []), + ...(opts.port ? ['-p', opts.port.toString()] : []), + `${opts.username}@ssh.${opts.proxyUrl}`, + opts.command, + ]; + } +} diff --git a/lib/actions/help_ts.ts b/lib/actions/help_ts.ts index 8d3cd3d3..6534a8f5 100644 --- a/lib/actions/help_ts.ts +++ b/lib/actions/help_ts.ts @@ -55,7 +55,11 @@ function getCmdUsageDescriptionLinePair(cmd: typeof Command): [string, string] { // same effect as the 's' regex flag which is only supported by Node 9+ const matches = /\s*([^]+?)\n[^]*/.exec(cmd.description || ''); if (matches && matches.length > 1) { - description = _.lowerFirst(_.trimEnd(matches[1], '.')); + description = _.trimEnd(matches[1], '.'); + // Only do .lowerFirst() if the second char is not uppercase (e.g. for 'SSH'); + if (description[1] !== description[1]?.toUpperCase()) { + description = _.lowerFirst(description); + } } return [usage, description]; } diff --git a/lib/actions/index.ts b/lib/actions/index.ts index 7732f13c..fff9c849 100644 --- a/lib/actions/index.ts +++ b/lib/actions/index.ts @@ -21,7 +21,6 @@ export * as local from './local'; export * as logs from './logs'; export * as os from './os'; export * as push from './push'; -export * as ssh from './ssh'; export * as tunnel from './tunnel'; export * as util from './util'; diff --git a/lib/actions/ssh.ts b/lib/actions/ssh.ts deleted file mode 100644 index fc3f8e32..00000000 --- a/lib/actions/ssh.ts +++ /dev/null @@ -1,368 +0,0 @@ -/* -Copyright 2016-2020 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 type * as BalenaSdk from 'balena-sdk'; -import type { CommandDefinition } from 'capitano'; - -import { getBalenaSdk, stripIndent } from '../utils/lazy'; -import { validateDotLocalUrl, validateIPAddress } from '../utils/validation'; - -async function getContainerId( - sdk: BalenaSdk.BalenaSDK, - uuid: string, - serviceName: string, - sshOpts: { - port?: number; - proxyCommand?: string[]; - proxyUrl: string; - username: string; - }, - version?: string, - id?: number, -): Promise { - const semver = await import('balena-semver'); - - if (version == null || id == null) { - const device = await sdk.models.device.get(uuid, { - $select: ['id', 'supervisor_version'], - }); - version = device.supervisor_version; - id = device.id; - } - - let containerId: string | undefined; - if (semver.gte(version, '8.6.0')) { - const apiUrl = await sdk.settings.get('apiUrl'); - // TODO: Move this into the SDKs device model - const request = await sdk.request.send({ - method: 'POST', - url: '/supervisor/v2/containerId', - baseUrl: apiUrl, - body: { - method: 'GET', - deviceId: id, - }, - }); - if (request.status !== 200) { - throw new Error( - `There was an error connecting to device ${uuid}, HTTP response code: ${request.status}.`, - ); - } - const body = request.body; - if (body.status !== 'success') { - throw new Error( - `There was an error communicating with device ${uuid}.\n\tError: ${body.message}`, - ); - } - containerId = body.services[serviceName]; - } else { - console.error(stripIndent` - Using legacy method to detect container ID. This will be slow. - To speed up this process, please update your device to an OS - which has a supervisor version of at least v8.6.0. - `); - // We need to execute a balena ps command on the device, - // and parse the output, looking for a specific - // container - const childProcess = await import('child_process'); - const escapeRegex = await import('lodash/escapeRegExp'); - const { which } = await import('../utils/helpers'); - const { deviceContainerEngineBinary } = await import('../utils/device/ssh'); - - const sshBinary = await which('ssh'); - const sshArgs = generateVpnSshCommand({ - uuid, - verbose: false, - port: sshOpts.port, - command: `host ${uuid} "${deviceContainerEngineBinary}" ps --format "{{.ID}} {{.Names}}"`, - proxyCommand: sshOpts.proxyCommand, - proxyUrl: sshOpts.proxyUrl, - username: sshOpts.username, - }); - - if (process.env.DEBUG) { - console.error(`[debug] [${sshBinary}, ${sshArgs.join(', ')}]`); - } - const subprocess = childProcess.spawn(sshBinary, sshArgs, { - stdio: [null, 'pipe', null], - }); - const containers = await new Promise((resolve, reject) => { - const output: string[] = []; - subprocess.stdout.on('data', (chunk) => output.push(chunk.toString())); - subprocess.on('close', (code: number) => { - if (code !== 0) { - reject( - new Error( - `Non-zero error code when looking for service container: ${code}`, - ), - ); - } else { - resolve(output.join('')); - } - }); - }); - - const lines = containers.split('\n'); - const regex = new RegExp(`\\/?${escapeRegex(serviceName)}_\\d+_\\d+`); - for (const container of lines) { - const [cId, name] = container.split(' '); - if (regex.test(name)) { - containerId = cId; - break; - } - } - } - - if (containerId == null) { - throw new Error( - `Could not find a service ${serviceName} on device ${uuid}.`, - ); - } - return containerId; -} - -function generateVpnSshCommand(opts: { - uuid: string; - command: string; - verbose: boolean; - port?: number; - username: string; - proxyUrl: string; - proxyCommand?: string[]; -}) { - return [ - ...(opts.verbose ? ['-vvv'] : []), - '-t', - ...['-o', 'LogLevel=ERROR'], - ...['-o', 'StrictHostKeyChecking=no'], - ...['-o', 'UserKnownHostsFile=/dev/null'], - ...(opts.proxyCommand && opts.proxyCommand.length - ? ['-o', `ProxyCommand=${opts.proxyCommand.join(' ')}`] - : []), - ...(opts.port ? ['-p', opts.port.toString()] : []), - `${opts.username}@ssh.${opts.proxyUrl}`, - opts.command, - ]; -} - -export const ssh: CommandDefinition< - { - applicationOrDevice: string; - // when Capitano converts a positional parameter (but not an option) - // to a number, the original value is preserved with the _raw suffix - applicationOrDevice_raw: string; - serviceName?: string; - }, - { - port: string; - service: string; - tty: boolean; - verbose: true | undefined; - noProxy: boolean; - } -> = { - signature: 'ssh [serviceName]', - description: 'SSH into the host or application container of a device', - primary: true, - help: stripIndent` - This command can be used to start a shell on a local or remote device. - - If a service name is not provided, a shell will be opened on the host OS. - - If an application name is provided, an interactive menu will be presented - for the selection of an online device. A shell will then be opened for the - host OS or service container of the chosen device. - - For local devices, the IP address and .local domain name are supported. - If the device is referenced by IP or \`.local\` address, the connection - is initiated directly to balenaOS on port \`22222\` via an - openssh-compatible client. Otherwise, any connection initiated remotely - traverses the balenaCloud VPN. - - Examples: - balena ssh MyApp - - balena ssh f49cefd - balena ssh f49cefd my-service - balena ssh f49cefd --port - - balena ssh 192.168.0.1 --verbose - balena ssh f49cefd.local my-service - - Warning: \`balena ssh\` requires an openssh-compatible client to be correctly - installed in your shell environment. For more information (including Windows - support) please check: - https://github.com/balena-io/balena-cli/blob/master/INSTALL.md#additional-dependencies`, - options: [ - { - signature: 'port', - parameter: 'port', - description: stripIndent` - SSH server port number (default 22222) if the target is an IP address or .local - hostname. Otherwise, port number for the balenaCloud gateway (default 22).`, - alias: 'p', - }, - { - signature: 'tty', - boolean: true, - description: - 'Force pseudo-terminal allocation (bypass TTY autodetection for stdin)', - alias: 't', - }, - { - signature: 'verbose', - boolean: true, - description: 'Increase verbosity', - alias: 'v', - }, - { - signature: 'noproxy', - boolean: true, - description: 'Bypass global proxy configuration for the ssh connection', - }, - ], - action: async (params, options) => { - const applicationOrDevice = - params.applicationOrDevice_raw || params.applicationOrDevice; - const { ExpectedError } = await import('../errors'); - const { getProxyConfig, which } = await import('../utils/helpers'); - const { checkLoggedIn, getOnlineTargetUuid } = await import( - '../utils/patterns' - ); - const { spawnSshAndExitOnError } = await import('../utils/ssh'); - const sdk = getBalenaSdk(); - - const verbose = options.verbose === true; - const proxyConfig = getProxyConfig(); - const useProxy = !!proxyConfig && !options.noProxy; - const port = options.port != null ? parseInt(options.port, 10) : undefined; - - // if we're doing a direct SSH connection locally... - if ( - validateDotLocalUrl(applicationOrDevice) || - validateIPAddress(applicationOrDevice) - ) { - const { performLocalDeviceSSH } = await import('../utils/device/ssh'); - return await performLocalDeviceSSH({ - address: applicationOrDevice, - port, - forceTTY: options.tty === true, - verbose, - service: params.serviceName, - }); - } - - // this will be a tunnelled SSH connection... - await checkLoggedIn(); - const uuid = await getOnlineTargetUuid(sdk, applicationOrDevice); - let version: string | undefined; - let id: number | undefined; - - const device = await sdk.models.device.get(uuid, { - $select: ['id', 'supervisor_version', 'is_online'], - }); - id = device.id; - version = device.supervisor_version; - - const [whichProxytunnel, username, proxyUrl] = await Promise.all([ - useProxy ? which('proxytunnel', false) : undefined, - sdk.auth.whoami(), - // note that `proxyUrl` refers to the balenaCloud "resin-proxy" - // service, currently "balena-devices.com", rather than some - // local proxy server URL - sdk.settings.get('proxyUrl'), - ]); - - const getSshProxyCommand = () => { - if (!proxyConfig) { - return; - } - if (!whichProxytunnel) { - console.warn(stripIndent` - Proxy is enabled but the \`proxytunnel\` binary cannot be found. - Please install it if you want to route the \`balena ssh\` requests through the proxy. - Alternatively you can pass \`--noproxy\` param to the \`balena ssh\` command to ignore the proxy config - for the \`ssh\` requests. - - Attempting the unproxied request for now.`); - return; - } - - const p = proxyConfig; - if (p.username && p.password) { - // proxytunnel understands these variables for proxy authentication. - // Setting the variables instead of command-line options avoids the - // need for shell-specific escaping of special characters like '$'. - process.env.PROXYUSER = p.username; - process.env.PROXYPASS = p.password; - } - - return [ - 'proxytunnel', - `--proxy=${p.host}:${p.port}`, - // ssh replaces these %h:%p variables in the ProxyCommand option - // https://linux.die.net/man/5/ssh_config - '--dest=%h:%p', - ...(verbose ? ['--verbose'] : []), - ]; - }; - - const proxyCommand = useProxy ? getSshProxyCommand() : undefined; - - if (username == null) { - throw new ExpectedError( - `Opening an SSH connection to a remote device requires you to be logged in.`, - ); - } - - // At this point, we have a long uuid with a device - // that we know exists and is accessible - let containerId: string | undefined; - if (params.serviceName != null) { - containerId = await getContainerId( - sdk, - uuid, - params.serviceName, - { - port, - proxyCommand, - proxyUrl: proxyUrl || '', - username: username!, - }, - version, - id, - ); - } - - let accessCommand: string; - if (containerId != null) { - accessCommand = `enter ${uuid} ${containerId}`; - } else { - accessCommand = `host ${uuid}`; - } - - const command = generateVpnSshCommand({ - uuid, - command: accessCommand, - verbose, - port, - proxyCommand, - proxyUrl: proxyUrl || '', - username: username!, - }); - - return spawnSshAndExitOnError(command); - }, -}; diff --git a/lib/app-capitano.js b/lib/app-capitano.js index c095e8bb..f0fa7873 100644 --- a/lib/app-capitano.js +++ b/lib/app-capitano.js @@ -72,9 +72,6 @@ capitano.command(actions.tunnel.tunnel); // ---------- Preload Module ---------- capitano.command(actions.preload); -// ---------- SSH Module ---------- -capitano.command(actions.ssh.ssh); - // ---------- Local balenaOS Module ---------- capitano.command(actions.local.configure); capitano.command(actions.local.flash); diff --git a/lib/errors.ts b/lib/errors.ts index 88d5d561..8fc5479a 100644 --- a/lib/errors.ts +++ b/lib/errors.ts @@ -141,6 +141,7 @@ const EXPECTED_ERROR_REGEXES = [ /^Unexpected argument/, // oclif parser: UnexpectedArgsError /to be one of/, // oclif parser: FlagInvalidOptionError, ArgInvalidOptionError /must also be provided when using/, // oclif parser (depends-on) + /^Expected an integer/, // oclif parser (flags.integer) ]; // Support unit testing of handleError diff --git a/lib/preparser.ts b/lib/preparser.ts index 6a9b6913..703fb282 100644 --- a/lib/preparser.ts +++ b/lib/preparser.ts @@ -171,6 +171,7 @@ export const convertedCommands = [ 'os:configure', 'scan', 'settings', + 'ssh', 'tags', 'tag:rm', 'tag:set', diff --git a/tests/commands/help.spec.ts b/tests/commands/help.spec.ts index 54b6d6d1..dc78e019 100644 --- a/tests/commands/help.spec.ts +++ b/tests/commands/help.spec.ts @@ -10,7 +10,7 @@ Primary commands: login login to balena push Start a remote build on the balena cloud build servers or a local mode device logs show device logs - ssh [serviceName] SSH into the host or application container of a device + ssh [servicename] SSH into the host or application container of a device apps list all applications app display information about a single application devices list all devices diff --git a/tests/errors.spec.ts b/tests/errors.spec.ts index 4212b743..7fed2016 100644 --- a/tests/errors.spec.ts +++ b/tests/errors.spec.ts @@ -125,6 +125,7 @@ describe('handleError() function', () => { 'Unexpected arguments', // oclif 'to be one of', // oclif 'must also be provided when using', // oclif + 'Expected an integer', // oclif ]; messagesToMatch.forEach((message) => {