diff --git a/src/features/device-config/download.ts b/src/features/device-config/download.ts index 1aa5da2b0..11af5c1b2 100644 --- a/src/features/device-config/download.ts +++ b/src/features/device-config/download.ts @@ -10,7 +10,7 @@ import { import type { Application, DeviceType } from '../../balena-model.js'; import { generateConfig } from './device-config.js'; -import { findBySlug } from '../device-types/device-types.js'; +import { findDeviceTypeJsonBySlug } from '../device-types/device-types.js'; import { checkInt } from '../../lib/utils.js'; const { UnauthorizedError, NotFoundError } = errors; @@ -63,7 +63,7 @@ export const downloadImageConfig: RequestHandler = async (req, res) => { const resinApi = api.resin.clone({ passthrough: { req } }); const app = await getApp(appId, req); - const deviceTypeJson = await findBySlug( + const deviceTypeJson = await findDeviceTypeJsonBySlug( resinApi, deviceTypeSlug || app.is_for__device_type[0].slug, ); diff --git a/src/features/device-types/device-types.ts b/src/features/device-types/device-types.ts index a88eb594e..06fcbb9c9 100644 --- a/src/features/device-types/device-types.ts +++ b/src/features/device-types/device-types.ts @@ -1,9 +1,6 @@ -import _ from 'lodash'; - import type { DeviceTypeJson } from './device-type-json.js'; import type { sbvrUtils } from '@balena/pinejs'; import { errors } from '@balena/pinejs'; -const { InternalRequestError } = errors; import { captureException } from '../../infra/error-handling/index.js'; @@ -61,6 +58,13 @@ export const getDeviceTypeBySlug = async ( return dt; }; +export const validateSlug = (slug?: string) => { + if (slug == null || !/^[\w-]+$/.test(slug)) { + throw new BadRequestError('Invalid device type'); + } + return slug; +}; + const findDeviceTypeInfoBySlug = async ( resinApi: sbvrUtils.PinejsClient, slug: string, @@ -74,28 +78,19 @@ const findDeviceTypeInfoBySlug = async ( return deviceTypeInfo; }; -export const validateSlug = (slug?: string) => { - if (slug == null || !/^[\w-]+$/.test(slug)) { - throw new BadRequestError('Invalid device type'); - } - return slug; -}; - -/** @deprecated */ -const getAllDeviceTypes = async () => { - const dtInfo = await getDeviceTypes(); - return _.uniqBy( - Object.values(dtInfo).map((dtEntry) => dtEntry.latest), - (dt) => dt.slug, - ); -}; +/** @deprecated Use the getDeviceTypeBySlug unless you need the device-type.json contents. */ +export const findDeviceTypeJsonBySlug = async ( + resinApi: sbvrUtils.PinejsClient, + slug: string, +): Promise => + (await findDeviceTypeInfoBySlug(resinApi, slug)).latest; /** @deprecated */ export const getAccessibleDeviceTypes = async ( resinApi: sbvrUtils.PinejsClient, ): Promise => { - const [deviceTypes, accessibleDeviceTypes] = await Promise.all([ - getAllDeviceTypes(), + const [deviceTypeInfosBySlug, accessibleDeviceTypes] = await Promise.all([ + getDeviceTypes(), resinApi.get({ resource: 'device_type', options: { @@ -104,30 +99,22 @@ export const getAccessibleDeviceTypes = async ( }) as Promise>, ]); - const accessSet = new Set(accessibleDeviceTypes.map((dt) => dt.slug)); - return deviceTypes.filter((deviceType) => { - return accessSet.has(deviceType.slug); - }); + return accessibleDeviceTypes + .map((dt) => deviceTypeInfosBySlug[dt.slug]?.latest) + .filter((dtJson) => dtJson != null); }; -/** @deprecated Use the getDeviceTypeBySlug unless you need the device-type.json contents. */ -export const findBySlug = async ( - resinApi: sbvrUtils.PinejsClient, - slug: string, -): Promise => - (await findDeviceTypeInfoBySlug(resinApi, slug)).latest; - export const getImageSize = async ( resinApi: sbvrUtils.PinejsClient, slug: string, buildId: string, ): Promise => { const deviceTypeInfo = await findDeviceTypeInfoBySlug(resinApi, slug); - const deviceType = deviceTypeInfo.latest; - const normalizedSlug = deviceType.slug; + const deviceTypeJson = deviceTypeInfo.latest; + const normalizedSlug = deviceTypeJson.slug; if (buildId === 'latest') { - buildId = deviceType.buildId; + buildId = deviceTypeJson.buildId; } if (!deviceTypeInfo.versions.includes(buildId)) { @@ -150,45 +137,3 @@ export const getImageSize = async ( throw err; } }; - -export interface ImageVersions { - versions: string[]; - latest: string; -} - -export const getImageVersions = async ( - resinApi: sbvrUtils.PinejsClient, - slug: string, -): Promise => { - const deviceTypeInfo = await findDeviceTypeInfoBySlug(resinApi, slug); - const deviceType = deviceTypeInfo.latest; - const normalizedSlug = deviceType.slug; - - const versionInfo = await Promise.all( - deviceTypeInfo.versions.map(async (buildId) => { - try { - return { - buildId, - hasDeviceTypeJson: await getDeviceTypeJson(normalizedSlug, buildId), - }; - } catch { - return; - } - }), - ); - const filteredInfo = versionInfo.filter( - (buildInfo): buildInfo is NonNullable => - buildInfo != null && !!buildInfo.hasDeviceTypeJson, - ); - if (_.isEmpty(filteredInfo) && !_.isEmpty(deviceTypeInfo.versions)) { - throw new InternalRequestError( - `Could not retrieve any image version for device type ${slug}`, - ); - } - - const buildIds = filteredInfo.map(({ buildId }) => buildId); - return { - versions: buildIds, - latest: buildIds[0], - }; -}; diff --git a/src/features/device-types/index.ts b/src/features/device-types/index.ts index db43d24b6..da5aee1d1 100644 --- a/src/features/device-types/index.ts +++ b/src/features/device-types/index.ts @@ -1,11 +1,6 @@ import type { Application } from 'express'; import { middleware } from '../../infra/auth/index.js'; -import { - downloadImageSize, - getDeviceType, - getDeviceTypes, - listAvailableImageVersions, -} from './routes.js'; +import { downloadImageSize, getDeviceType, getDeviceTypes } from './routes.js'; export const setup = (app: Application) => { app.get( @@ -18,11 +13,6 @@ export const setup = (app: Application) => { middleware.resolveCredentialsAndUser, getDeviceType, ); - app.get( - '/device-types/v1/:deviceType/images', - middleware.resolveCredentialsAndUser, - listAvailableImageVersions, - ); app.get( '/device-types/v1/:deviceType/images/:version/download-size', middleware.resolveCredentialsAndUser, diff --git a/src/features/device-types/routes.ts b/src/features/device-types/routes.ts index 9e8a52b5d..58f3d0209 100644 --- a/src/features/device-types/routes.ts +++ b/src/features/device-types/routes.ts @@ -31,7 +31,7 @@ export const getDeviceType: RequestHandler = async (req, res) => { try { const resinApi = api.resin.clone({ passthrough: { req } }); const slug = deviceTypesLib.validateSlug(req.params.deviceType); - const data = await deviceTypesLib.findBySlug(resinApi, slug); + const data = await deviceTypesLib.findDeviceTypeJsonBySlug(resinApi, slug); res.json(data); } catch (err) { captureException(err, 'Error getting device type', { @@ -44,21 +44,6 @@ export const getDeviceType: RequestHandler = async (req, res) => { } }; -export const listAvailableImageVersions: RequestHandler = async (req, res) => { - try { - const resinApi = api.resin.clone({ passthrough: { req } }); - const slug = deviceTypesLib.validateSlug(req.params.deviceType); - const data = await deviceTypesLib.getImageVersions(resinApi, slug); - res.json(data); - } catch (err) { - captureException(err, 'Error getting image versions', { req }); - if (handleHttpErrors(req, res, err)) { - return; - } - res.status(500).send(translateError(err)); - } -}; - const DOWNLOAD_TIMEOUT = 30000; // we must respond within this time export const downloadImageSize: RequestHandler = async (req, res) => { diff --git a/src/index.ts b/src/index.ts index ff6e1f367..eeb03c7f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -117,7 +117,7 @@ import { } from './infra/rate-limiting/index.js'; import { getAccessibleDeviceTypes, - findBySlug, + findDeviceTypeJsonBySlug, getDeviceTypeBySlug, } from './features/device-types/device-types.js'; import { proxy as supervisorProxy } from './features/device-proxy/device-proxy.js'; @@ -242,7 +242,7 @@ export const release = { }; export const deviceTypes = { getAccessibleDeviceTypes, - findBySlug, + findBySlug: findDeviceTypeJsonBySlug, getDeviceTypeBySlug, }; export * as contracts from './exports/contracts.js'; diff --git a/test/02_device-types.ts b/test/02_device-types.ts index a961a2248..8e39ef361 100644 --- a/test/02_device-types.ts +++ b/test/02_device-types.ts @@ -277,107 +277,5 @@ export default () => { expect(rpiConfig).to.not.have.property('logoUrl'); }); }); - - describe('/device-types/v1/:deviceType/images', () => { - it('should return a proper result', async () => { - const res = await supertest() - .get('/device-types/v1/raspberrypi3-64/images') - .expect(200); - expect(res.body).to.deep.equal({ - versions: [ - '2.0.2+rev2', - '2.0.2+rev2.dev', - '2.0.2+rev1', - '2.0.2+rev1.dev', - ], - latest: '2.0.2+rev2', - }); - }); - - it('should return a proper result for an alias', async () => { - const res = await supertest() - .get('/device-types/v1/raspberrypi364/images') - .expect(200); - expect(res.body).to.deep.equal({ - versions: [ - '2.0.2+rev2', - '2.0.2+rev2.dev', - '2.0.2+rev1', - '2.0.2+rev1.dev', - ], - latest: '2.0.2+rev2', - }); - }); - - it('should omit releases that have an IGNORE file', async () => { - const res = await supertest() - .get('/device-types/v1/dt-with-ignored-release/images') - .expect(200); - expect(res.body).to.deep.equal({ - versions: ['2.0.1+rev1.prod', '2.0.0+rev1.prod'], - latest: '2.0.1+rev1.prod', - }); - }); - - it('should omit releases that have an IGNORE file that gives an unauthorized error', async () => { - const res = await supertest() - .get('/device-types/v1/dt-with-403-ignore-file-release/images') - .expect(200); - expect(res.body).to.deep.equal({ - versions: ['2.0.1+rev1.prod', '2.0.0+rev1.prod'], - latest: '2.0.1+rev1.prod', - }); - }); - - it('should omit releases that have an empty device-type.json', async () => { - const res = await supertest() - .get('/device-types/v1/dt-with-empty-device-type-json-release/images') - .expect(200); - expect(res.body).to.deep.equal({ - versions: ['2.0.1+rev1.prod', '2.0.0+rev1.prod'], - latest: '2.0.1+rev1.prod', - }); - }); - - it('should omit releases that do not have a device-type.json', async () => { - const res = await supertest() - .get('/device-types/v1/dt-with-404-device-type-json-release/images') - .expect(200); - expect(res.body).to.deep.equal({ - versions: ['2.0.1+rev1.prod', '2.0.0+rev1.prod'], - latest: '2.0.1+rev1.prod', - }); - }); - - it('should succeed and omit releases that the retrieval of the IGNORE file fails', async () => { - const res = await supertest() - .get('/device-types/v1/dt-with-500-ignore-file-release/images') - .expect(200); - expect(res.body).to.deep.equal({ - versions: ['2.0.1+rev1.prod', '2.0.0+rev1.prod'], - latest: '2.0.1+rev1.prod', - }); - }); - - it('should succeed and omit releases that the retrieval of device-type.json fails', async () => { - const res = await supertest() - .get('/device-types/v1/dt-with-500-device-type-json-release/images') - .expect(200); - expect(res.body).to.deep.equal({ - versions: ['2.0.1+rev1.prod', '2.0.0+rev1.prod'], - latest: '2.0.1+rev1.prod', - }); - }); - - it('should succeed and omit device types whose details fail to be retrieved', async () => { - const res = await supertest() - .get('/device-types/v1/dt-with-500-device-type-json-release/images') - .expect(200); - expect(res.body).to.deep.equal({ - versions: ['2.0.1+rev1.prod', '2.0.0+rev1.prod'], - latest: '2.0.1+rev1.prod', - }); - }); - }); }); };