Skip to content

Commit

Permalink
Separate routes & actions, add tests for GET /v2/containerId
Browse files Browse the repository at this point in the history
Signed-off-by: Christina Ying Wang <[email protected]>
  • Loading branch information
cywang117 committed Dec 12, 2023
1 parent 9443281 commit e9535cd
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 22 deletions.
6 changes: 6 additions & 0 deletions src/compose/application-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1050,3 +1050,9 @@ export async function removeOrphanedVolumes(

await volumeManager.removeOrphanedVolumes(referencedVolumes);
}

export async function getAllServices(
inScope: (id: number) => boolean = () => true,
) {
return (await serviceManager.getAll()).filter((svc) => inScope(svc.appId));
}
34 changes: 34 additions & 0 deletions src/device-api/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,3 +484,37 @@ export const getLogStream = (opts: JournalctlOpts) => {
export const getSupervisorVersion = () => {
return supervisorVersion;
};

/**
* Get the containerId(s) associated with a service.
* If no serviceName is provided, get all containerIds.
* Used by:
* - GET /v2/containerId
*/
export const getContainerIds = async (
serviceName: string = '',
withScope: AuthorizedRequest['auth']['isScoped'] = () => true,
) => {
const services = await applicationManager.getAllServices((id) =>
withScope({ apps: [id] }),
);

// Return all containerIds if no serviceName is provided
if (!serviceName) {
return services.reduce(
(svcToContainerIdMap, svc) => ({
[svc.serviceName]: svc.containerId,
...svcToContainerIdMap,
}),
{},
);
}

// Otherwise, only return containerId of provided serviceNmae
const service = services.find((svc) => svc.serviceName === serviceName);
if (service != null) {
return service.containerId;
} else {
throw new Error(`Could not find service with name '${serviceName}'`);
}
};
49 changes: 27 additions & 22 deletions src/device-api/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,33 +389,38 @@ router.get('/v2/version', (_req, res, next) => {
}
});

