diff --git a/lib/models/balenaos-contract.ts b/lib/models/balenaos-contract.ts index 32e0d8889..e0af75109 100644 --- a/lib/models/balenaos-contract.ts +++ b/lib/models/balenaos-contract.ts @@ -8,6 +8,7 @@ const BalenaOS: Contract = { type: 'sw.os', description: 'Balena OS', partials: { + image: [`{{#each deviceType.partials.instructions}}{{{this}}} {{/each}}`], internalFlash: [ `{{#each deviceType.partials.connectDevice}}{{{this}}} {{/each}}`, `Write the {{name}} file you downloaded to the {{deviceType.name}}. We recommend using Etcher.`, @@ -16,24 +17,24 @@ const BalenaOS: Contract = { `{{{deviceType.partials.bootDevice}}} to boot the device.`, ], externalFlash: [ - `Insert the {{deviceType.data.media.installation}} to the host machine.`, - `Write the {{name}} file you downloaded to the {{deviceType.data.media.installation}}. We recommend using Etcher.`, + `Insert the {{deviceType.data.media.altBoot.[0]}} to the host machine.`, + `Write the {{name}} file you downloaded to the {{deviceType.data.media.altBoot.[0]}}. We recommend using Etcher.`, `Wait for writing of {{name}} to complete.`, - `Remove the {{deviceType.data.media.installation}} from the host machine.`, - `Insert the freshly flashed {{deviceType.data.media.installation}} into the {{deviceType.name}}.`, + `Remove the {{deviceType.data.media.altBoot.[0]}} from the host machine.`, + `Insert the freshly flashed {{deviceType.data.media.altBoot.[0]}} into the {{deviceType.name}}.`, `Warning! This will also completely erase internal storage medium, so please make a backup first.`, `{{#each deviceType.partials.bootDeviceExternal}}{{{this}}} {{/each}}`, `Wait for the {{deviceType.name}} to finish flashing and shutdown. {{#if deviceType.partials.flashIndicator}}Please wait until {{deviceType.partials.flashIndicator}}.{{/if}}`, - `Remove the {{deviceType.data.media.installation}} from the {{deviceType.name}}.`, + `Remove the {{deviceType.data.media.altBoot.[0]}} from the {{deviceType.name}}.`, `{{#each deviceType.partials.bootDeviceInternal}}{{{this}}} {{/each}}`, `{{{deviceType.partials.bootDevice}}} to boot the device.`, ], externalBoot: [ - `Insert the {{deviceType.data.media.installation}} to the host machine.`, - `Write the {{name}} file you downloaded to the {{deviceType.data.media.installation}}. We recommend using Etcher.`, + `Insert the {{deviceType.data.media.defaultBoot}} to the host machine.`, + `Write the {{name}} file you downloaded to the {{deviceType.data.media.defaultBoot}}. We recommend using Etcher.`, `Wait for writing of {{name}} to complete.`, - `Remove the {{deviceType.data.media.installation}} from the host machine.`, - `Insert the freshly flashed {{deviceType.data.media.installation}} into the {{deviceType.name}}.`, + `Remove the {{deviceType.data.media.defaultBoot}} from the host machine.`, + `Insert the freshly flashed {{deviceType.data.media.defaultBoot}} into the {{deviceType.name}}.`, `{{{deviceType.partials.bootDevice}}} to boot the device.`, ], jetsonFlash: [ @@ -43,10 +44,32 @@ const BalenaOS: Contract = { `Wait for writing of {{name}} to complete.`, `{{{deviceType.partials.bootDevice}}} to boot the device.`, ], - custom: [ - `{{#each deviceType.partials.instructions}}{{{this}}} {{/each}}`, - `{{{deviceType.partials.bootDevice}}} to boot the device.`, - ], + edisonFlash: { + Linux: [ + `{{#each deviceType.partials.Linux.flashDependencies}}{{{this}}} {{/each}}`, + `Unplug the {{deviceType.name}} from your system`, + `Unzip the downloaded {{name}} file`, + `{{#each deviceType.partials.Linux.flashInstructions}}{{{this}}} {{/each}}`, + `Plug the {{deviceType.name}} as per the instructions on your terminal.`, + `You can check the progress of the provisioning on your terminal.`, + ], + MacOS: [ + `{{#each deviceType.partials.MacOS.flashDependencies}}{{{this}}} {{/each}}`, + `Unplug the {{deviceType.name}} from your system`, + `Unzip the downloaded {{name}} file`, + `{{#each deviceType.partials.MacOS.flashInstructions}}{{{this}}} {{/each}}`, + `Plug the {{deviceType.name}} as per the instructions on your terminal.`, + `You can check the progress of the provisioning on your terminal.`, + ], + Windows: [ + `{{#each deviceType.partials.Windows.flashDependencies}}{{{this}}} {{/each}}`, + `Unplug the {{deviceType.name}} from your system`, + `Unzip the downloaded {{name}} file`, + `{{#each deviceType.partials.Windows.flashInstructions}}{{{this}}} {{/each}}`, + `Plug the {{deviceType.name}} as per the instructions on your terminal.`, + `You can check the progress of the provisioning on your terminal.`, + ], + }, }, }; diff --git a/lib/models/device-type.ts b/lib/models/device-type.ts index 707137583..7783bb887 100644 --- a/lib/models/device-type.ts +++ b/lib/models/device-type.ts @@ -25,24 +25,64 @@ import cloneDeep = require('lodash/cloneDeep'); // REPLACE ONCE HOST OS CONTRACTS ARE GENERATED THROUGH YOCTO import { BalenaOS } from './balenaos-contract'; +const traversingCompile = (partials: any, initial: any, keys: string[]) => { + return Object.keys(partials).reduce( + (interpolated: any, partialKey) => { + const current = partials[partialKey]; + if (Array.isArray(current)) { + let location = interpolated; + for (const key of keys) { + location = location[key]; + } + // if array of partials, compile the template + location[partialKey] = current + .map((partial: string) => Handlebars.compile(partial)(interpolated)) + .filter((n) => n); + } else { + // if it's another dictionary, keep traversing + interpolated = traversingCompile( + current, + interpolated, + keys.concat([partialKey]), + ); + } + return interpolated; + }, + { ...initial }, + ); +}; + const interpolatedPartials = (contract: Contract, initial: any = {}) => { const fullInitial = { ...contract, ...initial }; if (contract.partials) { - const partials = contract.partials; - return Object.keys(partials).reduce( - (interpolated: any, partialKey) => { - interpolated.partials[partialKey] = partials[partialKey].map( - (partial: string) => Handlebars.compile(partial)(interpolated), - ); - return interpolated; - }, - { ...fullInitial }, - ); + return traversingCompile(contract.partials, fullInitial, ['partials']); } else { return fullInitial; } }; +const calculateInstallMethod = (contract: Contract): string => { + const flashProtocol = contract.data?.flashProtocol; + const defaultBoot = contract.data?.media?.defaultBoot; + if (flashProtocol) { + if (flashProtocol === 'RPIBOOT') { + return 'internalFlash'; + } else { + return flashProtocol; + } + } else if (defaultBoot) { + if (defaultBoot === 'internal') { + return 'externalFlash'; + } else { + return 'externalBoot'; + } + } else { + throw new errors.BalenaError( + `Unable to determine installation method for contract: ${contract.slug}`, + ); + } +}; + const getDeviceTypeModel = function (deps: InjectedDependenciesParam) { const { pine } = deps; @@ -362,56 +402,37 @@ const getDeviceTypeModel = function (deps: InjectedDependenciesParam) { * // Insert the freshly flashed sdcard into the Raspberry Pi (v1 / Zero / Zero W). * // Connect power to the Raspberry Pi (v1 / Zero / Zero W) to boot the device. * }); - * @example - * balena.models.deviceType.getInstructions('raspberry-pi', baseInstructions = { - * `Use the form on the left to configure and download {{name}} for your new {{deviceType.name}}. - * {{#each instructions}} - * {{{this}}} - * {{/each}} - * Your device should appear in your application dashboard within a few minutes. Have fun!` - * }).then(function(instructions) { - * for (let instruction of instructions.values()) { - * console.log(instruction); - * } - * // Use the form on the left to configure and download BalenaOS for your new Raspberry Pi (v1 / Zero / Zero W). - * // Insert the sdcard to the host machine. - * // Write the BalenaOS file you downloaded to the sdcard. We recommend using Etcher. - * // Wait for writing of BalenaOS to complete. - * // Remove the sdcard from the host machine. - * // Insert the freshly flashed sdcard into the Raspberry Pi (v1 / Zero / Zero W). - * // Connect power to the Raspberry Pi (v1 / Zero / Zero W) to boot the device. - * // Your device should appear in your application dashboard within a few minutes. Have fun! - * }); */ getInstructions: async ( deviceTypeSlug: string, - baseInstructions?: string, - ): Promise => { + ): Promise => { const contract = ( await exports.getBySlugOrName(deviceTypeSlug, { $select: 'contract' }) ).contract; - if (contract) { - const installMethod = contract?.data?.installation?.method; - if (!installMethod || !contract.partials) { - throw new Error( - `Install method or instruction partials not defined for ${deviceTypeSlug}`, - ); - } - const interpolatedDeviceType = interpolatedPartials(contract); - const interpolatedHostOS = interpolatedPartials(cloneDeep(BalenaOS), { - deviceType: interpolatedDeviceType, - }); + if (!contract || !contract.partials) { + throw new Error( + `Instruction partials not defined for ${deviceTypeSlug}`, + ); + } + const installMethod = calculateInstallMethod(contract); + const interpolatedDeviceType = interpolatedPartials(contract); + const interpolatedHostOS = interpolatedPartials(cloneDeep(BalenaOS), { + deviceType: interpolatedDeviceType, + }); - let instructions: string[] = interpolatedHostOS.partials[installMethod]; - if (baseInstructions) { - instructions = Handlebars.compile(baseInstructions)({ - ...interpolatedHostOS, - instructions, - }).split('\n'); - } - return instructions.map((s) => s.trim()).filter((s) => s); + return interpolatedHostOS.partials[installMethod]; + }, + + getInstallMethod: async ( + deviceTypeSlug: string, + ): Promise => { + const contract = ( + await exports.getBySlugOrName(deviceTypeSlug, { $select: 'contract' }) + ).contract; + if (contract) { + return calculateInstallMethod(contract); } else { - return []; + return null; } }, diff --git a/lib/types/contract.ts b/lib/types/contract.ts index 4c0e371e7..4135c5963 100644 --- a/lib/types/contract.ts +++ b/lib/types/contract.ts @@ -1,5 +1,7 @@ import { AnyObject, Dictionary } from '../../typings/utils'; +type Partials = Dictionary; + export interface Contract { slug: string; type: string; @@ -15,5 +17,5 @@ export interface Contract { requires?: string[]; provides?: string[]; composedOf?: AnyObject; - partials?: Dictionary; + partials?: Partials; }