balena-cli
Version:
The official balena Command Line Interface
326 lines • 12.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.join = join;
exports.leave = leave;
const errors_1 = require("../errors");
const lazy_1 = require("./lazy");
const patterns_1 = require("./patterns");
const ssh_1 = require("./ssh");
const MIN_BALENAOS_VERSION = 'v2.14.0';
async function join(logger, sdk, deviceHostnameOrIp, appName, appUpdatePollInterval) {
logger.logDebug('Determining device...');
deviceHostnameOrIp = deviceHostnameOrIp || (await selectLocalDevice());
await assertDeviceIsCompatible(deviceHostnameOrIp);
logger.logDebug(`Using device: ${deviceHostnameOrIp}`);
logger.logDebug('Determining device type...');
const deviceType = await getDeviceType(deviceHostnameOrIp);
logger.logDebug(`Device type: ${deviceType}`);
logger.logDebug('Determining fleet...');
const app = await getOrSelectApplication(sdk, deviceType, appName);
logger.logDebug(`Using fleet: ${app.app_name} (${app.is_for__device_type[0].slug})`);
if (app.is_for__device_type[0].slug !== deviceType) {
logger.logDebug(`Forcing device type to: ${deviceType}`);
app.is_for__device_type[0].slug = deviceType;
}
logger.logDebug('Determining device OS version...');
const deviceOsVersion = await getOsVersion(deviceHostnameOrIp);
logger.logDebug(`Device OS version: ${deviceOsVersion}`);
logger.logDebug('Generating fleet config...');
const config = await generateApplicationConfig(sdk, app, {
version: deviceOsVersion,
appUpdatePollInterval,
});
logger.logDebug(`Using config: ${JSON.stringify(config, null, 2)}`);
logger.logDebug('Configuring...');
await configure(deviceHostnameOrIp, config);
const platformUrl = await sdk.settings.get('balenaUrl');
logger.logSuccess(`Device successfully joined ${platformUrl}!`);
}
async function leave(logger, deviceHostnameOrIp) {
logger.logDebug('Determining device...');
deviceHostnameOrIp = deviceHostnameOrIp || (await selectLocalDevice());
await assertDeviceIsCompatible(deviceHostnameOrIp);
logger.logDebug(`Using device: ${deviceHostnameOrIp}`);
logger.logDebug('Deconfiguring...');
await deconfigure(deviceHostnameOrIp);
logger.logSuccess((0, lazy_1.stripIndent) `
Device successfully left the platform. The device will still be listed as part
of the fleet, but changes to the fleet will no longer affect the device and its
status will eventually be reported as 'Offline'. To irrecoverably delete the
device from the fleet, use the 'balena device rm' command or delete it through
the balenaCloud web dashboard.`);
}
async function execCommand(deviceIp, cmd, msg) {
const { Writable } = await Promise.resolve().then(() => require('stream'));
const visuals = (0, lazy_1.getVisuals)();
const spinner = new visuals.Spinner(`[${deviceIp}] Connecting...`);
const innerSpinner = spinner.spinner;
const stream = new Writable({
write(_chunk, _enc, callback) {
innerSpinner.setSpinnerTitle(`%s [${deviceIp}] ${msg}`);
callback();
},
});
spinner.start();
try {
await (0, ssh_1.getLocalDeviceCmdStdout)(deviceIp, cmd, stream);
}
finally {
spinner.stop();
}
}
async function configure(deviceIp, config) {
const json = JSON.stringify(config);
const b64 = Buffer.from(json).toString('base64');
const str = `"$(base64 -d <<< ${b64})"`;
await execCommand(deviceIp, `os-config join ${str}`, 'Configuring...');
}
async function deconfigure(deviceIp) {
await execCommand(deviceIp, 'os-config leave', 'Configuring...');
}
async function assertDeviceIsCompatible(deviceIp) {
const cmd = 'os-config --version';
try {
await (0, ssh_1.getLocalDeviceCmdStdout)(deviceIp, cmd);
}
catch (err) {
if (err instanceof errors_1.ExpectedError) {
throw err;
}
console.error(`${err}\n`);
throw new errors_1.ExpectedError((0, lazy_1.stripIndent) `
Failed to execute "${cmd}" on device "${deviceIp}".
Depending on more specific error messages above, this may mean that the device
is incompatible. Please ensure that the device is running a balenaOS release
newer than ${MIN_BALENAOS_VERSION}.`);
}
}
async function getDeviceType(deviceIp) {
const output = await (0, ssh_1.getDeviceOsRelease)(deviceIp);
const match = /^SLUG="([^"]+)"$/m.exec(output);
if (!match) {
throw new Error('Failed to determine device type');
}
return match[1];
}
async function getOsVersion(deviceIp) {
const output = await (0, ssh_1.getDeviceOsRelease)(deviceIp);
const match = /^VERSION_ID="([^"]+)"$/m.exec(output);
if (!match) {
throw new Error('Failed to determine OS version ID');
}
return match[1];
}
const dockerPort = 2375;
const dockerTimeout = 2000;
async function selectLocalBalenaOsDevice(timeout = 4000) {
const { discoverLocalBalenaOsDevices } = await Promise.resolve().then(() => require('../utils/discover'));
const { SpinnerPromise } = (0, lazy_1.getVisuals)();
const devices = await new SpinnerPromise({
promise: discoverLocalBalenaOsDevices(timeout),
startMessage: 'Discovering local balenaOS devices..',
stopMessage: 'Reporting discovered devices',
});
const responsiveDevices = [];
const Docker = await Promise.resolve().then(() => require('dockerode'));
await Promise.all(devices.map(async function (device) {
const address = device === null || device === void 0 ? void 0 : device.address;
if (!address) {
return;
}
try {
const docker = new Docker({
host: address,
port: dockerPort,
timeout: dockerTimeout,
});
await docker.ping();
responsiveDevices.push(device);
}
catch (_a) {
return;
}
}));
if (!responsiveDevices.length) {
throw new Error('Could not find any local balenaOS devices');
}
return (0, lazy_1.getCliForm)().ask({
message: 'select a device',
type: 'list',
default: devices[0].address,
choices: responsiveDevices.map((device) => ({
name: `${device.host || 'untitled'} (${device.address})`,
value: device.address,
})),
});
}
async function selectLocalDevice() {
try {
const hostnameOrIp = await selectLocalBalenaOsDevice();
console.error(`==> Selected device: ${hostnameOrIp}`);
return hostnameOrIp;
}
catch (e) {
if (e.message.toLowerCase().includes('could not find any')) {
throw new errors_1.ExpectedError(e);
}
else {
throw e;
}
}
}
async function selectAppFromList(applications) {
const _ = await Promise.resolve().then(() => require('lodash'));
const { selectFromList } = await Promise.resolve().then(() => require('../utils/patterns'));
return selectFromList('Select fleet', _.map(applications, (app) => {
return { name: app.slug, ...app };
}));
}
async function getOrSelectApplication(sdk, deviceTypeSlug, appName) {
const pineOptions = {
$select: 'slug',
$expand: {
is_of__cpu_architecture: {
$select: 'slug',
},
},
};
const deviceType = (await sdk.models.deviceType.get(deviceTypeSlug, pineOptions));
const allCpuArches = await sdk.pine.get({
resource: 'cpu_architecture',
options: {
$select: ['id', 'slug'],
},
});
const compatibleCpuArchIds = allCpuArches
.filter((cpuArch) => sdk.models.os.isArchitectureCompatibleWith(deviceType.is_of__cpu_architecture[0].slug, cpuArch.slug))
.map((cpu) => cpu.id);
if (!appName) {
return createOrSelectApp(sdk, {
is_for__device_type: {
$any: {
$alias: 'dt',
$expr: {
dt: {
is_of__cpu_architecture: { $in: compatibleCpuArchIds },
},
},
},
},
}, deviceTypeSlug);
}
const options = {
$expand: {
is_for__device_type: { $select: ['slug', 'is_of__cpu_architecture'] },
},
};
let name;
const match = appName.split('/');
if (match.length > 1) {
options.$filter = {
slug: appName.toLowerCase(),
};
name = match[1];
}
else {
options.$filter = {
app_name: appName,
};
name = appName;
}
const applications = (await sdk.models.application.getAllDirectlyAccessible(options));
if (applications.length === 0) {
await (0, patterns_1.confirm)(false, `No fleet found with name "${appName}".\n` +
'Would you like to create it now?', undefined, true);
return await createApplication(sdk, deviceTypeSlug, name);
}
const compatibleCpuArchIdsSet = new Set(compatibleCpuArchIds);
const validApplications = applications.filter((app) => compatibleCpuArchIdsSet.has(app.is_for__device_type[0].is_of__cpu_architecture.__id));
if (validApplications.length === 0) {
throw new errors_1.ExpectedError('No fleet found with a matching device type');
}
if (validApplications.length === 1) {
return validApplications[0];
}
return selectAppFromList(applications);
}
async function createOrSelectApp(sdk, compatibleDeviceTypesFilter, deviceType) {
const applications = (await sdk.models.application.getAllDirectlyAccessible({
$expand: { is_for__device_type: { $select: 'slug' } },
$filter: compatibleDeviceTypesFilter,
}));
if (applications.length === 0) {
await (0, patterns_1.confirm)(false, 'You have no fleets this device can join.\n' +
'Would you like to create one now?', undefined, true);
return await createApplication(sdk, deviceType);
}
return selectAppFromList(applications);
}
async function createApplication(sdk, deviceType, name) {
const validation = await Promise.resolve().then(() => require('./validation'));
let username;
try {
const userInfo = await sdk.auth.getUserInfo();
username = userInfo.username;
}
catch (err) {
throw new sdk.errors.BalenaNotLoggedIn();
}
const applicationName = await new Promise(async (resolve, reject) => {
while (true) {
try {
const appName = await (0, lazy_1.getCliForm)().ask({
message: 'Enter a name for your new fleet:',
type: 'input',
default: name,
validate: validation.validateApplicationName,
});
try {
await sdk.models.application.getDirectlyAccessible(appName, {
$filter: {
slug: { $startswith: `${username.toLowerCase()}/` },
},
});
(0, errors_1.printErrorMessage)('You already have a fleet with that name; please choose another.');
continue;
}
catch (err) {
return resolve(appName);
}
}
catch (err) {
return reject(err);
}
}
});
const app = await sdk.models.application.create({
name: applicationName,
deviceType,
organization: username,
});
return (await sdk.models.application.get(app.id, {
$expand: {
is_for__device_type: { $select: 'slug' },
},
}));
}
async function generateApplicationConfig(sdk, app, options) {
const { generateApplicationConfig: configGen } = await Promise.resolve().then(() => require('./config'));
const manifest = await sdk.models.config.getDeviceTypeManifestBySlug(app.is_for__device_type[0].slug);
const opts = manifest.options &&
manifest.options.filter((opt) => opt.name !== 'network');
const override = {
appUpdatePollInterval: options.appUpdatePollInterval,
};
const values = {
...(opts ? await (0, lazy_1.getCliForm)().run(opts, { override }) : {}),
...options,
};
const config = await configGen(app, values);
if (config.connectivity === 'connman') {
delete config.connectivity;
delete config.files;
}
return config;
}
//# sourceMappingURL=promote.js.map
;