Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Service installs: Fix creating two tasks when moving a device #1857

Merged
merged 3 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions src/features/ci-cd/hooks/service-installs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,18 +287,25 @@ hooks.addPureHook('PATCH', 'resin', 'device', {
const affectedIds = await sbvrUtils.getAffectedIds(args);
if (affectedIds.length !== 0) {
await deleteServiceInstallsForCurrentApp(args.api, newAppId, affectedIds);
await createAppServiceInstalls(args.api, newAppId, affectedIds, args.tx);
}
},
});

hooks.addPureHook('PATCH', 'resin', 'device', {
POSTRUN: async ({ api, request, tx }) => {
const affectedIds = request.affectedIds!;
if (
request.values.is_pinned_on__release !== undefined &&
affectedIds.length !== 0
) {
if (affectedIds.length === 0) {
return;
}
const newAppId = request.values.belongs_to__application;
if (newAppId != null) {
// We could also have an optimization for the case that `values.is_pinned_on__release != null`
// to make the part that finds the target release faster, but chose to keep this simpler since:
// a) We expect that in the majority of device move requests users will not be also be pinning
// the device at the same time.
// b) Only the sync approach would benefit from it, since creating the service installs via tasks
// (which is going to be the default), only accepts the deviceIds as a parameter,
Comment on lines +299 to +304
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very much a TODO comment and would make more sense in the commit body imo, having it in the code like this is probably not going to end up being useful as no one will look at it anyway. Fwiw I do agree we don't need the extra levels of optimization as well so the only reason we would ever act on this is if we found performance issues that triggered us to investigate anyway, in which case we probably wouldn't find this

await createAppServiceInstalls(api, newAppId, affectedIds, tx);
return;
Comment on lines +305 to +306
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd appreciate a comment saying that since we're moving apps we can just create the app service installs and not worry about targeting the specific release, otherwise I would wonder why we're returning here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
if (request.values.is_pinned_on__release !== undefined) {
// If the device was preloaded, and then pinned, service_installs do not exist
// for this device+release combination. We need to create these
if (request.values.is_pinned_on__release != null) {
Expand Down
63 changes: 63 additions & 0 deletions test/25_service-installs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as fixtures from './test-lib/fixtures.js';
import { assertExists, expectToEventually } from './test-lib/common.js';
import * as config from '../src/lib/config.js';
import { supertest } from './test-lib/supertest.js';
import { expectNewTasks, resetLatestTaskIds } from './test-lib/api-helpers.js';

export default () => {
versions.test((version, pineTest) => {
Expand Down Expand Up @@ -45,6 +46,8 @@ export default () => {

config.TEST_MOCK_ONLY.ASYNC_TASK_CREATE_SERVICE_INSTALLS_ENABLED =
isServiceInstallEnabled;

await resetLatestTaskIds('create_service_installs');
});

after(async () => {
Expand Down Expand Up @@ -74,6 +77,19 @@ export default () => {
ctx.app1Service1.id,
);
});
await expectNewTasks(
'create_service_installs',
isServiceInstallEnabled
? [
{
is_executed_with__parameter_set: {
devices: [ctx.device.id],
},
status: 'succeeded',
},
]
: [],
);
});

it('for pinning an application to a release', async () => {
Expand Down Expand Up @@ -104,6 +120,19 @@ export default () => {
ctx.app1Service2.id,
);
});
await expectNewTasks(
'create_service_installs',
isServiceInstallEnabled
? [
{
is_executed_with__parameter_set: {
devices: [ctx.device.id],
},
status: 'succeeded',
},
]
: [],
);
});

it('when a device is pinned on a different release', async () => {
Expand Down Expand Up @@ -133,6 +162,19 @@ export default () => {
ctx.app1Service3.id,
);
});
await expectNewTasks(
'create_service_installs',
isServiceInstallEnabled
? [
{
is_executed_with__parameter_set: {
devices: [ctx.device.id],
},
status: 'succeeded',
},
]
: [],
);
});

it('when device is moved to different application', async () => {
Expand Down Expand Up @@ -170,6 +212,27 @@ export default () => {
ctx.app2Service1.id,
);
});
await expectNewTasks(
'create_service_installs',
isServiceInstallEnabled
? [
// the first one is from unpinning
{
is_executed_with__parameter_set: {
devices: [ctx.device.id],
},
status: 'succeeded',
},
// the second one is from moving application
{
is_executed_with__parameter_set: {
devices: [ctx.device.id],
},
status: 'succeeded',
},
]
: [],
);
});

it('should be able to use service_install to create a device_service_environment_variable', async () => {
Expand Down
89 changes: 87 additions & 2 deletions test/test-lib/api-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import type { UserObjectParam } from '../test-lib/supertest.js';
import { supertest } from '../test-lib/supertest.js';
import type { TokenUserPayload } from '../../src/index.js';
import type { RequiredField } from '@balena/pinejs/out/sbvr-api/common-types.js';
import { assertExists } from './common.js';
import { assertExists, expectToEventually } from './common.js';
import { sbvrUtils, permissions } from '@balena/pinejs';
import type { tasks } from '@balena/pinejs';

const version = 'resin';

Expand Down Expand Up @@ -208,7 +210,7 @@ export const thatIsDateStringAfter = (

const validJwtProps = ['id', 'jwt_secret', 'authTime', 'iat', 'exp'].sort();

export function expectJwt(tokenOrJwt: string | AnyObject) {
export function expectJwt(tokenOrJwt: string | object) {
const decoded = (
typeof tokenOrJwt === 'string'
? jsonwebtoken.decode(tokenOrJwt)
Expand All @@ -232,3 +234,86 @@ export function expectJwt(tokenOrJwt: string | AnyObject) {

return decoded;
}

export type TaskExpectation = Pick<
tasks.Task['Read'],
'is_executed_with__parameter_set' | 'status'
>;

const expectTasks = async (
handler: string,
expectedTasks: TaskExpectation[],
lastId = '0',
) => {
const tasks = await sbvrUtils.api.tasks.get({
resource: 'task',
passthrough: { req: permissions.rootRead },
options: {
$select: ['id', 'is_executed_with__parameter_set', 'status'],
$filter: {
id: { $gt: lastId },
is_executed_by__handler: handler,
},
$orderby: { id: 'asc' },
},
});
const actual = tasks.map((t) =>
_.pick(t, ['is_executed_with__parameter_set', 'status']),
);

expect(actual).to.deep.equal(expectedTasks);
return tasks;
};

const latestTaskIdByHandler: Dictionary<string | undefined> = {};

export const expectNewTasks = async (
handler: string,
expectedUsage: TaskExpectation[],
) => {
const tasks = await expectTasks(
handler,
expectedUsage,
latestTaskIdByHandler[handler],
);
const lastTaskId = tasks.at(-1)?.id;
if (lastTaskId != null) {
latestTaskIdByHandler[handler] = lastTaskId;
}
};

export const resetLatestTaskIds = async (handler: string) => {
await expectToEventually(async () => {
const runningTasks = await sbvrUtils.api.tasks.get({
resource: 'task',
passthrough: { req: permissions.rootRead },
options: {
$top: 1,
$select: 'id',
$filter: {
status: 'queued',
is_executed_by__handler: handler,
},
$orderby: { id: 'desc' },
},
});
expect(runningTasks).to.have.lengthOf(0);
});
const [latestTask] = await sbvrUtils.api.tasks.get({
resource: 'task',
passthrough: { req: permissions.rootRead },
options: {
$top: 1,
$select: 'id',
$filter: {
status: 'succeeded',
is_executed_by__handler: handler,
},
$orderby: { id: 'desc' },
},
});
if (latestTask == null) {
return;
}
latestTaskIdByHandler[handler] = latestTask?.id;
};
Loading