Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from '@kbn/core/server/mocks';
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
import type { SavedObject } from '@kbn/core-saved-objects-server';
import type { RawRule } from '../../../../types';
import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock';
import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
Expand Down Expand Up @@ -114,7 +115,9 @@ describe('bulkDisableRules', () => {
let actionsClient: jest.Mocked<ActionsClient>;

const mockCreatePointInTimeFinderAsInternalUser = (
response = { saved_objects: [enabledRule1, enabledRule2] }
response: { saved_objects: Array<SavedObject<Partial<RawRule>>> } = {
saved_objects: [enabledRule1, enabledRule2],
}
) => {
encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest
.fn()
Expand Down Expand Up @@ -555,6 +558,109 @@ describe('bulkDisableRules', () => {
});
});

describe('lastRun outcome message migration', () => {
test('migrates legacy string lastRun.outcomeMsg to string[] when bulk disabling', async () => {
mockCreatePointInTimeFinderAsInternalUser({
saved_objects: [
{
...enabledRule1,
attributes: {
...enabledRule1.attributes,
lastRun: {
outcome: 'failed',
// @ts-expect-error test legacy outcomeMsg migration
outcomeMsg: 'legacy message',
},
},
},
enabledRule2,
],
});
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: [disabledRuleForBulkDisable1, disabledRuleForBulkDisable2],
});

await rulesClient.bulkDisableRules({ filter: 'fake_filter' });

expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
id: 'id1',
attributes: expect.objectContaining({
lastRun: {
outcome: 'failed',
outcomeMsg: ['legacy message'],
},
}),
}),
]),
{ overwrite: true }
);
});

test('leaves lastRun unchanged when outcomeMsg is already a string array', async () => {
const lastRun = {
outcome: 'succeeded' as const,
outcomeMsg: ['msg a', 'msg b'],
alertsCount: {
new: 0,
ignored: 0,
recovered: 0,
active: 0,
},
};
mockCreatePointInTimeFinderAsInternalUser({
saved_objects: [
{
...enabledRule1,
attributes: {
...enabledRule1.attributes,
lastRun,
},
},
],
});
mockUnsecuredSavedObjectFind(1);
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: [disabledRuleForBulkDisable1],
});

await rulesClient.bulkDisableRules({ filter: 'fake_filter' });

expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
id: 'id1',
attributes: expect.objectContaining({
lastRun,
}),
}),
]),
{ overwrite: true }
);
});

test('does not add lastRun when the rule has no lastRun', async () => {
mockCreatePointInTimeFinderAsInternalUser({
saved_objects: [enabledRule1],
});
mockUnsecuredSavedObjectFind(1);
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: [disabledRuleForBulkDisable1],
});

await rulesClient.bulkDisableRules({ filter: 'fake_filter' });

const bulkCreateObjects = unsecuredSavedObjectsClient.bulkCreate.mock.calls[0][0] as Array<{
id: string;
attributes: Record<string, unknown>;
}>;
const attributesForRule = bulkCreateObjects.find((o) => o.id === 'id1')?.attributes;
expect(attributesForRule).toBeDefined();
expect(attributesForRule).not.toHaveProperty('lastRun');
});
});

describe('taskManager', () => {
test('should call task manager bulkDisable', async () => {
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
untrackRuleAlerts,
updateMeta,
bulkMigrateLegacyActions,
migrateLegacyLastRunOutcomeMsg,
} from '../../../../rules_client/lib';
import { transformRuleAttributesToRuleDomain, transformRuleDomainToRule } from '../../transforms';
import type {
Expand Down Expand Up @@ -180,6 +181,9 @@ const bulkDisableRulesWithOCC = async (
: null,
updatedBy: username,
updatedAt: new Date().toISOString(),
...(castedAttributes.lastRun
? { lastRun: migrateLegacyLastRunOutcomeMsg(castedAttributes.lastRun) }
: {}),
});

rulesToDisable.push({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
savedObjectsRepositoryMock,
uiSettingsServiceMock,
} from '@kbn/core/server/mocks';
import type { SavedObject } from '@kbn/core/server';
import type { RawRule } from '../../../../types';
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock';
import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock';
Expand Down Expand Up @@ -118,7 +120,9 @@ describe('bulkEnableRules', () => {
let actionsClient: jest.Mocked<ActionsClient>;

const mockCreatePointInTimeFinderAsInternalUser = (
response = { saved_objects: [disabledRule1, disabledRule2] }
response: { saved_objects: Array<SavedObject<Partial<RawRule>>> } = {
saved_objects: [disabledRule1, disabledRule2],
}
) => {
encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest
.fn()
Expand Down Expand Up @@ -675,6 +679,110 @@ describe('bulkEnableRules', () => {
});
});

describe('lastRun outcome message migration', () => {
test('migrates legacy string lastRun.outcomeMsg to string[] when bulk enabling', async () => {
mockCreatePointInTimeFinderAsInternalUser({
saved_objects: [
{
...disabledRule1,
attributes: {
...disabledRule1.attributes,
lastRun: {
outcome: 'failed',
// @ts-expect-error test legacy outcomeMsg migration
outcomeMsg: 'legacy message',
},
},
},
disabledRule2,
],
});
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: [enabledRuleForBulkOps1, enabledRuleForBulkOps2],
});

await rulesClient.bulkEnableRules({ filter: 'fake_filter' });

expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
id: 'id1',
attributes: expect.objectContaining({
lastRun: {
outcome: 'failed',
outcomeMsg: ['legacy message'],
},
}),
}),
]),
{ overwrite: true }
);
});

