mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-19 21:57:54 +00:00
c08de8701e
Each service, when requesting access to the Supervisor API, will now get an individual key which can be scoped to specific resources. In this iteration the default scope will be to the application that the service belongs to. We also have a `global` scope which is used by the cloud API when in managed mode. Change-type: patch Signed-off-by: Rich Bayliss <rich@balena.io>
366 lines
11 KiB
TypeScript
366 lines
11 KiB
TypeScript
import * as _ from 'lodash';
|
|
import { stub } from 'sinon';
|
|
|
|
import chai = require('./lib/chai-config');
|
|
import { StatusCodeError } from '../src/lib/errors';
|
|
import prepare = require('./lib/prepare');
|
|
import * as dockerUtils from '../src/lib/docker-utils';
|
|
import * as config from '../src/config';
|
|
import * as images from '../src/compose/images';
|
|
import { ConfigTxt } from '../src/config/backends/config-txt';
|
|
import * as deviceState from '../src/device-state';
|
|
import * as deviceConfig from '../src/device-config';
|
|
import { loadTargetFromFile } from '../src/device-state/preload';
|
|
import Service from '../src/compose/service';
|
|
import { intialiseContractRequirements } from '../src/lib/contracts';
|
|
|
|
// tslint:disable-next-line
|
|
chai.use(require('chai-events'));
|
|
const { expect } = chai;
|
|
|
|
const mockedInitialConfig = {
|
|
RESIN_SUPERVISOR_CONNECTIVITY_CHECK: 'true',
|
|
RESIN_SUPERVISOR_DELTA: 'false',
|
|
RESIN_SUPERVISOR_DELTA_APPLY_TIMEOUT: '0',
|
|
RESIN_SUPERVISOR_DELTA_REQUEST_TIMEOUT: '30000',
|
|
RESIN_SUPERVISOR_DELTA_RETRY_COUNT: '30',
|
|
RESIN_SUPERVISOR_DELTA_RETRY_INTERVAL: '10000',
|
|
RESIN_SUPERVISOR_DELTA_VERSION: '2',
|
|
RESIN_SUPERVISOR_INSTANT_UPDATE_TRIGGER: 'true',
|
|
RESIN_SUPERVISOR_LOCAL_MODE: 'false',
|
|
RESIN_SUPERVISOR_LOG_CONTROL: 'true',
|
|
RESIN_SUPERVISOR_OVERRIDE_LOCK: 'false',
|
|
RESIN_SUPERVISOR_POLL_INTERVAL: '60000',
|
|
RESIN_SUPERVISOR_VPN_CONTROL: 'true',
|
|
};
|
|
|
|
const testTarget2 = {
|
|
local: {
|
|
name: 'aDeviceWithDifferentName',
|
|
config: {
|
|
RESIN_HOST_CONFIG_gpu_mem: '512',
|
|
},
|
|
apps: {
|
|
'1234': {
|
|
name: 'superapp',
|
|
commit: 'afafafa',
|
|
releaseId: 2,
|
|
services: {
|
|
'23': {
|
|
serviceName: 'aservice',
|
|
imageId: 12345,
|
|
image: 'registry2.resin.io/superapp/edfabc',
|
|
environment: {
|
|
FOO: 'bar',
|
|
},
|
|
labels: {},
|
|
},
|
|
'24': {
|
|
serviceName: 'anotherService',
|
|
imageId: 12346,
|
|
image: 'registry2.resin.io/superapp/afaff',
|
|
environment: {
|
|
FOO: 'bro',
|
|
},
|
|
labels: {},
|
|
},
|
|
},
|
|
volumes: {},
|
|
networks: {},
|
|
},
|
|
},
|
|
},
|
|
dependent: { apps: [], devices: [] },
|
|
};
|
|
|
|
const testTargetWithDefaults2 = {
|
|
local: {
|
|
name: 'aDeviceWithDifferentName',
|
|
config: {
|
|
HOST_CONFIG_gpu_mem: '512',
|
|
HOST_FIREWALL_MODE: 'off',
|
|
HOST_DISCOVERABILITY: 'true',
|
|
SUPERVISOR_CONNECTIVITY_CHECK: 'true',
|
|
SUPERVISOR_DELTA: 'false',
|
|
SUPERVISOR_DELTA_APPLY_TIMEOUT: '0',
|
|
SUPERVISOR_DELTA_REQUEST_TIMEOUT: '30000',
|
|
SUPERVISOR_DELTA_RETRY_COUNT: '30',
|
|
SUPERVISOR_DELTA_RETRY_INTERVAL: '10000',
|
|
SUPERVISOR_DELTA_VERSION: '2',
|
|
SUPERVISOR_INSTANT_UPDATE_TRIGGER: 'true',
|
|
SUPERVISOR_LOCAL_MODE: 'false',
|
|
SUPERVISOR_LOG_CONTROL: 'true',
|
|
SUPERVISOR_OVERRIDE_LOCK: 'false',
|
|
SUPERVISOR_POLL_INTERVAL: '60000',
|
|
SUPERVISOR_VPN_CONTROL: 'true',
|
|
SUPERVISOR_PERSISTENT_LOGGING: 'false',
|
|
},
|
|
apps: {
|
|
'1234': {
|
|
appId: 1234,
|
|
name: 'superapp',
|
|
commit: 'afafafa',
|
|
releaseId: 2,
|
|
services: [
|
|
_.merge(
|
|
{ appId: 1234, serviceId: 23, releaseId: 2 },
|
|
_.clone(testTarget2.local.apps['1234'].services['23']),
|
|
),
|
|
_.merge(
|
|
{ appId: 1234, serviceId: 24, releaseId: 2 },
|
|
_.clone(testTarget2.local.apps['1234'].services['24']),
|
|
),
|
|
],
|
|
volumes: {},
|
|
networks: {},
|
|
},
|
|
},
|
|
},
|
|
dependent: { apps: [], devices: [] },
|
|
};
|
|
|
|
const testTargetInvalid = {
|
|
local: {
|
|
name: 'aDeviceWithDifferentName',
|
|
config: {
|
|
RESIN_HOST_CONFIG_gpu_mem: '512',
|
|
},
|
|
apps: {
|
|
1234: {
|
|
appId: '1234',
|
|
name: 'superapp',
|
|
commit: 'afafafa',
|
|
releaseId: '2',
|
|
config: {},
|
|
services: {
|
|
23: {
|
|
serviceId: '23',
|
|
serviceName: 'aservice',
|
|
imageId: '12345',
|
|
image: 'registry2.resin.io/superapp/edfabc',
|
|
config: {},
|
|
environment: {
|
|
' FOO': 'bar',
|
|
},
|
|
labels: {},
|
|
},
|
|
24: {
|
|
serviceId: '24',
|
|
serviceName: 'anotherService',
|
|
imageId: '12346',
|
|
image: 'registry2.resin.io/superapp/afaff',
|
|
config: {},
|
|
environment: {
|
|
FOO: 'bro',
|
|
},
|
|
labels: {},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
dependent: { apps: [], devices: [] },
|
|
};
|
|
|
|
describe('deviceState', () => {
|
|
let source: string;
|
|
const originalImagesSave = images.save;
|
|
const originalImagesInspect = images.inspectByName;
|
|
const originalGetCurrent = deviceConfig.getCurrent;
|
|
|
|
before(async () => {
|
|
await prepare();
|
|
await config.initialized;
|
|
await deviceState.initialized;
|
|
|
|
source = await config.get('apiEndpoint');
|
|
|
|
stub(Service as any, 'extendEnvVars').callsFake((env) => {
|
|
env['ADDITIONAL_ENV_VAR'] = 'foo';
|
|
return env;
|
|
});
|
|
|
|
intialiseContractRequirements({
|
|
supervisorVersion: '11.0.0',
|
|
deviceType: 'intel-nuc',
|
|
});
|
|
|
|
stub(dockerUtils, 'getNetworkGateway').returns(
|
|
Promise.resolve('172.17.0.1'),
|
|
);
|
|
|
|
// @ts-expect-error Assigning to a RO property
|
|
images.cleanupDatabase = () => {
|
|
console.log('Cleanup database called');
|
|
};
|
|
|
|
// @ts-expect-error Assigning to a RO property
|
|
images.save = () => Promise.resolve();
|
|
|
|
// @ts-expect-error Assigning to a RO property
|
|
images.inspectByName = () => {
|
|
const err: StatusCodeError = new Error();
|
|
err.statusCode = 404;
|
|
return Promise.reject(err);
|
|
};
|
|
|
|
// @ts-expect-error Assigning to a RO property
|
|
deviceConfig.configBackend = new ConfigTxt();
|
|
|
|
// @ts-expect-error Assigning to a RO property
|
|
deviceConfig.getCurrent = async () => mockedInitialConfig;
|
|
});
|
|
|
|
after(() => {
|
|
(Service as any).extendEnvVars.restore();
|
|
(dockerUtils.getNetworkGateway as sinon.SinonStub).restore();
|
|
|
|
// @ts-expect-error Assigning to a RO property
|
|
images.save = originalImagesSave;
|
|
// @ts-expect-error Assigning to a RO property
|
|
images.inspectByName = originalImagesInspect;
|
|
// @ts-expect-error Assigning to a RO property
|
|
deviceConfig.getCurrent = originalGetCurrent;
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await prepare();
|
|
});
|
|
|
|
it('loads a target state from an apps.json file and saves it as target state, then returns it', async () => {
|
|
await loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps.json');
|
|
const targetState = await deviceState.getTarget();
|
|
|
|
expect(targetState)
|
|
.to.have.property('local')
|
|
.that.has.property('apps')
|
|
.that.has.property('1234')
|
|
.that.is.an('object');
|
|
const app = targetState.local.apps[1234];
|
|
expect(app).to.have.property('appName').that.equals('superapp');
|
|
expect(app).to.have.property('services').that.is.an('array').with.length(1);
|
|
expect(app.services[0])
|
|
.to.have.property('config')
|
|
.that.has.property('image')
|
|
.that.equals('registry2.resin.io/superapp/abcdef:latest');
|
|
expect(app.services[0].config)
|
|
.to.have.property('labels')
|
|
.that.has.property('io.balena.something')
|
|
.that.equals('bar');
|
|
expect(app).to.have.property('appName').that.equals('superapp');
|
|
expect(app).to.have.property('services').that.is.an('array').with.length(1);
|
|
expect(app.services[0])
|
|
.to.have.property('config')
|
|
.that.has.property('image')
|
|
.that.equals('registry2.resin.io/superapp/abcdef:latest');
|
|
expect(app.services[0].config)
|
|
.to.have.property('labels')
|
|
.that.has.property('io.balena.something')
|
|
.that.equals('bar');
|
|
expect(app).to.have.property('appName').that.equals('superapp');
|
|
expect(app).to.have.property('services').that.is.an('array').with.length(1);
|
|
expect(app.services[0])
|
|
.to.have.property('config')
|
|
.that.has.property('image')
|
|
.that.equals('registry2.resin.io/superapp/abcdef:latest');
|
|
expect(app.services[0].config)
|
|
.to.have.property('labels')
|
|
.that.has.property('io.balena.something')
|
|
.that.equals('bar');
|
|
expect(app).to.have.property('appName').that.equals('superapp');
|
|
expect(app).to.have.property('services').that.is.an('array').with.length(1);
|
|
expect(app.services[0])
|
|
.to.have.property('config')
|
|
.that.has.property('image')
|
|
.that.equals('registry2.resin.io/superapp/abcdef:latest');
|
|
expect(app.services[0].config)
|
|
.to.have.property('labels')
|
|
.that.has.property('io.balena.something')
|
|
.that.equals('bar');
|
|
expect(app).to.have.property('appName').that.equals('superapp');
|
|
expect(app).to.have.property('services').that.is.an('array').with.length(1);
|
|
expect(app.services[0])
|
|
.to.have.property('config')
|
|
.that.has.property('image')
|
|
.that.equals('registry2.resin.io/superapp/abcdef:latest');
|
|
expect(app.services[0].config)
|
|
.to.have.property('labels')
|
|
.that.has.property('io.balena.something')
|
|
.that.equals('bar');
|
|
});
|
|
|
|
it('stores info for pinning a device after loading an apps.json with a pinDevice field', async () => {
|
|
await loadTargetFromFile(process.env.ROOT_MOUNTPOINT + '/apps-pin.json');
|
|
|
|
const pinned = await config.get('pinDevice');
|
|
expect(pinned).to.have.property('app').that.equals(1234);
|
|
expect(pinned).to.have.property('commit').that.equals('abcdef');
|
|
});
|
|
|
|
it('emits a change event when a new state is reported', (done) => {
|
|
deviceState.once('change', done);
|
|
deviceState.reportCurrentState({ someStateDiff: 'someValue' } as any);
|
|
});
|
|
|
|
it('returns the current state');
|
|
|
|
it.skip('writes the target state to the db with some extra defaults', async () => {
|
|
const testTarget = _.cloneDeep(testTargetWithDefaults2);
|
|
|
|
const services: Service[] = [];
|
|
for (const service of testTarget.local.apps['1234'].services) {
|
|
const imageName = await images.normalise(service.image);
|
|
service.image = imageName;
|
|
(service as any).imageName = imageName;
|
|
services.push(
|
|
await Service.fromComposeObject(service, {
|
|
appName: 'supertest',
|
|
} as any),
|
|
);
|
|
}
|
|
|
|
(testTarget as any).local.apps['1234'].services = _.keyBy(
|
|
services,
|
|
'serviceId',
|
|
);
|
|
(testTarget as any).local.apps['1234'].source = source;
|
|
await deviceState.setTarget(testTarget2);
|
|
const target = await deviceState.getTarget();
|
|
expect(JSON.parse(JSON.stringify(target))).to.deep.equal(
|
|
JSON.parse(JSON.stringify(testTarget)),
|
|
);
|
|
});
|
|
|
|
it('does not allow setting an invalid target state', () => {
|
|
expect(deviceState.setTarget(testTargetInvalid as any)).to.be.rejected;
|
|
});
|
|
|
|
it('allows triggering applying the target state', (done) => {
|
|
const applyTargetStub = stub(deviceState, 'applyTarget').returns(
|
|
Promise.resolve(),
|
|
);
|
|
|
|
deviceState.triggerApplyTarget({ force: true });
|
|
expect(applyTargetStub).to.not.be.called;
|
|
|
|
setTimeout(() => {
|
|
expect(applyTargetStub).to.be.calledWith({
|
|
force: true,
|
|
initial: false,
|
|
});
|
|
applyTargetStub.restore();
|
|
done();
|
|
}, 1000);
|
|
});
|
|
|
|
// TODO: There is no easy way to test this behaviour with the current
|
|
// interface of device-state. We should really think about the device-state
|
|
// interface to allow this flexibility (and to avoid having to change module
|
|
// internal variables)
|
|
it.skip('cancels current promise applying the target state');
|
|
|
|
it.skip('applies the target state for device config');
|
|
|
|
it.skip('applies the target state for applications');
|
|
});
|