mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-19 05:37:53 +00:00
Convert target state in local endpoints
Convert target state from to v3 in `/v2/local/target-state`. Add tests for target state conversion
This commit is contained in:
parent
1edd060143
commit
063bd400a4
41
package-lock.json
generated
41
package-lock.json
generated
@ -6627,6 +6627,12 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
|
||||
},
|
||||
"lodash.set": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
|
||||
"integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
|
||||
"dev": true
|
||||
},
|
||||
"log-symbols": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
|
||||
@ -7635,6 +7641,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"nock": {
|
||||
"version": "13.1.2",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-13.1.2.tgz",
|
||||
"integrity": "sha512-BDjokoeGZnBghmvwCcDJ1yM5TDRMRAJfGi1xIzX5rKTlifbyx1oRpAVl3aNhEA3kGbUSEPD7gBLmwVdnQibrIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^4.1.0",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"lodash.set": "^4.3.2",
|
||||
"propagate": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
|
||||
"integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-libs-browser": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
|
||||
@ -8415,6 +8450,12 @@
|
||||
"integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=",
|
||||
"dev": true
|
||||
},
|
||||
"propagate": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
|
||||
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
|
||||
"dev": true
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
|
||||
|
@ -108,6 +108,7 @@
|
||||
"mock-fs": "^4.14.0",
|
||||
"morgan": "^1.10.0",
|
||||
"network-checker": "^0.1.1",
|
||||
"nock": "^13.1.2",
|
||||
"nodemon": "^2.0.4",
|
||||
"pinejs-client-request": "^7.2.1",
|
||||
"pretty-ms": "^7.0.1",
|
||||
|
@ -32,6 +32,7 @@ import { checkInt, checkTruthy } from '../lib/validation';
|
||||
import { isVPNActive } from '../network';
|
||||
import { doPurge, doRestart, safeStateClone } from './common';
|
||||
import { AuthorizedRequest } from '../lib/api-keys';
|
||||
import { fromV2TargetState } from '../lib/legacy';
|
||||
|
||||
export function createV2Api(router: Router) {
|
||||
const handleServiceAction = (
|
||||
@ -346,7 +347,10 @@ export function createV2Api(router: Router) {
|
||||
|
||||
// Now attempt to set the state
|
||||
const force = req.body.force;
|
||||
const targetState = req.body;
|
||||
|
||||
// Migrate target state from v2 to v3 to maintain API compatibility
|
||||
const targetState = await fromV2TargetState(req.body, true);
|
||||
|
||||
try {
|
||||
await deviceState.setTarget(targetState, true);
|
||||
await deviceState.triggerApplyTarget({ force });
|
||||
|
@ -9,6 +9,7 @@ import * as serviceManager from '../compose/service-manager';
|
||||
import * as deviceState from '../device-state';
|
||||
import * as applicationManager from '../compose/application-manager';
|
||||
import {
|
||||
StatusError,
|
||||
DatabaseParseError,
|
||||
NotFoundError,
|
||||
InternalInconsistencyError,
|
||||
@ -23,7 +24,7 @@ import type {
|
||||
DatabaseService,
|
||||
} from '../device-state/target-state-cache';
|
||||
|
||||
import { TargetApp, TargetApps } from '../types';
|
||||
import { TargetApp, TargetApps, TargetState } from '../types';
|
||||
|
||||
const defaultLegacyVolume = () => 'resin-data';
|
||||
|
||||
@ -226,14 +227,40 @@ export async function normaliseLegacyDatabase() {
|
||||
export type TargetAppsV2 = {
|
||||
[id: string]: {
|
||||
name: string;
|
||||
commit: string;
|
||||
releaseId: number;
|
||||
commit?: string;
|
||||
releaseId?: number;
|
||||
services: { [id: string]: any };
|
||||
volumes: { [name: string]: any };
|
||||
networks: { [name: string]: any };
|
||||
};
|
||||
};
|
||||
|
||||
type TargetStateV2 = {
|
||||
local: {
|
||||
name: string;
|
||||
config: { [name: string]: string };
|
||||
apps: TargetAppsV2;
|
||||
};
|
||||
};
|
||||
|
||||
export async function fromV2TargetState(
|
||||
target: TargetStateV2,
|
||||
local = false,
|
||||
): Promise<TargetState> {
|
||||
const { uuid, name } = await config.getMany(['uuid', 'name']);
|
||||
if (!uuid) {
|
||||
throw new InternalInconsistencyError('No UUID for device');
|
||||
}
|
||||
|
||||
return {
|
||||
[uuid]: {
|
||||
name: target?.local?.name ?? name,
|
||||
config: target?.local?.config ?? {},
|
||||
apps: await fromV2TargetApps(target?.local?.apps ?? {}, local),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert v2 to v3 target apps. If local is false
|
||||
* it will query the API to get the app uuid
|
||||
@ -262,9 +289,7 @@ export async function fromV2TargetApps(
|
||||
});
|
||||
|
||||
if (!appDetails || !appDetails.uuid) {
|
||||
throw new InternalInconsistencyError(
|
||||
`No app with id ${appId} found on the API.`,
|
||||
);
|
||||
throw new StatusError(404, `No app with id ${appId} found on the API.`);
|
||||
}
|
||||
|
||||
return appDetails.uuid;
|
||||
@ -285,7 +310,7 @@ export async function fromV2TargetApps(
|
||||
? {
|
||||
[app.commit]: {
|
||||
id: app.releaseId,
|
||||
services: Object.keys(app.services)
|
||||
services: Object.keys(app.services ?? {})
|
||||
.map((serviceId) => {
|
||||
const {
|
||||
imageId,
|
||||
@ -320,8 +345,8 @@ export async function fromV2TargetApps(
|
||||
}),
|
||||
{},
|
||||
),
|
||||
volumes: app.volumes,
|
||||
networks: app.networks,
|
||||
volumes: app.volumes ?? {},
|
||||
networks: app.networks ?? {},
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
265
test/src/lib/legacy.spec.ts
Normal file
265
test/src/lib/legacy.spec.ts
Normal file
@ -0,0 +1,265 @@
|
||||
import { expect } from 'chai';
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
import * as sinon from 'sinon';
|
||||
import * as nock from 'nock';
|
||||
|
||||
import { TargetState } from '../../../src/types';
|
||||
import * as config from '../../../src/config';
|
||||
import * as legacy from '../../../src/lib/legacy';
|
||||
import log from '../../../src/lib/supervisor-console';
|
||||
|
||||
describe('lib/legacy', () => {
|
||||
before(async () => {
|
||||
// disable log output during testing
|
||||
sinon.stub(log, 'debug');
|
||||
sinon.stub(log, 'warn');
|
||||
sinon.stub(log, 'info');
|
||||
sinon.stub(log, 'event');
|
||||
sinon.stub(log, 'success');
|
||||
|
||||
await config.initialized;
|
||||
|
||||
// Set the device uuid and name
|
||||
await config.set({ uuid: 'local' });
|
||||
await config.set({ name: 'my-device' });
|
||||
});
|
||||
|
||||
after(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe('Converting target state v2 to v3', () => {
|
||||
it('accepts a local target state with empty configuration', async () => {
|
||||
const target = await legacy.fromV2TargetState({} as any, true);
|
||||
|
||||
const decoded = TargetState.decode(target);
|
||||
if (!isRight(decoded)) {
|
||||
console.log(decoded.left);
|
||||
// We do it this way let the type guard be triggered
|
||||
expect.fail('Resulting target state is a valid v3 target state');
|
||||
}
|
||||
|
||||
const decodedTarget = decoded.right;
|
||||
expect(decodedTarget)
|
||||
.to.have.property('local')
|
||||
.that.has.property('name')
|
||||
.that.equals('my-device');
|
||||
expect(decodedTarget)
|
||||
.to.have.property('local')
|
||||
.that.has.property('apps')
|
||||
.that.deep.equals({});
|
||||
expect(decodedTarget)
|
||||
.to.have.property('local')
|
||||
.that.has.property('config')
|
||||
.that.deep.equals({});
|
||||
});
|
||||
|
||||
it('accepts a local target state for an app without releases', async () => {
|
||||
const target = await legacy.fromV2TargetState(
|
||||
{
|
||||
local: {
|
||||
name: 'my-new-name',
|
||||
config: {
|
||||
BALENA_SUPERVISOR_PORT: '11111',
|
||||
},
|
||||
apps: {
|
||||
'1': {
|
||||
name: 'hello-world',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
true,
|
||||
);
|
||||
|
||||
const decoded = TargetState.decode(target);
|
||||
if (!isRight(decoded)) {
|
||||
console.log(decoded.left);
|
||||
// We do it this way let the type guard be triggered
|
||||
expect.fail('Resulting target state is a valid v3 target state');
|
||||
}
|
||||
|
||||
const decodedTarget = decoded.right;
|
||||
|
||||
expect(decodedTarget)
|
||||
.to.have.property('local')
|
||||
.that.has.property('config')
|
||||
.that.has.property('BALENA_SUPERVISOR_PORT')
|
||||
.that.equals('11111');
|
||||
expect(decodedTarget)
|
||||
.to.have.property('local')
|
||||
.that.has.property('name')
|
||||
.that.equals('my-new-name');
|
||||
expect(decodedTarget).to.have.property('local').that.has.property('apps');
|
||||
|
||||
const apps = decodedTarget.local.apps;
|
||||
|
||||
expect(apps)
|
||||
.to.have.property('1')
|
||||
.that.has.property('name')
|
||||
.that.equals('hello-world');
|
||||
expect(apps)
|
||||
.to.have.property('1')
|
||||
.that.has.property('releases')
|
||||
.that.deep.equals({});
|
||||
});
|
||||
|
||||
it('accepts a local target state with valid config and apps', async () => {
|
||||
const target = await legacy.fromV2TargetState(
|
||||
{
|
||||
local: {
|
||||
name: 'my-new-name',
|
||||
config: {
|
||||
BALENA_SUPERVISOR_PORT: '11111',
|
||||
},
|
||||
apps: {
|
||||
'1': {
|
||||
releaseId: 1,
|
||||
commit: 'localrelease',
|
||||
name: 'hello-world',
|
||||
services: {
|
||||
'1': {
|
||||
imageId: 1,
|
||||
serviceName: 'hello',
|
||||
image: 'ubuntu:latest',
|
||||
running: true,
|
||||
environment: {},
|
||||
labels: { 'io.balena.features.api': 'true' },
|
||||
privileged: true,
|
||||
ports: ['3001:3001'],
|
||||
},
|
||||
},
|
||||
networks: {
|
||||
my_net: {
|
||||
labels: {
|
||||
'io.balena.some.label': 'foo',
|
||||
},
|
||||
},
|
||||
},
|
||||
volumes: {
|
||||
my_volume: {
|
||||
labels: {
|
||||
'io.balena.some.label': 'foo',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
const decoded = TargetState.decode(target);
|
||||
if (!isRight(decoded)) {
|
||||
console.log(decoded.left);
|
||||
// We do it this way let the type guard be triggered
|
||||
expect.fail('Resulting target state is a valid v3 target state');
|
||||
}
|
||||
|
||||
const decodedTarget = decoded.right;
|
||||
|
||||
expect(decodedTarget)
|
||||
.to.have.property('local')
|
||||
.that.has.property('config')
|
||||
.that.has.property('BALENA_SUPERVISOR_PORT')
|
||||
.that.equals('11111');
|
||||
expect(decodedTarget)
|
||||
.to.have.property('local')
|
||||
.that.has.property('name')
|
||||
.that.equals('my-new-name');
|
||||
expect(decodedTarget).to.have.property('local').that.has.property('apps');
|
||||
|
||||
const apps = decodedTarget.local.apps;
|
||||
expect(apps)
|
||||
.to.have.property('1')
|
||||
.that.has.property('releases')
|
||||
.that.has.property('localrelease');
|
||||
expect(apps)
|
||||
.to.have.property('1')
|
||||
.that.has.property('name')
|
||||
.that.equals('hello-world');
|
||||
|
||||
const release = apps['1'].releases.localrelease;
|
||||
expect(release).to.have.property('id').that.equals(1);
|
||||
expect(release).to.have.property('services').that.has.property('hello');
|
||||
|
||||
const service = release.services.hello;
|
||||
expect(service).to.have.property('image').that.equals('ubuntu:latest');
|
||||
expect(service)
|
||||
.to.have.property('composition')
|
||||
.that.deep.equals({ privileged: true, ports: ['3001:3001'] });
|
||||
|
||||
expect(release)
|
||||
.to.have.property('networks')
|
||||
.that.has.property('my_net')
|
||||
.that.has.property('labels')
|
||||
.that.deep.equals({ 'io.balena.some.label': 'foo' });
|
||||
expect(release)
|
||||
.to.have.property('volumes')
|
||||
.that.has.property('my_volume')
|
||||
.that.has.property('labels')
|
||||
.that.deep.equals({ 'io.balena.some.label': 'foo' });
|
||||
});
|
||||
|
||||
it('accepts a cloud target state and requests app uuid from API', async () => {
|
||||
const apiEndpoint = await config.get('apiEndpoint');
|
||||
|
||||
nock(apiEndpoint)
|
||||
.get('/v6/application(1)?$select=uuid')
|
||||
.reply(200, { d: [{ uuid: 'some-uuid' }] });
|
||||
|
||||
const target = await legacy.fromV2TargetState(
|
||||
{
|
||||
local: {
|
||||
name: 'my-new-name',
|
||||
config: {
|
||||
BALENA_SUPERVISOR_PORT: '11111',
|
||||
},
|
||||
apps: {
|
||||
'1': {
|
||||
name: 'hello-world',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
false, // local = false
|
||||
);
|
||||
|
||||
const decoded = TargetState.decode(target);
|
||||
if (!isRight(decoded)) {
|
||||
console.log(decoded.left);
|
||||
// We do it this way let the type guard be triggered
|
||||
expect.fail('Resulting target state is a valid v3 target state');
|
||||
}
|
||||
|
||||
const decodedTarget = decoded.right;
|
||||
|
||||
expect(decodedTarget)
|
||||
.to.have.property('local')
|
||||
.that.has.property('config')
|
||||
.that.has.property('BALENA_SUPERVISOR_PORT')
|
||||
.that.equals('11111');
|
||||
expect(decodedTarget)
|
||||
.to.have.property('local')
|
||||
.that.has.property('name')
|
||||
.that.equals('my-new-name');
|
||||
expect(decodedTarget).to.have.property('local').that.has.property('apps');
|
||||
|
||||
const apps = decodedTarget.local.apps;
|
||||
|
||||
expect(apps)
|
||||
.to.have.property('some-uuid')
|
||||
.that.has.property('name')
|
||||
.that.equals('hello-world');
|
||||
expect(apps)
|
||||
.to.have.property('some-uuid')
|
||||
.that.has.property('id')
|
||||
.that.equals(1);
|
||||
expect(apps)
|
||||
.to.have.property('some-uuid')
|
||||
.that.has.property('releases')
|
||||
.that.deep.equals({});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user