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
3 changes: 0 additions & 3 deletions packages/kbn-check-saved-objects-cli/current_mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,6 @@
}
}
},
"scheduledTaskId": {
"type": "keyword"
},
"tags": {
"type": "keyword"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"action_task_params": "7a110800310c2f46ea543a1efb176f45b532c661b81964e9e200c79171cf978a",
"ad_hoc_run_params": "8b7348894a386c748195543d3eda2efa735320de2f0fee11f5aa4afed680b7a6",
"alert": "b2c0f19befa0ee4572ca42f45937e29633a618b5ca0ae004a070e78b60449868",
"alerting_rule": "ab9efda4fbfaacdd8cd8c78920003021c688dece09eba35cc1702ccb18e9c9d7",
"alerting_rule": "fcc94226b490999dfc7b538586cb84c5c5fed00e2eec1e3b1a6acff35b20954f",
"alerting_rule_template": "7c0ce40abc7416e49e3729b5189623be396c31b6c7ce2f915d9f4908405eca74",
"api_key_pending_invalidation": "19ece0ac908352a86624e7c487f452077db878a9bf15286892c6da8e76bbb479",
"api_key_to_invalidate": "2e202e95f580920dd23c8e39659817f1d210f15d8a55af5b3ae9469a6e98a2d7",
Expand Down Expand Up @@ -293,9 +293,9 @@ describe('checking migration metadata changes on all registered SO types', () =>
"alert|warning: The SO type owner should ensure these transform functions DO NOT mutate after they are defined.",
"==============================================================================================================",
"alerting_rule|global: e78adb1490c02adb4c705491c87e08332c0f668e",
"alerting_rule|mappings: 1ef9fcf4d9ceace7c4f7eaeb79e2c267f16b6d3a",
"alerting_rule|mappings: 2b5b6a304eca69896b2ea95243e2e437b12d07e1",
"alerting_rule|schemas: da39a3ee5e6b4b0d3255bfef95601890afd80709",
"alerting_rule|10.1.0: f3fb0872fd6be14dbffc82d0f005edcf9bb73ffcf4701b34830c90b100621c85",
"alerting_rule|10.1.0: 8c3c78b6ed6c4728bfc81cc8100c2f18e4cbfbfe753f6193882ae7fc263fd4bd",
"======================================================================================",
"alerting_rule_template|global: a8ee387a4bc794ff6450017a92742b39b79e0446",
"alerting_rule_template|mappings: 6556e5b0800a79f7a18c17736ac3e795f262c23b",
Expand Down
6 changes: 6 additions & 0 deletions x-pack/platform/plugins/shared/alerting_v2/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import { configSchema } from './config';
import { AlertingRetryService } from './lib/services/retry_service';
import { registerFeaturePrivileges } from './lib/security/privileges';
import { CreateRuleRoute } from './routes/create_rule_route';
import { DeleteRuleRoute } from './routes/delete_rule_route';
import { GetRuleRoute } from './routes/get_rule_route';
import { GetRulesRoute } from './routes/get_rules_route';
import { UpdateRuleRoute } from './routes/update_rule_route';
import { initializeRuleExecutorTaskDefinition } from './lib/rule_executor';
import { AlertingResourcesService } from './lib/services/alerting_resources_service';
Expand All @@ -29,6 +32,9 @@ export const config: PluginConfigDescriptor<PluginConfig> = {
export const module = new ContainerModule(({ bind }) => {
// Register HTTP routes via DI
bind(Route).toConstantValue(CreateRuleRoute);
bind(Route).toConstantValue(DeleteRuleRoute);
bind(Route).toConstantValue(GetRuleRoute);
bind(Route).toConstantValue(GetRulesRoute);
bind(Route).toConstantValue(UpdateRuleRoute);

// Request-scoped rules client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ describe('writeEsqlAlerts', () => {
timeField: '@timestamp',
lookbackWindow: '5m',
groupingKey: ['host.name'],
scheduledTaskId: null,
createdBy: 'u',
createdAt: '2025-01-01T00:00:00.000Z',
updatedBy: 'u',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@

export { RulesClient } from './rules_client';
export { createRuleDataSchema, updateRuleDataSchema } from './schemas';
export type { CreateRuleData, CreateRuleParams, RuleResponse, UpdateRuleData } from './types';
export type {
CreateRuleData,
CreateRuleParams,
FindRulesParams,
FindRulesResponse,
RuleResponse,
UpdateRuleData,
} from './types';
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ describe('RulesClient', () => {
expect.objectContaining({
id: 'rule-id-1',
enabled: false,
scheduledTaskId: null,
createdBy: 'elastic',
updatedBy: 'elastic',
createdAt: '2025-01-01T00:00:00.000Z',
Expand All @@ -126,7 +125,7 @@ describe('RulesClient', () => {
);
});

it('schedules a task when enabled and persists scheduledTaskId', async () => {
it('schedules a task when enabled', async () => {
const client = createClient();

const res = await client.createRule({
Expand All @@ -142,10 +141,13 @@ describe('RulesClient', () => {
spaceId: 'space-1',
}),
});
expect(savedObjectsClient.update).toHaveBeenCalledWith(RULE_SAVED_OBJECT_TYPE, 'rule-id-2', {
scheduledTaskId: 'task-123',
});
expect(res.scheduledTaskId).toBe('task-123');
expect(savedObjectsClient.update).not.toHaveBeenCalled();
expect(res).toEqual(
expect.objectContaining({
id: 'rule-id-2',
enabled: true,
})
);
});

it('cleans up the saved object if scheduling fails', async () => {
Expand Down Expand Up @@ -206,7 +208,7 @@ describe('RulesClient', () => {
});
});

it('disables a rule by removing task and clearing scheduledTaskId', async () => {
it('disables a rule by removing task', async () => {
const client = createClient();

const existing: SavedObject<RuleSavedObjectAttributes> = {
Expand All @@ -216,7 +218,6 @@ describe('RulesClient', () => {
attributes: {
...baseCreateData,
enabled: true,
scheduledTaskId: 'task-aaa',
createdBy: 'elastic',
createdAt: '2025-01-01T00:00:00.000Z',
updatedBy: 'elastic',
Expand All @@ -231,16 +232,20 @@ describe('RulesClient', () => {
data: { enabled: false } as UpdateRuleData,
});

expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-aaa');
expect(getRuleExecutorTaskIdMock).toHaveBeenCalledWith({
ruleId: 'rule-id-1',
spaceId: 'space-1',
});
expect(taskManager.removeIfExists).toHaveBeenCalledWith('task:fallback');
expect(savedObjectsClient.update).toHaveBeenCalledWith(
RULE_SAVED_OBJECT_TYPE,
'rule-id-1',
expect.objectContaining({ scheduledTaskId: null }),
expect.objectContaining({ enabled: false }),
expect.any(Object)
);
});

it('enables a rule by ensuring the task exists and updating scheduledTaskId', async () => {
it('enables a rule by ensuring the task exists', async () => {
const client = createClient();

const existing: SavedObject<RuleSavedObjectAttributes> = {
Expand All @@ -250,7 +255,6 @@ describe('RulesClient', () => {
attributes: {
...baseCreateData,
enabled: false,
scheduledTaskId: null,
createdBy: 'elastic',
createdAt: '2025-01-01T00:00:00.000Z',
updatedBy: 'elastic',
Expand All @@ -269,22 +273,53 @@ describe('RulesClient', () => {
expect(savedObjectsClient.update).toHaveBeenCalledWith(
RULE_SAVED_OBJECT_TYPE,
'rule-id-2',
expect.objectContaining({ scheduledTaskId: 'task-123' }),
expect.objectContaining({ enabled: true }),
expect.any(Object)
);
});

it('uses fallback task id when scheduledTaskId is missing on disable', async () => {
it('throws 409 conflict when version is stale', async () => {
const client = createClient();

const existing: SavedObject<RuleSavedObjectAttributes> = {
id: 'rule-id-3',
id: 'rule-id-4',
type: RULE_SAVED_OBJECT_TYPE,
version: 'WzEsMV0=',
attributes: {
...baseCreateData,
enabled: false,
createdBy: 'elastic',
createdAt: '2025-01-01T00:00:00.000Z',
updatedBy: 'elastic',
updatedAt: '2025-01-01T00:00:00.000Z',
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(existing);

savedObjectsClient.update.mockRejectedValueOnce(
SavedObjectsErrorHelpers.createConflictError(RULE_SAVED_OBJECT_TYPE, 'rule-id-4')
);

await expect(
client.updateRule({ id: 'rule-id-4', data: {} as UpdateRuleData })
).rejects.toMatchObject({
output: { statusCode: 409 },
});
});
});

describe('getRule', () => {
it('returns a rule by id', async () => {
const client = createClient();

const existing: SavedObject<RuleSavedObjectAttributes> = {
id: 'rule-id-get-1',
type: RULE_SAVED_OBJECT_TYPE,
version: 'WzEsMV0=',
attributes: {
...baseCreateData,
enabled: true,
scheduledTaskId: null,
createdBy: 'elastic',
createdAt: '2025-01-01T00:00:00.000Z',
updatedBy: 'elastic',
Expand All @@ -294,46 +329,139 @@ describe('RulesClient', () => {
};
savedObjectsClient.get.mockResolvedValueOnce(existing);

await client.updateRule({
id: 'rule-id-3',
data: { enabled: false } as UpdateRuleData,
const res = await client.getRule({ id: 'rule-id-get-1' });

expect(savedObjectsClient.get).toHaveBeenCalledWith(RULE_SAVED_OBJECT_TYPE, 'rule-id-get-1');
expect(res).toEqual({
id: 'rule-id-get-1',
...existing.attributes,
});
});

it('throws 404 when rule is not found', async () => {
const client = createClient();
savedObjectsClient.get.mockRejectedValueOnce(
SavedObjectsErrorHelpers.createGenericNotFoundError(
RULE_SAVED_OBJECT_TYPE,
'rule-id-get-404'
)
);

await expect(client.getRule({ id: 'rule-id-get-404' })).rejects.toMatchObject({
output: { statusCode: 404 },
});
});
});

describe('deleteRule', () => {
it('removes the scheduled task and deletes the rule', async () => {
const client = createClient();

const existing: SavedObject<RuleSavedObjectAttributes> = {
id: 'rule-id-del-1',
type: RULE_SAVED_OBJECT_TYPE,
version: 'WzEsMV0=',
attributes: {
...baseCreateData,
enabled: true,
createdBy: 'elastic',
createdAt: '2025-01-01T00:00:00.000Z',
updatedBy: 'elastic',
updatedAt: '2025-01-01T00:00:00.000Z',
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(existing);
getRuleExecutorTaskIdMock.mockReturnValueOnce('task:delete');

await client.deleteRule({ id: 'rule-id-del-1' });

expect(getRuleExecutorTaskIdMock).toHaveBeenCalledWith({
ruleId: 'rule-id-3',
ruleId: 'rule-id-del-1',
spaceId: 'space-1',
});
expect(taskManager.removeIfExists).toHaveBeenCalledWith('task:fallback');
expect(taskManager.removeIfExists).toHaveBeenCalledWith('task:delete');
expect(savedObjectsClient.delete).toHaveBeenCalledWith(
RULE_SAVED_OBJECT_TYPE,
'rule-id-del-1'
);
});

it('throws 409 conflict when version is stale', async () => {
it('throws 404 when rule is not found', async () => {
const client = createClient();
savedObjectsClient.get.mockRejectedValueOnce(
SavedObjectsErrorHelpers.createGenericNotFoundError(
RULE_SAVED_OBJECT_TYPE,
'rule-id-del-404'
)
);

const existing: SavedObject<RuleSavedObjectAttributes> = {
id: 'rule-id-4',
await expect(client.deleteRule({ id: 'rule-id-del-404' })).rejects.toMatchObject({
output: { statusCode: 404 },
});
});
});

describe('findRules', () => {
it('returns a paginated list of rules', async () => {
const client = createClient();

const so1: SavedObject<RuleSavedObjectAttributes> = {
id: 'rule-1',
type: RULE_SAVED_OBJECT_TYPE,
version: 'WzEsMV0=',
attributes: {
...baseCreateData,
name: 'rule-1',
enabled: true,
createdBy: 'elastic',
createdAt: '2025-01-01T00:00:00.000Z',
updatedBy: 'elastic',
updatedAt: '2025-01-01T00:00:00.000Z',
},
references: [],
};
const so2: SavedObject<RuleSavedObjectAttributes> = {
id: 'rule-2',
type: RULE_SAVED_OBJECT_TYPE,
version: 'WzEsMV0=',
attributes: {
...baseCreateData,
name: 'rule-2',
enabled: false,
scheduledTaskId: null,
createdBy: 'elastic',
createdAt: '2025-01-01T00:00:00.000Z',
updatedBy: 'elastic',
updatedAt: '2025-01-01T00:00:00.000Z',
},
references: [],
};
savedObjectsClient.get.mockResolvedValueOnce(existing);

savedObjectsClient.update.mockRejectedValueOnce(
SavedObjectsErrorHelpers.createConflictError(RULE_SAVED_OBJECT_TYPE, 'rule-id-4')
savedObjectsClient.find.mockResolvedValueOnce({
saved_objects: [so1, so2],
total: 2,
} as any);

const res = await client.findRules({ page: 2, perPage: 50 });

expect(savedObjectsClient.find).toHaveBeenCalledWith(
expect.objectContaining({
type: RULE_SAVED_OBJECT_TYPE,
page: 2,
perPage: 50,
sortField: 'updatedAt',
sortOrder: 'desc',
})
);

await expect(
client.updateRule({ id: 'rule-id-4', data: {} as UpdateRuleData })
).rejects.toMatchObject({
output: { statusCode: 409 },
expect(res).toEqual({
items: [
{ id: 'rule-1', ...so1.attributes },
{ id: 'rule-2', ...so2.attributes },
],
total: 2,
page: 2,
perPage: 50,
});
});
});
Expand Down
Loading