diff --git a/src/features/vars-schema/schema.ts b/src/features/vars-schema/schema.ts index 421f3b653..fac58eb4c 100644 --- a/src/features/vars-schema/schema.ts +++ b/src/features/vars-schema/schema.ts @@ -1,6 +1,6 @@ import type { RequestHandler } from 'express'; import type { JSONSchema6 } from 'json-schema'; - +import { sbvrUtils } from '@balena/pinejs'; import { BLOCKED_NAMES, DEVICE_TYPE_SPECIFIC_CONFIG_VAR_PROPERTIES, @@ -13,19 +13,54 @@ import { ALLOWED_NAMESPACES, } from './env-vars'; +const { api } = sbvrUtils; + // Return config variable constants for use by external components. // A query string parameter of 'deviceType' is accepted, which should // be a device type slug. -export const schema: RequestHandler = (req, res) => { +export const schema: RequestHandler = async (req, res) => { + const deviceTypeSlug = await (async () => { + if (typeof req.query.deviceType !== 'string') { + return; + } + + const resinApi = api.resin.clone({ passthrough: { req } }); + // Ensure that the user has access to the provided device type. + const [dt] = (await resinApi.get({ + resource: 'device_type', + options: { + $top: 1, + $select: 'slug', + $filter: { + device_type_alias: { + $any: { + $alias: 'dta', + $expr: { + dta: { + is_referenced_by__alias: req.query.deviceType, + }, + }, + }, + }, + }, + }, + })) as Array<{ slug: string }>; + + // We do not throw when the DT is not found for backwards compatibility reasons. + return dt?.slug; + })(); + const configVarSchema: JSONSchema6 = { type: 'object', $schema: 'http://json-schema.org/draft-06/schema#', properties: Object.assign( {}, SUPERVISOR_CONFIG_VAR_PROPERTIES, - ...DEVICE_TYPE_SPECIFIC_CONFIG_VAR_PROPERTIES.filter((config) => - config.capableDeviceTypes.includes(req.query.deviceType as string), - ).map((config) => config.properties), + ...(deviceTypeSlug != null + ? DEVICE_TYPE_SPECIFIC_CONFIG_VAR_PROPERTIES.filter((config) => + config.capableDeviceTypes.includes(deviceTypeSlug), + ).map((config) => config.properties) + : []), ), }; diff --git a/test/01_basic.ts b/test/01_basic.ts index 789a75f67..bff12e9d9 100644 --- a/test/01_basic.ts +++ b/test/01_basic.ts @@ -109,6 +109,14 @@ describe('Basic', () => { checkBaseVarsResult(vars); }); + it(`should return the base vars when device type is not found`, async () => { + const { body: vars } = await supertest() + .get(`/config/vars?deviceType=wrong-device-type`) + .expect(200); + + checkBaseVarsResult(vars); + }); + [ { deviceType: 'beaglebone-black' }, { diff --git a/test/02_device-types.ts b/test/02_device-types.ts index 5c8105768..de2940878 100644 --- a/test/02_device-types.ts +++ b/test/02_device-types.ts @@ -90,7 +90,7 @@ describe('device type resource', () => { expect(deviceType).to.have.property('name').that.is.a('string'); }); - expect(res.body.d).to.have.property('length', 14); + expect(res.body.d).to.have.property('length', 16); }); }); @@ -217,7 +217,7 @@ describe('device type endpoints', () => { it('should return a proper result', async () => { const res = await supertest().get('/device-types/v1').expect(200); expect(res.body).to.be.an('array'); - expect(res.body).to.have.property('length', 15); + expect(res.body).to.have.property('length', 17); const rpi3config = _.find(res.body, { slug: 'raspberrypi3' }); expect(rpi3config).to.be.an('object'); expect(rpi3config).to.have.property('buildId', '2.19.0+rev1.prod'); diff --git a/test/09_contracts.ts b/test/09_contracts.ts index 63491f8cf..3de24a9e4 100644 --- a/test/09_contracts.ts +++ b/test/09_contracts.ts @@ -78,7 +78,7 @@ describe('contracts', () => { await fetchContractsLocally([contractRepository]); const contracts = await getContracts('hw.device-type'); - expect(contracts).to.have.length(14); + expect(contracts).to.have.length(16); expect(contracts.find((contract) => contract.slug === 'raspberrypi3')).to .not.be.undefined; }); @@ -96,7 +96,7 @@ describe('contracts', () => { ]); const contracts = await getContracts('hw.device-type'); - expect(contracts).to.have.length(15); + expect(contracts).to.have.length(17); expect( contracts.find((contract) => contract.slug === 'other-contract-dt'), ).to.not.be.undefined; @@ -163,7 +163,7 @@ describe('contracts', () => { (dbDeviceType) => dbDeviceType.slug === 'fincm3', ); - expect(contracts).to.have.length(15); + expect(contracts).to.have.length(17); expect(newDt).to.not.be.undefined; expect(finDt).to.have.property('name', 'Fin'); }); @@ -232,7 +232,7 @@ describe('contracts', () => { (dbDeviceType) => dbDeviceType.slug === 'raspberry-pi', ); - expect(dbDeviceTypes).to.have.length(15); + expect(dbDeviceTypes).to.have.length(17); expect(newDt).to.not.be.undefined; expect(finDt).to.have.property('name', 'Fin'); expect(finDt).to.have.deep.property( diff --git a/test/fixtures/contracts/base-contracts/contracts/hw.device-type/jetson-tx2/contract.json b/test/fixtures/contracts/base-contracts/contracts/hw.device-type/jetson-tx2/contract.json new file mode 100644 index 000000000..a48d1c24d --- /dev/null +++ b/test/fixtures/contracts/base-contracts/contracts/hw.device-type/jetson-tx2/contract.json @@ -0,0 +1,37 @@ +{ + "slug": "jetson-tx2", + "version": "1", + "type": "hw.device-type", + "aliases": [], + "name": "Nvidia Jetson TX2", + "assets": { + "logo": { + "url": "./jetson-tx2.svg", + "name": "logo" + } + }, + "data": { + "arch": "aarch64", + "hdmi": true, + "led": false, + "connectivity": { + "bluetooth": true, + "wifi": true + }, + "storage": { + "internal": true + }, + "media": { + "defaultBoot": "internal", + "altBoot": ["sdcard"] + }, + "is_private": false + }, + "partials": { + "bootDeviceExternal": [ + "Connect power to the {{name}} and press and hold the POWER push button for 1 second" + ], + "flashIndicator": ["all LEDs are off"], + "bootDevice": ["Remove and re-connect power to the {{name}}"] + } +} diff --git a/test/fixtures/contracts/base-contracts/contracts/hw.device-type/jetson-tx2/jetson-tx2.svg b/test/fixtures/contracts/base-contracts/contracts/hw.device-type/jetson-tx2/jetson-tx2.svg new file mode 100644 index 000000000..8b77d60d4 --- /dev/null +++ b/test/fixtures/contracts/base-contracts/contracts/hw.device-type/jetson-tx2/jetson-tx2.svg @@ -0,0 +1,27 @@ + + + + +Nvidia_logo +Created with Sketch. + + + + + + + + + + diff --git a/test/fixtures/contracts/base-contracts/contracts/hw.device-type/up-board/contract.json b/test/fixtures/contracts/base-contracts/contracts/hw.device-type/up-board/contract.json new file mode 100644 index 000000000..b56e6e930 --- /dev/null +++ b/test/fixtures/contracts/base-contracts/contracts/hw.device-type/up-board/contract.json @@ -0,0 +1,40 @@ +{ + "slug": "up-board", + "version": "1", + "type": "hw.device-type", + "aliases": [], + "name": "UP Board", + "assets": { + "logo": { + "url": "./up-board.svg", + "name": "logo" + } + }, + "data": { + "arch": "amd64", + "family": "family-upboard", + "hdmi": true, + "led": true, + "connectivity": { + "bluetooth": false, + "wifi": false + }, + "storage": { + "internal": true + }, + "media": { + "defaultBoot": "internal", + "altBoot": ["usb_mass_storage"] + }, + "is_private": false + }, + "partials": { + "bootDeviceExternal": [ + "Power on the {{name}} with a keyboard connected.", + "Press the F7 key while BIOS is loading to enter the boot menu.", + "Select the \"UEFI:\" option from the boot menu." + ], + "flashIndicator": ["all LEDs are off"], + "bootDevice": ["Power up the {{name}}"] + } +} diff --git a/test/fixtures/contracts/base-contracts/contracts/hw.device-type/up-board/up-board.svg b/test/fixtures/contracts/base-contracts/contracts/hw.device-type/up-board/up-board.svg new file mode 100644 index 000000000..86d814c54 --- /dev/null +++ b/test/fixtures/contracts/base-contracts/contracts/hw.device-type/up-board/up-board.svg @@ -0,0 +1,12 @@ + + + + + + + + +