From 424fc463a236379a85047aa6c473db0ba740b935 Mon Sep 17 00:00:00 2001 From: Micah Halter Date: Thu, 10 Jun 2021 18:47:56 +0000 Subject: [PATCH 1/2] Add contract partial based instruction generation Change-type: minor Signed-off-by: Micah Halter --- DOCUMENTATION.md | 76 +++++++++ lib/models/balenaos-contract.ts | 76 +++++++++ lib/models/device-type.ts | 170 +++++++++++++++++++ lib/types/contract.ts | 6 +- lib/types/device-type-json.ts | 2 +- package.json | 1 + tests/integration/models/device-type.spec.ts | 30 ++++ 7 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 lib/models/balenaos-contract.ts diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 0868cd062..33388ce06 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -315,6 +315,9 @@ const sdk = fromSharedOptions(); * [.getBySlugOrName(slugOrName)](#balena.models.deviceType.getBySlugOrName) ⇒ Promise * [.getName(deviceTypeSlug)](#balena.models.deviceType.getName) ⇒ Promise * [.getSlugByName(deviceTypeName)](#balena.models.deviceType.getSlugByName) ⇒ Promise + * [.getInterpolatedPartials(deviceTypeSlug, initial)](#balena.models.deviceType.getInterpolatedPartials) ⇒ Promise + * [.getInstructions(deviceTypeSlug)](#balena.models.deviceType.getInstructions) ⇒ Promise + * [.getInstallMethod(deviceTypeSlug)](#balena.models.deviceType.getInstallMethod) ⇒ Promise * [.apiKey](#balena.models.apiKey) : object * [.create(name, [description])](#balena.models.apiKey.create) ⇒ Promise * [.getAll([options])](#balena.models.apiKey.getAll) ⇒ Promise @@ -710,6 +713,9 @@ balena.models.device.get(123).catch(function (error) { * [.getBySlugOrName(slugOrName)](#balena.models.deviceType.getBySlugOrName) ⇒ Promise * [.getName(deviceTypeSlug)](#balena.models.deviceType.getName) ⇒ Promise * [.getSlugByName(deviceTypeName)](#balena.models.deviceType.getSlugByName) ⇒ Promise + * [.getInterpolatedPartials(deviceTypeSlug, initial)](#balena.models.deviceType.getInterpolatedPartials) ⇒ Promise + * [.getInstructions(deviceTypeSlug)](#balena.models.deviceType.getInstructions) ⇒ Promise + * [.getInstallMethod(deviceTypeSlug)](#balena.models.deviceType.getInstallMethod) ⇒ Promise * [.apiKey](#balena.models.apiKey) : object * [.create(name, [description])](#balena.models.apiKey.create) ⇒ Promise * [.getAll([options])](#balena.models.apiKey.getAll) ⇒ Promise @@ -5216,6 +5222,9 @@ balena.models.device.restartService('7cf02a6', 123, function(error) { * [.getBySlugOrName(slugOrName)](#balena.models.deviceType.getBySlugOrName) ⇒ Promise * [.getName(deviceTypeSlug)](#balena.models.deviceType.getName) ⇒ Promise * [.getSlugByName(deviceTypeName)](#balena.models.deviceType.getSlugByName) ⇒ Promise + * [.getInterpolatedPartials(deviceTypeSlug, initial)](#balena.models.deviceType.getInterpolatedPartials) ⇒ Promise + * [.getInstructions(deviceTypeSlug)](#balena.models.deviceType.getInstructions) ⇒ Promise + * [.getInstallMethod(deviceTypeSlug)](#balena.models.deviceType.getInstallMethod) ⇒ Promise @@ -5396,6 +5405,73 @@ balena.models.deviceType.getSlugByName('Raspberry Pi', function(error, deviceTyp // 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. +}); +``` + + +##### deviceType.getInstallMethod(deviceTypeSlug) ⇒ Promise +**Kind**: static method of [deviceType](#balena.models.deviceType) +**Summary**: Get installation method on a given device type +**Access**: public +**Fulfil**: String - the installation method supported for the given device type slug + +| Param | Type | Description | +| --- | --- | --- | +| deviceTypeSlug | String | device type slug | + +**Example** +```js +balena.models.deviceType.getInstallMethod('raspberry-pi').then(function(method) { + console.log(method); + // externalBoot +}); +``` #### models.apiKey : object diff --git a/lib/models/balenaos-contract.ts b/lib/models/balenaos-contract.ts new file mode 100644 index 000000000..e0af75109 --- /dev/null +++ b/lib/models/balenaos-contract.ts @@ -0,0 +1,76 @@ +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: { + 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.`, + `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.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.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.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.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.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: [ + `Put the device in recovery mode and connect to the host computer via USB`, + `{{#if deviceType.partials.jetsonNotes}}{{#each deviceType.partials.jetsonNotes}}{{{this}}} {{/each}}{{/if}}`, + `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.`, + ], + 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.`, + ], + }, + }, +}; + +export { BalenaOS }; diff --git a/lib/models/device-type.ts b/lib/models/device-type.ts index 23c9660bb..a2f326ff4 100644 --- a/lib/models/device-type.ts +++ b/lib/models/device-type.ts @@ -16,8 +16,75 @@ limitations under the License. import type { InjectedDependenciesParam, PineOptions } from '..'; import { DeviceType } from '../types/models'; +import { Partials, 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 traversingCompile = ( + partials: Partials, + initial: Contract, + path: string[], +): Contract => { + let interpolated: Contract = { ...initial } + for (const partialKey of Object.keys(partials)) { + const current = partials[partialKey]; + if (Array.isArray(current)) { + let location: any = interpolated; + for (const key of path) { + location = location[key]; + } + // if array of partials, compile the template + location[partialKey] = current + .map((partial) => Handlebars.compile(partial)(interpolated)) + .filter((n) => n); + } else { + // if it's another dictionary, keep traversing + interpolated = traversingCompile( + current, + interpolated, + path.concat([partialKey]), + ); + } + } + return interpolated; +} + +const interpolatedPartials = (contract: Contract): Contract => { + if (contract.partials) { + return traversingCompile(contract.partials, contract, ['partials']); + } else { + return contract; + } +}; + +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 === 'image') { + return 'image'; + } else 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; @@ -323,6 +390,109 @@ const getDeviceTypeModel = function (deps: InjectedDependenciesParam) { await exports.getBySlugOrName(deviceTypeName, { $select: 'slug' }) ).slug; }, + + /** + * @summary Get a contract with resolved partial templates + * @name getInterpolatedPartials + * @public + * @function + * @memberof balena.models.deviceType + * + * @param {String} deviceTypeSlug - device type slug + * @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, + ): Promise => { + const { contract } = await exports.getBySlugOrName(deviceTypeSlug, { + $select: 'contract', + }); + if (!contract) { + throw new Error(`Could not find contract for device type ${deviceTypeSlug}`); + } + return interpolatedPartials(contract); + }, + + /** + * @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. + * }); + */ + getInstructions: async ( + deviceTypeSlug: string, + ): Promise => { + const { contract } = await exports.getBySlugOrName(deviceTypeSlug, { + $select: 'contract', + }); + 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), ...interpolatedDeviceType}); + + return interpolatedHostOS.partials?.[installMethod]; + }, + + /** + * @summary Get installation method on a given device type + * @name getInstallMethod + * @public + * @function + * @memberof balena.models.deviceType + * + * @param {String} deviceTypeSlug - device type slug + * @fulfil {String} - the installation method supported for the given device type slug + * @returns {Promise} + * + * @example + * balena.models.deviceType.getInstallMethod('raspberry-pi').then(function(method) { + * console.log(method); + * // externalBoot + * }); + */ + getInstallMethod: async ( + deviceTypeSlug: string, + ): Promise => { + const { contract } = await exports.getBySlugOrName(deviceTypeSlug, { + $select: 'contract', + }); + if (contract) { + return calculateInstallMethod(contract); + } else { + return null; + } + }, }; return exports; diff --git a/lib/types/contract.ts b/lib/types/contract.ts index d5e9b85af..755116f27 100644 --- a/lib/types/contract.ts +++ b/lib/types/contract.ts @@ -1,4 +1,6 @@ -import { AnyObject } from '../../typings/utils'; +import { AnyObject, Dictionary } from '../../typings/utils'; + +export type Partials = Dictionary; export interface Contract { slug: string; @@ -15,5 +17,5 @@ export interface Contract { requires?: string[]; provides?: string[]; composedOf?: AnyObject; - partials?: AnyObject; + partials?: Partials; } diff --git a/lib/types/device-type-json.ts b/lib/types/device-type-json.ts index 8eae44b05..b61f914fc 100644 --- a/lib/types/device-type-json.ts +++ b/lib/types/device-type-json.ts @@ -6,7 +6,7 @@ export interface DeviceType { slug: string; name: string; aliases: string[]; - + arch: string; state?: string; community?: boolean; diff --git a/package.json b/package.json index 1cbe2f328..36a122f1f 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "balena-semver": "^2.3.0", "balena-settings-client": "^4.0.6", "date-fns": "^2.29.3", + "handlebars": "^4.7.7", "lodash": "^4.17.21", "memoizee": "^0.4.15", "ndjson": "^2.0.0", diff --git a/tests/integration/models/device-type.spec.ts b/tests/integration/models/device-type.spec.ts index ca9c156e6..4b3d6987e 100644 --- a/tests/integration/models/device-type.spec.ts +++ b/tests/integration/models/device-type.spec.ts @@ -7,6 +7,7 @@ import type * as BalenaSdk from '../../..'; const DEVICE_TYPE_NAME = 'Raspberry Pi 2'; const DEVICE_TYPE_SLUG = 'raspberry-pi2'; const DEVICE_TYPE_ALIAS = 'raspberrypi2'; +const DEVICE_TYPE_INSTALL_METHOD = 'externalBoot'; const DEVICE_TYPE_ID = 1; describe('Device Type model', function () { @@ -91,4 +92,33 @@ describe('Device Type model', function () { expect(slug).to.equal(DEVICE_TYPE_SLUG); }); }); + + parallel('balena.models.deviceType.getInterpolatedPartials()', function () { + it(`should get just the device type partials with template strings resolved`, async function () { + const partials = await balena.models.deviceType.getInterpolatedPartials( + DEVICE_TYPE_NAME, + ); + expect(partials).to.be.an('object'); + expect(Object.keys(partials)).to.not.have.length(0); + }); + }); + + parallel('balena.models.deviceType.getInstructions()', function () { + it(`should get just the full instructions for installing BalenaOS on a device type with templates strings resolved`, async function () { + const partials = await balena.models.deviceType.getInstructions( + DEVICE_TYPE_NAME, + ); + expect(partials).to.be.an('Array'); + expect(partials).to.not.have.length(0); + }); + }); + + parallel('balena.models.deviceType.getInstallMethod()', function () { + it(`should get device type installation method`, async function () { + const installMethod = await balena.models.deviceType.getInstallMethod( + DEVICE_TYPE_NAME, + ); + expect(installMethod).to.equal(DEVICE_TYPE_INSTALL_METHOD); + }); + }); }); From f767972395ef8d2c7b047af09d4d5bad8e8dc3a8 Mon Sep 17 00:00:00 2001 From: "Vipul Gupta (@vipulgupta2048)" Date: Wed, 15 Feb 2023 01:51:36 +0530 Subject: [PATCH 2/2] Add tests for contract partial functions Signed-off-by: Vipul Gupta (@vipulgupta2048) --- DOCUMENTATION.md | 73 +++++++++++++++++--- lib/models/device-type.ts | 17 +++-- lib/types/device-type-json.ts | 2 +- tests/integration/models/device-type.spec.ts | 14 ++++ 4 files changed, 91 insertions(+), 15 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 33388ce06..e9fc08d1a 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -246,7 +246,7 @@ const sdk = fromSharedOptions(); * [.getDashboardUrl(uuid)](#balena.models.device.getDashboardUrl) ⇒ String * [.getAll([options])](#balena.models.device.getAll) ⇒ Promise * [.getAllByApplication(slugOrUuidOrId, [options])](#balena.models.device.getAllByApplication) ⇒ Promise - * [.getAllByParentDevice(parentUuidOrId, [options])](#balena.models.device.getAllByParentDevice) ⇒ Promise + * ~~[.getAllByParentDevice(parentUuidOrId, [options])](#balena.models.device.getAllByParentDevice) ⇒ Promise~~ * [.get(uuidOrId, [options])](#balena.models.device.get) ⇒ Promise * [.getWithServiceDetails(uuidOrId, [options])](#balena.models.device.getWithServiceDetails) ⇒ Promise * [.getByName(name)](#balena.models.device.getByName) ⇒ Promise @@ -315,7 +315,7 @@ const sdk = fromSharedOptions(); * [.getBySlugOrName(slugOrName)](#balena.models.deviceType.getBySlugOrName) ⇒ Promise * [.getName(deviceTypeSlug)](#balena.models.deviceType.getName) ⇒ Promise * [.getSlugByName(deviceTypeName)](#balena.models.deviceType.getSlugByName) ⇒ Promise - * [.getInterpolatedPartials(deviceTypeSlug, initial)](#balena.models.deviceType.getInterpolatedPartials) ⇒ Promise + * [.getInterpolatedPartials(deviceTypeSlug)](#balena.models.deviceType.getInterpolatedPartials) ⇒ Promise * [.getInstructions(deviceTypeSlug)](#balena.models.deviceType.getInstructions) ⇒ Promise * [.getInstallMethod(deviceTypeSlug)](#balena.models.deviceType.getInstallMethod) ⇒ Promise * [.apiKey](#balena.models.apiKey) : object @@ -393,6 +393,9 @@ const sdk = fromSharedOptions(); * [.image](#balena.models.image) : object * [.get(id, [options])](#balena.models.image.get) ⇒ Promise * [.getLogs(id)](#balena.models.image.getLogs) ⇒ Promise + * [.creditBundle](#balena.models.creditBundle) : object + * [.getAllByOrg(orgId, [options])](#balena.models.creditBundle.getAllByOrg) ⇒ Promise + * [.create(orgId, featureId, creditsToPurchase)](#balena.models.creditBundle.create) ⇒ Promise * [.billing](#balena.models.billing) : object * [.getAccount(organization)](#balena.models.billing.getAccount) ⇒ Promise * [.getPlan(organization)](#balena.models.billing.getPlan) ⇒ Promise @@ -644,7 +647,7 @@ balena.models.device.get(123).catch(function (error) { * [.getDashboardUrl(uuid)](#balena.models.device.getDashboardUrl) ⇒ String * [.getAll([options])](#balena.models.device.getAll) ⇒ Promise * [.getAllByApplication(slugOrUuidOrId, [options])](#balena.models.device.getAllByApplication) ⇒ Promise - * [.getAllByParentDevice(parentUuidOrId, [options])](#balena.models.device.getAllByParentDevice) ⇒ Promise + * ~~[.getAllByParentDevice(parentUuidOrId, [options])](#balena.models.device.getAllByParentDevice) ⇒ Promise~~ * [.get(uuidOrId, [options])](#balena.models.device.get) ⇒ Promise * [.getWithServiceDetails(uuidOrId, [options])](#balena.models.device.getWithServiceDetails) ⇒ Promise * [.getByName(name)](#balena.models.device.getByName) ⇒ Promise @@ -713,7 +716,7 @@ balena.models.device.get(123).catch(function (error) { * [.getBySlugOrName(slugOrName)](#balena.models.deviceType.getBySlugOrName) ⇒ Promise * [.getName(deviceTypeSlug)](#balena.models.deviceType.getName) ⇒ Promise * [.getSlugByName(deviceTypeName)](#balena.models.deviceType.getSlugByName) ⇒ Promise - * [.getInterpolatedPartials(deviceTypeSlug, initial)](#balena.models.deviceType.getInterpolatedPartials) ⇒ Promise + * [.getInterpolatedPartials(deviceTypeSlug)](#balena.models.deviceType.getInterpolatedPartials) ⇒ Promise * [.getInstructions(deviceTypeSlug)](#balena.models.deviceType.getInstructions) ⇒ Promise * [.getInstallMethod(deviceTypeSlug)](#balena.models.deviceType.getInstallMethod) ⇒ Promise * [.apiKey](#balena.models.apiKey) : object @@ -791,6 +794,9 @@ balena.models.device.get(123).catch(function (error) { * [.image](#balena.models.image) : object * [.get(id, [options])](#balena.models.image.get) ⇒ Promise * [.getLogs(id)](#balena.models.image.getLogs) ⇒ Promise + * [.creditBundle](#balena.models.creditBundle) : object + * [.getAllByOrg(orgId, [options])](#balena.models.creditBundle.getAllByOrg) ⇒ Promise + * [.create(orgId, featureId, creditsToPurchase)](#balena.models.creditBundle.create) ⇒ Promise * [.billing](#balena.models.billing) : object * [.getAccount(organization)](#balena.models.billing.getAccount) ⇒ Promise * [.getPlan(organization)](#balena.models.billing.getPlan) ⇒ Promise @@ -2565,7 +2571,7 @@ balena.models.application.revokeSupportAccess('myorganization/myapp', function(e * [.getDashboardUrl(uuid)](#balena.models.device.getDashboardUrl) ⇒ String * [.getAll([options])](#balena.models.device.getAll) ⇒ Promise * [.getAllByApplication(slugOrUuidOrId, [options])](#balena.models.device.getAllByApplication) ⇒ Promise - * [.getAllByParentDevice(parentUuidOrId, [options])](#balena.models.device.getAllByParentDevice) ⇒ Promise + * ~~[.getAllByParentDevice(parentUuidOrId, [options])](#balena.models.device.getAllByParentDevice) ⇒ Promise~~ * [.get(uuidOrId, [options])](#balena.models.device.get) ⇒ Promise * [.getWithServiceDetails(uuidOrId, [options])](#balena.models.device.getWithServiceDetails) ⇒ Promise * [.getByName(name)](#balena.models.device.getByName) ⇒ Promise @@ -3396,7 +3402,9 @@ balena.models.device.getAllByApplication('myorganization/myapp', function(error, ``` -##### device.getAllByParentDevice(parentUuidOrId, [options]) ⇒ Promise +##### ~~device.getAllByParentDevice(parentUuidOrId, [options]) ⇒ Promise~~ +***Deprecated*** + **Kind**: static method of [device](#balena.models.device) **Summary**: Get all devices by parent device **Access**: public @@ -5222,7 +5230,7 @@ balena.models.device.restartService('7cf02a6', 123, function(error) { * [.getBySlugOrName(slugOrName)](#balena.models.deviceType.getBySlugOrName) ⇒ Promise * [.getName(deviceTypeSlug)](#balena.models.deviceType.getName) ⇒ Promise * [.getSlugByName(deviceTypeName)](#balena.models.deviceType.getSlugByName) ⇒ Promise - * [.getInterpolatedPartials(deviceTypeSlug, initial)](#balena.models.deviceType.getInterpolatedPartials) ⇒ Promise + * [.getInterpolatedPartials(deviceTypeSlug)](#balena.models.deviceType.getInterpolatedPartials) ⇒ Promise * [.getInstructions(deviceTypeSlug)](#balena.models.deviceType.getInstructions) ⇒ Promise * [.getInstallMethod(deviceTypeSlug)](#balena.models.deviceType.getInstallMethod) ⇒ Promise @@ -5407,7 +5415,7 @@ balena.models.deviceType.getSlugByName('Raspberry Pi', function(error, deviceTyp ``` -##### deviceType.getInterpolatedPartials(deviceTypeSlug, initial) ⇒ Promise +##### deviceType.getInterpolatedPartials(deviceTypeSlug) ⇒ Promise **Kind**: static method of [deviceType](#balena.models.deviceType) **Summary**: Get a contract with resolved partial templates **Access**: public @@ -5416,7 +5424,6 @@ balena.models.deviceType.getSlugByName('Raspberry Pi', function(error, deviceTyp | Param | Type | Description | | --- | --- | --- | | deviceTypeSlug | String | device type slug | -| initial | any | Other contract values necessary for interpreting contracts | **Example** ```js @@ -7384,6 +7391,54 @@ balena.models.image.getLogs(123, function(error, logs) { console.log(logs); }); ``` + + +#### models.creditBundle : object +**Kind**: static namespace of [models](#balena.models) + +* [.creditBundle](#balena.models.creditBundle) : object + * [.getAllByOrg(orgId, [options])](#balena.models.creditBundle.getAllByOrg) ⇒ Promise + * [.create(orgId, featureId, creditsToPurchase)](#balena.models.creditBundle.create) ⇒ Promise + + + +##### creditBundle.getAllByOrg(orgId, [options]) ⇒ Promise +**Kind**: static method of [creditBundle](#balena.models.creditBundle) +**Summary**: Get all of the credit bundles purchased by the given org +**Access**: public +**Fulfil**: Object[] - credit bundles + +| Param | Type | Default | Description | +| --- | --- | --- | --- | +| orgId | String \| Number | | handle (string) or id (number) of the target organization. | +| [options] | Object | {} | extra pine options to use | + +**Example** +```js +balena.models.creditBundle.getAllByOrg(orgId).then(function(creditBundles) { + console.log(creditBundles); +}); +``` + + +##### creditBundle.create(orgId, featureId, creditsToPurchase) ⇒ Promise +**Kind**: static method of [creditBundle](#balena.models.creditBundle) +**Summary**: Purchase a credit bundle for the given feature and org of the given quantity +**Access**: public +**Fulfil**: Object[] - credit bundles + +| Param | Type | Description | +| --- | --- | --- | +| orgId | String \| Number | handle (string) or id (number) of the target organization. | +| featureId | String \| Number | id (number) of the feature for which credits are being purchased. | +| creditsToPurchase | String \| Number | number of credits being purchased. | + +**Example** +```js +balena.models.creditBundle.create(orgId, featureId, creditsToPurchase).then(function(creditBundle) { + console.log(creditBundle); +}); +``` #### models.billing : object diff --git a/lib/models/device-type.ts b/lib/models/device-type.ts index a2f326ff4..eb2e34730 100644 --- a/lib/models/device-type.ts +++ b/lib/models/device-type.ts @@ -30,7 +30,7 @@ const traversingCompile = ( initial: Contract, path: string[], ): Contract => { - let interpolated: Contract = { ...initial } + let interpolated: Contract = { ...initial }; for (const partialKey of Object.keys(partials)) { const current = partials[partialKey]; if (Array.isArray(current)) { @@ -52,7 +52,7 @@ const traversingCompile = ( } } return interpolated; -} +}; const interpolatedPartials = (contract: Contract): Contract => { if (contract.partials) { @@ -417,7 +417,9 @@ const getDeviceTypeModel = function (deps: InjectedDependenciesParam) { $select: 'contract', }); if (!contract) { - throw new Error(`Could not find contract for device type ${deviceTypeSlug}`); + throw new Error( + `Could not find contract for device type ${deviceTypeSlug}`, + ); } return interpolatedPartials(contract); }, @@ -458,8 +460,13 @@ const getDeviceTypeModel = function (deps: InjectedDependenciesParam) { ); } const installMethod = calculateInstallMethod(contract); - const interpolatedDeviceType = interpolatedPartials(contract); - const interpolatedHostOS = interpolatedPartials({...cloneDeep(BalenaOS), ...interpolatedDeviceType}); + const interpolatedDeviceType = { + deviceType: interpolatedPartials(contract), + }; + const interpolatedHostOS = interpolatedPartials({ + ...cloneDeep(BalenaOS), + ...interpolatedDeviceType, + }); return interpolatedHostOS.partials?.[installMethod]; }, diff --git a/lib/types/device-type-json.ts b/lib/types/device-type-json.ts index b61f914fc..8eae44b05 100644 --- a/lib/types/device-type-json.ts +++ b/lib/types/device-type-json.ts @@ -6,7 +6,7 @@ export interface DeviceType { slug: string; name: string; aliases: string[]; - + arch: string; state?: string; community?: boolean; diff --git a/tests/integration/models/device-type.spec.ts b/tests/integration/models/device-type.spec.ts index 4b3d6987e..5e046cdd3 100644 --- a/tests/integration/models/device-type.spec.ts +++ b/tests/integration/models/device-type.spec.ts @@ -100,6 +100,12 @@ describe('Device Type model', function () { ); expect(partials).to.be.an('object'); expect(Object.keys(partials)).to.not.have.length(0); + expect(partials) + .to.have.property('partials') + .to.have.property('bootDevice'); + expect(partials?.partials?.bootDevice[0]).to.equal( + 'Connect power to the Raspberry Pi 2', + ); }); }); @@ -110,6 +116,14 @@ describe('Device Type model', function () { ); expect(partials).to.be.an('Array'); expect(partials).to.not.have.length(0); + expect(partials).to.eql([ + '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 2.', + 'Connect power to the Raspberry Pi 2 to boot the device.', + ]); }); });