test('leaves lastRun unchanged when outcomeMsg is already a string array', async () => {
const lastRun = {
outcome: 'succeeded' as const,
outcomeMsg: ['msg a', 'msg b'],
alertsCount: {
new: 0,
ignored: 0,
recovered: 0,
active: 0,
},
};

mockCreatePointInTimeFinderAsInternalUser({
saved_objects: [
{
...disabledRule1,
attributes: {
...disabledRule1.attributes,
lastRun,
},
},
],
});
mockUnsecuredSavedObjectFind(1);
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: [enabledRuleForBulkOps1],
});

await rulesClient.bulkEnableRules({ filter: 'fake_filter' });

expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
id: 'id1',
attributes: expect.objectContaining({
lastRun,
}),
}),
]),
{ overwrite: true }
);
});

test('does not add lastRun when the rule has no lastRun', async () => {
mockCreatePointInTimeFinderAsInternalUser({
saved_objects: [disabledRule1],
});
mockUnsecuredSavedObjectFind(1);
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: [enabledRuleForBulkOps1],
});

await rulesClient.bulkEnableRules({ filter: 'fake_filter' });

const bulkCreateObjects = unsecuredSavedObjectsClient.bulkCreate.mock.calls[0][0] as Array<{
id: string;
attributes: Record<string, unknown>;
}>;
const attributesForRule = bulkCreateObjects.find((o) => o.id === 'id1')?.attributes;
expect(attributesForRule).toBeDefined();
expect(attributesForRule).not.toHaveProperty('lastRun');
});
});

describe('taskManager', () => {
test('should return task id if enabling task failed', async () => {
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
createNewAPIKeySet,
updateMetaAttributes,
bulkMigrateLegacyActions,
migrateLegacyLastRunOutcomeMsg,
} from '../../../../rules_client/lib';
import type { RulesClientContext, BulkOperationError } from '../../../../rules_client/types';
import { validateScheduleLimit } from '../get_schedule_frequency';
Expand Down Expand Up @@ -257,6 +258,9 @@ const bulkEnableRulesWithOCC = async (
warning: null,
},
scheduledTaskId: rule.id,
...(rule.attributes.lastRun
? { lastRun: migrateLegacyLastRunOutcomeMsg(rule.attributes.lastRun) }
: {}),
});

const shouldScheduleTask = await getShouldScheduleTask(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
createNewAPIKeySet,
updateMetaAttributes,
bulkMigrateLegacyActions,
migrateLegacyLastRunOutcomeMsg,
} from '../../../../rules_client/lib';
import type { RuleParams } from '../../types';
import type { UpdateRuleData } from './types';
Expand Down Expand Up @@ -415,25 +416,3 @@ async function updateRuleAttributes<Params extends RuleParams = never>({
// without fixing all of other solution types
return rule as SanitizedRule<Params>;
}

/**
* Migrates legacy lastRun.outcomeMsg from string to string[]
*
* Rule SO schema forces lastRun.outcomeMsg to be string[].
* However, some rules may have lastRun.outcomeMsg as string after upgrading from 7.x due to
* lack of migration. lastRun.outcomeMsg schema change from string to string[] happened after
* classical migrations were deprecated due to Serverless. And quite often it's not an issue
* as lastRun is absent.
*/
function migrateLegacyLastRunOutcomeMsg<LastRun extends { outcomeMsg?: unknown }>(
lastRun: LastRun
): LastRun {
if (typeof lastRun.outcomeMsg === 'string') {
return {
...lastRun,
outcomeMsg: [lastRun.outcomeMsg],
};
}

return lastRun;
}
Loading
Loading