mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-06-16 06:48:08 +00:00
Update commands ssh, tunnel to support orgs
Change-type: patch Connects-to: #2119 Signed-off-by: Scott Lowe <scott@balena.io>
This commit is contained in:
@ -1768,7 +1768,7 @@ produce JSON output instead of tabular output
|
|||||||
Start a shell on a local or remote device. If a service name is not provided,
|
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.
|
a shell will be opened on the host OS.
|
||||||
|
|
||||||
If an application name is provided, an interactive menu will be presented
|
If an application is provided, an interactive menu will be presented
|
||||||
for the selection of an online device. A shell will then be opened for the
|
for the selection of an online device. A shell will then be opened for the
|
||||||
host OS or service container of the chosen device.
|
host OS or service container of the chosen device.
|
||||||
|
|
||||||
@ -1803,7 +1803,7 @@ Examples:
|
|||||||
|
|
||||||
#### APPLICATIONORDEVICE
|
#### APPLICATIONORDEVICE
|
||||||
|
|
||||||
application name, device uuid, or address of local device
|
application name/slug/id, device uuid, or address of local device
|
||||||
|
|
||||||
#### SERVICE
|
#### SERVICE
|
||||||
|
|
||||||
@ -1818,15 +1818,15 @@ hostname. Otherwise, port number for the balenaCloud gateway (default 22).
|
|||||||
|
|
||||||
#### -t, --tty
|
#### -t, --tty
|
||||||
|
|
||||||
Force pseudo-terminal allocation (bypass TTY autodetection for stdin)
|
force pseudo-terminal allocation (bypass TTY autodetection for stdin)
|
||||||
|
|
||||||
#### -v, --verbose
|
#### -v, --verbose
|
||||||
|
|
||||||
Increase verbosity
|
increase verbosity
|
||||||
|
|
||||||
#### --noproxy
|
#### --noproxy
|
||||||
|
|
||||||
Bypass global proxy configuration for the ssh connection
|
bypass global proxy configuration for the ssh connection
|
||||||
|
|
||||||
## tunnel <deviceOrApplication>
|
## tunnel <deviceOrApplication>
|
||||||
|
|
||||||
@ -1863,7 +1863,7 @@ Examples:
|
|||||||
|
|
||||||
#### DEVICEORAPPLICATION
|
#### DEVICEORAPPLICATION
|
||||||
|
|
||||||
device uuid or application name/id
|
device uuid or application name/slug/id
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
|
@ -19,11 +19,7 @@ import { flags } from '@oclif/command';
|
|||||||
import Command from '../command';
|
import Command from '../command';
|
||||||
import * as cf from '../utils/common-flags';
|
import * as cf from '../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||||
import {
|
import { parseAsInteger, validateLocalHostnameOrIp } from '../utils/validation';
|
||||||
parseAsInteger,
|
|
||||||
validateDotLocalUrl,
|
|
||||||
validateIPAddress,
|
|
||||||
} from '../utils/validation';
|
|
||||||
import * as BalenaSdk from 'balena-sdk';
|
import * as BalenaSdk from 'balena-sdk';
|
||||||
|
|
||||||
interface FlagsDef {
|
interface FlagsDef {
|
||||||
@ -39,14 +35,14 @@ interface ArgsDef {
|
|||||||
service?: string;
|
service?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class NoteCmd extends Command {
|
export default class SshCmd extends Command {
|
||||||
public static description = stripIndent`
|
public static description = stripIndent`
|
||||||
SSH into the host or application container of a device.
|
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,
|
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.
|
a shell will be opened on the host OS.
|
||||||
|
|
||||||
If an application name is provided, an interactive menu will be presented
|
If an application is provided, an interactive menu will be presented
|
||||||
for the selection of an online device. A shell will then be opened for the
|
for the selection of an online device. A shell will then be opened for the
|
||||||
host OS or service container of the chosen device.
|
host OS or service container of the chosen device.
|
||||||
|
|
||||||
@ -81,7 +77,8 @@ export default class NoteCmd extends Command {
|
|||||||
public static args = [
|
public static args = [
|
||||||
{
|
{
|
||||||
name: 'applicationOrDevice',
|
name: 'applicationOrDevice',
|
||||||
description: 'application name, device uuid, or address of local device',
|
description:
|
||||||
|
'application name/slug/id, device uuid, or address of local device',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -104,17 +101,17 @@ export default class NoteCmd extends Command {
|
|||||||
tty: flags.boolean({
|
tty: flags.boolean({
|
||||||
default: false,
|
default: false,
|
||||||
description:
|
description:
|
||||||
'Force pseudo-terminal allocation (bypass TTY autodetection for stdin)',
|
'force pseudo-terminal allocation (bypass TTY autodetection for stdin)',
|
||||||
char: 't',
|
char: 't',
|
||||||
}),
|
}),
|
||||||
verbose: flags.boolean({
|
verbose: flags.boolean({
|
||||||
default: false,
|
default: false,
|
||||||
description: 'Increase verbosity',
|
description: 'increase verbosity',
|
||||||
char: 'v',
|
char: 'v',
|
||||||
}),
|
}),
|
||||||
noproxy: flags.boolean({
|
noproxy: flags.boolean({
|
||||||
default: false,
|
default: false,
|
||||||
description: 'Bypass global proxy configuration for the ssh connection',
|
description: 'bypass global proxy configuration for the ssh connection',
|
||||||
}),
|
}),
|
||||||
help: cf.help,
|
help: cf.help,
|
||||||
};
|
};
|
||||||
@ -123,14 +120,11 @@ export default class NoteCmd extends Command {
|
|||||||
|
|
||||||
public async run() {
|
public async run() {
|
||||||
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
const { args: params, flags: options } = this.parse<FlagsDef, ArgsDef>(
|
||||||
NoteCmd,
|
SshCmd,
|
||||||
);
|
);
|
||||||
|
|
||||||
// if we're doing a direct SSH connection locally...
|
// Local connection
|
||||||
if (
|
if (validateLocalHostnameOrIp(params.applicationOrDevice)) {
|
||||||
validateDotLocalUrl(params.applicationOrDevice) ||
|
|
||||||
validateIPAddress(params.applicationOrDevice)
|
|
||||||
) {
|
|
||||||
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
|
const { performLocalDeviceSSH } = await import('../utils/device/ssh');
|
||||||
return await performLocalDeviceSSH({
|
return await performLocalDeviceSSH({
|
||||||
address: params.applicationOrDevice,
|
address: params.applicationOrDevice,
|
||||||
@ -141,26 +135,27 @@ export default class NoteCmd extends Command {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remote connection
|
||||||
const { getProxyConfig, which } = await import('../utils/helpers');
|
const { getProxyConfig, which } = await import('../utils/helpers');
|
||||||
const { checkLoggedIn, getOnlineTargetUuid } = await import(
|
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
|
||||||
'../utils/patterns'
|
|
||||||
);
|
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
|
|
||||||
const proxyConfig = getProxyConfig();
|
const proxyConfig = getProxyConfig();
|
||||||
const useProxy = !!proxyConfig && !options.noproxy;
|
const useProxy = !!proxyConfig && !options.noproxy;
|
||||||
|
|
||||||
// this will be a tunnelled SSH connection...
|
// this will be a tunnelled SSH connection...
|
||||||
await checkLoggedIn();
|
await Command.checkLoggedIn();
|
||||||
const uuid = await getOnlineTargetUuid(sdk, params.applicationOrDevice);
|
const deviceUuid = await getOnlineTargetDeviceUuid(
|
||||||
let version: string | undefined;
|
sdk,
|
||||||
let id: number | undefined;
|
params.applicationOrDevice,
|
||||||
|
);
|
||||||
|
|
||||||
const device = await sdk.models.device.get(uuid, {
|
const device = await sdk.models.device.get(deviceUuid, {
|
||||||
$select: ['id', 'supervisor_version', 'is_online'],
|
$select: ['id', 'supervisor_version', 'is_online'],
|
||||||
});
|
});
|
||||||
id = device.id;
|
|
||||||
version = device.supervisor_version;
|
const deviceId = device.id;
|
||||||
|
const supervisorVersion = device.supervisor_version;
|
||||||
|
|
||||||
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
|
const [whichProxytunnel, username, proxyUrl] = await Promise.all([
|
||||||
useProxy ? which('proxytunnel', false) : undefined,
|
useProxy ? which('proxytunnel', false) : undefined,
|
||||||
@ -207,20 +202,13 @@ export default class NoteCmd extends Command {
|
|||||||
|
|
||||||
const proxyCommand = useProxy ? getSshProxyCommand() : undefined;
|
const proxyCommand = useProxy ? getSshProxyCommand() : undefined;
|
||||||
|
|
||||||
if (username == null) {
|
// At this point, we have a long uuid of a device
|
||||||
const { ExpectedError } = await import('../errors');
|
|
||||||
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
|
// that we know exists and is accessible
|
||||||
let containerId: string | undefined;
|
let containerId: string | undefined;
|
||||||
if (params.service != null) {
|
if (params.service != null) {
|
||||||
containerId = await this.getContainerId(
|
containerId = await this.getContainerId(
|
||||||
sdk,
|
sdk,
|
||||||
uuid,
|
deviceUuid,
|
||||||
params.service,
|
params.service,
|
||||||
{
|
{
|
||||||
port: options.port,
|
port: options.port,
|
||||||
@ -228,20 +216,20 @@ export default class NoteCmd extends Command {
|
|||||||
proxyUrl: proxyUrl || '',
|
proxyUrl: proxyUrl || '',
|
||||||
username: username!,
|
username: username!,
|
||||||
},
|
},
|
||||||
version,
|
supervisorVersion,
|
||||||
id,
|
deviceId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let accessCommand: string;
|
let accessCommand: string;
|
||||||
if (containerId != null) {
|
if (containerId != null) {
|
||||||
accessCommand = `enter ${uuid} ${containerId}`;
|
accessCommand = `enter ${deviceUuid} ${containerId}`;
|
||||||
} else {
|
} else {
|
||||||
accessCommand = `host ${uuid}`;
|
accessCommand = `host ${deviceUuid}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const command = this.generateVpnSshCommand({
|
const command = this.generateVpnSshCommand({
|
||||||
uuid,
|
uuid: deviceUuid,
|
||||||
command: accessCommand,
|
command: accessCommand,
|
||||||
verbose: options.verbose,
|
verbose: options.verbose,
|
||||||
port: options.port,
|
port: options.port,
|
||||||
|
@ -24,11 +24,7 @@ import {
|
|||||||
} from '../errors';
|
} from '../errors';
|
||||||
import * as cf from '../utils/common-flags';
|
import * as cf from '../utils/common-flags';
|
||||||
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
import { getBalenaSdk, stripIndent } from '../utils/lazy';
|
||||||
import { getOnlineTargetUuid } from '../utils/patterns';
|
import type { Server, Socket } from 'net';
|
||||||
import * as _ from 'lodash';
|
|
||||||
import { tunnelConnectionToDevice } from '../utils/tunnel';
|
|
||||||
import { createServer, Server, Socket } from 'net';
|
|
||||||
import { IArg } from '@oclif/parser/lib/args';
|
|
||||||
|
|
||||||
interface FlagsDef {
|
interface FlagsDef {
|
||||||
port: string[];
|
port: string[];
|
||||||
@ -73,10 +69,10 @@ export default class TunnelCmd extends Command {
|
|||||||
'$ balena tunnel myApp -p 8080:3000 -p 8081:9000',
|
'$ balena tunnel myApp -p 8080:3000 -p 8081:9000',
|
||||||
];
|
];
|
||||||
|
|
||||||
public static args: Array<IArg<any>> = [
|
public static args = [
|
||||||
{
|
{
|
||||||
name: 'deviceOrApplication',
|
name: 'deviceOrApplication',
|
||||||
description: 'device uuid or application name/id',
|
description: 'device uuid or application name/slug/id',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -101,8 +97,7 @@ export default class TunnelCmd extends Command {
|
|||||||
TunnelCmd,
|
TunnelCmd,
|
||||||
);
|
);
|
||||||
|
|
||||||
const Logger = await import('../utils/logger');
|
const logger = await Command.getLogger();
|
||||||
const logger = Logger.getLogger();
|
|
||||||
const sdk = getBalenaSdk();
|
const sdk = getBalenaSdk();
|
||||||
|
|
||||||
const logConnection = (
|
const logConnection = (
|
||||||
@ -127,23 +122,30 @@ export default class TunnelCmd extends Command {
|
|||||||
throw new NoPortsDefinedError();
|
throw new NoPortsDefinedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const uuid = await getOnlineTargetUuid(sdk, params.deviceOrApplication);
|
// Ascertain device uuid
|
||||||
|
const { getOnlineTargetDeviceUuid } = await import('../utils/patterns');
|
||||||
|
const uuid = await getOnlineTargetDeviceUuid(
|
||||||
|
sdk,
|
||||||
|
params.deviceOrApplication,
|
||||||
|
);
|
||||||
const device = await sdk.models.device.get(uuid);
|
const device = await sdk.models.device.get(uuid);
|
||||||
|
|
||||||
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
|
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
|
||||||
|
|
||||||
|
const _ = await import('lodash');
|
||||||
const localListeners = _.chain(options.port)
|
const localListeners = _.chain(options.port)
|
||||||
.map((mapping) => {
|
.map((mapping) => {
|
||||||
return this.parsePortMapping(mapping);
|
return this.parsePortMapping(mapping);
|
||||||
})
|
})
|
||||||
.map(async ({ localPort, localAddress, remotePort }) => {
|
.map(async ({ localPort, localAddress, remotePort }) => {
|
||||||
try {
|
try {
|
||||||
|
const { tunnelConnectionToDevice } = await import('../utils/tunnel');
|
||||||
const handler = await tunnelConnectionToDevice(
|
const handler = await tunnelConnectionToDevice(
|
||||||
device.uuid,
|
device.uuid,
|
||||||
remotePort,
|
remotePort,
|
||||||
sdk,
|
sdk,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { createServer } = await import('net');
|
||||||
const server = createServer(async (client: Socket) => {
|
const server = createServer(async (client: Socket) => {
|
||||||
try {
|
try {
|
||||||
await handler(client);
|
await handler(client);
|
||||||
|
@ -26,7 +26,8 @@ import { getBalenaSdk, getVisuals, stripIndent, getCliForm } from './lazy';
|
|||||||
import validation = require('./validation');
|
import validation = require('./validation');
|
||||||
import { delay } from './helpers';
|
import { delay } from './helpers';
|
||||||
import { isV13 } from './version';
|
import { isV13 } from './version';
|
||||||
import { Organization } from 'balena-sdk';
|
import type { Application, Device, Organization } from 'balena-sdk';
|
||||||
|
import { getApplication } from './sdk';
|
||||||
|
|
||||||
export function authenticate(options: {}): Promise<void> {
|
export function authenticate(options: {}): Promise<void> {
|
||||||
const balena = getBalenaSdk();
|
const balena = getBalenaSdk();
|
||||||
@ -329,99 +330,94 @@ export function inferOrSelectDevice(preferredUuid: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getApplicationByIdOrName(
|
/*
|
||||||
sdk: BalenaSdk.BalenaSDK,
|
* Given applicationOrDevice, which may be
|
||||||
idOrName: string,
|
* - an application name
|
||||||
) {
|
* - an application slug
|
||||||
if (validation.looksLikeInteger(idOrName)) {
|
* - an application id (integer)
|
||||||
try {
|
* - a device uuid
|
||||||
return await sdk.models.application.get(Number(idOrName));
|
* Either:
|
||||||
} catch (error) {
|
* - in case of device uuid, return uuid of device after verifying that it exists and is online.
|
||||||
const { BalenaApplicationNotFound } = await import('balena-errors');
|
* - in case of application, return uuid of device user selects from list of online devices.
|
||||||
if (!instanceOf(error, BalenaApplicationNotFound)) {
|
*
|
||||||
throw error;
|
* TODO: Modify this when app IDs dropped.
|
||||||
}
|
*/
|
||||||
}
|
export async function getOnlineTargetDeviceUuid(
|
||||||
}
|
|
||||||
return await sdk.models.application.get(idOrName);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getOnlineTargetUuid(
|
|
||||||
sdk: BalenaSdk.BalenaSDK,
|
sdk: BalenaSdk.BalenaSDK,
|
||||||
applicationOrDevice: string,
|
applicationOrDevice: string,
|
||||||
) {
|
) {
|
||||||
// applicationOrDevice can be:
|
const logger = (await import('../utils/logger')).getLogger();
|
||||||
// * an application name
|
|
||||||
// * an application ID (integer)
|
|
||||||
// * a device uuid
|
|
||||||
const Logger = await import('../utils/logger');
|
|
||||||
const logger = Logger.getLogger();
|
|
||||||
const appTest = validation.validateApplicationName(applicationOrDevice);
|
|
||||||
const uuidTest = validation.validateUuid(applicationOrDevice);
|
|
||||||
|
|
||||||
if (!appTest && !uuidTest) {
|
// If looks like UUID, probably device
|
||||||
throw new ExpectedError(
|
if (validation.validateUuid(applicationOrDevice)) {
|
||||||
`Device or application not found: ${applicationOrDevice}`,
|
let device: Device;
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we have a definite device UUID...
|
|
||||||
if (uuidTest && !appTest) {
|
|
||||||
logger.logDebug(
|
|
||||||
`Fetching device by UUID ${applicationOrDevice} (${typeof applicationOrDevice})`,
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
await sdk.models.device.get(applicationOrDevice, {
|
|
||||||
$select: ['uuid'],
|
|
||||||
$filter: { is_online: true },
|
|
||||||
})
|
|
||||||
).uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, it may be a device OR an application...
|
|
||||||
try {
|
try {
|
||||||
logger.logDebug(
|
logger.logDebug(
|
||||||
`Fetching application by ID or name ${applicationOrDevice} (${typeof applicationOrDevice})`,
|
`Trying to fetch device by UUID ${applicationOrDevice} (${typeof applicationOrDevice})`,
|
||||||
);
|
);
|
||||||
const app = await getApplicationByIdOrName(sdk, applicationOrDevice);
|
device = await sdk.models.device.get(applicationOrDevice, {
|
||||||
|
$select: ['uuid', 'is_online'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!device.is_online) {
|
||||||
|
throw new ExpectedError(
|
||||||
|
`Device with UUID ${applicationOrDevice} is offline`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return device.uuid;
|
||||||
|
} catch (err) {
|
||||||
|
const { BalenaDeviceNotFound } = await import('balena-errors');
|
||||||
|
if (instanceOf(err, BalenaDeviceNotFound)) {
|
||||||
|
logger.logDebug(`Device with UUID ${applicationOrDevice} not found`);
|
||||||
|
// Now try app
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a device UUID, try app
|
||||||
|
let app: Application;
|
||||||
|
try {
|
||||||
|
logger.logDebug(
|
||||||
|
`Trying to fetch application by name/slug/ID: ${applicationOrDevice}`,
|
||||||
|
);
|
||||||
|
app = await getApplication(sdk, applicationOrDevice);
|
||||||
|
} catch (err) {
|
||||||
|
const { BalenaApplicationNotFound } = await import('balena-errors');
|
||||||
|
if (instanceOf(err, BalenaApplicationNotFound)) {
|
||||||
|
throw new ExpectedError(
|
||||||
|
`Application or Device not found: ${applicationOrDevice}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App found, load its devices
|
||||||
const devices = await sdk.models.device.getAllByApplication(app.id, {
|
const devices = await sdk.models.device.getAllByApplication(app.id, {
|
||||||
|
$select: ['device_name', 'uuid'],
|
||||||
$filter: { is_online: true },
|
$filter: { is_online: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Throw if no devices online
|
||||||
if (_.isEmpty(devices)) {
|
if (_.isEmpty(devices)) {
|
||||||
throw new ExpectedError('No accessible devices are online');
|
throw new ExpectedError(
|
||||||
|
`Application ${app.slug} found, but has no devices online.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await getCliForm().ask({
|
// Ask user to select from online devices for application
|
||||||
message: 'Select a device',
|
return getCliForm().ask({
|
||||||
|
message: `Select a device on application ${app.slug}`,
|
||||||
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, 7)})`,
|
||||||
0,
|
|
||||||
7,
|
|
||||||
)})`,
|
|
||||||
value: device.uuid,
|
value: device.uuid,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
const { BalenaApplicationNotFound } = await import('balena-errors');
|
|
||||||
if (!instanceOf(err, BalenaApplicationNotFound)) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
logger.logDebug(`Application not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// it wasn't an application, maybe it's a device...
|
|
||||||
logger.logDebug(
|
|
||||||
`Fetching device by UUID ${applicationOrDevice} (${typeof applicationOrDevice})`,
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
await sdk.models.device.get(applicationOrDevice, {
|
|
||||||
$select: ['uuid'],
|
|
||||||
$filter: { is_online: true },
|
|
||||||
})
|
|
||||||
).uuid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectFromList<T>(
|
export function selectFromList<T>(
|
||||||
|
@ -162,6 +162,7 @@ function sshErrorMessage(exitSignal?: string, exitCode?: number) {
|
|||||||
msg.push(`
|
msg.push(`
|
||||||
Are the SSH keys correctly configured in balenaCloud? See:
|
Are the SSH keys correctly configured in balenaCloud? See:
|
||||||
https://www.balena.io/docs/learn/manage/ssh-access/#add-an-ssh-key-to-balenacloud`);
|
https://www.balena.io/docs/learn/manage/ssh-access/#add-an-ssh-key-to-balenacloud`);
|
||||||
|
msg.push('Are you accidentally using `sudo`?');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return msg.join('\n');
|
return msg.join('\n');
|
||||||
|
@ -185,6 +185,7 @@ export class BalenaAPIMock extends NockMock {
|
|||||||
public expectGetDevice(opts: {
|
public expectGetDevice(opts: {
|
||||||
fullUUID: string;
|
fullUUID: string;
|
||||||
inaccessibleApp?: boolean;
|
inaccessibleApp?: boolean;
|
||||||
|
isOnline?: boolean;
|
||||||
optional?: boolean;
|
optional?: boolean;
|
||||||
persist?: boolean;
|
persist?: boolean;
|
||||||
}) {
|
}) {
|
||||||
@ -194,6 +195,7 @@ export class BalenaAPIMock extends NockMock {
|
|||||||
{
|
{
|
||||||
id,
|
id,
|
||||||
uuid: opts.fullUUID,
|
uuid: opts.fullUUID,
|
||||||
|
is_online: opts.isOnline,
|
||||||
belongs_to__application: opts.inaccessibleApp
|
belongs_to__application: opts.inaccessibleApp
|
||||||
? []
|
? []
|
||||||
: [{ app_name: 'test' }],
|
: [{ app_name: 'test' }],
|
||||||
|
@ -66,11 +66,11 @@ describe('balena ssh', function () {
|
|||||||
itSS('should succeed (mocked, device UUID)', async () => {
|
itSS('should succeed (mocked, device UUID)', async () => {
|
||||||
const deviceUUID = 'abc1234';
|
const deviceUUID = 'abc1234';
|
||||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||||
api.expectGetApplication({ notFound: true });
|
api.expectGetDevice({ fullUUID: deviceUUID, isOnline: true });
|
||||||
api.expectGetDevice({ fullUUID: deviceUUID });
|
|
||||||
mockedExitCode = 0;
|
mockedExitCode = 0;
|
||||||
|
|
||||||
const { err, out } = await runCommand(`ssh ${deviceUUID}`);
|
const { err, out } = await runCommand(`ssh ${deviceUUID}`);
|
||||||
|
|
||||||
expect(err).to.be.empty;
|
expect(err).to.be.empty;
|
||||||
expect(out).to.be.empty;
|
expect(out).to.be.empty;
|
||||||
});
|
});
|
||||||
@ -90,8 +90,7 @@ describe('balena ssh', function () {
|
|||||||
'Warning: ssh process exited with non-zero code "255"',
|
'Warning: ssh process exited with non-zero code "255"',
|
||||||
];
|
];
|
||||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||||
api.expectGetApplication({ notFound: true });
|
api.expectGetDevice({ fullUUID: deviceUUID, isOnline: true });
|
||||||
api.expectGetDevice({ fullUUID: deviceUUID });
|
|
||||||
mockedExitCode = 255;
|
mockedExitCode = 255;
|
||||||
|
|
||||||
const { err, out } = await runCommand(`ssh ${deviceUUID}`);
|
const { err, out } = await runCommand(`ssh ${deviceUUID}`);
|
||||||
@ -114,6 +113,19 @@ describe('balena ssh', function () {
|
|||||||
expect(cleanOutput(err, true)).to.include.members(expectedErrLines);
|
expect(cleanOutput(err, true)).to.include.members(expectedErrLines);
|
||||||
expect(out).to.be.empty;
|
expect(out).to.be.empty;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fail if device not online (mocked, device UUID)', async () => {
|
||||||
|
const deviceUUID = 'abc1234';
|
||||||
|
const expectedErrLines = ['Device with UUID abc1234 is offline'];
|
||||||
|
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||||
|
api.expectGetDevice({ fullUUID: deviceUUID, isOnline: false });
|
||||||
|
mockedExitCode = 0;
|
||||||
|
|
||||||
|
const { err, out } = await runCommand(`ssh ${deviceUUID}`);
|
||||||
|
|
||||||
|
expect(cleanOutput(err, true)).to.include.members(expectedErrLines);
|
||||||
|
expect(out).to.be.empty;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Check whether the 'ssh' tool (executable) exists in the PATH */
|
/** Check whether the 'ssh' tool (executable) exists in the PATH */
|
||||||
|
Reference in New Issue
Block a user