Add ability to push to remote device UUID

Change-type: minor
Signed-off-by: Josh Bowling <josh@balena.io>
This commit is contained in:
Josh Bowling 2021-10-12 10:45:49 +09:00
parent cc60e86507
commit 1de95a5f6c
4 changed files with 121 additions and 7 deletions

View File

@ -2913,12 +2913,14 @@ Examples:
$ balena push 23c73a1.local --system
$ balena push 23c73a1.local --system --service my-service
$ balena push 9f0cc5e4-0707-487c-ba84-edd0c36ff1c9
### Arguments
#### FLEETORDEVICE
fleet name or slug, or local device IP address or ".local" hostname
fleet name or slug, device UUID, or local device IP address or ".local" hostname
### Options

View File

@ -112,13 +112,15 @@ export default class PushCmd extends Command {
'',
'$ balena push 23c73a1.local --system',
'$ balena push 23c73a1.local --system --service my-service',
'',
'$ balena push 9f0cc5e4-0707-487c-ba84-edd0c36ff1c9',
];
public static args = [
{
name: 'fleetOrDevice',
description:
'fleet name or slug, or local device IP address or ".local" hostname',
'fleet name or slug, device UUID, or local device IP address or ".local" hostname',
required: true,
parse: lowercaseIfSlug,
},
@ -315,7 +317,7 @@ export default class PushCmd extends Command {
break;
case BuildTarget.Device:
logger.logDebug(`Pushing to local device: ${params.fleetOrDevice}`);
logger.logDebug(`Pushing to device: ${params.fleetOrDevice}`);
await this.pushToDevice(
params.fleetOrDevice,
options,
@ -442,9 +444,9 @@ export default class PushCmd extends Command {
}
protected async getBuildTarget(appOrDevice: string): Promise<BuildTarget> {
const { validateLocalHostnameOrIp } = await import('../utils/validation');
const { validateDeviceAddress } = await import('../utils/validation');
return validateLocalHostnameOrIp(appOrDevice)
return validateDeviceAddress(appOrDevice)
? BuildTarget.Device
: BuildTarget.Cloud;
}

View File

@ -40,7 +40,10 @@ import { DeviceAPI, DeviceInfo } from './api';
import * as LocalPushErrors from './errors';
import LivepushManager from './live';
import { displayBuildLog } from './logs';
import { stripIndent } from '../lazy';
import { getBalenaSdk, stripIndent } from '../lazy';
import { validateIPAddress } from '../validation';
import { Server, Socket } from 'net';
import { BalenaSDK, Device } from 'balena-sdk';
const LOCAL_APPNAME = 'localapp';
const LOCAL_RELEASEHASH = 'localrelease';
@ -121,7 +124,110 @@ async function environmentFromInput(
return ret;
}
const logConnection = (
logger: Logger,
fromHost: string,
fromPort: number,
localAddress: string,
localPort: number,
deviceAddress: string,
devicePort: number,
err?: Error,
) => {
const logMessage = `${fromHost}:${fromPort} => ${localAddress}:${localPort} ===> ${deviceAddress}:${devicePort}`;
if (err) {
logger.logError(`${logMessage} :: ${err.message}`);
} else {
logger.logLogs(logMessage);
}
};
async function openTunnel(
logger: Logger,
device: Device,
sdk: BalenaSDK,
port: number,
): Promise<any> {
const localhost = 'localhost';
try {
const { tunnelConnectionToDevice } = await import('../tunnel');
const handler = await tunnelConnectionToDevice(device.uuid, port, sdk);
const { createServer } = await import('net');
const server = createServer(async (client: Socket) => {
try {
await handler(client);
logConnection(
logger,
client.remoteAddress || '',
client.remotePort || 0,
client.localAddress,
client.localPort,
device.vpn_address || '',
port,
);
} catch (err: any) {
logConnection(
logger,
client.remoteAddress || '',
client.remotePort || 0,
client.localAddress,
client.localPort,
device.vpn_address || '',
port,
err,
);
}
});
await new Promise<Server>((resolve, reject) => {
server.on('error', reject);
server.listen(port, localhost, () => {
resolve(server);
});
});
logger.logInfo(
` - tunnelling ${localhost}:${port} to ${device.uuid}:${port}`,
);
// opts.deviceHost = localhost;
} catch (err: any) {
logger.logWarn(
` - tunnel failed ${localhost}:${port} to ${
device.uuid
}:${port}, failed ${JSON.stringify(err.message)}`,
);
}
}
export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
// Can only communicate with device using IP if local
const isLocal =
opts.deviceHost.includes('.local') || validateIPAddress(opts.deviceHost)
? true
: false;
if (!isLocal) {
// 1. Open tunnel from remote device to localhost
// 2. Deploy to localhost
const logger = Logger.getLogger();
const sdk = getBalenaSdk();
// Ascertain device uuid
const { getOnlineTargetDeviceUuid } = await import('../patterns');
const uuid = await getOnlineTargetDeviceUuid(sdk, opts.deviceHost);
const device = await sdk.models.device.get(uuid);
logger.logInfo(`Opening a tunnel to ${device.uuid}...`);
await openTunnel(logger, device, sdk, 48484);
await openTunnel(logger, device, sdk, 2375);
logger.logInfo('Opened tunnels to supervisor and docker...');
opts.deviceHost = 'localhost';
}
// Resolve .local addresses to IP to avoid
// issue with Windows and rapid repeat lookups.
// see: https://github.com/balena-io/balena-cli/issues/1518
@ -145,7 +251,7 @@ export async function deployToDevice(opts: DeviceDeployOptions): Promise<void> {
throw new ExpectedError(stripIndent`
Could not communicate with device supervisor at address ${opts.deviceHost}:${port}.
Device may not have local mode enabled. Check with:
balena device local-mode <device-uuid>
balena device local-mode <device-uuid>
`);
}

View File

@ -57,6 +57,10 @@ export function validateDotLocalUrl(input: string): boolean {
return DOTLOCAL_REGEX.test(input);
}
export function validateDeviceAddress(input: string): boolean {
return validateLocalHostnameOrIp(input) || validateUuid(input);
}
export function validateLocalHostnameOrIp(input: string): boolean {
return validateIPAddress(input) || validateDotLocalUrl(input);
}