api: Implement scoped Supervisor API keys

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>
This commit is contained in:
Rich Bayliss
2020-09-16 14:19:23 +00:00
parent 7d11e29f85
commit c08de8701e
22 changed files with 1059 additions and 430 deletions

View File

@ -64,12 +64,6 @@ describe('Config', () => {
});
});
it('allows removing a db key', async () => {
await conf.remove('apiSecret');
const secret = await conf.get('apiSecret');
return expect(secret).to.be.undefined;
});
it('allows deleting a config.json key and returns a default value if none is set', async () => {
await conf.remove('appUpdatePollInterval');
const poll = await conf.get('appUpdatePollInterval');

View File

@ -28,7 +28,7 @@ const configs = {
};
describe('compose/service', () => {
it('extends environment variables properly', () => {
it('extends environment variables properly', async () => {
const extendEnvVarsOpts = {
uuid: '1234',
appName: 'awesomeApp',
@ -50,7 +50,10 @@ describe('compose/service', () => {
A_VARIABLE: 'ITS_VALUE',
},
};
const s = Service.fromComposeObject(service, extendEnvVarsOpts as any);
const s = await Service.fromComposeObject(
service,
extendEnvVarsOpts as any,
);
expect(s.config.environment).to.deep.equal({
FOO: 'bar',
@ -81,8 +84,8 @@ describe('compose/service', () => {
});
});
it('returns the correct default bind mounts', () => {
const s = Service.fromComposeObject(
it('returns the correct default bind mounts', async () => {
const s = await Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
@ -99,8 +102,8 @@ describe('compose/service', () => {
]);
});
it('produces the correct port bindings and exposed ports', () => {
const s = Service.fromComposeObject(
it('produces the correct port bindings and exposed ports', async () => {
const s = await Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
@ -155,8 +158,8 @@ describe('compose/service', () => {
});
});
it('correctly handles port ranges', () => {
const s = Service.fromComposeObject(
it('correctly handles port ranges', async () => {
const s = await Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
@ -207,9 +210,9 @@ describe('compose/service', () => {
});
});
it('should correctly handle large port ranges', function () {
it('should correctly handle large port ranges', async function () {
this.timeout(60000);
const s = Service.fromComposeObject(
const s = await Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
@ -224,8 +227,8 @@ describe('compose/service', () => {
expect((s as any).generateExposeAndPorts()).to.not.throw;
});
it('should correctly report implied exposed ports from portMappings', () => {
const service = Service.fromComposeObject(
it('should correctly report implied exposed ports from portMappings', async () => {
const service = await Service.fromComposeObject(
{
appId: 123456,
serviceId: 123456,
@ -240,8 +243,8 @@ describe('compose/service', () => {
.that.deep.equals(['80/tcp', '100/tcp']);
});
it('should correctly handle spaces in volume definitions', () => {
const service = Service.fromComposeObject(
it('should correctly handle spaces in volume definitions', async () => {
const service = await Service.fromComposeObject(
{
appId: 123,
serviceId: 123,
@ -270,8 +273,8 @@ describe('compose/service', () => {
});
describe('Ordered array parameters', () => {
it('Should correctly compare ordered array parameters', () => {
const svc1 = Service.fromComposeObject(
it('Should correctly compare ordered array parameters', async () => {
const svc1 = await Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
@ -280,7 +283,7 @@ describe('compose/service', () => {
},
{ appName: 'test' } as any,
);
let svc2 = Service.fromComposeObject(
let svc2 = await Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
@ -291,7 +294,7 @@ describe('compose/service', () => {
);
assert(svc1.isEqualConfig(svc2, {}));
svc2 = Service.fromComposeObject(
svc2 = await Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
@ -303,8 +306,8 @@ describe('compose/service', () => {
assert(!svc1.isEqualConfig(svc2, {}));
});
it('should correctly compare non-ordered array parameters', () => {
const svc1 = Service.fromComposeObject(
it('should correctly compare non-ordered array parameters', async () => {
const svc1 = await Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
@ -313,7 +316,7 @@ describe('compose/service', () => {
},
{ appName: 'test' } as any,
);
let svc2 = Service.fromComposeObject(
let svc2 = await Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
@ -324,7 +327,7 @@ describe('compose/service', () => {
);
assert(svc1.isEqualConfig(svc2, {}));
svc2 = Service.fromComposeObject(
svc2 = await Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
@ -336,8 +339,8 @@ describe('compose/service', () => {
assert(svc1.isEqualConfig(svc2, {}));
});
it('should correctly compare both ordered and non-ordered array parameters', () => {
const svc1 = Service.fromComposeObject(
it('should correctly compare both ordered and non-ordered array parameters', async () => {
const svc1 = await Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
@ -347,7 +350,7 @@ describe('compose/service', () => {
},
{ appName: 'test' } as any,
);
const svc2 = Service.fromComposeObject(
const svc2 = await Service.fromComposeObject(
{
appId: 1,
serviceId: 1,
@ -362,8 +365,8 @@ describe('compose/service', () => {
});
describe('parseMemoryNumber()', () => {
const makeComposeServiceWithLimit = (memLimit?: string | number) =>
Service.fromComposeObject(
const makeComposeServiceWithLimit = async (memLimit?: string | number) =>
await Service.fromComposeObject(
{
appId: 123456,
serviceId: 123456,
@ -373,74 +376,82 @@ describe('compose/service', () => {
{ appName: 'test' } as any,
);
it('should correctly parse memory number strings without a unit', () =>
expect(makeComposeServiceWithLimit('64').config.memLimit).to.equal(64));
it('should correctly parse memory number strings without a unit', async () =>
expect(
(await makeComposeServiceWithLimit('64')).config.memLimit,
).to.equal(64));
it('should correctly apply the default value', () =>
expect(makeComposeServiceWithLimit(undefined).config.memLimit).to.equal(
0,
it('should correctly apply the default value', async () =>
expect(
(await makeComposeServiceWithLimit(undefined)).config.memLimit,
).to.equal(0));
it('should correctly support parsing numbers as memory limits', async () =>
expect((await makeComposeServiceWithLimit(64)).config.memLimit).to.equal(
64,
));
it('should correctly support parsing numbers as memory limits', () =>
expect(makeComposeServiceWithLimit(64).config.memLimit).to.equal(64));
it('should correctly parse memory number strings that use a byte unit', () => {
expect(makeComposeServiceWithLimit('64b').config.memLimit).to.equal(64);
expect(makeComposeServiceWithLimit('64B').config.memLimit).to.equal(64);
it('should correctly parse memory number strings that use a byte unit', async () => {
expect(
(await makeComposeServiceWithLimit('64b')).config.memLimit,
).to.equal(64);
expect(
(await makeComposeServiceWithLimit('64B')).config.memLimit,
).to.equal(64);
});
it('should correctly parse memory number strings that use a kilobyte unit', () => {
expect(makeComposeServiceWithLimit('64k').config.memLimit).to.equal(
65536,
);
expect(makeComposeServiceWithLimit('64K').config.memLimit).to.equal(
65536,
);
it('should correctly parse memory number strings that use a kilobyte unit', async () => {
expect(
(await makeComposeServiceWithLimit('64k')).config.memLimit,
).to.equal(65536);
expect(
(await makeComposeServiceWithLimit('64K')).config.memLimit,
).to.equal(65536);
expect(makeComposeServiceWithLimit('64kb').config.memLimit).to.equal(
65536,
);
expect(makeComposeServiceWithLimit('64Kb').config.memLimit).to.equal(
65536,
);
expect(
(await makeComposeServiceWithLimit('64kb')).config.memLimit,
).to.equal(65536);
expect(
(await makeComposeServiceWithLimit('64Kb')).config.memLimit,
).to.equal(65536);
});
it('should correctly parse memory number strings that use a megabyte unit', () => {
expect(makeComposeServiceWithLimit('64m').config.memLimit).to.equal(
67108864,
);
expect(makeComposeServiceWithLimit('64M').config.memLimit).to.equal(
67108864,
);
it('should correctly parse memory number strings that use a megabyte unit', async () => {
expect(
(await makeComposeServiceWithLimit('64m')).config.memLimit,
).to.equal(67108864);
expect(
(await makeComposeServiceWithLimit('64M')).config.memLimit,
).to.equal(67108864);
expect(makeComposeServiceWithLimit('64mb').config.memLimit).to.equal(
67108864,
);
expect(makeComposeServiceWithLimit('64Mb').config.memLimit).to.equal(
67108864,
);
expect(
(await makeComposeServiceWithLimit('64mb')).config.memLimit,
).to.equal(67108864);
expect(
(await makeComposeServiceWithLimit('64Mb')).config.memLimit,
).to.equal(67108864);
});
it('should correctly parse memory number strings that use a gigabyte unit', () => {
expect(makeComposeServiceWithLimit('64g').config.memLimit).to.equal(
68719476736,
);
expect(makeComposeServiceWithLimit('64G').config.memLimit).to.equal(
68719476736,
);
it('should correctly parse memory number strings that use a gigabyte unit', async () => {
expect(
(await makeComposeServiceWithLimit('64g')).config.memLimit,
).to.equal(68719476736);
expect(
(await makeComposeServiceWithLimit('64G')).config.memLimit,
).to.equal(68719476736);
expect(makeComposeServiceWithLimit('64gb').config.memLimit).to.equal(
68719476736,
);
expect(makeComposeServiceWithLimit('64Gb').config.memLimit).to.equal(
68719476736,
);
expect(
(await makeComposeServiceWithLimit('64gb')).config.memLimit,
).to.equal(68719476736);
expect(
(await makeComposeServiceWithLimit('64Gb')).config.memLimit,
).to.equal(68719476736);
});
});
describe('getWorkingDir', () => {
const makeComposeServiceWithWorkdir = (workdir?: string) =>
Service.fromComposeObject(
const makeComposeServiceWithWorkdir = async (workdir?: string) =>
await Service.fromComposeObject(
{
appId: 123456,
serviceId: 123456,
@ -450,17 +461,20 @@ describe('compose/service', () => {
{ appName: 'test' } as any,
);
it('should remove a trailing slash', () => {
it('should remove a trailing slash', async () => {
expect(
makeComposeServiceWithWorkdir('/usr/src/app/').config.workingDir,
(await makeComposeServiceWithWorkdir('/usr/src/app/')).config
.workingDir,
).to.equal('/usr/src/app');
expect(makeComposeServiceWithWorkdir('/').config.workingDir).to.equal(
'/',
);
expect(
makeComposeServiceWithWorkdir('/usr/src/app').config.workingDir,
(await makeComposeServiceWithWorkdir('/')).config.workingDir,
).to.equal('/');
expect(
(await makeComposeServiceWithWorkdir('/usr/src/app')).config.workingDir,
).to.equal('/usr/src/app');
expect(makeComposeServiceWithWorkdir('').config.workingDir).to.equal('');
expect(
(await makeComposeServiceWithWorkdir('')).config.workingDir,
).to.equal('');
});
});
@ -469,8 +483,8 @@ describe('compose/service', () => {
Count: 1,
Capabilities: [['gpu']],
};
it('should succeed from compose object', () => {
const s = Service.fromComposeObject(
it('should succeed from compose object', async () => {
const s = await Service.fromComposeObject(
{
appId: 123,
serviceId: 123,
@ -504,8 +518,8 @@ describe('compose/service', () => {
const omitConfigForComparison = (config: ServiceConfig) =>
_.omit(config, ['running', 'networks']);
it('should be identical when converting a simple service', () => {
const composeSvc = Service.fromComposeObject(
it('should be identical when converting a simple service', async () => {
const composeSvc = await Service.fromComposeObject(
configs.simple.compose,
configs.simple.imageInfo,
);
@ -518,8 +532,8 @@ describe('compose/service', () => {
expect(dockerSvc.isEqualConfig(composeSvc, {})).to.be.true;
});
it('should correctly convert formats with a null entrypoint', () => {
const composeSvc = Service.fromComposeObject(
it('should correctly convert formats with a null entrypoint', async () => {
const composeSvc = await Service.fromComposeObject(
configs.entrypoint.compose,
configs.entrypoint.imageInfo,
);
@ -533,11 +547,11 @@ describe('compose/service', () => {
});
describe('Networks', () => {
it('should correctly convert networks from compose to docker format', () => {
const makeComposeServiceWithNetwork = (
it('should correctly convert networks from compose to docker format', async () => {
const makeComposeServiceWithNetwork = async (
networks: ServiceComposeConfig['networks'],
) =>
Service.fromComposeObject(
await Service.fromComposeObject(
{
appId: 123456,
serviceId: 123456,
@ -548,11 +562,13 @@ describe('compose/service', () => {
);
expect(
makeComposeServiceWithNetwork({
balena: {
ipv4Address: '1.2.3.4',
},
}).toDockerContainer({ deviceName: 'foo' } as any).NetworkingConfig,
(
await makeComposeServiceWithNetwork({
balena: {
ipv4Address: '1.2.3.4',
},
})
).toDockerContainer({ deviceName: 'foo' } as any).NetworkingConfig,
).to.deep.equal({
EndpointsConfig: {
'123456_balena': {
@ -565,14 +581,16 @@ describe('compose/service', () => {
});
expect(
makeComposeServiceWithNetwork({
balena: {
aliases: ['test', '1123'],
ipv4Address: '1.2.3.4',
ipv6Address: '5.6.7.8',
linkLocalIps: ['123.123.123'],
},
}).toDockerContainer({ deviceName: 'foo' } as any).NetworkingConfig,
(
await makeComposeServiceWithNetwork({
balena: {
aliases: ['test', '1123'],
ipv4Address: '1.2.3.4',
ipv6Address: '5.6.7.8',
linkLocalIps: ['123.123.123'],
},
})
).toDockerContainer({ deviceName: 'foo' } as any).NetworkingConfig,
).to.deep.equal({
EndpointsConfig: {
'123456_balena': {
@ -638,8 +656,8 @@ describe('compose/service', () => {
});
return describe('Network mode=service:', () => {
it('should correctly add a depends_on entry for the service', () => {
let s = Service.fromComposeObject(
it('should correctly add a depends_on entry for the service', async () => {
let s = await Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
@ -653,7 +671,7 @@ describe('compose/service', () => {
expect(s.dependsOn).to.deep.equal(['test']);
s = Service.fromComposeObject(
s = await Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
@ -669,8 +687,8 @@ describe('compose/service', () => {
expect(s.dependsOn).to.deep.equal(['another_service', 'test']);
});
it('should correctly convert a network_mode service: to a container:', () => {
const s = Service.fromComposeObject(
it('should correctly convert a network_mode service: to a container:', async () => {
const s = await Service.fromComposeObject(
{
appId: '1234',
serviceName: 'foo',
@ -692,8 +710,8 @@ describe('compose/service', () => {
.that.equals('container:abcdef');
});
it('should not cause a container restart if a service: container has not changed', () => {
const composeSvc = Service.fromComposeObject(
it('should not cause a container restart if a service: container has not changed', async () => {
const composeSvc = await Service.fromComposeObject(
configs.networkModeService.compose,
configs.networkModeService.imageInfo,
);
@ -709,8 +727,8 @@ describe('compose/service', () => {
.true;
});
it('should restart a container when its dependent network mode container changes', () => {
const composeSvc = Service.fromComposeObject(
it('should restart a container when its dependent network mode container changes', async () => {
const composeSvc = await Service.fromComposeObject(
configs.networkModeService.compose,
configs.networkModeService.imageInfo,
);

View File

@ -313,7 +313,9 @@ describe('deviceState', () => {
service.image = imageName;
(service as any).imageName = imageName;
services.push(
Service.fromComposeObject(service, { appName: 'supertest' } as any),
await Service.fromComposeObject(service, {
appName: 'supertest',
} as any),
);
}

View File

@ -12,13 +12,14 @@ import mockedAPI = require('./lib/mocked-device-api');
import * as applicationManager from '../src/compose/application-manager';
import { InstancedAppState } from '../src/types/state';
import * as apiKeys from '../src/lib/api-keys';
import * as db from '../src/db';
const mockedOptions = {
listenPort: 54321,
timeout: 30000,
};
const VALID_SECRET = mockedAPI.STUBBED_VALUES.config.apiSecret;
describe('SupervisorAPI', () => {
let api: SupervisorAPI;
let healthCheckStubs: SinonStub[];
@ -40,6 +41,10 @@ describe('SupervisorAPI', () => {
// Start test API
await api.listen(mockedOptions.listenPort, mockedOptions.timeout);
// Create a scoped key
await apiKeys.initialized;
await apiKeys.generateCloudKey();
});
after(async () => {
@ -56,6 +61,104 @@ describe('SupervisorAPI', () => {
await mockedAPI.cleanUp();
});
describe('API Key Scope', () => {
it('should generate a key which is scoped for a single application', async () => {
// single app scoped key...
const appScopedKey = await apiKeys.generateScopedKey(1, 1);
await request
.get('/v2/applications/1/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${appScopedKey}`)
.expect(200);
});
it('should generate a key which is scoped for multiple applications', async () => {
// multi-app scoped key...
const multiAppScopedKey = await apiKeys.generateScopedKey(1, 2, {
scopes: [1, 2].map((appId) => {
return { type: 'app', appId };
}),
});
await request
.get('/v2/applications/1/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${multiAppScopedKey}`)
.expect(200);
await request
.get('/v2/applications/2/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${multiAppScopedKey}`)
.expect(200);
});
it('should generate a key which is scoped for all applications', async () => {
await request
.get('/v2/applications/1/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(200);
await request
.get('/v2/applications/2/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(200);
});
it('should have a cached lookup of the key scopes to save DB loading', async () => {
const scopes = await apiKeys.getScopesForKey(apiKeys.cloudApiKey);
const key = 'not-a-normal-key';
await db.initialized;
await db
.models('apiSecret')
.update({
key,
})
.where({
key: apiKeys.cloudApiKey,
});
// the key we had is now gone, but the cache should return values
const cachedScopes = await apiKeys.getScopesForKey(apiKeys.cloudApiKey);
expect(cachedScopes).to.deep.equal(scopes);
// this should bust the cache...
await apiKeys.generateCloudKey(true);
// the key we changed should be gone now, and the new key should have the cloud scopes
const missingScopes = await apiKeys.getScopesForKey(key);
const freshScopes = await apiKeys.getScopesForKey(apiKeys.cloudApiKey);
expect(missingScopes).to.be.null;
expect(freshScopes).to.deep.equal(scopes);
});
it('should regenerate a key and invalidate the old one', async () => {
// single app scoped key...
const appScopedKey = await apiKeys.generateScopedKey(1, 1);
await request
.get('/v2/applications/1/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${appScopedKey}`)
.expect(200);
const newScopedKey = await apiKeys.refreshKey(appScopedKey);
await request
.get('/v2/applications/1/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${appScopedKey}`)
.expect(401);
await request
.get('/v2/applications/1/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${newScopedKey}`)
.expect(200);
});
});
describe('/ping', () => {
it('responds with OK (without auth)', async () => {
await request.get('/ping').set('Accept', 'application/json').expect(200);
@ -64,7 +167,7 @@ describe('SupervisorAPI', () => {
await request
.get('/ping')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(200);
});
});
@ -77,7 +180,7 @@ describe('SupervisorAPI', () => {
await request
.get('/v1/healthy')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.GET['/healthy'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
@ -138,7 +241,7 @@ describe('SupervisorAPI', () => {
await request
.get('/v1/apps/2')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.GET['/apps/2'].statusCode)
.expect('Content-Type', /json/)
.then((response) => {
@ -154,7 +257,7 @@ describe('SupervisorAPI', () => {
await request
.post('/v1/apps/2/stop')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.GET['/apps/2/stop'].statusCode)
.expect('Content-Type', /json/)
.then((response) => {
@ -170,7 +273,7 @@ describe('SupervisorAPI', () => {
const response = await request
.get('/v1/device')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(200);
expect(response.body).to.have.property('mac_address').that.is.not.empty;
@ -184,7 +287,7 @@ describe('SupervisorAPI', () => {
await request
.get('/v2/device/vpn')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect('Content-Type', /json/)
.expect(sampleResponses.V2.GET['/device/vpn'].statusCode)
.then((response) => {
@ -200,9 +303,9 @@ describe('SupervisorAPI', () => {
await request
.get('/v2/applications/1/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.expect('Content-Type', /json/)
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V2.GET['/applications/1/state'].statusCode)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V2.GET['/applications/1/state'].body,
@ -214,7 +317,7 @@ describe('SupervisorAPI', () => {
await request
.get('/v2/applications/123invalid/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect('Content-Type', /json/)
.expect(
sampleResponses.V2.GET['/applications/123invalid/state'].statusCode,
@ -230,8 +333,7 @@ describe('SupervisorAPI', () => {
await request
.get('/v2/applications/9000/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.expect('Content-Type', /json/)
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V2.GET['/applications/9000/state'].statusCode)
.then((response) => {
expect(response.body).to.deep.equal(
@ -239,6 +341,17 @@ describe('SupervisorAPI', () => {
);
});
});
describe('Scoped API Keys', () => {
it('returns 409 because app is out of scope of the key', async () => {
const apiKey = await apiKeys.generateScopedKey(3, 1);
await request
.get('/v2/applications/2/state')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKey}`)
.expect(409);
});
});
});
// TODO: add tests for rest of V2 endpoints

View File

@ -2,13 +2,13 @@ import * as supertest from 'supertest';
import SupervisorAPI from '../src/supervisor-api';
import mockedAPI = require('./lib/mocked-device-api');
import { cloudApiKey } from '../src/lib/api-keys';
const mockedOptions = {
listenPort: 12345,
timeout: 30000,
};
const VALID_SECRET = mockedAPI.STUBBED_VALUES.config.apiSecret;
const INVALID_SECRET = 'bad_api_secret';
describe('SupervisorAPI authentication', () => {
@ -39,20 +39,20 @@ describe('SupervisorAPI authentication', () => {
});
it('finds apiKey from query', async () => {
return request.post(`/v1/blink?apikey=${VALID_SECRET}`).expect(200);
return request.post(`/v1/blink?apikey=${cloudApiKey}`).expect(200);
});
it('finds apiKey from Authorization header (ApiKey scheme)', async () => {
return request
.post('/v1/blink')
.set('Authorization', `ApiKey ${VALID_SECRET}`)
.set('Authorization', `ApiKey ${cloudApiKey}`)
.expect(200);
});
it('finds apiKey from Authorization header (Bearer scheme)', async () => {
return request
.post('/v1/blink')
.set('Authorization', `Bearer ${VALID_SECRET}`)
.set('Authorization', `Bearer ${cloudApiKey}`)
.expect(200);
});
@ -70,7 +70,7 @@ describe('SupervisorAPI authentication', () => {
for (const scheme of randomCases) {
return request
.post('/v1/blink')
.set('Authorization', `${scheme} ${VALID_SECRET}`)
.set('Authorization', `${scheme} ${cloudApiKey}`)
.expect(200);
}
});

View File

@ -45,7 +45,7 @@ function createApp(
);
}
function createService(
async function createService(
conf: Partial<ServiceComposeConfig>,
appId = 1,
serviceName = 'test',
@ -54,7 +54,7 @@ function createService(
imageId = 4,
extraState?: Partial<Service>,
) {
const svc = Service.fromComposeObject(
const svc = await Service.fromComposeObject(
{
appId,
serviceName,
@ -265,15 +265,15 @@ describe('compose/app', () => {
.that.deep.equals({ 'io.balena.supervised': 'true', test: 'test' });
});
it('should kill dependencies of a volume before changing config', () => {
it('should kill dependencies of a volume before changing config', async () => {
const current = createApp(
[createService({ volumes: ['test-volume'] })],
[await createService({ volumes: ['test-volume'] })],
[],
[Volume.fromComposeObject('test-volume', 1, {})],
false,
);
const target = createApp(
[createService({ volumes: ['test-volume'] })],
[await createService({ volumes: ['test-volume'] })],
[],
[
Volume.fromComposeObject('test-volume', 1, {
@ -381,14 +381,14 @@ describe('compose/app', () => {
.that.equals('test-network');
});
it('should kill dependencies of networks before removing', () => {
it('should kill dependencies of networks before removing', async () => {
const current = createApp(
[createService({ networks: { 'test-network': {} } })],
[await createService({ networks: { 'test-network': {} } })],
[Network.fromComposeObject('test-network', 1, {})],
[],
false,
);
const target = createApp([createService({})], [], [], true);
const target = createApp([await createService({})], [], [], true);
const steps = current.nextStepsForAppUpdate(defaultContext, target);
const idx = expectStep('kill', steps);
@ -398,15 +398,15 @@ describe('compose/app', () => {
.that.equals('test');
});
it('should kill dependencies of networks before changing config', () => {
it('should kill dependencies of networks before changing config', async () => {
const current = createApp(
[createService({ networks: { 'test-network': {} } })],
[await createService({ networks: { 'test-network': {} } })],
[Network.fromComposeObject('test-network', 1, {})],
[],
false,
);
const target = createApp(
[createService({ networks: { 'test-network': {} } })],
[await createService({ networks: { 'test-network': {} } })],
[
Network.fromComposeObject('test-network', 1, {
labels: { test: 'test' },
@ -426,8 +426,8 @@ describe('compose/app', () => {
expect(() => expectStep('removeNetwork', steps)).to.throw();
});
it('should not output a kill step for a service which is already stopping when changing a volume', () => {
const service = createService({ volumes: ['test-volume'] });
it('should not output a kill step for a service which is already stopping when changing a volume', async () => {
const service = await createService({ volumes: ['test-volume'] });
service.status = 'Stopping';
const current = createApp(
[service],
@ -464,13 +464,16 @@ describe('compose/app', () => {
it('should create a kill step for service which is no longer referenced', async () => {
const current = createApp(
[createService({}, 1, 'main', 1, 1), createService({}, 1, 'aux', 1, 2)],
[
await createService({}, 1, 'main', 1, 1),
await createService({}, 1, 'aux', 1, 2),
],
[Network.fromComposeObject('test-network', 1, {})],
[],
false,
);
const target = createApp(
[createService({}, 1, 'main', 2, 1)],
[await createService({}, 1, 'main', 2, 1)],
[Network.fromComposeObject('test-network', 1, {})],
[],
true,
@ -486,7 +489,7 @@ describe('compose/app', () => {
it('should emit a noop when a service which is no longer referenced is already stopping', async () => {
const current = createApp(
[createService({}, 1, 'main', 1, 1, 1, { status: 'Stopping' })],
[await createService({}, 1, 'main', 1, 1, 1, { status: 'Stopping' })],
[],
[],
false,
@ -497,15 +500,15 @@ describe('compose/app', () => {
expectStep('noop', steps);
});
it('should remove a dead container that is still referenced in the target state', () => {
it('should remove a dead container that is still referenced in the target state', async () => {
const current = createApp(
[createService({}, 1, 'main', 1, 1, 1, { status: 'Dead' })],
[await createService({}, 1, 'main', 1, 1, 1, { status: 'Dead' })],
[],
[],
false,
);
const target = createApp(
[createService({}, 1, 'main', 1, 1, 1)],
[await createService({}, 1, 'main', 1, 1, 1)],
[],
[],
true,
@ -515,9 +518,9 @@ describe('compose/app', () => {
expectStep('remove', steps);
});
it('should remove a dead container that is not referenced in the target state', () => {
it('should remove a dead container that is not referenced in the target state', async () => {
const current = createApp(
[createService({}, 1, 'main', 1, 1, 1, { status: 'Dead' })],
[await createService({}, 1, 'main', 1, 1, 1, { status: 'Dead' })],
[],
[],
false,
@ -528,10 +531,10 @@ describe('compose/app', () => {
expectStep('remove', steps);
});
it('should emit a noop when a service has an image downloading', () => {
it('should emit a noop when a service has an image downloading', async () => {
const current = createApp([], [], [], false);
const target = createApp(
[createService({}, 1, 'main', 1, 1, 1)],
[await createService({}, 1, 'main', 1, 1, 1)],
[],
[],
true,
@ -544,15 +547,15 @@ describe('compose/app', () => {
expectStep('noop', steps);
});
it('should emit an updateMetadata step when a service has not changed but the release has', () => {
it('should emit an updateMetadata step when a service has not changed but the release has', async () => {
const current = createApp(
[createService({}, 1, 'main', 1, 1, 1)],
[await createService({}, 1, 'main', 1, 1, 1)],
[],
[],
false,
);
const target = createApp(
[createService({}, 1, 'main', 2, 1, 1)],
[await createService({}, 1, 'main', 2, 1, 1)],
[],
[],
true,
@ -562,15 +565,15 @@ describe('compose/app', () => {
expectStep('updateMetadata', steps);
});
it('should stop a container which has stoppped as its target', () => {
it('should stop a container which has stoppped as its target', async () => {
const current = createApp(
[createService({}, 1, 'main', 1, 1, 1)],
[await createService({}, 1, 'main', 1, 1, 1)],
[],
[],
false,
);
const target = createApp(
[createService({ running: false }, 1, 'main', 1, 1, 1)],
[await createService({ running: false }, 1, 'main', 1, 1, 1)],
[],
[],
true,
@ -580,7 +583,7 @@ describe('compose/app', () => {
expectStep('stop', steps);
});
it('should recreate a container if the target configuration changes', () => {
it('should recreate a container if the target configuration changes', async () => {
const contextWithImages = {
...defaultContext,
...{
@ -598,13 +601,13 @@ describe('compose/app', () => {
},
};
let current = createApp(
[createService({}, 1, 'main', 1, 1, 1, {})],
[await createService({}, 1, 'main', 1, 1, 1, {})],
[defaultNetwork],
[],
false,
);
const target = createApp(
[createService({ privileged: true }, 1, 'main', 1, 1, 1, {})],
[await createService({ privileged: true }, 1, 'main', 1, 1, 1, {})],
[defaultNetwork],
[],
true,
@ -624,7 +627,7 @@ describe('compose/app', () => {
.forTarget((t) => t.serviceName === 'main').to.exist;
});
it('should not start a container when it depends on a service which is being installed', () => {
it('should not start a container when it depends on a service which is being installed', async () => {
const mainImage: Image = {
appId: 1,
dependent: 0,
@ -651,7 +654,7 @@ describe('compose/app', () => {
try {
let current = createApp(
[
createService({ running: false }, 1, 'dep', 1, 2, 2, {
await createService({ running: false }, 1, 'dep', 1, 2, 2, {
status: 'Installing',
containerId: 'id',
}),
@ -662,8 +665,8 @@ describe('compose/app', () => {
);
const target = createApp(
[
createService({}, 1, 'main', 1, 1, 1, { dependsOn: ['dep'] }),
createService({}, 1, 'dep', 1, 2, 2),
await createService({}, 1, 'main', 1, 1, 1, { dependsOn: ['dep'] }),
await createService({}, 1, 'dep', 1, 2, 2),
],
[defaultNetwork],
[],
@ -681,7 +684,7 @@ describe('compose/app', () => {
// we now make our current state have the 'dep' service as started...
current = createApp(
[createService({}, 1, 'dep', 1, 2, 2, { containerId: 'id' })],
[await createService({}, 1, 'dep', 1, 2, 2, { containerId: 'id' })],
[defaultNetwork],
[],
false,
@ -704,10 +707,10 @@ describe('compose/app', () => {
}
});
it('should emit a fetch step when an image has not been downloaded for a service', () => {
it('should emit a fetch step when an image has not been downloaded for a service', async () => {
const current = createApp([], [], [], false);
const target = createApp(
[createService({}, 1, 'main', 1, 1, 1)],
[await createService({}, 1, 'main', 1, 1, 1)],
[],
[],
true,
@ -717,15 +720,15 @@ describe('compose/app', () => {
withSteps(steps).expectStep('fetch').to.exist;
});
it('should stop a container which has stoppped as its target', () => {
it('should stop a container which has stoppped as its target', async () => {
const current = createApp(
[createService({}, 1, 'main', 1, 1, 1)],
[await createService({}, 1, 'main', 1, 1, 1)],
[],
[],
false,
);
const target = createApp(
[createService({ running: false }, 1, 'main', 1, 1, 1)],
[await createService({ running: false }, 1, 'main', 1, 1, 1)],
[],
[],
true,
@ -735,7 +738,7 @@ describe('compose/app', () => {
withSteps(steps).expectStep('stop');
});
it('should create a start step when all that changes is a running state', () => {
it('should create a start step when all that changes is a running state', async () => {
const contextWithImages = {
...defaultContext,
...{
@ -753,13 +756,13 @@ describe('compose/app', () => {
},
};
const current = createApp(
[createService({ running: false }, 1, 'main', 1, 1, 1, {})],
[await createService({ running: false }, 1, 'main', 1, 1, 1, {})],
[defaultNetwork],
[],
false,
);
const target = createApp(
[createService({}, 1, 'main', 1, 1, 1, {})],
[await createService({}, 1, 'main', 1, 1, 1, {})],
[defaultNetwork],
[],
true,
@ -772,7 +775,7 @@ describe('compose/app', () => {
.forTarget((t) => t.serviceName === 'main').to.exist;
});
it('should not infer a fetch step when the download is already in progress', () => {
it('should not infer a fetch step when the download is already in progress', async () => {
const contextWithDownloading = {
...defaultContext,
...{
@ -781,7 +784,7 @@ describe('compose/app', () => {
};
const current = createApp([], [], [], false);
const target = createApp(
[createService({}, 1, 'main', 1, 1, 1)],
[await createService({}, 1, 'main', 1, 1, 1)],
[],
[],
true,
@ -791,7 +794,7 @@ describe('compose/app', () => {
withSteps(steps).expectStep('fetch').forTarget('main').to.not.exist;
});
it('should create a kill step when a service has to be updated but the strategy is kill-then-download', () => {
it('should create a kill step when a service has to be updated but the strategy is kill-then-download', async () => {
const contextWithImages = {
...defaultContext,
...{
@ -814,14 +817,24 @@ describe('compose/app', () => {
};
const current = createApp(
[createService({ labels, image: 'main-image' }, 1, 'main', 1, 1, 1, {})],
[
await createService(
{ labels, image: 'main-image' },
1,
'main',
1,
1,
1,
{},
),
],
[defaultNetwork],
[],
false,
);
const target = createApp(
[
createService(
await createService(
{ labels, image: 'main-image-2' },
1,
'main',
@ -854,7 +867,7 @@ describe('compose/app', () => {
.that.equals('main-image-2');
});
it('should not infer a kill step with the default strategy if a dependency is not downloaded', () => {
it('should not infer a kill step with the default strategy if a dependency is not downloaded', async () => {
const contextWithImages = {
...defaultContext,
...{
@ -893,10 +906,10 @@ describe('compose/app', () => {
const current = createApp(
[
createService({ image: 'main-image' }, 1, 'main', 1, 1, 1, {
await createService({ image: 'main-image' }, 1, 'main', 1, 1, 1, {
dependsOn: ['dep'],
}),
createService({ image: 'dep-image' }, 1, 'dep', 1, 2, 2, {}),
await createService({ image: 'dep-image' }, 1, 'dep', 1, 2, 2, {}),
],
[defaultNetwork],
[],
@ -904,10 +917,10 @@ describe('compose/app', () => {
);
const target = createApp(
[
createService({ image: 'main-image-2' }, 1, 'main', 2, 1, 3, {
await createService({ image: 'main-image-2' }, 1, 'main', 2, 1, 3, {
dependsOn: ['dep'],
}),
createService({ image: 'dep-image-2' }, 1, 'dep', 2, 2, 4, {}),
await createService({ image: 'dep-image-2' }, 1, 'dep', 2, 2, 4, {}),
],
[defaultNetwork],
[],
@ -918,7 +931,7 @@ describe('compose/app', () => {
withSteps(steps).expectStep('kill').forCurrent('main').to.not.exist;
});
it('should create several kill steps as long as there is no unmet dependencies', () => {
it('should create several kill steps as long as there is no unmet dependencies', async () => {
const contextWithImages = {
...defaultContext,
...{
@ -946,13 +959,13 @@ describe('compose/app', () => {
};
const current = createApp(
[createService({ image: 'main-image' }, 1, 'main', 1, 1, 1, {})],
[await createService({ image: 'main-image' }, 1, 'main', 1, 1, 1, {})],
[defaultNetwork],
[],
false,
);
const target = createApp(
[createService({ image: 'main-image-2' }, 1, 'main', 2, 1, 2)],
[await createService({ image: 'main-image-2' }, 1, 'main', 2, 1, 2)],
[defaultNetwork],
[],
true,
@ -966,14 +979,14 @@ describe('compose/app', () => {
withSteps(steps).expectStep('kill').forCurrent('main').to.exist;
});
it('should create a kill step when a service has to be updated but the strategy is kill-then-download', () => {
it('should create a kill step when a service has to be updated but the strategy is kill-then-download', async () => {
const labels = {
'io.balena.update.strategy': 'kill-then-download',
};
const current = createApp([createService({ labels })], [], [], false);
const current = createApp([await createService({ labels })], [], [], false);
const target = createApp(
[createService({ privileged: true })],
[await createService({ privileged: true })],
[],
[],
true,
@ -983,15 +996,15 @@ describe('compose/app', () => {
withSteps(steps).expectStep('kill').forCurrent('main');
});
it('should not infer a kill step with the default strategy if a dependency is not downloaded', () => {
it('should not infer a kill step with the default strategy if a dependency is not downloaded', async () => {
const current = createApp(
[createService({ image: 'image1' })],
[await createService({ image: 'image1' })],
[],
[],
false,
);
const target = createApp(
[createService({ image: 'image2' })],
[await createService({ image: 'image2' })],
[],
[],
true,
@ -1002,19 +1015,19 @@ describe('compose/app', () => {
withSteps(steps).rejectStep('kill');
});
it('should create several kill steps as long as there is no unmet dependencies', () => {
it('should create several kill steps as long as there is no unmet dependencies', async () => {
const current = createApp(
[
createService({}, 1, 'one', 1, 2),
createService({}, 1, 'two', 1, 3),
createService({}, 1, 'three', 1, 4),
await createService({}, 1, 'one', 1, 2),
await createService({}, 1, 'two', 1, 3),
await createService({}, 1, 'three', 1, 4),
],
[],
[],
false,
);
const target = createApp(
[createService({}, 1, 'three', 1, 4)],
[await createService({}, 1, 'three', 1, 4)],
[],
[],
true,
@ -1023,10 +1036,10 @@ describe('compose/app', () => {
const steps = current.nextStepsForAppUpdate(defaultContext, target);
withSteps(steps).expectStep('kill').to.have.length(2);
});
it('should not create a service when a network it depends on is not ready', () => {
it('should not create a service when a network it depends on is not ready', async () => {
const current = createApp([], [defaultNetwork], [], false);
const target = createApp(
[createService({ networks: ['test'] }, 1)],
[await createService({ networks: ['test'] }, 1)],
[defaultNetwork, Network.fromComposeObject('test', 1, {})],
[],
true,

View File

@ -18,7 +18,6 @@ const DB_PATH = './test/data/supervisor-api.sqlite';
// Holds all values used for stubbing
const STUBBED_VALUES = {
config: {
apiSecret: 'secure_api_secret',
currentCommit: '7fc9c5bea8e361acd49886fe6cc1e1cd',
},
services: [
@ -109,10 +108,7 @@ async function createAPIOpts(): Promise<void> {
async function initConfig(): Promise<void> {
// Initialize this config
await config.initialized;
// Set testing secret
await config.set({
apiSecret: STUBBED_VALUES.config.apiSecret,
});
// Set a currentCommit
await config.set({
currentCommit: STUBBED_VALUES.config.currentCommit,