router.get('/v2/containerId', async (req: AuthorizedRequest, res) => {
const services = (await serviceManager.getAll()).filter((service) =>
req.auth.isScoped({ apps: [service.appId] }),
);

if (req.query.serviceName != null || req.query.service != null) {
const serviceName = req.query.serviceName || req.query.service;
const service = _.find(services, (svc) => svc.serviceName === serviceName);
if (service != null) {
res.status(200).json({
router.get('/v2/containerId', async (req: AuthorizedRequest, res, next) => {
try {
// While technically query parameters support entering a query multiple times
// as in ?service=foo&service=bar, we don't explicitly support this,
// so only pass the serviceName / service if it's a string
let serviceQuery: string = '';
const { serviceName, service } = req.query;
if (typeof serviceName === 'string' && !!serviceName) {
serviceQuery = serviceName;
}
if (typeof service === 'string' && !!service) {
serviceQuery = service;
}
const result = await actions.getContainerIds(
serviceQuery,
req.auth.isScoped,
);
// Single containerId
if (typeof result === 'string') {
return res.status(200).json({
status: 'success',
containerId: service.containerId,
containerId: result,
});
} else {
res.status(503).json({
status: 'failed',
message: 'Could not find service with that name',
// Multiple containerIds
return res.status(200).json({
status: 'success',
services: result,
});
}
} else {
res.status(200).json({
status: 'success',
services: _(services)
.keyBy('serviceName')
.mapValues('containerId')
.value(),
});
} catch (e: unknown) {
next(e);
}
});

Expand Down
49 changes: 49 additions & 0 deletions test/integration/compose/application-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as serviceManager from '~/src/compose/service-manager';
import Network from '~/src/compose/network';
import * as networkManager from '~/src/compose/network-manager';
import Volume from '~/src/compose/volume';
import Service from '~/src/compose/service';
import * as config from '~/src/config';
import { createDockerImage } from '~/test-lib/docker-helper';
import {
Expand Down Expand Up @@ -1600,4 +1601,52 @@ describe('compose/application-manager', () => {
expect(steps).to.have.lengthOf(0);
});
});

describe('getting all services', () => {
let getAllServices: sinon.SinonStub;
let testServices: Service[];
before(async () => {
testServices = [
await createService(
{
serviceName: 'one',
appId: 1,
},
{ state: { containerId: 'abc' } },
),
await createService(
{
serviceName: 'two',
appId: 2,
},
{ state: { containerId: 'def' } },
),
];
getAllServices = sinon
.stub(serviceManager, 'getAll')
.resolves(testServices);
});

after(() => {
getAllServices.restore();
});

it('should get all services by default', async () => {
expect(await applicationManager.getAllServices()).to.deep.equal(
testServices,
);
});

it('should get services scoped by appId', async () => {
expect(
await applicationManager.getAllServices((appId) => appId === 1),
).to.deep.equal([testServices[0]]);
expect(
await applicationManager.getAllServices((appId) => appId === 2),
).to.deep.equal([testServices[1]]);
expect(
await applicationManager.getAllServices((appId) => appId === 3),
).to.deep.equal([]);
});
});
});
53 changes: 53 additions & 0 deletions test/integration/device-api/actions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as actions from '~/src/device-api/actions';
import * as TargetState from '~/src/device-state/target-state';
import * as applicationManager from '~/src/compose/application-manager';
import { cleanupDocker } from '~/test-lib/docker-helper';
import { createService } from '~/test-lib/state-helper';

import { exec } from '~/lib/fs-utils';
import * as journald from '~/lib/journald';
Expand Down Expand Up @@ -939,3 +940,55 @@ describe('spawns a journal process', () => {
expect(spawnJournalctlStub).to.have.been.calledOnceWith(opts);
});
});

describe('gets service container ids', () => {
// getAllServicesStub is tested in app manager tests
// so we can stub it here
let getAllServicesStub: SinonStub;
before(async () => {
getAllServicesStub = stub(applicationManager, 'getAllServices').resolves([
await createService(
{
serviceName: 'one',
appId: 1,
},
{ state: { containerId: 'abc' } },
),
await createService(
{
serviceName: 'two',
appId: 2,
},
{ state: { containerId: 'def' } },
),
]);
});
after(() => {
getAllServicesStub.restore();
});

it('gets all containerIds by default', async () => {
expect(await actions.getContainerIds()).to.deep.equal({
one: 'abc',
two: 'def',
});
});

it('gets a single containerId associated with provided service', async () => {
expect(await actions.getContainerIds('one')).to.deep.equal('abc');
expect(await actions.getContainerIds('two')).to.deep.equal('def');
});

it('errors if no containerId found associated with provided service', async () => {
try {
await actions.getContainerIds('three');
expect.fail(
'getContainerIds should throw for a nonexistent serviceName parameter',
);
} catch (e: unknown) {
expect((e as Error).message).to.equal(
"Could not find service with name 'three'",
);
}
});
});
67 changes: 67 additions & 0 deletions test/integration/device-api/v2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -829,4 +829,71 @@ describe('device-api/v2', () => {
.expect(503);
});
});

describe('GET /v2/containerId', () => {
let getContainerIdStub: SinonStub;
beforeEach(() => {
getContainerIdStub = stub(actions, 'getContainerIds');
});
afterEach(() => {
getContainerIdStub.restore();
});

it('accepts query parameters if they are strings', async () => {
getContainerIdStub.resolves('test');
await request(api)
.get('/v2/containerId?serviceName=one')
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
.expect(200, { status: 'success', containerId: 'test' });
expect(getContainerIdStub.firstCall.args[0]).to.equal('one');

await request(api)
.get('/v2/containerId?service=two')
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
.expect(200, { status: 'success', containerId: 'test' });
expect(getContainerIdStub.secondCall.args[0]).to.equal('two');
});

it('ignores query parameters that are repeated', async () => {
getContainerIdStub.resolves('test');
await request(api)
.get('/v2/containerId?serviceName=one&serviceName=two')
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
.expect(200, { status: 'success', containerId: 'test' });
expect(getContainerIdStub.firstCall.args[0]).to.equal('');

await request(api)
.get('/v2/containerId?service=one&service=two')
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
.expect(200, { status: 'success', containerId: 'test' });
expect(getContainerIdStub.secondCall.args[0]).to.equal('');
});

it('responds with 200 and single containerId', async () => {
getContainerIdStub.resolves('test');
await request(api)
.get('/v2/containerId')
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
.expect(200, { status: 'success', containerId: 'test' });
});

it('responds with 200 and multiple containerIds', async () => {
getContainerIdStub.resolves({ one: 'abc', two: 'def' });
await request(api)
.get('/v2/containerId')
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
.expect(200, {
status: 'success',
services: { one: 'abc', two: 'def' },
});
});

it('responds with 503 if an error occurred', async () => {
getContainerIdStub.throws(new Error());
await request(api)
.get('/v2/containerId')
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
.expect(503);
});
});
});

0 comments on commit e9535cd

Please sign in to comment.