mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-19 13:47:52 +00:00
c898747468
Change-type: minor Connects-to: #2119 Signed-off-by: Scott Lowe <scott@balena.io>
247 lines
8.1 KiB
TypeScript
247 lines
8.1 KiB
TypeScript
/**
|
|
* @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, getCliForm, stripIndent } from '../../utils/lazy';
|
|
import { applicationIdInfo } from '../../utils/messages';
|
|
import type { PineDeferred } from 'balena-sdk';
|
|
|
|
interface FlagsDef {
|
|
version: string; // OS version
|
|
application?: string;
|
|
app?: string; // application alias
|
|
device?: string;
|
|
deviceApiKey?: string;
|
|
deviceType?: string;
|
|
'generate-device-api-key': boolean;
|
|
output?: string;
|
|
// Options for non-interactive configuration
|
|
network?: string;
|
|
wifiSsid?: string;
|
|
wifiKey?: string;
|
|
appUpdatePollInterval?: string;
|
|
help: void;
|
|
}
|
|
|
|
export default class ConfigGenerateCmd extends Command {
|
|
public static description = stripIndent`
|
|
Generate a config.json file.
|
|
|
|
Generate a config.json file for a device or application.
|
|
|
|
Calling this command with the exact version number of the targeted image is required.
|
|
|
|
This command is interactive by default, but you can do this automatically without interactivity
|
|
by specifying an option for each question on the command line, if you know the questions
|
|
that will be asked for the relevant device type.
|
|
|
|
In case that you want to configure an image for an application with mixed device types,
|
|
you can pass the --deviceType argument along with --application to specify the target device type.
|
|
|
|
${applicationIdInfo.split('\n').join('\n\t\t')}
|
|
`;
|
|
|
|
public static examples = [
|
|
'$ balena config generate --device 7cf02a6 --version 2.12.7',
|
|
'$ balena config generate --device 7cf02a6 --version 2.12.7 --generate-device-api-key',
|
|
'$ balena config generate --device 7cf02a6 --version 2.12.7 --device-api-key <existingDeviceKey>',
|
|
'$ balena config generate --device 7cf02a6 --version 2.12.7 --output config.json',
|
|
'$ balena config generate --app MyApp --version 2.12.7',
|
|
'$ balena config generate --app myorg/myapp --version 2.12.7',
|
|
'$ balena config generate --app MyApp --version 2.12.7 --deviceType fincm3',
|
|
'$ balena config generate --app MyApp --version 2.12.7 --output config.json',
|
|
'$ balena config generate --app MyApp --version 2.12.7 --network wifi --wifiSsid mySsid --wifiKey abcdefgh --appUpdatePollInterval 1',
|
|
];
|
|
|
|
public static usage = 'config generate';
|
|
|
|
public static flags: flags.Input<FlagsDef> = {
|
|
version: flags.string({
|
|
description: 'a balenaOS version',
|
|
required: true,
|
|
}),
|
|
application: { ...cf.application, exclusive: ['app', 'device'] },
|
|
app: { ...cf.app, exclusive: ['application', 'device'] },
|
|
device: flags.string({
|
|
description: 'device uuid',
|
|
char: 'd',
|
|
exclusive: ['application', 'app'],
|
|
}),
|
|
deviceApiKey: flags.string({
|
|
description:
|
|
'custom device key - note that this is only supported on balenaOS 2.0.3+',
|
|
char: 'k',
|
|
}),
|
|
deviceType: flags.string({
|
|
description: 'device type slug',
|
|
}),
|
|
'generate-device-api-key': flags.boolean({
|
|
description: 'generate a fresh device key for the device',
|
|
}),
|
|
output: flags.string({
|
|
description: 'path of output file',
|
|
char: 'o',
|
|
}),
|
|
// Options for non-interactive configuration
|
|
network: flags.string({
|
|
description: 'the network type to use: ethernet or wifi',
|
|
options: ['ethernet', 'wifi'],
|
|
}),
|
|
wifiSsid: flags.string({
|
|
description:
|
|
'the wifi ssid to use (used only if --network is set to wifi)',
|
|
}),
|
|
wifiKey: flags.string({
|
|
description:
|
|
'the wifi key to use (used only if --network is set to wifi)',
|
|
}),
|
|
appUpdatePollInterval: flags.string({
|
|
description:
|
|
'how frequently (in minutes) to poll for application updates',
|
|
}),
|
|
help: cf.help,
|
|
};
|
|
|
|
public static authenticated = true;
|
|
|
|
public async run() {
|
|
const { flags: options } = this.parse<FlagsDef, {}>(ConfigGenerateCmd);
|
|
|
|
const { getApplication } = await import('../../utils/sdk');
|
|
|
|
const balena = getBalenaSdk();
|
|
|
|
await this.validateOptions(options);
|
|
|
|
let resourceDeviceType: string;
|
|
let application: ApplicationWithDeviceType | null = null;
|
|
let device:
|
|
| (DeviceWithDeviceType & { belongs_to__application: PineDeferred })
|
|
| null = null;
|
|
if (options.device != null) {
|
|
const { tryAsInteger } = await import('../../utils/validation');
|
|
const rawDevice = await balena.models.device.get(
|
|
tryAsInteger(options.device),
|
|
{ $expand: { is_of__device_type: { $select: 'slug' } } },
|
|
);
|
|
if (!rawDevice.belongs_to__application) {
|
|
const { ExpectedError } = await import('../../errors');
|
|
throw new ExpectedError(stripIndent`
|
|
Device ${options.device} does not appear to belong to an accessible application.
|
|
Try with a different device, or use '--application' instead of '--device'.`);
|
|
}
|
|
device = rawDevice as DeviceWithDeviceType & {
|
|
belongs_to__application: PineDeferred;
|
|
};
|
|
resourceDeviceType = device.is_of__device_type[0].slug;
|
|
} else {
|
|
// Disambiguate application (if is a number, it could either be an ID or a numerical name)
|
|
application = (await getApplication(balena, options.application!, {
|
|
$expand: {
|
|
is_for__device_type: { $select: 'slug' },
|
|
},
|
|
})) as ApplicationWithDeviceType;
|
|
resourceDeviceType = application.is_for__device_type[0].slug;
|
|
}
|
|
|
|
const deviceType = options.deviceType || resourceDeviceType;
|
|
|
|
const deviceManifest = await balena.models.device.getManifestBySlug(
|
|
deviceType,
|
|
);
|
|
|
|
// Check compatibility if application and deviceType provided
|
|
if (options.application && options.deviceType) {
|
|
const appDeviceManifest = await balena.models.device.getManifestBySlug(
|
|
resourceDeviceType,
|
|
);
|
|
|
|
const helpers = await import('../../utils/helpers');
|
|
if (
|
|
!helpers.areDeviceTypesCompatible(appDeviceManifest, deviceManifest)
|
|
) {
|
|
throw new balena.errors.BalenaInvalidDeviceType(
|
|
`Device type ${options.deviceType} is incompatible with application ${options.application}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Prompt for values
|
|
// Pass params as an override: if there is any param with exactly the same name as a
|
|
// required option, that value is used (and the corresponding question is not asked)
|
|
const answers = await getCliForm().run(deviceManifest.options, {
|
|
override: options,
|
|
});
|
|
answers.version = options.version;
|
|
|
|
// Generate config
|
|
const { generateDeviceConfig, generateApplicationConfig } = await import(
|
|
'../../utils/config'
|
|
);
|
|
|
|
let config;
|
|
if (device) {
|
|
config = await generateDeviceConfig(
|
|
device,
|
|
options.deviceApiKey || options['generate-device-api-key'] || undefined,
|
|
answers,
|
|
);
|
|
} else if (application) {
|
|
answers.deviceType = deviceType;
|
|
config = await generateApplicationConfig(application, answers);
|
|
}
|
|
|
|
// Output
|
|
if (options.output != null) {
|
|
const fs = await import('fs');
|
|
await fs.promises.writeFile(options.output, JSON.stringify(config));
|
|
}
|
|
|
|
const prettyjson = await import('prettyjson');
|
|
console.log(prettyjson.render(config));
|
|
}
|
|
|
|
protected readonly missingDeviceOrAppMessage = stripIndent`
|
|
Either a device or an application must be specified.
|
|
|
|
See the help page for examples:
|
|
|
|
$ balena help config generate
|
|
`;
|
|
|
|
protected readonly deviceTypeNotAllowedMessage =
|
|
'The --deviceType option can only be used alongside the --application option';
|
|
|
|
protected async validateOptions(options: FlagsDef) {
|
|
const { ExpectedError } = await import('../../errors');
|
|
|
|
// Prefer options.application over options.app
|
|
options.application = options.application || options.app;
|
|
delete options.app;
|
|
|
|
if (options.device == null && options.application == null) {
|
|
throw new ExpectedError(this.missingDeviceOrAppMessage);
|
|
}
|
|
|
|
if (!options.application && options.deviceType) {
|
|
throw new ExpectedError(this.deviceTypeNotAllowedMessage);
|
|
}
|
|
}
|
|
}
|