From c4f9d72172ef678c73ee05e8766c9bcfddd47073 Mon Sep 17 00:00:00 2001 From: Christina Ying Wang Date: Mon, 30 Jan 2023 15:30:17 -0600 Subject: [PATCH] Remove dependent devices content in codebase This includes: - proxyvisor.js - references in docs - references device-state, api-binder, compose modules, API - references in tests The commit also adds a migration to remove the 4 dependent device tables from the DB. Change-type: minor Signed-off-by: Christina Ying Wang --- docs/API.md | 9 - docs/debugging-supervisor.md | 4 - docs/dependent-apps.md | 295 ------- src/api-binder/index.ts | 37 - src/compose/application-manager.ts | 100 +-- src/compose/images.ts | 3 - src/device-api/actions.ts | 6 - src/device-api/index.ts | 3 - src/device-api/v2.ts | 1 - src/device-state.ts | 7 +- src/lib/constants.ts | 1 - src/lib/legacy.ts | 1 - src/logger.ts | 7 - src/migrations/M00010.js | 26 + src/proxyvisor.js | 1006 ----------------------- src/types/state.ts | 32 +- test/integration/compose/images.spec.ts | 2 - test/integration/db.spec.ts | 20 +- test/integration/device-state.spec.ts | 1 - test/legacy/40-target-state.spec.ts | 4 - test/lib/state-helper.ts | 1 - test/unit/compose/app.spec.ts | 2 - 22 files changed, 63 insertions(+), 1505 deletions(-) delete mode 100644 docs/dependent-apps.md create mode 100644 src/migrations/M00010.js delete mode 100644 src/proxyvisor.js diff --git a/docs/API.md b/docs/API.md index 50465f40..fd2c1cc1 100644 --- a/docs/API.md +++ b/docs/API.md @@ -736,7 +736,6 @@ Response: } } }, - "dependent": {}, "commit": "7fc9c5bea8e361acd49886fe6cc1e1cd" } ``` @@ -1010,10 +1009,6 @@ Response: "SUPERVISOR_OVERRIDE_LOCK": "false" }, "apps": {} - }, - "dependent": { - "apps": [], - "devices": [] } } } @@ -1080,10 +1075,6 @@ TARGET_STATE='{ "networks": {} } } - }, - "dependent": { - "apps": [], - "devices": [] } } ' diff --git a/docs/debugging-supervisor.md b/docs/debugging-supervisor.md index a42febc0..241a4b31 100644 --- a/docs/debugging-supervisor.md +++ b/docs/debugging-supervisor.md @@ -277,10 +277,6 @@ Database { open: true, filename: '/data/database.sqlite', mode: 65542 } { name: 'config' }, { name: 'containerLogs' }, { name: 'currentCommit' }, - { name: 'dependentApp' }, - { name: 'dependentAppTarget' }, - { name: 'dependentDevice' }, - { name: 'dependentDeviceTarget' }, { name: 'deviceConfig' }, { name: 'engineSnapshot' }, { name: 'image' }, diff --git a/docs/dependent-apps.md b/docs/dependent-apps.md deleted file mode 100644 index 8736dcf8..00000000 --- a/docs/dependent-apps.md +++ /dev/null @@ -1,295 +0,0 @@ -# Using the balena Supervisor to manage dependent applications - -Since version 2.5.0 the balena Supervisor can act as a proxy for dependent apps. - -Only Supervisors after version 2.5.0 have this functionality, and some of the endpoints appeared in later versions (we've noted it down where this is the case). - -## What is a dependent application - -A **dependent application** is a balena application that targets devices not capable of interacting directly with the balena API - the reasons can be several, the most common are: - -- no direct Internet capabilities -- not able to run balenaOS (being a microcontroller, for example) - -The **dependent application** is scoped under a balena application, which gets the definition of **gateway application**. - -The **gateway application** is responsible for detecting, provisioning and managing **dependent devices** belonging to one of its **dependent applications**. This is possible leveraging a new set of endpoints exposed by the balena Supervisor. - -When a new version of the dependent application is git pushed, the supervisor will download the docker image and expose the assets in one of the endpoints detailed below. It is then the gateway application (i.e. the user app that is run by the supervisor) that is responsible for ensuring those assets get deployed to the dependent devices, using the provided endpoints to perform the management. - -A dependent application follows the same development cycle of a conventional balena application: - -- it binds to your git workspace via the **balena remote** -- it consists in a Docker application -- it offers the same environment and configuration variables management - -There are some differences: - -- it does not support Dockerfile templating -- the Dockerfile must target either an `x86` or `amd64` base image -- the actual firmware/business logic must be stored in the `/assets` folder within the built docker image. - - You can either just `COPY` a pre-built artifact in that folder, or build your artifact at push time and then store it in the `/assets` folder. -- **a dependent application Docker image is only used to build, package and deliver the firmware on the dependent device via balena-supervisor - it won't be run at any point.** - -## How a dependent application works - -### Endpoints - -The supervisor exposes a REST API to interact with the dependent applications and dependent devices models that come from the balena API - it also allows using a set of hooks to have push functionality, both documented below. - -# HTTP API reference - -## Applications - -### GET /v1/dependent-apps -Dependent Applications List - -**Example** - -```bash -curl -X GET $BALENA_SUPERVISOR_ADDRESS/v1/dependent-apps?apikey=$BALENA_SUPERVISOR_API_KEY -``` - -**Response** -`HTTP/1.1 200 OK` - -```javascript -[ - { - "id": 13015, - "name": "edgeApp1", - "commit": "d43bea5e16658e653088ce4b9a91b6606c3c2a0d", - "config": {} - }, - { - "id": 13016, - "name": "edgeApp2", - "commit": "d0f6624d6410fa079159fa3ebe0d3af46753d75d", - "config": {} - } - ] -``` - -### GET /v1/dependent-apps/:appId/assets/:commit -Dependent Application Updates Registry - -**Example** - -```bash -curl -X GET $BALENA_SUPERVISOR_ADDRESS/v1/dependent-apps//assets/?apikey=$BALENA_SUPERVISOR_API_KEY -``` - -**Response** -`HTTP/1.1 200 OK` - - -```none -[application/x-tar] .tar -``` - -## Devices - -### GET /v1/devices -Dependent Devices List - -**Example** - -```bash -curl -X GET $BALENA_SUPERVISOR_ADDRESS/v1/devices?apikey=$BALENA_SUPERVISOR_API_KEY -``` - -**Response** -`HTTP/1.1 200 OK` - - -```javascript -[ - { - "id": 1, - "uuid": "5ae8cf6e062c033ea38435498ad9b487bcc714e9eab0fed0404ee56e397790", - "appId": 13015, - "device_type": "generic-amd64", - "logs_channel": "69f961abffaad1ff66031b29f712be4fb19e1bfabf1fee7a9ebfb5fa75a1fbdb", - "deviceId": "47270", - "is_online": null, - "name": "blue-sun", - "status": "Provisioned", - "download_progress": null, - "commit": "d43bea5e16658e693088ce4b9a91b6606c3c2a0d", - "targetCommit": "d43bea5e16653e653088ce4b9a91b6606c3c2a0d", - "environment":{}, - "targetEnvironment":{}, - "config":{}, - "targetConfig":{"RESIN_SUPERVISOR_DELTA":"1"} - }, - { - "id": 3, - "uuid": "8dc608765fd32665d49d218a7eb4657bc2ab8a56db06d2c57ef7c7e9a115da", - "appId": 13015, - "device_type": "generic-amd64", - "logs_channel": "d0244a90e8cd6e9a1ab410d3d599dea7f15110a6fe37b2a8fd69bb6ee0bce043", - "deviceId": "47318", - "is_online": null, - "name": "wild-paper", - "status": "Provisioned", - "download_progress": null, - "commit": "d43bea5e16658e253088ce4b9a91b6606c3c2a0d", - "targetCommit": "d43bea5e11658e653088ce4b9a91b6606c3c2a0d", - "environment":{}, - "targetEnvironment":{}, - "config":{}, - "targetConfig":{"RESIN_SUPERVISOR_DELTA":"1"} - } - ] -``` - -### POST /v1/devices -Dependent Device Provision - -The `device_type` parameter is optional, defaulting to `generic-amd64`. You -should only set this if you need to provision a dependent device to an -application with the deprecated `edge` device type. - -**Example** - -```bash -curl -H "Content-Type: application/json" -X POST --data '{"appId": , -"device_type": "edge"}' / -$BALENA_SUPERVISOR_ADDRESS/v1/devices?apikey=$BALENA_SUPERVISOR_API_KEY -``` - -**Response** -`HTTP/1.1 201 CREATED` - - -```javascript -{ - "id": 47318, - "uuid": "8dc608765fd32665d49a268a7eb4657bc2ab8a56db06d2c57ef7c7e9a115da", - "name": "wild-paper", - "note": null, - "device_type": "edge" - } -``` - -### GET /v1/devices/:uuid -Dependent Device Information - -**Example** - -```bash -curl -X GET $BALENA_SUPERVISOR_ADDRESS/v1/devices/?apikey=$BALENA_SUPERVISOR_API_KEY -``` - -**Response** -`HTTP/1.1 200 OK` - -```javascript -{ - "id": 1, - "uuid": "5ae8cf6e062c033ea57837498ad9b487bfc714e9eab0fed0404ee56e397790", - "appId": 13015, - "device_type": "generic-amd64", - "logs_channel": "69f961abffaad2ff00031b29f718be4fb19e1bfabf1fee7a9ebfb5fa75a1fbdb", - "deviceId": "47270", - "is_online": null, - "name": "blue-sun", - "status": "Provisioned", - "download_progress": null, - "commit": "d43bea5e16658e623088je4b9a91b6606c3c2a0d", - "targetCommit": "d43bea5e16658e651088ce4b9a21b6606c3c2a0d", - "env": null, - "targetEnv": null - } -``` - -### PUT /v1/devices/:uuid -Dependent Device Information Update - -**Example** - -```bash -curl -H "Content-Type: application/json" -X PUT --data / -'{"is_online":true, "status": "Updating", "commit": "339125a7529cb2c2a8c93a0bbd8af69f2d96286ab4f4552cb5cfe99b0d3ee9"}' / -$BALENA_SUPERVISOR_ADDRESS/v1/devices/?apikey=$BALENA_SUPERVISOR_API_KEY -``` - -**Response** -`HTTP/1.1 200 OK` - -```javascript -{ - "id": 1, - "uuid": "5ae8cf6e062c033ea38437498ad9b482bcc714e9eab0fed0404ee56e397790", - "appId": 13015, - "device_type": "generic-amd64", - "logs_channel": "69f961abffaad2ff66031b29f712be4fb19e1bfabf1fee7a9ebfb5fa05a1fbdb", - "deviceId": "47270", - "is_online": true, - "name": "blue-sun", - "status": "Updating", - "download_progress": null, - "commit": "d43bea5e16658e653088ce4b9a11b6606c3c2a0d", - "targetCommit": "d43bea5e16658e653088se4b9a91b6606c3c2a0d", - "env": null, - "targetEnv": null - } -``` - -### POST /v1/devices/:uuid/logs -Dependent Device Log - -**Example** - -```bash -curl -H "Content-Type: application/json" -X POST --data '{"message":"detected movement","timestamp":1472142960}' / -$BALENA_SUPERVISOR_ADDRESS/v1/devices//logs?apikey=$BALENA_SUPERVISOR_API_KEY -``` -**Response** -`HTTP/1.1 202 ACCEPTED` - -## Hooks (the requests the balena Supervisor performs) - -### Hook configuration - -You can point the supervisor where to find the hook server via a configuration variable. - -- `BALENA_DEPENDENT_DEVICES_HOOK_ADDRESS` _(defaults to `http://0.0.0.0:1337/v1/devices/`)_ - -It's worth mentioning (as described below) that the supervisor will append the dependent device uuid (`` in the hook descriptions) to every hook request URL - -### PUT /v1/devices/:uuid/restart -Dependent Device Restart Notification - -**Example** - -```bash -curl -H "Content-Type: application/json" -X PUT / -http://127.0.0.1:1337/v1/devices//restart -``` -**Response** -`HTTP/1.1 200 OK` - -### PUT /v1/devices/:uuid -Dependent Device Update Notification - -**Example** - -```bash -curl -H "Content-Type: application/json" -X PUT / ---data '{"commit":" ","environment": ""}' http://127.0.0.1:1337/v1/devices/ -``` -**Responses** -* `HTTP/1.1 200 OK` Acknowledgement of the notification without further trials: The Supervisor won't repeat the hook request -* `HTTP/1.1 202 ACCEPTED`Acknowledgement of the notification with validation: the Supervisor will repeat the hook request until the dependent device information gets updated via `Dependent Device Information Update` endpoint - -### DELETE /v1/devices/:uuid -Dependent Device Delete Notification - -**Example** - -```bash -curl -X DELETE http://127.0.0.1:1337/v1/devices/ -``` -**Response** -`HTTP/1.1 200 OK` diff --git a/src/api-binder/index.ts b/src/api-binder/index.ts index c2545884..69968fa1 100644 --- a/src/api-binder/index.ts +++ b/src/api-binder/index.ts @@ -5,7 +5,6 @@ import * as t from 'io-ts'; import * as _ from 'lodash'; import { PinejsClientRequest } from 'pinejs-client-request'; import * as url from 'url'; -import * as deviceRegister from '../lib/register-device'; import * as config from '../config'; import * as deviceConfig from '../device-config'; @@ -28,7 +27,6 @@ import * as TargetState from '../device-state/target-state'; import * as logger from '../logger'; import * as apiHelper from '../lib/api-helper'; -import { Device } from '../lib/api-helper'; import { startReporting, stateReportErrors } from './report'; interface DevicePinInfo { @@ -230,41 +228,6 @@ export async function patchDevice( ).timeout(conf.apiTimeout); } -export async function provisionDependentDevice( - device: Partial, -): Promise { - const conf = await config.getMany([ - 'unmanaged', - 'provisioned', - 'apiTimeout', - 'deviceId', - ]); - - if (conf.unmanaged) { - throw new Error('Cannot provision dependent device in unmanaged mode'); - } - if (!conf.provisioned) { - throw new Error( - 'Device must be provisioned to provision a dependent device', - ); - } - if (balenaApi == null) { - throw new InternalInconsistencyError( - 'Attempt to provision a dependent device without an API client', - ); - } - - _.defaults(device, { - is_managed_by__device: conf.deviceId, - uuid: deviceRegister.generateUniqueKey(), - registered_at: Math.floor(Date.now() / 1000), - }); - - return (await Bluebird.resolve( - balenaApi.post({ resource: 'device', body: device }), - ).timeout(conf.apiTimeout)) as Device; -} - export function startCurrentStateReport() { if (balenaApi == null) { throw new InternalInconsistencyError( diff --git a/src/compose/application-manager.ts b/src/compose/application-manager.ts index 5d2aa900..598c88d1 100644 --- a/src/compose/application-manager.ts +++ b/src/compose/application-manager.ts @@ -6,7 +6,6 @@ import * as config from '../config'; import { transaction, Transaction } from '../db'; import * as logger from '../logger'; import LocalModeManager from '../local-mode'; -import proxyvisor from '../proxyvisor'; import * as dbFormat from '../device-state/db-format'; import { validateTargetContracts } from '../lib/contracts'; @@ -123,10 +122,6 @@ export const initialized = _.once(async () => { serviceManager.on('change', reportCurrentState); }); -export function getDependentState() { - return proxyvisor.getCurrentStates(); -} - function reportCurrentState(data?: Partial) { events.emit('change', data ?? {}); } @@ -300,16 +295,6 @@ export async function inferNextSteps( steps.push(generateStep('noop', {})); } - steps = steps.concat( - await proxyvisor.getRequiredSteps( - availableImages, - downloading, - currentApps, - targetApps, - steps, - ), - ); - return steps; } @@ -500,10 +485,6 @@ export async function executeStep( step: CompositionStep, { force = false, skipLock = false } = {}, ): Promise { - if (proxyvisor.validActions.includes(step.action)) { - return proxyvisor.executeStepAction(step); - } - if (!validActions.includes(step.action)) { return Promise.reject( new InternalInconsistencyError( @@ -645,10 +626,6 @@ export function clearTargetVolatileForServices(imageIds: number[]) { } } -export function getDependentTargets() { - return proxyvisor.getTarget(); -} - /** * This is only used by the API. Do not use as the use of serviceIds is getting * deprecated @@ -684,22 +661,12 @@ export function bestDeltaSource( image: Image, available: Image[], ): string | null { - if (!image.dependent) { - for (const availableImage of available) { - if ( - availableImage.serviceName === image.serviceName && - availableImage.appId === image.appId - ) { - return availableImage.name; - } - } - } else { - // This only makes sense for dependent devices which are still - // single app. - for (const availableImage of available) { - if (availableImage.appId === image.appId) { - return availableImage.name; - } + for (const availableImage of available) { + if ( + availableImage.serviceName === image.serviceName && + availableImage.appId === image.appId + ) { + return availableImage.name; } } return null; @@ -831,17 +798,9 @@ function saveAndRemoveImages( .map((img) => bestDeltaSource(img, availableImages)) .filter((img) => img != null); - const proxyvisorImages = proxyvisor.imagesInUse(current, target); - - const imagesToRemove = availableAndUnused.filter((image) => { - const notUsedForDelta = !deltaSources.includes(image.name); - const notUsedByProxyvisor = !proxyvisorImages.some((proxyvisorImage) => - imageManager.isSameImage(image, { - name: proxyvisorImage, - }), - ); - return notUsedForDelta && notUsedByProxyvisor; - }); + const imagesToRemove = availableAndUnused.filter( + (image) => !deltaSources.includes(image.name), + ); return imagesToSave .map((image) => ({ action: 'saveImage', image } as CompositionStep)) @@ -897,7 +856,6 @@ export async function getLegacyState() { ]); const apps: Dictionary = {}; - const dependent: Dictionary = {}; let releaseId: number | boolean | null | undefined = null; // ???? const creationTimesAndReleases: Dictionary = {}; // We iterate over the current running services and add them to the current state @@ -944,37 +902,23 @@ export async function getLegacyState() { for (const image of images) { const { appId } = image; - if (!image.dependent) { - if (apps[appId] == null) { - apps[appId] = {}; - } - if (apps[appId].services == null) { - apps[appId].services = {}; - } - if (apps[appId].services[image.imageId] == null) { - apps[appId].services[image.imageId] = _.pick(image, [ - 'status', - 'releaseId', - ]); - apps[appId].services[image.imageId].download_progress = - image.downloadProgress; - } - } else if (image.imageId != null) { - if (dependent[appId] == null) { - dependent[appId] = {}; - } - if (dependent[appId].images == null) { - dependent[appId].images = {}; - } - dependent[appId].images[image.imageId] = _.pick(image, ['status']); - dependent[appId].images[image.imageId].download_progress = + if (apps[appId] == null) { + apps[appId] = {}; + } + if (apps[appId].services == null) { + apps[appId].services = {}; + } + if (apps[appId].services[image.imageId] == null) { + apps[appId].services[image.imageId] = _.pick(image, [ + 'status', + 'releaseId', + ]); + apps[appId].services[image.imageId].download_progress = image.downloadProgress; - } else { - log.debug('Ignoring legacy dependent image', image); } } - return { local: apps, dependent }; + return { local: apps }; } // TODO: this function is probably more inefficient than it needs to be, since diff --git a/src/compose/images.ts b/src/compose/images.ts index 69427e6c..44b050dd 100644 --- a/src/compose/images.ts +++ b/src/compose/images.ts @@ -53,7 +53,6 @@ export interface Image { */ releaseId: number; commit: string; - dependent: number; dockerImageId?: string; status?: 'Downloading' | 'Downloaded' | 'Deleting'; downloadProgress?: number | null; @@ -184,7 +183,6 @@ export function imageFromService(service: ServiceInfo): Image { imageId: service.imageId!, releaseId: service.releaseId!, commit: service.commit!, - dependent: 0, }; } @@ -756,7 +754,6 @@ function format(image: Image): Partial> { imageId: null, releaseId: null, commit: null, - dependent: 0, dockerImageId: null, }) .omit('id') diff --git a/src/device-api/actions.ts b/src/device-api/actions.ts index 388b9bb8..a9873929 100644 --- a/src/device-api/actions.ts +++ b/src/device-api/actions.ts @@ -139,9 +139,6 @@ export function safeStateClone( local: { config: {}, }, - dependent: { - config: {}, - }, }; if (targetState.local != null) { @@ -151,9 +148,6 @@ export function safeStateClone( apps: _.mapValues(targetState.local.apps, safeAppClone), }; } - if (targetState.dependent != null) { - cloned.dependent = _.cloneDeep(targetState.dependent); - } return cloned as InstancedDeviceState; } diff --git a/src/device-api/index.ts b/src/device-api/index.ts index 03327c80..0690493a 100644 --- a/src/device-api/index.ts +++ b/src/device-api/index.ts @@ -3,7 +3,6 @@ import * as express from 'express'; import * as middleware from './middleware'; import * as apiKeys from './api-keys'; import * as actions from './actions'; -import proxyvisor from '../proxyvisor'; import log from '../lib/supervisor-console'; import type { Server } from 'http'; @@ -78,8 +77,6 @@ export class SupervisorAPI { this.api.use(router); } - this.api.use(proxyvisor.router); - this.api.use(middleware.errors); } diff --git a/src/device-api/v2.ts b/src/device-api/v2.ts index 1fdd8dd4..f58e36e8 100644 --- a/src/device-api/v2.ts +++ b/src/device-api/v2.ts @@ -153,7 +153,6 @@ router.post( }, ); -// TODO: Support dependent applications when this feature is complete router.get( '/v2/applications/state', async (req: AuthorizedRequest, res: Response, next: NextFunction) => { diff --git a/src/device-state.ts b/src/device-state.ts index 1b7b64d9..86323c1a 100644 --- a/src/device-state.ts +++ b/src/device-state.ts @@ -364,7 +364,6 @@ export function getTarget({ config: await deviceConfig.getTarget({ initial }), apps: await dbFormat.getApps(), }, - dependent: await applicationManager.getDependentTargets(), }; }); } @@ -378,14 +377,12 @@ export async function getLegacyState(): Promise { const appsStatus = await applicationManager.getLegacyState(); const theState: DeepPartial = { local: {}, - dependent: {}, }; theState.local = { ...theState.local, ...currentVolatile, }; theState.local!.apps = appsStatus.local; - theState.dependent!.apps = appsStatus.dependent; // Multi-app warning! // If we have more than one app, simply return the first commit. @@ -503,11 +500,10 @@ export async function getCurrentForReport( // Get the current state as object instances export async function getCurrentState(): Promise { - const [name, devConfig, apps, dependent] = await Promise.all([ + const [name, devConfig, apps] = await Promise.all([ config.get('name'), deviceConfig.getCurrent(), applicationManager.getCurrentApps(), - applicationManager.getDependentState(), ]); return { @@ -516,7 +512,6 @@ export async function getCurrentState(): Promise { config: devConfig, apps, }, - dependent, }; } diff --git a/src/lib/constants.ts b/src/lib/constants.ts index b5a00591..adf193e8 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -37,7 +37,6 @@ const constants = { bootMountPointFromEnv, bootMountPoint: bootMountPointFromEnv || '/boot', configJsonPathOnHost: checkString(process.env.CONFIG_JSON_PATH), - proxyvisorHookReceiver: 'http://0.0.0.0:1337', configJsonNonAtomicPath: '/boot/config.json', supervisorNetworkInterface, allowedInterfaces: [ diff --git a/src/lib/legacy.ts b/src/lib/legacy.ts index a78e9c97..e4736a2b 100644 --- a/src/lib/legacy.ts +++ b/src/lib/legacy.ts @@ -177,7 +177,6 @@ export async function normaliseLegacyDatabase() { imageId: image.id, releaseId: release.id, commit: app.commit, - dependent: 0, dockerImageId: imageFromDocker.Id, }); } else { diff --git a/src/logger.ts b/src/logger.ts index d57e5930..f673d089 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -101,13 +101,6 @@ export function enable(value: boolean = true) { } } -export function logDependent(message: LogMessage, device: { uuid: string }) { - if (backend != null) { - message.uuid = device.uuid; - backend.log(message); - } -} - export function log(message: LogMessage) { if (backend != null) { backend.log(message); diff --git a/src/migrations/M00010.js b/src/migrations/M00010.js new file mode 100644 index 00000000..180b9912 --- /dev/null +++ b/src/migrations/M00010.js @@ -0,0 +1,26 @@ +// This migration removes references to dependent devices from +// the database, as we are no longer pursuing dependent devices. +export async function up(knex) { + // Remove dependent key from each image + if (await knex.schema.hasColumn('image', 'dependent')) { + await knex.schema.table('image', (t) => { + return t.dropColumn('dependent'); + }); + } + + // Delete dependent device/app tables + const dropTable = async (table) => { + const exists = await knex.schema.hasTable(table); + if (exists) { + await knex.schema.dropTable(table); + } + }; + await dropTable('dependentDeviceTarget'); + await dropTable('dependentDevice'); + await dropTable('dependentAppTarget'); + await dropTable('dependentApp'); +} + +export function down() { + throw new Error('Not Implemented'); +} diff --git a/src/proxyvisor.js b/src/proxyvisor.js deleted file mode 100644 index c57dcb32..00000000 --- a/src/proxyvisor.js +++ /dev/null @@ -1,1006 +0,0 @@ -import * as Promise from 'bluebird'; -import * as _ from 'lodash'; -import * as express from 'express'; -import { promises as fs } from 'fs'; -import * as path from 'path'; -import * as url from 'url'; - -import * as request from './lib/request'; -import * as constants from './lib/constants'; -import { - checkInt, - validStringOrUndefined, - validObjectOrUndefined, -} from './lib/validation'; -import { log } from './lib/supervisor-console'; -import * as dockerUtils from './lib/docker-utils'; -import { InternalInconsistencyError } from './lib/errors'; -import * as apiHelper from './lib/api-helper'; -import { exec, mkdirp } from './lib/fs-utils'; - -import { normalise } from './compose/images'; -import * as db from './db'; -import * as config from './config'; -import * as logger from './logger'; -import * as apiBinder from './api-binder'; -import * as dbFormat from './device-state/db-format'; -import * as deviceConfig from './device-config'; - -const isDefined = _.negate(_.isUndefined); - -const parseDeviceFields = function (device) { - device.id = parseInt(device.deviceId, 10); - device.appId = parseInt(device.appId, 10); - device.config = JSON.parse(device.config ?? '{}'); - device.environment = JSON.parse(device.environment ?? '{}'); - device.targetConfig = JSON.parse(device.targetConfig ?? '{}'); - device.targetEnvironment = JSON.parse(device.targetEnvironment ?? '{}'); - return _.omit(device, 'markedForDeletion', 'logs_channel'); -}; - -const tarDirectory = (appId) => `/data/dependent-assets/${appId}`; - -const tarFilename = (appId, commit) => `${appId}-${commit}.tar`; - -const tarPath = (appId, commit) => - `${tarDirectory(appId)}/${tarFilename(appId, commit)}`; - -const getTarArchive = (source, destination) => - fs - .lstat(destination) - .catch(() => - mkdirp(path.dirname(destination)).then(() => - exec(`tar -cvf '${destination}' *`, { cwd: source }), - ), - ); - -const cleanupTars = function (appId, commit) { - let fileToKeep; - if (commit != null) { - fileToKeep = tarFilename(appId, commit); - } else { - fileToKeep = null; - } - const dir = tarDirectory(appId); - return fs - .readdir(dir) - .catch(() => []) - .then(function (files) { - if (fileToKeep != null) { - files = _.reject(files, fileToKeep); - } - return Promise.map(files, (file) => fs.unlink(path.join(dir, file))); - }); -}; - -const formatTargetAsState = (device) => ({ - appId: parseInt(device.appId, 10), - commit: device.targetCommit, - environment: device.targetEnvironment, - config: device.targetConfig, -}); - -const formatCurrentAsState = (device) => ({ - appId: parseInt(device.appId, 10), - commit: device.commit, - environment: device.environment, - config: device.config, -}); - -const createProxyvisorRouter = function (pv) { - const router = express.Router(); - router.get('/v1/devices', async (_req, res) => { - try { - const fields = await db.models('dependentDevice').select(); - const devices = fields.map(parseDeviceFields); - res.json(devices); - } catch (/** @type {any} */ err) { - res.status(503).send(err?.message || err || 'Unknown error'); - } - }); - - router.post('/v1/devices', function (req, res) { - let { appId, device_type } = req.body; - - if ( - appId == null || - _.isNaN(parseInt(appId, 10)) || - parseInt(appId, 10) <= 0 - ) { - res.status(400).send('appId must be a positive integer'); - return; - } - if (device_type == null) { - device_type = 'generic'; - } - const d = { - belongs_to__application: req.body.appId, - device_type, - }; - return apiBinder - .provisionDependentDevice(d) - .then(function (dev) { - // If the response has id: null then something was wrong in the request - // but we don't know precisely what. - if (dev.id == null) { - res - .status(400) - .send('Provisioning failed, invalid appId or credentials'); - return; - } - const deviceForDB = { - uuid: dev.uuid, - appId, - device_type: dev.device_type, - deviceId: dev.id, - name: dev.name, - status: dev.status, - }; - return db - .models('dependentDevice') - .insert(deviceForDB) - .then(() => res.status(201).send(dev)); - }) - .catch(function (err) { - log.error(`Error on ${req.method} ${url.parse(req.url).pathname}`, err); - return res.status(503).send(err?.message || err || 'Unknown error'); - }); - }); - - router.get('/v1/devices/:uuid', function (req, res) { - const { uuid } = req.params; - return db - .models('dependentDevice') - .select() - .where({ uuid }) - .then(function ([device]) { - if (device == null) { - return res.status(404).send('Device not found'); - } - if (device.markedForDeletion) { - return res.status(410).send('Device deleted'); - } - return res.json(parseDeviceFields(device)); - }) - .catch(function (err) { - log.error(`Error on ${req.method} ${url.parse(req.url).pathname}`, err); - return res.status(503).send(err?.message || err || 'Unknown error'); - }); - }); - - router.post('/v1/devices/:uuid/logs', function (req, res) { - const { uuid } = req.params; - const m = { - message: req.body.message, - timestamp: req.body.timestamp || Date.now(), - }; - if (req.body.isSystem != null) { - m.isSystem = req.body.isSystem; - } - - return db - .models('dependentDevice') - .select() - .where({ uuid }) - .then(function ([device]) { - if (device == null) { - return res.status(404).send('Device not found'); - } - if (device.markedForDeletion) { - return res.status(410).send('Device deleted'); - } - logger.logDependent(m, { uuid }); - return res.status(202).send('OK'); - }) - .catch(function (err) { - log.error(`Error on ${req.method} ${url.parse(req.url).pathname}`, err); - return res.status(503).send(err?.message || err || 'Unknown error'); - }); - }); - - router.put('/v1/devices/:uuid', function (req, res) { - const { uuid } = req.params; - let { - status, - is_online, - commit, - releaseId, - environment, - config: conf, - } = req.body; - const validateDeviceFields = function () { - if (isDefined(is_online) && !_.isBoolean(is_online)) { - return 'is_online must be a boolean'; - } - if (!validStringOrUndefined(status)) { - return 'status must be a non-empty string'; - } - if (!validStringOrUndefined(commit)) { - return 'commit must be a non-empty string'; - } - if (!validStringOrUndefined(releaseId)) { - return 'commit must be a non-empty string'; - } - if (!validObjectOrUndefined(environment)) { - return 'environment must be an object'; - } - if (!validObjectOrUndefined(conf)) { - return 'config must be an object'; - } - return null; - }; - const requestError = validateDeviceFields(); - if (requestError != null) { - res.status(400).send(requestError); - return; - } - - if (isDefined(environment)) { - environment = JSON.stringify(environment); - } - if (isDefined(conf)) { - conf = JSON.stringify(conf); - } - - const fieldsToUpdateOnDB = _.pickBy( - { status, is_online, commit, releaseId, config: conf, environment }, - isDefined, - ); - /** @type {Dictionary} */ - const fieldsToUpdateOnAPI = _.pick( - fieldsToUpdateOnDB, - 'status', - 'is_online', - 'releaseId', - ); - if (fieldsToUpdateOnDB.commit != null) { - fieldsToUpdateOnAPI.is_on__commit = fieldsToUpdateOnDB.commit; - } - - if (_.isEmpty(fieldsToUpdateOnDB)) { - res.status(400).send('At least one device attribute must be updated'); - return; - } - - return db - .models('dependentDevice') - .select() - .where({ uuid }) - .then(function ([device]) { - if (device == null) { - return res.status(404).send('Device not found'); - } - if (device.markedForDeletion) { - return res.status(410).send('Device deleted'); - } - if (device.deviceId == null) { - throw new Error('Device is invalid'); - } - return Promise.try(function () { - if (!_.isEmpty(fieldsToUpdateOnAPI)) { - return apiBinder.patchDevice(device.deviceId, fieldsToUpdateOnAPI); - } - }) - .then(() => - db - .models('dependentDevice') - .update(fieldsToUpdateOnDB) - .where({ uuid }), - ) - .then(() => db.models('dependentDevice').select().where({ uuid })) - .then(function ([dbDevice]) { - return res.json(parseDeviceFields(dbDevice)); - }); - }) - .catch(function (err) { - log.error(`Error on ${req.method} ${url.parse(req.url).pathname}`, err); - return res.status(503).send(err?.message || err || 'Unknown error'); - }); - }); - - router.get('/v1/dependent-apps/:appId/assets/:commit', async (req, res) => { - try { - const [app] = await db - .models('dependentApp') - .select() - .where(_.pick(req.params, 'appId', 'commit')); - - if (!app) { - return res.status(404).send('Not found'); - } - const dest = tarPath(app.appId, app.commit); - try { - await fs.lstat(dest); - } catch { - await Promise.using( - pv.docker.imageRootDirMounted(app.image), - (rootDir) => getTarArchive(rootDir + '/assets', dest), - ); - } - res.sendFile(dest); - } catch (/** @type {any} */ err) { - log.error(`Error on ${req.method} ${url.parse(req.url).pathname}`, err); - return res.status(503).send(err?.message || err || 'Unknown error'); - } - }); - - router.get('/v1/dependent-apps', async (req, res) => { - try { - const apps = await db.models('dependentApp').select(); - - const $apps = apps.map((app) => ({ - id: parseInt(app.appId, 10), - commit: app.commit, - name: app.name, - config: JSON.parse(app.config ?? '{}'), - })); - res.json($apps); - } catch (/** @type {any} */ err) { - log.error(`Error on ${req.method} ${url.parse(req.url).pathname}`, err); - return res.status(503).send(err?.message || err || 'Unknown error'); - } - }); - - return router; -}; - -class Proxyvisor { - constructor() { - this.executeStepAction = this.executeStepAction.bind(this); - this.getCurrentStates = this.getCurrentStates.bind(this); - this.normaliseDependentAppForDB = - this.normaliseDependentAppForDB.bind(this); - this.setTargetInTransaction = this.setTargetInTransaction.bind(this); - this.getTarget = this.getTarget.bind(this); - this._getHookStep = this._getHookStep.bind(this); - this.nextStepsForDependentApp = this.nextStepsForDependentApp.bind(this); - this.getRequiredSteps = this.getRequiredSteps.bind(this); - this.getHookEndpoint = this.getHookEndpoint.bind(this); - this.sendUpdate = this.sendUpdate.bind(this); - this.sendDeleteHook = this.sendDeleteHook.bind(this); - this.sendUpdates = this.sendUpdates.bind(this); - this.acknowledgedState = {}; - this.lastRequestForDevice = {}; - this.router = createProxyvisorRouter(this); - this.actionExecutors = { - updateDependentTargets: (step) => { - return config - .initialized() - .then(() => config.getMany(['currentApiKey', 'apiTimeout'])) - .then(({ currentApiKey, apiTimeout }) => { - // - take each of the step.devices and update dependentDevice with it (targetCommit, targetEnvironment, targetConfig) - // - if update returns 0, then use APIBinder to fetch the device, then store it to the db - // - set markedForDeletion: true for devices that are not in the step.devices list - // - update dependentApp with step.app - return Promise.map(step.devices, (device) => { - const { uuid } = device; - // Only consider one app per dependent device for now - const appId = _(device.apps).keys().head(); - if (appId == null) { - throw new Error( - 'Could not find an app for the dependent device', - ); - } - const targetCommit = device.apps[appId].commit; - const targetEnvironment = JSON.stringify( - device.apps[appId].environment, - ); - const targetConfig = JSON.stringify(device.apps[appId].config); - return db - .models('dependentDevice') - .update({ - appId, - targetEnvironment, - targetConfig, - targetCommit, - name: device.name, - }) - .where({ uuid }) - .then((n) => { - if (n !== 0) { - return; - } - // If the device is not in the DB it means it was provisioned externally - // so we need to fetch it. - if (apiBinder.balenaApi == null) { - throw new InternalInconsistencyError( - 'proxyvisor called fetchDevice without an initialized API client', - ); - } - - return apiHelper - .fetchDevice( - apiBinder.balenaApi, - uuid, - currentApiKey, - apiTimeout, - ) - .then((dev) => { - if (dev == null) { - throw new InternalInconsistencyError( - `Could not fetch a device with UUID: ${uuid}`, - ); - } - const deviceForDB = { - uuid, - appId, - device_type: dev.device_type, - deviceId: dev.id, - is_online: dev.is_online, - name: dev.name, - status: dev.status, - targetCommit, - targetConfig, - targetEnvironment, - }; - return db.models('dependentDevice').insert(deviceForDB); - }); - }); - }) - .then(() => { - return db - .models('dependentDevice') - .where({ appId: step.appId }) - .whereNotIn('uuid', _.map(step.devices, 'uuid')) - .update({ markedForDeletion: true }); - }) - .then(() => { - return this.normaliseDependentAppForDB(step.app); - }) - .then((appForDB) => { - return db.upsertModel('dependentApp', appForDB, { - appId: step.appId, - }); - }) - .then(() => cleanupTars(step.appId, step.app.commit)); - }); - }, - - sendDependentHooks: (step) => { - return Promise.join( - config.get('apiTimeout'), - this.getHookEndpoint(step.appId), - (apiTimeout, endpoint) => { - return Promise.mapSeries(step.devices, (device) => { - return Promise.try(() => { - if (this.lastRequestForDevice[device.uuid] != null) { - const diff = - Date.now() - this.lastRequestForDevice[device.uuid]; - if (diff < 30000) { - return Promise.delay(30001 - diff); - } - } - }).then(() => { - this.lastRequestForDevice[device.uuid] = Date.now(); - if (device.markedForDeletion) { - return this.sendDeleteHook(device, apiTimeout, endpoint); - } else { - return this.sendUpdate(device, apiTimeout, endpoint); - } - }); - }); - }, - ); - }, - - removeDependentApp: (step) => { - // find step.app and delete it from the DB - // find devices with step.appId and delete them from the DB - return db.transaction((trx) => - trx('dependentApp') - .where({ appId: step.appId }) - .del() - .then(() => - trx('dependentDevice').where({ appId: step.appId }).del(), - ) - .then(() => cleanupTars(step.appId)), - ); - }, - }; - this.validActions = _.keys(this.actionExecutors); - } - - executeStepAction(step) { - return Promise.try(() => { - if (this.actionExecutors[step.action] == null) { - throw new Error(`Invalid proxyvisor action ${step.action}`); - } - - return this.actionExecutors[step.action](step); - }); - } - - getCurrentStates() { - return Promise.join( - Promise.map( - db.models('dependentApp').select(), - this.normaliseDependentAppFromDB, - ), - db.models('dependentDevice').select(), - function (apps, devicesFromDB) { - const devices = _.map(devicesFromDB, function (device) { - const dev = { - uuid: device.uuid, - name: device.name, - lock_expiry_date: device.lock_expiry_date, - markedForDeletion: device.markedForDeletion, - apps: {}, - }; - dev.apps[device.appId] = { - commit: device.commit, - config: JSON.parse(device.config), - environment: JSON.parse(device.environment), - targetCommit: device.targetCommit, - targetEnvironment: JSON.parse(device.targetEnvironment), - targetConfig: JSON.parse(device.targetConfig), - }; - return dev; - }); - return { apps, devices }; - }, - ); - } - - normaliseDependentAppForDB(app) { - let image; - if (app.image != null) { - image = normalise(app.image); - } else { - image = null; - } - const dbApp = { - appId: app.appId, - name: app.name, - commit: app.commit, - releaseId: app.releaseId, - imageId: app.imageId, - parentApp: app.parentApp, - image, - config: JSON.stringify(app.config ?? {}), - environment: JSON.stringify(app.environment ?? {}), - }; - return Promise.props(dbApp); - } - - normaliseDependentDeviceTargetForDB(device, appCommit) { - return Promise.try(function () { - const apps = _.mapValues(_.clone(device.apps ?? {}), function (app) { - app.commit = appCommit || null; - if (app.config == null) { - app.config = {}; - } - if (app.environment == null) { - app.environment = {}; - } - return app; - }); - const outDevice = { - uuid: device.uuid, - name: device.name, - apps: JSON.stringify(apps), - }; - return outDevice; - }); - } - - setTargetInTransaction(dependent, trx) { - return Promise.try(() => { - if (dependent?.apps != null) { - const appsArray = _.map(dependent.apps, function (app, appId) { - const appClone = _.clone(app); - appClone.appId = checkInt(appId); - return appClone; - }); - return Promise.map(appsArray, this.normaliseDependentAppForDB) - .tap((appsForDB) => { - return Promise.map(appsForDB, (app) => { - return db.upsertModel( - 'dependentAppTarget', - app, - { appId: app.appId }, - trx, - ); - }); - }) - .then((appsForDB) => - trx('dependentAppTarget') - .whereNotIn('appId', _.map(appsForDB, 'appId')) - .del(), - ); - } - }).then(() => { - if (dependent?.devices != null) { - const devicesArray = _.map(dependent.devices, function (dev, uuid) { - const devClone = _.clone(dev); - devClone.uuid = uuid; - return devClone; - }); - return Promise.map(devicesArray, (device) => { - const appId = _.keys(device.apps)[0]; - return this.normaliseDependentDeviceTargetForDB( - device, - dependent.apps[appId]?.commit, - ); - }).then((devicesForDB) => { - return Promise.map(devicesForDB, (device) => { - return db.upsertModel( - 'dependentDeviceTarget', - device, - { uuid: device.uuid }, - trx, - ); - }).then(() => - trx('dependentDeviceTarget') - .whereNotIn('uuid', _.map(devicesForDB, 'uuid')) - .del(), - ); - }); - } - }); - } - - normaliseDependentAppFromDB(app) { - return Promise.try(function () { - const outApp = { - appId: app.appId, - name: app.name, - commit: app.commit, - releaseId: app.releaseId, - image: app.image, - imageId: app.imageId, - config: JSON.parse(app.config), - environment: JSON.parse(app.environment), - parentApp: app.parentApp, - }; - return outApp; - }); - } - - normaliseDependentDeviceTargetFromDB(device) { - return Promise.try(function () { - const outDevice = { - uuid: device.uuid, - name: device.name, - apps: _.mapValues(JSON.parse(device.apps), function (a) { - if (a.commit == null) { - a.commit = null; - } - return a; - }), - }; - return outDevice; - }); - } - - normaliseDependentDeviceFromDB(device) { - return Promise.try(function () { - const outDevice = _.clone(device); - for (const prop of [ - 'environment', - 'config', - 'targetEnvironment', - 'targetConfig', - ]) { - outDevice[prop] = JSON.parse(device[prop]); - } - return outDevice; - }); - } - - getTarget() { - return Promise.props({ - apps: Promise.map( - db.models('dependentAppTarget').select(), - this.normaliseDependentAppFromDB, - ), - devices: Promise.map( - db.models('dependentDeviceTarget').select(), - this.normaliseDependentDeviceTargetFromDB, - ), - }); - } - - imagesInUse(current, target) { - const images = []; - if (current?.dependent?.apps != null) { - _.forEach(current.dependent.apps, (app) => { - images.push(app.image); - }); - } - if (target?.dependent?.apps != null) { - _.forEach(target.dependent.apps, (app) => { - images.push(app.image); - }); - } - return images; - } - - _imageAvailable(image, available) { - return _.some(available, { name: image }); - } - - _getHookStep(currentDevices, appId) { - const hookStep = { - action: 'sendDependentHooks', - /** @type {Array<{uuid: string, target?: any, markedForDeletion?: true}>} */ - devices: [], - appId, - }; - for (const device of currentDevices) { - if (device.markedForDeletion) { - hookStep.devices.push({ - uuid: device.uuid, - markedForDeletion: true, - }); - } else { - const targetState = { - appId, - commit: device.apps[appId].targetCommit, - config: device.apps[appId].targetConfig, - environment: device.apps[appId].targetEnvironment, - }; - const currentState = { - appId, - commit: device.apps[appId].commit, - config: device.apps[appId].config, - environment: device.apps[appId].environment, - }; - if ( - device.apps[appId].targetCommit != null && - !_.isEqual(targetState, currentState) && - !_.isEqual(targetState, this.acknowledgedState[device.uuid]) - ) { - hookStep.devices.push({ - uuid: device.uuid, - target: targetState, - }); - } - } - } - return hookStep; - } - - _compareDevices(currentDevices, targetDevices, appId) { - let currentDeviceTargets = _.map(currentDevices, function (dev) { - if (dev.markedForDeletion) { - return null; - } - const devTarget = _.clone(dev); - delete devTarget.markedForDeletion; - delete devTarget.lock_expiry_date; - devTarget.apps = {}; - devTarget.apps[appId] = { - commit: dev.apps[appId].targetCommit, - environment: dev.apps[appId].targetEnvironment || {}, - config: dev.apps[appId].targetConfig || {}, - }; - return devTarget; - }); - currentDeviceTargets = _.filter( - currentDeviceTargets, - (dev) => !_.isNull(dev), - ); - return !_.isEmpty( - _.xorWith(currentDeviceTargets, targetDevices, _.isEqual), - ); - } - - imageForDependentApp(app) { - return { - name: app.image, - imageId: app.imageId, - appId: app.appId, - dependent: true, - }; - } - - nextStepsForDependentApp( - appId, - availableImages, - downloading, - current, - target, - currentDevices, - targetDevices, - stepsInProgress, - ) { - // - if there's current but not target, push a removeDependentApp step - if (target == null) { - return [ - { - action: 'removeDependentApp', - appId: current.appId, - }, - ]; - } - - if (_.some(stepsInProgress, (step) => step.appId === target.parentApp)) { - return [{ action: 'noop' }]; - } - - const needsDownload = - target.commit != null && - target.image != null && - !this._imageAvailable(target.image, availableImages); - - // - if toBeDownloaded includes this app, push a fetch step - if (needsDownload) { - if (_.includes(downloading, target.imageId)) { - return [{ action: 'noop' }]; - } else { - return [ - { - action: 'fetch', - appId, - image: this.imageForDependentApp(target), - }, - ]; - } - } - - const devicesDiffer = this._compareDevices( - currentDevices, - targetDevices, - appId, - ); - - // - if current doesn't match target, or the devices differ, push an updateDependentTargets step - if (!_.isEqual(current, target) || devicesDiffer) { - return [ - { - action: 'updateDependentTargets', - devices: targetDevices, - app: target, - appId, - }, - ]; - } - - // if we got to this point, the current app is up to date and devices have the - // correct targetCommit, targetEnvironment and targetConfig. - const hookStep = this._getHookStep(currentDevices, appId); - if (!_.isEmpty(hookStep.devices)) { - return [hookStep]; - } - return []; - } - - getRequiredSteps( - availableImages, - downloading, - current, - target, - stepsInProgress, - ) { - return Promise.try(() => { - const targetApps = _.keyBy(target.dependent?.apps ?? [], 'appId'); - const targetAppIds = _.keys(targetApps); - const currentApps = _.keyBy(current.dependent?.apps ?? [], 'appId'); - const currentAppIds = _.keys(currentApps); - const allAppIds = _.union(targetAppIds, currentAppIds); - - let steps = []; - for (const appId of allAppIds) { - const devicesForApp = (devices) => - _.filter(devices, (d) => _.has(d.apps, appId)); - - const currentDevices = devicesForApp(current.dependent.devices); - const targetDevices = devicesForApp(target.dependent.devices); - const stepsForApp = this.nextStepsForDependentApp( - appId, - availableImages, - downloading, - currentApps[appId], - targetApps[appId], - currentDevices, - targetDevices, - stepsInProgress, - ); - steps = steps.concat(stepsForApp); - } - return steps; - }); - } - - getHookEndpoint(appId) { - return db - .models('dependentApp') - .select('parentApp') - .where({ appId }) - .then(([{ parentApp }]) => dbFormat.getApp(parseInt(parentApp, 10))) - .then((parentApp) => { - return Promise.map(parentApp.services ?? [], (service) => { - return dockerUtils.getImageEnv(service.config.image); - }).then(function (imageEnvs) { - const imageHookAddresses = _.map( - imageEnvs, - (env) => - env.BALENA_DEPENDENT_DEVICES_HOOK_ADDRESS ?? - env.RESIN_DEPENDENT_DEVICES_HOOK_ADDRESS, - ); - for (const addr of imageHookAddresses) { - if (addr != null) { - return addr; - } - } - // If we don't find the hook address in the images, we take it from - // the global config - return deviceConfig - .getTarget() - .then( - (target) => - target.BALENA_DEPENDENT_DEVICES_HOOK_ADDRESS ?? - target.RESIN_DEPENDENT_DEVICES_HOOK_ADDRESS ?? - `${constants.proxyvisorHookReceiver}/v1/devices/`, - ); - }); - }); - } - - sendUpdate(device, timeout, endpoint) { - return Promise.resolve(request.getRequestInstance()) - .then((instance) => - instance.putAsync(`${endpoint}${device.uuid}`, { - json: true, - body: device.target, - }), - ) - .timeout(timeout) - .spread((response, body) => { - if (response.statusCode === 200) { - return (this.acknowledgedState[device.uuid] = device.target); - } else { - this.acknowledgedState[device.uuid] = null; - if (response.statusCode !== 202) { - throw new Error(`Hook returned ${response.statusCode}: ${body}`); - } - } - }) - .catch((err) => log.error(`Error updating device ${device.uuid}`, err)); - } - - sendDeleteHook({ uuid }, timeout, endpoint) { - return Promise.resolve(request.getRequestInstance()) - .then((instance) => instance.delAsync(`${endpoint}${uuid}`)) - .timeout(timeout) - .spread((response, body) => { - if (response.statusCode === 200) { - return db.models('dependentDevice').del().where({ uuid }); - } else { - throw new Error(`Hook returned ${response.statusCode}: ${body}`); - } - }) - .catch((err) => log.error(`Error deleting device ${uuid}`, err)); - } - - sendUpdates({ uuid }) { - return Promise.join( - db.models('dependentDevice').where({ uuid }).select(), - config.get('apiTimeout'), - ([dev], apiTimeout) => { - if (dev == null) { - log.warn(`Trying to send update to non-existent device ${uuid}`); - return; - } - return this.normaliseDependentDeviceFromDB(dev).then((device) => { - const currentState = formatCurrentAsState(device); - const targetState = formatTargetAsState(device); - return this.getHookEndpoint(device.appId).then((endpoint) => { - if (device.markedForDeletion) { - return this.sendDeleteHook(device, apiTimeout, endpoint); - } else if ( - device.targetCommit != null && - !_.isEqual(targetState, currentState) && - !_.isEqual(targetState, this.acknowledgedState[device.uuid]) - ) { - return this.sendUpdate(device, apiTimeout, endpoint); - } - }); - }); - }, - ); - } -} - -const proxyvisor = new Proxyvisor(); -export default proxyvisor; diff --git a/src/types/state.ts b/src/types/state.ts index 3982f4ae..23956daa 100644 --- a/src/types/state.ts +++ b/src/types/state.ts @@ -51,10 +51,6 @@ export interface DeviceLegacyState { }; }; } & DeviceLegacyReport; - // TODO: Type the dependent entry correctly - dependent?: { - [key: string]: any; - }; commit?: string; } @@ -109,14 +105,6 @@ export type DeviceReport = { export type DeviceState = { [deviceUuid: string]: DeviceReport & { - /** - * Used for setting dependent devices as online - */ - is_online?: boolean; - /** - * Used for setting gateway device of dependent devices - */ - parent_device?: number; apps?: { [appUuid: string]: AppState; }; @@ -272,22 +260,16 @@ export type TargetApps = t.TypeOf; /** * A device has a name, config and collection of apps */ -const TargetDevice = t.intersection([ - t.type({ - name: DeviceName, - config: ConfigVarObject, - apps: TargetApps, - }), - t.partial({ - parent_device: UUID, - }), -]); +const TargetDevice = t.type({ + name: DeviceName, + config: ConfigVarObject, + apps: TargetApps, +}); export type TargetDevice = t.TypeOf; /** * Target state is a collection of devices one local device - * (with uuid matching the one in config.json) and zero or more dependent - * devices + * (with uuid matching the one in config.json) * * * When all io-ts types are composed, the final type of the target state @@ -296,7 +278,6 @@ export type TargetDevice = t.TypeOf; * { * [uuid: string]: { * name: string; - * parent_device?: string; * config?: { * [varName: string]: string; * }; @@ -366,5 +347,4 @@ export interface InstancedDeviceState { config: Dictionary; apps: InstancedAppState; }; - dependent: any; } diff --git a/test/integration/compose/images.spec.ts b/test/integration/compose/images.spec.ts index 57a015b5..475550fe 100644 --- a/test/integration/compose/images.spec.ts +++ b/test/integration/compose/images.spec.ts @@ -14,13 +14,11 @@ function createDBImage( appId = 1, name = 'test-image', serviceName = 'test', - dependent = 0, ...extra } = {} as Partial, ) { return { appId, - dependent, name, serviceName, ...extra, diff --git a/test/integration/db.spec.ts b/test/integration/db.spec.ts index 1374835c..cfd03c96 100644 --- a/test/integration/db.spec.ts +++ b/test/integration/db.spec.ts @@ -1,4 +1,3 @@ -import * as Bluebird from 'bluebird'; import { knex, Knex } from 'knex'; import { promises as fs } from 'fs'; @@ -67,7 +66,7 @@ describe('db', () => { const knexForDB = await createOldDatabase(constants.databasePath); const testDb = require('~/src/db') as Db; await testDb.initialized(); - await Bluebird.all([ + await Promise.all([ expect(knexForDB.schema.hasColumn('app', 'appId')).to.eventually.be.true, expect(knexForDB.schema.hasColumn('app', 'releaseId')).to.eventually.be .true, @@ -77,16 +76,13 @@ describe('db', () => { .false, expect(knexForDB.schema.hasColumn('app', 'containerId')).to.eventually.be .false, - expect(knexForDB.schema.hasColumn('dependentApp', 'environment')).to - .eventually.be.true, - expect(knexForDB.schema.hasColumn('dependentDevice', 'markedForDeletion')) - .to.eventually.be.true, - expect(knexForDB.schema.hasColumn('dependentDevice', 'localId')).to - .eventually.be.true, - expect(knexForDB.schema.hasColumn('dependentDevice', 'is_managed_by')).to - .eventually.be.true, - expect(knexForDB.schema.hasColumn('dependentDevice', 'lock_expiry_date')) - .to.eventually.be.true, + expect(knexForDB.schema.hasTable('dependentDeviceTarget')).to.eventually + .be.false, + expect(knexForDB.schema.hasTable('dependentDevice')).to.eventually.be + .false, + expect(knexForDB.schema.hasTable('dependentAppTarget')).to.eventually.be + .false, + expect(knexForDB.schema.hasTable('dependentApp')).to.eventually.be.false, ]); }); diff --git a/test/integration/device-state.spec.ts b/test/integration/device-state.spec.ts index f194e343..e698c9ec 100644 --- a/test/integration/device-state.spec.ts +++ b/test/integration/device-state.spec.ts @@ -234,7 +234,6 @@ describe('device-state', () => { }, }, }, - dependent: { apps: {}, devices: {} }, } as any), ).to.be.rejected; }); diff --git a/test/legacy/40-target-state.spec.ts b/test/legacy/40-target-state.spec.ts index 814dd829..ed362001 100644 --- a/test/legacy/40-target-state.spec.ts +++ b/test/legacy/40-target-state.spec.ts @@ -26,10 +26,6 @@ const stateEndpointBody = { }, }, }, - dependent: { - apps: {}, - devices: {}, - }, }; const req = { diff --git a/test/lib/state-helper.ts b/test/lib/state-helper.ts index 676de0d5..191e0f4d 100644 --- a/test/lib/state-helper.ts +++ b/test/lib/state-helper.ts @@ -70,7 +70,6 @@ export function createImage( imageId: 1, releaseId: 1, serviceId: 1, - dependent: 0, ...extra, } as Image; } diff --git a/test/unit/compose/app.spec.ts b/test/unit/compose/app.spec.ts index 58a9f36d..3fe07c4b 100644 --- a/test/unit/compose/app.spec.ts +++ b/test/unit/compose/app.spec.ts @@ -73,7 +73,6 @@ function createImage( { appId = 1, appUuid = 'appuuid', - dependent = 0, name = 'test-image', serviceName = 'test', commit = 'test-commit', @@ -84,7 +83,6 @@ function createImage( appId, appUuid, commit, - dependent, name, serviceName, ...extra,