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

[Task Manager][8.0] Added migrations to savedObject Ids for "actions:* and "alerting:*" task types #109180

4 changes: 2 additions & 2 deletions x-pack/plugins/task_manager/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import type { SavedObjectsServiceSetup, SavedObjectsTypeMappingDefinition } from 'kibana/server';
import { estypes } from '@elastic/elasticsearch';
import mappings from './mappings.json';
import { migrations } from './migrations';
import { getMigrations } from './migrations';
import { TaskManagerConfig } from '../config.js';
import { getOldestIdleActionTask } from '../queries/oldest_idle_action_task';

Expand All @@ -22,7 +22,7 @@ export function setupSavedObjects(
hidden: true,
convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id; ctx._source.remove("kibana")`,
mappings: mappings.task as SavedObjectsTypeMappingDefinition,
migrations,
migrations: getMigrations(),
indexPattern: config.index,
excludeOnUpgrade: async ({ readonlyEsClient }) => {
const oldestNeededActionParams = await getOldestIdleActionTask(
Expand Down
143 changes: 143 additions & 0 deletions x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import uuid from 'uuid';
import { getMigrations } from './migrations';
import { SavedObjectUnsanitizedDoc } from 'kibana/server';
import { migrationMocks } from 'src/core/server/mocks';
import { TaskInstanceWithDeprecatedFields } from '../task';

const migrationContext = migrationMocks.createContext();

describe('successful migrations', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('7.4.0', () => {
test('extend task instance with updated_at', () => {
const migration740 = getMigrations()['7.4.0'];
const taskInstance = getMockData({});
expect(migration740(taskInstance, migrationContext).attributes.updated_at).not.toBeNull();
});
});

describe('7.6.0', () => {
test('rename property Internal to Schedule', () => {
const migration760 = getMigrations()['7.6.0'];
const taskInstance = getMockData({});
expect(migration760(taskInstance, migrationContext)).toEqual({
...taskInstance,
attributes: {
...taskInstance.attributes,
schedule: taskInstance.attributes.schedule,
},
});
});
});

describe('8.0.0', () => {
test('transforms actionsTasksLegacyIdToSavedObjectIds', () => {
const migration800 = getMigrations()['8.0.0'];
const taskInstance = getMockData({
taskType: 'actions:123456',
params: JSON.stringify({ spaceId: 'user1', actionTaskParamsId: '123456' }),
});

expect(migration800(taskInstance, migrationContext)).toEqual({
...taskInstance,
attributes: {
...taskInstance.attributes,
params: '{"spaceId":"user1","actionTaskParamsId":"800f81f8-980e-58ca-b710-d1b0644adea2"}',
},
});
});

test('it is only applicable for saved objects that live in a custom space', () => {
YulNaumenko marked this conversation as resolved.
Show resolved Hide resolved
const migration800 = getMigrations()['8.0.0'];
const taskInstance = getMockData({
taskType: 'actions:123456',
params: JSON.stringify({ spaceId: 'default', actionTaskParamsId: '123456' }),
});

expect(migration800(taskInstance, migrationContext)).toEqual(taskInstance);
});

test('transforms alertingTaskLegacyIdToSavedObjectIds', () => {
YulNaumenko marked this conversation as resolved.
Show resolved Hide resolved
const migration800 = getMigrations()['8.0.0'];
const taskInstance = getMockData({
taskType: 'alerting:123456',
params: JSON.stringify({ spaceId: 'user1', alertId: '123456' }),
});

expect(migration800(taskInstance, migrationContext)).toEqual({
...taskInstance,
attributes: {
...taskInstance.attributes,
params: '{"spaceId":"user1","alertId":"1a4f9206-e25f-58e6-bad5-3ff21e90648e"}',
},
});
});
});
});

describe('handles errors during migrations', () => {
describe('8.0.0 throws if migration fails', () => {
test('should throw the exception if task instance params format is wrong', () => {
const migration800 = getMigrations()['8.0.0'];
const taskInstance = getMockData({
taskType: 'alerting:123456',
params: `{ spaceId: 'user1', customId: '123456' }`,
});
expect(() => {
migration800(taskInstance, migrationContext);
}).toThrowError();
expect(migrationContext.log.error).toHaveBeenCalledWith(
`savedObject 8.0.0 migration failed for task instance ${taskInstance.id} with error: Unexpected token s in JSON at position 2`,
{
migrations: {
taskInstanceDocument: {
...taskInstance,
attributes: {
...taskInstance.attributes,
},
},
},
}
);
});
});
});

function getUpdatedAt(): string {
const updatedAt = new Date();
updatedAt.setHours(updatedAt.getHours() + 2);
return updatedAt.toISOString();
}

function getMockData(
overwrites: Record<string, unknown> = {}
): SavedObjectUnsanitizedDoc<Partial<TaskInstanceWithDeprecatedFields>> {
return {
attributes: {
scheduledAt: new Date(),
state: { runs: 0, total_cleaned_up: 0 },
runAt: new Date(),
startedAt: new Date(),
retryAt: new Date(),
ownerId: '234',
taskType: 'foo',
schedule: { interval: '10s' },
params: {
bar: true,
},
...overwrites,
},
updated_at: getUpdatedAt(),
id: uuid.v4(),
type: 'task',
};
}
128 changes: 120 additions & 8 deletions x-pack/plugins/task_manager/server/saved_objects/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,123 @@
* 2.0.
*/

import { SavedObjectMigrationMap, SavedObjectUnsanitizedDoc } from '../../../../../src/core/server';
import {
LogMeta,
SavedObjectMigrationContext,
SavedObjectMigrationFn,
SavedObjectMigrationMap,
SavedObjectsUtils,
SavedObjectUnsanitizedDoc,
} from '../../../../../src/core/server';
import { TaskInstance, TaskInstanceWithDeprecatedFields } from '../task';

export const migrations: SavedObjectMigrationMap = {
'7.4.0': (doc) => ({
...doc,
updated_at: new Date().toISOString(),
}),
'7.6.0': moveIntervalIntoSchedule,
};
interface TaskInstanceLogMeta extends LogMeta {
migrations: { taskInstanceDocument: SavedObjectUnsanitizedDoc<TaskInstanceWithDeprecatedFields> };
}

type TaskInstanceMigration = (
doc: SavedObjectUnsanitizedDoc<TaskInstanceWithDeprecatedFields>
) => SavedObjectUnsanitizedDoc<TaskInstanceWithDeprecatedFields>;

export function getMigrations(): SavedObjectMigrationMap {
return {
'7.4.0': executeMigrationWithErrorHandling(
(doc) => ({
...doc,
updated_at: new Date().toISOString(),
}),
'7.4.0'
),
'7.6.0': executeMigrationWithErrorHandling(moveIntervalIntoSchedule, '7.16.0'),
'8.0.0': executeMigrationWithErrorHandling(
pipeMigrations(alertingTaskLegacyIdToSavedObjectIds, actionsTasksLegacyIdToSavedObjectIds),
'8.0.0'
),
};
}

function executeMigrationWithErrorHandling(
migrationFunc: SavedObjectMigrationFn<
TaskInstanceWithDeprecatedFields,
TaskInstanceWithDeprecatedFields
>,
version: string
) {
return (
doc: SavedObjectUnsanitizedDoc<TaskInstanceWithDeprecatedFields>,
context: SavedObjectMigrationContext
) => {
try {
return migrationFunc(doc, context);
} catch (ex) {
context.log.error<TaskInstanceLogMeta>(
`savedObject ${version} migration failed for task instance ${doc.id} with error: ${ex.message}`,
{
migrations: {
taskInstanceDocument: doc,
},
}
);
throw ex;
}
};
}

function alertingTaskLegacyIdToSavedObjectIds(
doc: SavedObjectUnsanitizedDoc<TaskInstanceWithDeprecatedFields>
): SavedObjectUnsanitizedDoc<TaskInstanceWithDeprecatedFields> {
if (doc.attributes.taskType.startsWith('alerting:')) {
let params: { spaceId?: string; alertId?: string } = {};
params = JSON.parse((doc.attributes.params as unknown) as string);

if (params.alertId && params.spaceId && params.spaceId !== 'default') {
const newId = SavedObjectsUtils.getConvertedObjectId(params.spaceId, 'alert', params.alertId);
return {
...doc,
attributes: {
...doc.attributes,
params: JSON.stringify({
...params,
alertId: newId,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
},
};
}
}

return doc;
}

function actionsTasksLegacyIdToSavedObjectIds(
doc: SavedObjectUnsanitizedDoc<TaskInstanceWithDeprecatedFields>
): SavedObjectUnsanitizedDoc<TaskInstanceWithDeprecatedFields> {
if (doc.attributes.taskType.startsWith('actions:')) {
let params: { spaceId?: string; actionTaskParamsId?: string } = {};
params = JSON.parse((doc.attributes.params as unknown) as string);

if (params.actionTaskParamsId && params.spaceId && params.spaceId !== 'default') {
const newId = SavedObjectsUtils.getConvertedObjectId(
params.spaceId,
'action_task_params',
params.actionTaskParamsId
);
return {
...doc,
attributes: {
...doc.attributes,
params: JSON.stringify({
...params,
actionTaskParamsId: newId,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
},
};
}
}

return doc;
}

function moveIntervalIntoSchedule({
attributes: { interval, ...attributes },
Expand All @@ -34,3 +141,8 @@ function moveIntervalIntoSchedule({
},
};
}

function pipeMigrations(...migrations: TaskInstanceMigration[]): TaskInstanceMigration {
return (doc: SavedObjectUnsanitizedDoc<TaskInstanceWithDeprecatedFields>) =>
migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc);
}