Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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