Skip to content

Commit

Permalink
Add more generic approach to compiling partials
Browse files Browse the repository at this point in the history
  • Loading branch information
mehalter committed Oct 25, 2022
1 parent b2f9e2b commit 7b718e5
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 66 deletions.
49 changes: 36 additions & 13 deletions lib/models/balenaos-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="http://www.etcher.io/">Etcher</a>.`,
Expand All @@ -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 <a href="http://www.etcher.io/">Etcher</a>.`,
`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 <a href="http://www.etcher.io/">Etcher</a>.`,
`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}}.`,
`<strong role="alert">Warning!</strong> 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 <a href="http://www.etcher.io/">Etcher</a>.`,
`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 <a href="http://www.etcher.io/">Etcher</a>.`,
`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: [
Expand All @@ -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.`,
],
},
},
};

Expand Down
125 changes: 73 additions & 52 deletions lib/models/device-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 <a href="http://www.etcher.io/">Etcher</a>.
* // 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<string[]> => {
): Promise<any | string[]> => {
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<string | null> => {
const contract = (
await exports.getBySlugOrName(deviceTypeSlug, { $select: 'contract' })
).contract;
if (contract) {
return calculateInstallMethod(contract);
} else {
return [];
return null;
}
},

Expand Down
4 changes: 3 additions & 1 deletion lib/types/contract.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { AnyObject, Dictionary } from '../../typings/utils';

type Partials = Dictionary<string[] | Partials>;

export interface Contract {
slug: string;
type: string;
Expand All @@ -15,5 +17,5 @@ export interface Contract {
requires?: string[];
provides?: string[];
composedOf?: AnyObject;
partials?: Dictionary<string[]>;
partials?: Partials;
}

0 comments on commit 7b718e5

Please sign in to comment.