diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md
index 936f9a4ce..cb0211883 100644
--- a/DOCUMENTATION.md
+++ b/DOCUMENTATION.md
@@ -263,6 +263,8 @@ const sdk = fromSharedOptions();
* [.getAllSupported([options])](#balena.models.deviceType.getAllSupported) ⇒ Promise
* [.getBySlugOrName(slugOrName)](#balena.models.deviceType.getBySlugOrName) ⇒ Promise
* [.getName(deviceTypeSlug)](#balena.models.deviceType.getName) ⇒ Promise
+ * [.getInterpolatedPartials(deviceTypeSlug, initial)](#balena.models.deviceType.getInterpolatedPartials) ⇒ Promise
+ * [.getInstructions(deviceTypeSlug)](#balena.models.deviceType.getInstructions) ⇒ Promise
* [.getSlugByName(deviceTypeName)](#balena.models.deviceType.getSlugByName) ⇒ Promise
* [.apiKey](#balena.models.apiKey) : object
* [.create(name, [description])](#balena.models.apiKey.create) ⇒ Promise
@@ -639,6 +641,8 @@ balena.models.device.get(123).catch(function (error) {
* [.getAllSupported([options])](#balena.models.deviceType.getAllSupported) ⇒ Promise
* [.getBySlugOrName(slugOrName)](#balena.models.deviceType.getBySlugOrName) ⇒ Promise
* [.getName(deviceTypeSlug)](#balena.models.deviceType.getName) ⇒ Promise
+ * [.getInterpolatedPartials(deviceTypeSlug, initial)](#balena.models.deviceType.getInterpolatedPartials) ⇒ Promise
+ * [.getInstructions(deviceTypeSlug)](#balena.models.deviceType.getInstructions) ⇒ Promise
* [.getSlugByName(deviceTypeName)](#balena.models.deviceType.getSlugByName) ⇒ Promise
* [.apiKey](#balena.models.apiKey) : object
* [.create(name, [description])](#balena.models.apiKey.create) ⇒ Promise
@@ -5112,6 +5116,8 @@ balena.models.device.restartService('7cf02a6', 123, function(error) {
* [.getAllSupported([options])](#balena.models.deviceType.getAllSupported) ⇒ Promise
* [.getBySlugOrName(slugOrName)](#balena.models.deviceType.getBySlugOrName) ⇒ Promise
* [.getName(deviceTypeSlug)](#balena.models.deviceType.getName) ⇒ Promise
+ * [.getInterpolatedPartials(deviceTypeSlug, initial)](#balena.models.deviceType.getInterpolatedPartials) ⇒ Promise
+ * [.getInstructions(deviceTypeSlug)](#balena.models.deviceType.getInstructions) ⇒ Promise
* [.getSlugByName(deviceTypeName)](#balena.models.deviceType.getSlugByName) ⇒ Promise
@@ -5266,6 +5272,76 @@ balena.models.deviceType.getName('raspberry-pi', function(error, deviceTypeName)
// Raspberry Pi
});
```
+
+
+##### deviceType.getInterpolatedPartials(deviceTypeSlug, initial) ⇒ Promise
+**Kind**: static method of [deviceType
](#balena.models.deviceType)
+**Summary**: Get a contract with resolved partial templates
+**Access**: public
+**Fulfil**: Contract
- device type contract with resolved partials
+
+| Param | Type | Description |
+| --- | --- | --- |
+| deviceTypeSlug | String
| device type slug |
+| initial | any
| Other contract values necessary for interpreting contracts |
+
+**Example**
+```js
+balena.models.deviceType.getInterpolatedPartials('raspberry-pi').then(function(contract) {
+ for (const partial in contract.partials) {
+ console.log(`${partial}: ${contract.partials[partial]}`);
+ }
+ // bootDevice: ["Connect power to the Raspberry Pi (v1 / Zero / Zero W)"]
+});
+```
+
+
+##### deviceType.getInstructions(deviceTypeSlug) ⇒ Promise
+**Kind**: static method of [deviceType
](#balena.models.deviceType)
+**Summary**: Get instructions for installing a host OS on a given device type
+**Access**: public
+**Fulfil**: String[]
- step by step instructions for installing the host OS to the device
+
+| Param | Type | Description |
+| --- | --- | --- |
+| deviceTypeSlug | String
| device type slug |
+
+**Example**
+```js
+balena.models.deviceType.getInstructions('raspberry-pi').then(function(instructions) {
+ for (let instruction of instructions.values()) {
+ console.log(instruction);
+ }
+ // 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.
+});
+```
+**Example**
+```js
+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!
+});
+```
##### deviceType.getSlugByName(deviceTypeName) ⇒ Promise
diff --git a/lib/models/balenaos-contract.ts b/lib/models/balenaos-contract.ts
new file mode 100644
index 000000000..015873e55
--- /dev/null
+++ b/lib/models/balenaos-contract.ts
@@ -0,0 +1,55 @@
+import { Contract } from '../types/contract';
+
+// Hardcoded host OS contract, this should be moved to the Yocto build process with meta-balena.
+// Here for initial implementatin and testing purposes
+const BalenaOS: Contract = {
+ name: 'balenaOS',
+ slug: 'balenaos',
+ type: 'sw.os',
+ description: 'Balena OS',
+ partials: {
+ internalFlash: [
+ `{{#each deviceType.partials.connectDevice}}{{{this}}} {{/each}}`,
+ `Write the {{name}} file you downloaded to the {{deviceType.name}}. We recommend using Etcher.`,
+ `Wait for writing of {{name}} to complete.`,
+ `{{#each deviceType.partials.disconnectDevice}}{{{this}}} {{/each}}`,
+ `{{{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.`,
+ `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}}.`,
+ `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}}.`,
+ `{{#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.`,
+ `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}}.`,
+ `{{{deviceType.partials.bootDevice}}} to boot the device.`,
+ ],
+ jetsonFlash: [
+ `Put the device in recovery mode and connect to the host computer via USB`,
+ `{{#each deviceType.partials.jetsonNotes}}{{{this}}} {{/each}}`,
+ `Unzip the {{name}} image and use the Jetson Flash tool to flash the {{deviceType.name}} found at https://github.com/balena-os/jetson-flash.`,
+ `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.`,
+ ],
+ },
+};
+
+export { BalenaOS };
diff --git a/lib/models/device-type.ts b/lib/models/device-type.ts
index 2bf7607cc..3766adaec 100644
--- a/lib/models/device-type.ts
+++ b/lib/models/device-type.ts
@@ -5,7 +5,7 @@ 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
+ 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,
@@ -16,8 +16,32 @@ limitations under the License.
import type { InjectedDependenciesParam, PineOptions } from '..';
import { DeviceType } from '../types/models';
+import { Contract } from '../types/contract';
import { mergePineOptions } from '../util';
import * as errors from 'balena-errors';
+import * as Handlebars from 'handlebars';
+import cloneDeep = require('lodash/cloneDeep');
+
+// REPLACE ONCE HOST OS CONTRACTS ARE GENERATED THROUGH YOCTO
+import { BalenaOS } from './balenaos-contract';
+
+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 },
+ );
+ } else {
+ return fullInitial;
+ }
+};
const getDeviceTypeModel = function (deps: InjectedDependenciesParam) {
const { pine } = deps;
@@ -282,6 +306,118 @@ const getDeviceTypeModel = function (deps: InjectedDependenciesParam) {
).name;
},
+ /**
+ * @summary Get a contract with resolved partial templates
+ * @name getInterpolatedPartials
+ * @public
+ * @function
+ * @memberof balena.models.deviceType
+ *
+ * @param {String} deviceTypeSlug - device type slug
+ * @param {any} initial - Other contract values necessary for interpreting contracts
+ * @fulfil {Contract} - device type contract with resolved partials
+ * @returns {Promise}
+ *
+ * @example
+ * balena.models.deviceType.getInterpolatedPartials('raspberry-pi').then(function(contract) {
+ * for (const partial in contract.partials) {
+ * console.log(`${partial}: ${contract.partials[partial]}`);
+ * }
+ * // bootDevice: ["Connect power to the Raspberry Pi (v1 / Zero / Zero W)"]
+ * });
+ */
+ getInterpolatedPartials: async (
+ deviceTypeSlug: string,
+ initial: any = {},
+ ): Promise => {
+ const contract = (
+ await exports.getBySlugOrName(deviceTypeSlug, { $select: 'contract' })
+ ).contract;
+ if (!contract) {
+ throw new Error('Slug does not contain contract');
+ }
+ return interpolatedPartials(contract, initial);
+ },
+
+ /**
+ * @summary Get instructions for installing a host OS on a given device type
+ * @name getInstructions
+ * @public
+ * @function
+ * @memberof balena.models.deviceType
+ *
+ * @param {String} deviceTypeSlug - device type slug
+ * @fulfil {String[]} - step by step instructions for installing the host OS to the device
+ * @returns {Promise}
+ *
+ * @example
+ * balena.models.deviceType.getInstructions('raspberry-pi').then(function(instructions) {
+ * for (let instruction of instructions.values()) {
+ * console.log(instruction);
+ * }
+ * // 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.
+ * });
+ * @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 => {
+ 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 (baseInstructions) {
+ return Handlebars.compile(baseInstructions)({
+ ...interpolatedHostOS,
+ instructions: interpolatedHostOS.partials[installMethod],
+ })
+ .split('\n')
+ .map((s) => s.trim())
+ .filter((s) => s);
+ } else {
+ return interpolatedHostOS.partials[installMethod];
+ }
+ } else {
+ return [];
+ }
+ },
+
/**
* @summary Get device slug
* @name getSlugByName
diff --git a/lib/types/contract.ts b/lib/types/contract.ts
index d5e9b85af..4c0e371e7 100644
--- a/lib/types/contract.ts
+++ b/lib/types/contract.ts
@@ -1,4 +1,4 @@
-import { AnyObject } from '../../typings/utils';
+import { AnyObject, Dictionary } from '../../typings/utils';
export interface Contract {
slug: string;
@@ -15,5 +15,5 @@ export interface Contract {
requires?: string[];
provides?: string[];
composedOf?: AnyObject;
- partials?: AnyObject;
+ partials?: Dictionary;
}
diff --git a/lib/types/device-type-json.ts b/lib/types/device-type-json.ts
index 8eae44b05..71500c31c 100644
--- a/lib/types/device-type-json.ts
+++ b/lib/types/device-type-json.ts
@@ -1,8 +1,9 @@
+import type { Contract } from './contract';
import type { Dictionary } from '../../typings/utils';
/* types for the /device-types/v1 endppoints */
-export interface DeviceType {
+export interface DeviceType extends Contract {
slug: string;
name: string;
aliases: string[];
diff --git a/package.json b/package.json
index 26df9ec92..a32235c42 100644
--- a/package.json
+++ b/package.json
@@ -112,6 +112,7 @@
"balena-request": "^11.5.0",
"balena-semver": "^2.3.0",
"balena-settings-client": "^4.0.6",
+ "handlebars": "^4.7.7",
"lodash": "^4.17.21",
"memoizee": "^0.4.15",
"moment": "^2.29.1",