[RAM] Rule API Type Versioning POC - Create Rule (#158786)

## Summary
Resolves: https://github.com/elastic/kibana/issues/157883

# Summary:
This PR acts as a POC for how we would want to version our rule API
types in preparation for the versioning of our HTTP endpoints.

There is now an ES persisted type (called `RuleAttributes`, akin to the
old `RawRule` type). This type should never be used at the business
logic level as we want the ability not to reference saved object
attributes directly in the application. Instead, we should transform
this saved-object to its corresponding domain object type.

HTTP types (`CreateBodyParams`, `RuleResponse`) are now located in
`x-pack/plugins/alerting/common/routes/rule` with a versioning structure
like:


![image](1c8e886d-8983-40f2-9490-aa3864898535)

And follows the guideline here:
https://docs.elastic.dev/kibana-dev-docs/versioning-interfaces

Domain object types (rule for example) are derived from the
`config-schema` schemas using `TypeOf`, this was done to facilitate the
reuse of validation schemas that we might want to run for strict IO
validation, potentially at the `rulesClient` level.

## API:
Only the `createRule` endpoint has been converted for the sake of this
POC, other endpoints might have broken types that will be fixed once we
have a finalized pattern for dealing with versioned types.

At the API route level, I think it would be wise to import versioned
types in our APIs, this way, it forces the developer to deal with broken
types when we have a breaking change to our rule domain object.

The API route level is also responsible for transforming domain objects
to response types, usually, this just entails renaming property names
from camel case to snake case.

in `create_rule_route.ts`

```ts
import type { CreateRuleRequestBodyV1, CreateRuleRequestParamsV1 } from '../../../../common/routes/rule/create';
import type { RuleParamsV1 } from '../../../../common/routes/rule/rule_response';

...

// In the Handler:
const rulesClient = (await context.alerting).getRulesClient();

const createRuleData: CreateRuleRequestBodyV1<RuleParamsV1> = req.body;
const params: CreateRuleRequestParamsV1 = req.params;

const createdRule: Rule<RuleParamsV1> = (await rulesClient.create<RuleParamsV1>({
  data: transformCreateBodyV1<RuleParamsV1>(createRuleData),
  options: { id: params?.id },
})) as Rule<RuleParamsV1>;

const response: CreateRuleResponseV1<RuleParamsV1> = {
  body: transformRuleToRuleResponseV1<RuleParamsV1>(createdRule),
};

return res.ok(response);
```

### RulesClient -> Create
At the rules client level, we now need to transform saved-objects to the
domain objects after we perform CRUD operations on them. We can also
consider validating schemas here too since other solutions are using our
rules client directly instead of through the API.

I don't think we need to version rules client input and output types
since the route level types should deal with breaking changes already.
Therefore, we can freely import the latest of any domain object types
(Rule, for example)

```ts
import { Rule, RuleDomain, RuleParams } from '../types';
```

The flow is the following:
```
1. calling rulesClient.create() with API body and params.
2. perform schema validation on params
3. perform other validation not achievable using config-schema
4. perform rule create -> returns a `savedObject` type
5. convert `savedObject` ES type to the domain object type (`RuleDomain`)
6. convert the domain object type to a public domain object type (`Rule`)
7. We can also validate the created rule using config-schema
```

# Open questions:
- Should we validate input params at both the route and the rules client
level? I think we should since our rules client is shared.

- How do we want to version and validate rule params? Right now they're
typed as generics.

- Should we leave all date fields as `string`, even for domain objects?
Since config-schema is unable to define a `Date` type.

- Should we support partial rule domain object types at all? I can't
really think why we should even for `updates`, since we need to
reconstruct the overall rule to send back to the client.

- Should we have `undefined | null` types in the domain object? I know
we need `null` specifically for the ES types since there is a different
between updating a field as `undefined` or `null`, but this shouldn't
manifest itself in the business logic.

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jiawei Wu 2023-07-05 14:15:59 -07:00 committed by GitHub
parent 9e52f7064f
commit 8de68afb40
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 2951 additions and 460 deletions

View file

@ -0,0 +1,40 @@
/*
* 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.
*/
export {
notifyWhenSchema,
actionFrequencySchema,
actionAlertsFilterSchema,
actionSchema,
createParamsSchema,
createBodySchema,
} from './schemas/latest';
export type {
CreateRuleAction,
CreateRuleActionFrequency,
CreateRuleRequestParams,
CreateRuleRequestBody,
CreateRuleResponse,
} from './types/latest';
export {
notifyWhenSchema as notifyWhenSchemaV1,
actionFrequencySchema as actionFrequencySchemaV1,
actionAlertsFilterSchema as actionAlertsFilterSchemaV1,
actionSchema as actionSchemaV1,
createParamsSchema as createParamsSchemaV1,
createBodySchema as createBodySchemaV1,
} from './schemas/v1';
export type {
CreateRuleAction as CreateRuleActionV1,
CreateRuleActionFrequency as CreateRuleActionFrequencyV1,
CreateRuleRequestParams as CreateRuleRequestParamsV1,
CreateRuleRequestBody as CreateRuleRequestBodyV1,
CreateRuleResponse as CreateRuleResponseV1,
} from './types/v1';

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,99 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { ruleNotifyWhenV1 } from '../../rule_response';
import {
validateNotifyWhenV1,
validateDurationV1,
validateHoursV1,
validateTimezoneV1,
} from '../../validation';
export const notifyWhenSchema = schema.oneOf(
[
schema.literal(ruleNotifyWhenV1.CHANGE),
schema.literal(ruleNotifyWhenV1.ACTIVE),
schema.literal(ruleNotifyWhenV1.THROTTLE),
],
{ validate: validateNotifyWhenV1 }
);
export const actionFrequencySchema = schema.object({
summary: schema.boolean(),
notify_when: notifyWhenSchema,
throttle: schema.nullable(schema.string({ validate: validateDurationV1 })),
});
export const actionAlertsFilterSchema = schema.object({
query: schema.maybe(
schema.object({
kql: schema.string(),
filters: schema.arrayOf(
schema.object({
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
meta: schema.recordOf(schema.string(), schema.any()),
state$: schema.maybe(schema.object({ store: schema.string() })),
})
),
dsl: schema.maybe(schema.string()),
})
),
timeframe: schema.maybe(
schema.object({
days: schema.arrayOf(
schema.oneOf([
schema.literal(1),
schema.literal(2),
schema.literal(3),
schema.literal(4),
schema.literal(5),
schema.literal(6),
schema.literal(7),
])
),
hours: schema.object({
start: schema.string({
validate: validateHoursV1,
}),
end: schema.string({
validate: validateHoursV1,
}),
}),
timezone: schema.string({ validate: validateTimezoneV1 }),
})
),
});
export const actionSchema = schema.object({
uuid: schema.maybe(schema.string()),
group: schema.string(),
id: schema.string(),
actionTypeId: schema.maybe(schema.string()),
params: schema.recordOf(schema.string(), schema.maybe(schema.any()), { defaultValue: {} }),
frequency: schema.maybe(actionFrequencySchema),
alerts_filter: schema.maybe(actionAlertsFilterSchema),
});
export const createBodySchema = schema.object({
name: schema.string(),
rule_type_id: schema.string(),
enabled: schema.boolean({ defaultValue: true }),
consumer: schema.string(),
tags: schema.arrayOf(schema.string(), { defaultValue: [] }),
throttle: schema.maybe(schema.nullable(schema.string({ validate: validateDurationV1 }))),
params: schema.recordOf(schema.string(), schema.maybe(schema.any()), { defaultValue: {} }),
schedule: schema.object({
interval: schema.string({ validate: validateDurationV1 }),
}),
actions: schema.arrayOf(actionSchema, { defaultValue: [] }),
notify_when: schema.maybe(schema.nullable(notifyWhenSchema)),
});
export const createParamsSchema = schema.object({
id: schema.maybe(schema.string()),
});

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,37 @@
/*
* 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 type { TypeOf } from '@kbn/config-schema';
import { RuleParamsV1, RuleResponseV1 } from '../../rule_response';
import {
actionSchema as actionSchemaV1,
actionFrequencySchema as actionFrequencySchemaV1,
createParamsSchema as createParamsSchemaV1,
createBodySchema as createBodySchemaV1,
} from '..';
export type CreateRuleAction = TypeOf<typeof actionSchemaV1>;
export type CreateRuleActionFrequency = TypeOf<typeof actionFrequencySchemaV1>;
export type CreateRuleRequestParams = TypeOf<typeof createParamsSchemaV1>;
type CreateBodySchema = TypeOf<typeof createBodySchemaV1>;
export interface CreateRuleRequestBody<Params extends RuleParamsV1 = never> {
name: CreateBodySchema['name'];
rule_type_id: CreateBodySchema['rule_type_id'];
enabled: CreateBodySchema['enabled'];
consumer: CreateBodySchema['consumer'];
tags: CreateBodySchema['tags'];
throttle?: CreateBodySchema['throttle'];
params: Params;
schedule: CreateBodySchema['schedule'];
actions: CreateBodySchema['actions'];
notify_when?: CreateBodySchema['notify_when'];
}
export interface CreateRuleResponse<Params extends RuleParamsV1 = never> {
body: RuleResponseV1<Params>;
}

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,53 @@
/*
* 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.
*/
export const ruleNotifyWhen = {
CHANGE: 'onActionGroupChange',
ACTIVE: 'onActiveAlert',
THROTTLE: 'onThrottleInterval',
} as const;
export const ruleLastRunOutcomeValues = {
SUCCEEDED: 'succeeded',
WARNING: 'warning',
FAILED: 'failed',
} as const;
export const ruleExecutionStatusValues = {
OK: 'ok',
ACTIVE: 'active',
ERROR: 'error',
WARNING: 'warning',
PENDING: 'pending',
UNKNOWN: 'unknown',
} as const;
export const ruleExecutionStatusErrorReason = {
READ: 'read',
DECRYPT: 'decrypt',
EXECUTE: 'execute',
UNKNOWN: 'unknown',
LICENSE: 'license',
TIMEOUT: 'timeout',
DISABLED: 'disabled',
VALIDATE: 'validate',
} as const;
export const ruleExecutionStatusWarningReason = {
MAX_EXECUTABLE_ACTIONS: 'maxExecutableActions',
MAX_ALERTS: 'maxAlerts',
} as const;
export type RuleNotifyWhen = typeof ruleNotifyWhen[keyof typeof ruleNotifyWhen];
export type RuleLastRunOutcomeValues =
typeof ruleLastRunOutcomeValues[keyof typeof ruleLastRunOutcomeValues];
export type RuleExecutionStatusValues =
typeof ruleExecutionStatusValues[keyof typeof ruleExecutionStatusValues];
export type RuleExecutionStatusErrorReason =
typeof ruleExecutionStatusErrorReason[keyof typeof ruleExecutionStatusErrorReason];
export type RuleExecutionStatusWarningReason =
typeof ruleExecutionStatusWarningReason[keyof typeof ruleExecutionStatusWarningReason];

View file

@ -0,0 +1,64 @@
/*
* 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.
*/
export {
ruleParamsSchema,
actionParamsSchema,
mappedParamsSchema,
ruleExecutionStatusSchema,
ruleLastRunSchema,
monitoringSchema,
rRuleSchema,
ruleResponseSchema,
} from './schemas/latest';
export type { RuleParams, RuleResponse } from './types/latest';
export {
ruleNotifyWhen,
ruleLastRunOutcomeValues,
ruleExecutionStatusValues,
ruleExecutionStatusErrorReason,
ruleExecutionStatusWarningReason,
} from './constants/latest';
export type {
RuleNotifyWhen,
RuleLastRunOutcomeValues,
RuleExecutionStatusValues,
RuleExecutionStatusErrorReason,
RuleExecutionStatusWarningReason,
} from './constants/latest';
export {
ruleParamsSchema as ruleParamsSchemaV1,
actionParamsSchema as actionParamsSchemaV1,
mappedParamsSchema as mappedParamsSchemaV1,
ruleExecutionStatusSchema as ruleExecutionStatusSchemaV1,
ruleLastRunSchema as ruleLastRunSchemaV1,
monitoringSchema as monitoringSchemaV1,
rRuleSchema as rRuleSchemaV1,
ruleResponseSchema as ruleResponseSchemaV1,
} from './schemas/v1';
export {
ruleNotifyWhen as ruleNotifyWhenV1,
ruleLastRunOutcomeValues as ruleLastRunOutcomeValuesV1,
ruleExecutionStatusValues as ruleExecutionStatusValuesV1,
ruleExecutionStatusErrorReason as ruleExecutionStatusErrorReasonV1,
ruleExecutionStatusWarningReason as ruleExecutionStatusWarningReasonV1,
} from './constants/v1';
export type {
RuleNotifyWhen as RuleNotifyWhenV1,
RuleLastRunOutcomeValues as RuleLastRunOutcomeValuesV1,
RuleExecutionStatusValues as RuleExecutionStatusValuesV1,
RuleExecutionStatusErrorReason as RuleExecutionStatusErrorReasonV1,
RuleExecutionStatusWarningReason as RuleExecutionStatusWarningReasonV1,
} from './constants/v1';
export type { RuleParams as RuleParamsV1, RuleResponse as RuleResponseV1 } from './types/v1';

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,259 @@
/*
* 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 { schema } from '@kbn/config-schema';
import {
ruleNotifyWhen as ruleNotifyWhenV1,
ruleExecutionStatusValues as ruleExecutionStatusValuesV1,
ruleExecutionStatusErrorReason as ruleExecutionStatusErrorReasonV1,
ruleExecutionStatusWarningReason as ruleExecutionStatusWarningReasonV1,
ruleLastRunOutcomeValues as ruleLastRunOutcomeValuesV1,
} from '../constants/v1';
export const ruleParamsSchema = schema.recordOf(schema.string(), schema.maybe(schema.any()));
export const actionParamsSchema = schema.recordOf(schema.string(), schema.maybe(schema.any()));
export const mappedParamsSchema = schema.recordOf(schema.string(), schema.maybe(schema.any()));
const notifyWhenSchema = schema.oneOf([
schema.literal(ruleNotifyWhenV1.CHANGE),
schema.literal(ruleNotifyWhenV1.ACTIVE),
schema.literal(ruleNotifyWhenV1.THROTTLE),
]);
const intervalScheduleSchema = schema.object({
interval: schema.string(),
});
const actionFrequencySchema = schema.object({
summary: schema.boolean(),
notify_when: notifyWhenSchema,
throttle: schema.nullable(schema.string()),
});
const actionAlertsFilterSchema = schema.object({
query: schema.maybe(
schema.object({
kql: schema.string(),
filters: schema.arrayOf(
schema.object({
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
meta: schema.recordOf(schema.string(), schema.any()),
state$: schema.maybe(schema.object({ store: schema.string() })),
})
),
})
),
timeframe: schema.maybe(
schema.object({
days: schema.arrayOf(
schema.oneOf([
schema.literal(1),
schema.literal(2),
schema.literal(3),
schema.literal(4),
schema.literal(5),
schema.literal(6),
schema.literal(7),
])
),
hours: schema.object({
start: schema.string(),
end: schema.string(),
}),
timezone: schema.string(),
})
),
});
const actionSchema = schema.object({
uuid: schema.maybe(schema.string()),
group: schema.string(),
id: schema.string(),
connector_type_id: schema.string(),
params: actionParamsSchema,
frequency: schema.maybe(actionFrequencySchema),
alerts_filter: schema.maybe(actionAlertsFilterSchema),
});
export const ruleExecutionStatusSchema = schema.object({
status: schema.oneOf([
schema.literal(ruleExecutionStatusValuesV1.OK),
schema.literal(ruleExecutionStatusValuesV1.ACTIVE),
schema.literal(ruleExecutionStatusValuesV1.ERROR),
schema.literal(ruleExecutionStatusValuesV1.WARNING),
schema.literal(ruleExecutionStatusValuesV1.PENDING),
schema.literal(ruleExecutionStatusValuesV1.UNKNOWN),
]),
last_execution_date: schema.string(),
last_duration: schema.maybe(schema.number()),
error: schema.maybe(
schema.object({
reason: schema.oneOf([
schema.literal(ruleExecutionStatusErrorReasonV1.READ),
schema.literal(ruleExecutionStatusErrorReasonV1.DECRYPT),
schema.literal(ruleExecutionStatusErrorReasonV1.EXECUTE),
schema.literal(ruleExecutionStatusErrorReasonV1.UNKNOWN),
schema.literal(ruleExecutionStatusErrorReasonV1.LICENSE),
schema.literal(ruleExecutionStatusErrorReasonV1.TIMEOUT),
schema.literal(ruleExecutionStatusErrorReasonV1.DISABLED),
schema.literal(ruleExecutionStatusErrorReasonV1.VALIDATE),
]),
message: schema.string(),
})
),
warning: schema.maybe(
schema.object({
reason: schema.oneOf([
schema.literal(ruleExecutionStatusWarningReasonV1.MAX_EXECUTABLE_ACTIONS),
schema.literal(ruleExecutionStatusWarningReasonV1.MAX_ALERTS),
]),
message: schema.string(),
})
),
});
export const ruleLastRunSchema = schema.object({
outcome: schema.oneOf([
schema.literal(ruleLastRunOutcomeValuesV1.SUCCEEDED),
schema.literal(ruleLastRunOutcomeValuesV1.WARNING),
schema.literal(ruleLastRunOutcomeValuesV1.FAILED),
]),
outcome_order: schema.maybe(schema.number()),
warning: schema.maybe(
schema.nullable(
schema.oneOf([
schema.literal(ruleExecutionStatusErrorReasonV1.READ),
schema.literal(ruleExecutionStatusErrorReasonV1.DECRYPT),
schema.literal(ruleExecutionStatusErrorReasonV1.EXECUTE),
schema.literal(ruleExecutionStatusErrorReasonV1.UNKNOWN),
schema.literal(ruleExecutionStatusErrorReasonV1.LICENSE),
schema.literal(ruleExecutionStatusErrorReasonV1.TIMEOUT),
schema.literal(ruleExecutionStatusErrorReasonV1.DISABLED),
schema.literal(ruleExecutionStatusErrorReasonV1.VALIDATE),
schema.literal(ruleExecutionStatusWarningReasonV1.MAX_EXECUTABLE_ACTIONS),
schema.literal(ruleExecutionStatusWarningReasonV1.MAX_ALERTS),
])
)
),
outcome_msg: schema.maybe(schema.nullable(schema.arrayOf(schema.string()))),
alerts_count: schema.object({
active: schema.maybe(schema.nullable(schema.number())),
new: schema.maybe(schema.nullable(schema.number())),
recovered: schema.maybe(schema.nullable(schema.number())),
ignored: schema.maybe(schema.nullable(schema.number())),
}),
});
export const monitoringSchema = schema.object({
run: schema.object({
history: schema.arrayOf(
schema.object({
success: schema.boolean(),
timestamp: schema.number(),
duration: schema.maybe(schema.number()),
outcome: schema.maybe(ruleLastRunSchema),
})
),
calculated_metrics: schema.object({
p50: schema.maybe(schema.number()),
p95: schema.maybe(schema.number()),
p99: schema.maybe(schema.number()),
success_ratio: schema.number(),
}),
last_run: schema.object({
timestamp: schema.string(),
metrics: schema.object({
duration: schema.maybe(schema.number()),
total_search_duration_ms: schema.maybe(schema.nullable(schema.number())),
total_indexing_duration_ms: schema.maybe(schema.nullable(schema.number())),
total_alerts_detected: schema.maybe(schema.nullable(schema.number())),
total_alerts_created: schema.maybe(schema.nullable(schema.number())),
gap_duration_s: schema.maybe(schema.nullable(schema.number())),
}),
}),
}),
});
export const rRuleSchema = schema.object({
dtstart: schema.string(),
tzid: schema.string(),
freq: schema.maybe(
schema.oneOf([
schema.literal(0),
schema.literal(1),
schema.literal(2),
schema.literal(3),
schema.literal(4),
schema.literal(5),
schema.literal(6),
])
),
until: schema.maybe(schema.string()),
count: schema.maybe(schema.number()),
interval: schema.maybe(schema.number()),
wkst: schema.maybe(
schema.oneOf([
schema.literal('MO'),
schema.literal('TU'),
schema.literal('WE'),
schema.literal('TH'),
schema.literal('FR'),
schema.literal('SA'),
schema.literal('SU'),
])
),
byweekday: schema.maybe(schema.arrayOf(schema.oneOf([schema.string(), schema.number()]))),
bymonth: schema.maybe(schema.arrayOf(schema.number())),
bysetpos: schema.maybe(schema.arrayOf(schema.number())),
bymonthday: schema.arrayOf(schema.number()),
byyearday: schema.arrayOf(schema.number()),
byweekno: schema.arrayOf(schema.number()),
byhour: schema.arrayOf(schema.number()),
byminute: schema.arrayOf(schema.number()),
bysecond: schema.arrayOf(schema.number()),
});
const snoozeScheduleSchema = schema.object({
duration: schema.number(),
rRule: rRuleSchema,
id: schema.maybe(schema.string()),
skipRecurrences: schema.maybe(schema.arrayOf(schema.string())),
});
export const ruleResponseSchema = schema.object({
id: schema.string(),
enabled: schema.boolean(),
name: schema.string(),
tags: schema.arrayOf(schema.string()),
rule_type_id: schema.string(),
consumer: schema.string(),
schedule: intervalScheduleSchema,
actions: schema.arrayOf(actionSchema),
params: ruleParamsSchema,
mapped_params: schema.maybe(mappedParamsSchema),
scheduled_task_id: schema.maybe(schema.string()),
created_by: schema.nullable(schema.string()),
updated_by: schema.nullable(schema.string()),
created_at: schema.string(),
updated_at: schema.string(),
api_key_owner: schema.nullable(schema.string()),
api_key_created_by_user: schema.maybe(schema.nullable(schema.boolean())),
throttle: schema.maybe(schema.nullable(schema.string())),
mute_all: schema.boolean(),
notify_when: schema.maybe(schema.nullable(notifyWhenSchema)),
muted_alert_ids: schema.arrayOf(schema.string()),
execution_status: ruleExecutionStatusSchema,
monitoring: schema.maybe(monitoringSchema),
snooze_schedule: schema.maybe(schema.arrayOf(snoozeScheduleSchema)),
active_snoozes: schema.maybe(schema.arrayOf(schema.string())),
is_snoozed_until: schema.maybe(schema.nullable(schema.string())),
last_run: schema.maybe(schema.nullable(ruleLastRunSchema)),
next_run: schema.maybe(schema.nullable(schema.string())),
revision: schema.number(),
running: schema.maybe(schema.nullable(schema.boolean())),
view_in_app_relative_url: schema.maybe(schema.nullable(schema.string())),
});

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,46 @@
/*
* 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 type { TypeOf } from '@kbn/config-schema';
import { ruleParamsSchemaV1, ruleResponseSchemaV1 } from '..';
export type RuleParams = TypeOf<typeof ruleParamsSchemaV1>;
type RuleResponseSchemaType = TypeOf<typeof ruleResponseSchemaV1>;
export interface RuleResponse<Params extends RuleParams = never> {
id: RuleResponseSchemaType['id'];
enabled: RuleResponseSchemaType['enabled'];
name: RuleResponseSchemaType['name'];
tags: RuleResponseSchemaType['tags'];
rule_type_id: RuleResponseSchemaType['rule_type_id'];
consumer: RuleResponseSchemaType['consumer'];
schedule: RuleResponseSchemaType['schedule'];
actions: RuleResponseSchemaType['actions'];
params: Params;
mapped_params?: RuleResponseSchemaType['mapped_params'];
scheduled_task_id?: RuleResponseSchemaType['scheduled_task_id'];
created_by: RuleResponseSchemaType['created_by'];
updated_by: RuleResponseSchemaType['updated_by'];
created_at: RuleResponseSchemaType['created_at'];
updated_at: RuleResponseSchemaType['updated_at'];
api_key_owner: RuleResponseSchemaType['api_key_owner'];
api_key_created_by_user?: RuleResponseSchemaType['api_key_created_by_user'];
throttle?: RuleResponseSchemaType['throttle'];
mute_all: RuleResponseSchemaType['mute_all'];
notify_when?: RuleResponseSchemaType['notify_when'];
muted_alert_ids: RuleResponseSchemaType['muted_alert_ids'];
execution_status: RuleResponseSchemaType['execution_status'];
monitoring?: RuleResponseSchemaType['monitoring'];
snooze_schedule?: RuleResponseSchemaType['snooze_schedule'];
active_snoozes?: RuleResponseSchemaType['active_snoozes'];
is_snoozed_until?: RuleResponseSchemaType['is_snoozed_until'];
last_run?: RuleResponseSchemaType['last_run'];
next_run?: RuleResponseSchemaType['next_run'];
revision: RuleResponseSchemaType['revision'];
running?: RuleResponseSchemaType['running'];
view_in_app_relative_url?: RuleResponseSchemaType['view_in_app_relative_url'];
}

View file

@ -0,0 +1,16 @@
/*
* 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.
*/
export { validateDuration } from './validate_duration/latest';
export { validateHours } from './validate_hours/latest';
export { validateNotifyWhen } from './validate_notify_when/latest';
export { validateTimezone } from './validate_timezone/latest';
export { validateDuration as validateDurationV1 } from './validate_duration/v1';
export { validateHours as validateHoursV1 } from './validate_hours/v1';
export { validateNotifyWhen as validateNotifyWhenV1 } from './validate_notify_when/v1';
export { validateTimezone as validateTimezoneV1 } from './validate_timezone/v1';

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,27 @@
/*
* 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.
*/
const SECONDS_REGEX = /^[1-9][0-9]*s$/;
const MINUTES_REGEX = /^[1-9][0-9]*m$/;
const HOURS_REGEX = /^[1-9][0-9]*h$/;
const DAYS_REGEX = /^[1-9][0-9]*d$/;
export function validateDuration(duration: string) {
if (duration.match(SECONDS_REGEX)) {
return;
}
if (duration.match(MINUTES_REGEX)) {
return;
}
if (duration.match(HOURS_REGEX)) {
return;
}
if (duration.match(DAYS_REGEX)) {
return;
}
return 'string is not a valid duration: ' + duration;
}

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
export function validateHours(time: string) {
if (/^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/.test(time)) {
return;
}
return 'string is not a valid time in HH:mm format ' + time;
}

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,15 @@
/*
* 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 { ruleNotifyWhenV1, RuleNotifyWhenV1 } from '../../rule_response';
export function validateNotifyWhen(notifyWhen: string) {
if (Object.values(ruleNotifyWhenV1).includes(notifyWhen as RuleNotifyWhenV1)) {
return;
}
return `string is not a valid RuleNotifyWhenType: ${notifyWhen}`;
}

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,15 @@
/*
* 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 moment from 'moment';
import 'moment-timezone';
export function validateTimezone(timezone: string) {
if (moment.tz.names().includes(timezone)) {
return;
}
return 'string is not a valid timezone: ' + timezone;
}

View file

@ -0,0 +1,43 @@
/*
* 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.
*/
export const ruleNotifyWhen = {
CHANGE: 'onActionGroupChange',
ACTIVE: 'onActiveAlert',
THROTTLE: 'onThrottleInterval',
} as const;
export const ruleLastRunOutcomeValues = {
SUCCEEDED: 'succeeded',
WARNING: 'warning',
FAILED: 'failed',
} as const;
export const ruleExecutionStatusValues = {
OK: 'ok',
ACTIVE: 'active',
ERROR: 'error',
WARNING: 'warning',
PENDING: 'pending',
UNKNOWN: 'unknown',
} as const;
export const ruleExecutionStatusErrorReason = {
READ: 'read',
DECRYPT: 'decrypt',
EXECUTE: 'execute',
UNKNOWN: 'unknown',
LICENSE: 'license',
TIMEOUT: 'timeout',
DISABLED: 'disabled',
VALIDATE: 'validate',
} as const;
export const ruleExecutionStatusWarningReason = {
MAX_EXECUTABLE_ACTIONS: 'maxExecutableActions',
MAX_ALERTS: 'maxAlerts',
} as const;

View file

@ -6,24 +6,25 @@
*/
import { schema } from '@kbn/config-schema';
import { CreateOptions } from '../methods/create';
import { RulesClient, ConstructorOptions } from '../rules_client';
import { CreateRuleParams } from './create_rule';
import { RulesClient, ConstructorOptions } from '../../../rules_client';
import { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
import { ruleTypeRegistryMock } from '../../rule_type_registry.mock';
import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock';
import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock';
import { alertingAuthorizationMock } from '../../../authorization/alerting_authorization.mock';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { AlertingAuthorization } from '../../../authorization/alerting_authorization';
import { ActionsAuthorization, ActionsClient } from '@kbn/actions-plugin/server';
import { RuleNotifyWhen } from '../../types';
import { ruleNotifyWhen } from '../constants';
import { TaskStatus } from '@kbn/task-manager-plugin/server';
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
import { getBeforeSetup, setGlobalDate } from './lib';
import { RecoveredActionGroup } from '../../../common';
import { getDefaultMonitoring } from '../../lib/monitoring';
import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
import { getBeforeSetup, setGlobalDate } from '../../../rules_client/tests/lib';
import { RecoveredActionGroup } from '../../../../common';
import { bulkMarkApiKeysForInvalidation } from '../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
import { getRuleExecutionStatusPending, getDefaultMonitoring } from '../../../lib';
jest.mock('../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
bulkMarkApiKeysForInvalidation: jest.fn(),
}));
@ -79,7 +80,9 @@ beforeEach(() => {
setGlobalDate();
function getMockData(overwrites: Record<string, unknown> = {}): CreateOptions<{
const now = new Date().toISOString();
function getMockData(overwrites: Record<string, unknown> = {}): CreateRuleParams<{
bar: boolean;
}>['data'] {
return {
@ -103,7 +106,6 @@ function getMockData(overwrites: Record<string, unknown> = {}): CreateOptions<{
},
},
],
running: false,
...overwrites,
};
}
@ -153,7 +155,7 @@ describe('create()', () => {
describe('authorization', () => {
function tryToExecuteOperation(
options: CreateOptions<{
options: CreateRuleParams<{
bar: boolean;
}>
): Promise<unknown> {
@ -166,6 +168,8 @@ describe('create()', () => {
params: {
bar: true,
},
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
running: false,
createdAt: '2019-02-12T21:01:22.479Z',
actions: [
{
@ -175,7 +179,7 @@ describe('create()', () => {
params: {
foo: true,
},
frequency: { summary: false, notifyWhen: RuleNotifyWhen.CHANGE, throttle: null },
frequency: { summary: false, notifyWhen: ruleNotifyWhen.CHANGE, throttle: null },
},
],
},
@ -254,7 +258,11 @@ describe('create()', () => {
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: data,
attributes: {
...data,
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
running: false,
},
references: [],
});
await rulesClient.create({ data });
@ -331,7 +339,11 @@ describe('create()', () => {
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: createdAttributes,
attributes: {
...createdAttributes,
running: false,
executionStatus: getRuleExecutionStatusPending(createdAttributes.createdAt),
},
references: [
{
name: 'action_0',
@ -345,6 +357,8 @@ describe('create()', () => {
type: 'alert',
attributes: {
...createdAttributes,
running: false,
executionStatus: getRuleExecutionStatusPending(createdAttributes.createdAt),
scheduledTaskId: 'task-123',
},
references: [
@ -379,6 +393,10 @@ describe('create()', () => {
"createdAt": 2019-02-12T21:01:22.479Z,
"createdBy": "elastic",
"enabled": true,
"executionStatus": Object {
"lastExecutionDate": 2019-02-12T21:01:22.000Z,
"status": "pending",
},
"id": "1",
"muteAll": false,
"mutedInstanceIds": Array [],
@ -425,10 +443,8 @@ describe('create()', () => {
"createdBy": "elastic",
"enabled": true,
"executionStatus": Object {
"error": null,
"lastExecutionDate": "2019-02-12T21:01:22.479Z",
"status": "pending",
"warning": null,
},
"legacyId": null,
"meta": Object {
@ -513,7 +529,7 @@ describe('create()', () => {
]
`);
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(3);
expect(unsecuredSavedObjectsClient.update.mock.calls[0]).toHaveLength(4);
expect(unsecuredSavedObjectsClient.update.mock.calls[0][0]).toEqual('alert');
expect(unsecuredSavedObjectsClient.update.mock.calls[0][1]).toEqual('1');
expect(unsecuredSavedObjectsClient.update.mock.calls[0][2]).toMatchInlineSnapshot(`
@ -554,7 +570,11 @@ describe('create()', () => {
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '123',
type: 'alert',
attributes: createdAttributes,
attributes: {
...createdAttributes,
running: false,
executionStatus: getRuleExecutionStatusPending(createdAttributes.createdAt),
},
references: [
{
name: 'action_0',
@ -614,7 +634,11 @@ describe('create()', () => {
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '123',
type: 'alert',
attributes: createdAttributes,
attributes: {
...createdAttributes,
running: false,
executionStatus: getRuleExecutionStatusPending(createdAttributes.createdAt),
},
references: [
{
name: 'action_0',
@ -647,10 +671,8 @@ describe('create()', () => {
"createdBy": "elastic",
"enabled": true,
"executionStatus": Object {
"error": null,
"lastExecutionDate": "2019-02-12T21:01:22.479Z",
"status": "pending",
"warning": null,
},
"legacyId": "123",
"meta": Object {
@ -765,6 +787,8 @@ describe('create()', () => {
id: '1',
type: 'alert',
attributes: {
running: false,
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
alertTypeId: '123',
schedule: { interval: '1m' },
params: {
@ -858,11 +882,16 @@ describe('create()', () => {
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"executionStatus": Object {
"lastExecutionDate": 2019-02-12T21:01:22.000Z,
"status": "pending",
},
"id": "1",
"notifyWhen": null,
"params": Object {
"bar": true,
},
"running": false,
"schedule": Object {
"interval": "1m",
},
@ -960,6 +989,7 @@ describe('create()', () => {
id: '1',
type: 'alert',
attributes: {
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
alertTypeId: '123',
schedule: { interval: '1m' },
params: {
@ -1049,6 +1079,10 @@ describe('create()', () => {
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"executionStatus": Object {
"lastExecutionDate": 2019-02-12T21:01:22.000Z,
"status": "pending",
},
"id": "1",
"notifyWhen": null,
"params": Object {
@ -1104,10 +1138,8 @@ describe('create()', () => {
enabled: true,
legacyId: null,
executionStatus: {
error: null,
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
warning: null,
},
monitoring: getDefaultMonitoring('2019-02-12T21:01:22.479Z'),
meta: { versionApiKeyLastmodified: kibanaVersion },
@ -1148,8 +1180,10 @@ describe('create()', () => {
params: {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
running: false,
executionStatus: getRuleExecutionStatusPending(now),
createdAt: now,
updatedAt: now,
notifyWhen: null,
actions: [
{
@ -1186,11 +1220,16 @@ describe('create()', () => {
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"enabled": false,
"executionStatus": Object {
"lastExecutionDate": 2019-02-12T21:01:22.000Z,
"status": "pending",
},
"id": "1",
"notifyWhen": null,
"params": Object {
"bar": true,
},
"running": false,
"schedule": Object {
"interval": 10000,
},
@ -1256,8 +1295,10 @@ describe('create()', () => {
bar: true,
parameterThatIsSavedObjectRef: 'soRef_0',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
running: false,
createdAt: now,
updatedAt: now,
notifyWhen: null,
actions: [
{
@ -1269,7 +1310,6 @@ describe('create()', () => {
},
},
],
running: false,
},
references: [
{
@ -1318,10 +1358,8 @@ describe('create()', () => {
enabled: true,
legacyId: null,
executionStatus: {
error: null,
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
warning: null,
},
monitoring: getDefaultMonitoring('2019-02-12T21:01:22.479Z'),
meta: { versionApiKeyLastmodified: kibanaVersion },
@ -1369,6 +1407,10 @@ describe('create()', () => {
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"executionStatus": Object {
"lastExecutionDate": 2019-02-12T21:01:22.000Z,
"status": "pending",
},
"id": "1",
"notifyWhen": null,
"params": Object {
@ -1440,8 +1482,9 @@ describe('create()', () => {
bar: true,
parameterThatIsSavedObjectRef: 'action_0',
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
executionStatus: getRuleExecutionStatusPending(now),
createdAt: now,
updatedAt: now,
notifyWhen: null,
actions: [
{
@ -1502,10 +1545,8 @@ describe('create()', () => {
createdBy: 'elastic',
enabled: true,
executionStatus: {
error: null,
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
warning: null,
},
monitoring: getDefaultMonitoring('2019-02-12T21:01:22.479Z'),
meta: { versionApiKeyLastmodified: kibanaVersion },
@ -1553,6 +1594,10 @@ describe('create()', () => {
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"executionStatus": Object {
"lastExecutionDate": 2019-02-12T21:01:22.000Z,
"status": "pending",
},
"id": "1",
"notifyWhen": null,
"params": Object {
@ -1582,8 +1627,10 @@ describe('create()', () => {
params: {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
executionStatus: getRuleExecutionStatusPending(now),
running: false,
createdAt: now,
updatedAt: now,
actions: [
{
group: 'default',
@ -1617,6 +1664,7 @@ describe('create()', () => {
params: {
bar: true,
},
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
updatedBy: 'elastic',
@ -1688,8 +1736,6 @@ describe('create()', () => {
executionStatus: {
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
error: null,
warning: null,
},
monitoring: getDefaultMonitoring('2019-02-12T21:01:22.479Z'),
revision: 0,
@ -1723,6 +1769,10 @@ describe('create()', () => {
"createdAt": 2019-02-12T21:01:22.479Z,
"createdBy": "elastic",
"enabled": true,
"executionStatus": Object {
"lastExecutionDate": 2019-02-12T21:01:22.000Z,
"status": "pending",
},
"id": "1",
"muteAll": false,
"mutedInstanceIds": Array [],
@ -1755,6 +1805,7 @@ describe('create()', () => {
params: {
bar: true,
},
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
updatedBy: 'elastic',
@ -1826,8 +1877,6 @@ describe('create()', () => {
executionStatus: {
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
error: null,
warning: null,
},
monitoring: getDefaultMonitoring('2019-02-12T21:01:22.479Z'),
revision: 0,
@ -1861,6 +1910,10 @@ describe('create()', () => {
"createdAt": 2019-02-12T21:01:22.479Z,
"createdBy": "elastic",
"enabled": true,
"executionStatus": Object {
"lastExecutionDate": 2019-02-12T21:01:22.000Z,
"status": "pending",
},
"id": "1",
"muteAll": false,
"mutedInstanceIds": Array [],
@ -1893,6 +1946,7 @@ describe('create()', () => {
params: {
bar: true,
},
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
updatedBy: 'elastic',
@ -1964,8 +2018,6 @@ describe('create()', () => {
executionStatus: {
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
error: null,
warning: null,
},
monitoring: getDefaultMonitoring('2019-02-12T21:01:22.479Z'),
revision: 0,
@ -1999,6 +2051,10 @@ describe('create()', () => {
"createdAt": 2019-02-12T21:01:22.479Z,
"createdBy": "elastic",
"enabled": true,
"executionStatus": Object {
"lastExecutionDate": 2019-02-12T21:01:22.000Z,
"status": "pending",
},
"id": "1",
"muteAll": false,
"mutedInstanceIds": Array [],
@ -2040,6 +2096,7 @@ describe('create()', () => {
risk_score: 42,
severity: 'low',
},
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
updatedBy: 'elastic',
@ -2117,8 +2174,6 @@ describe('create()', () => {
executionStatus: {
status: 'pending',
lastExecutionDate: '2019-02-12T21:01:22.479Z',
error: null,
warning: null,
},
monitoring: {
run: {
@ -2178,6 +2233,10 @@ describe('create()', () => {
"createdAt": 2019-02-12T21:01:22.479Z,
"createdBy": "elastic",
"enabled": true,
"executionStatus": Object {
"lastExecutionDate": 2019-02-12T21:01:22.000Z,
"status": "pending",
},
"id": "123",
"muteAll": false,
"mutedInstanceIds": Array [],
@ -2277,6 +2336,8 @@ describe('create()', () => {
params: {
bar: true,
},
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
running: false,
actions: [
{
group: 'default',
@ -2305,11 +2366,12 @@ describe('create()', () => {
);
expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"alert",
"1",
]
`);
Array [
"alert",
"1",
undefined,
]
`);
});
test('attempts to remove saved object if scheduling failed', async () => {
@ -2323,6 +2385,8 @@ describe('create()', () => {
params: {
bar: true,
},
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
running: false,
actions: [
{
group: 'default',
@ -2349,11 +2413,12 @@ describe('create()', () => {
);
expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.delete.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"alert",
"1",
]
`);
Array [
"alert",
"1",
undefined,
]
`);
});
test('returns task manager error if cleanup fails, logs to console', async () => {
@ -2367,6 +2432,8 @@ describe('create()', () => {
params: {
bar: true,
},
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
running: false,
actions: [
{
group: 'default',
@ -2398,7 +2465,7 @@ describe('create()', () => {
);
});
test('throws an error if alert type not registerd', async () => {
test('throws an error if alert type not registered', async () => {
const data = getMockData();
ruleTypeRegistry.get.mockImplementation(() => {
throw new Error('Invalid type');
@ -2423,6 +2490,8 @@ describe('create()', () => {
params: {
bar: true,
},
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
running: false,
actions: [
{
group: 'default',
@ -2497,8 +2566,6 @@ describe('create()', () => {
executionStatus: {
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
error: null,
warning: null,
},
monitoring: getDefaultMonitoring('2019-02-12T21:01:22.479Z'),
revision: 0,
@ -2538,6 +2605,7 @@ describe('create()', () => {
},
},
],
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
running: false,
},
references: [
@ -2604,8 +2672,6 @@ describe('create()', () => {
executionStatus: {
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
error: null,
warning: null,
},
monitoring: getDefaultMonitoring('2019-02-12T21:01:22.479Z'),
revision: 0,
@ -2703,6 +2769,7 @@ describe('create()', () => {
params: {
bar: true,
},
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
updatedBy: 'elastic',
@ -3182,8 +3249,9 @@ describe('create()', () => {
params: {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
executionStatus: getRuleExecutionStatusPending(now),
createdAt: now,
updatedAt: now,
notifyWhen: null,
actions: [
{
@ -3238,6 +3306,10 @@ describe('create()', () => {
],
"alertTypeId": "123",
"createdAt": 2019-02-12T21:01:22.479Z,
"executionStatus": Object {
"lastExecutionDate": 2019-02-12T21:01:22.000Z,
"status": "pending",
},
"id": "1",
"notifyWhen": null,
"params": Object {
@ -3376,6 +3448,8 @@ describe('create()', () => {
attributes: {
alertTypeId: '123',
schedule: { interval: '1m' },
running: false,
executionStatus: getRuleExecutionStatusPending('2019-02-12T21:01:22.479Z'),
params: {
bar: true,
},
@ -3455,8 +3529,6 @@ describe('create()', () => {
executionStatus: {
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
error: null,
warning: null,
},
monitoring: getDefaultMonitoring('2019-02-12T21:01:22.479Z'),
revision: 0,

View file

@ -0,0 +1,205 @@
/*
* 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 Semver from 'semver';
import Boom from '@hapi/boom';
import { SavedObject, SavedObjectsUtils } from '@kbn/core/server';
import { withSpan } from '@kbn/apm-utils';
import { parseDuration } from '../../../../common/parse_duration';
import { WriteOperations, AlertingAuthorizationEntity } from '../../../authorization';
import { validateRuleTypeParams, getRuleNotifyWhenType, getDefaultMonitoring } from '../../../lib';
import { getRuleExecutionStatusPending } from '../../../lib/rule_execution_status';
import {
extractReferences,
validateActions,
addGeneratedActionValues,
} from '../../../rules_client/lib';
import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../../../rules_client/common';
import { ruleAuditEvent, RuleAuditAction } from '../../../rules_client/common/audit_events';
import { RulesClientContext } from '../../../rules_client/types';
import { Rule, RuleDomain, RuleParams } from '../types';
import { SanitizedRule } from '../../../types';
import {
transformRuleAttributesToRuleDomain,
transformRuleDomainToRuleAttributes,
transformRuleDomainToRule,
} from '../transforms';
import { ruleDomainSchema } from '../schemas';
import { RuleAttributes } from '../../../data/rule/types';
import type { CreateRuleData } from './types';
import { createRuleDataSchema } from './schemas';
import { createRuleSavedObject } from '../../../rules_client/lib';
export interface CreateRuleOptions {
id?: string;
}
export interface CreateRuleParams<Params extends RuleParams = never> {
data: CreateRuleData<Params>;
options?: CreateRuleOptions;
allowMissingConnectorSecrets?: boolean;
}
export async function createRule<Params extends RuleParams = never>(
context: RulesClientContext,
createParams: CreateRuleParams<Params>
// TODO (http-versioning): This should be of type Rule, change this when all rule types are fixed
): Promise<SanitizedRule<Params>> {
const { data: initialData, options, allowMissingConnectorSecrets } = createParams;
const data = { ...initialData, actions: addGeneratedActionValues(initialData.actions) };
const id = options?.id || SavedObjectsUtils.generateId();
try {
createRuleDataSchema.validate(data);
} catch (error) {
throw Boom.badRequest(`Error validating create data - ${error.message}`);
}
try {
await withSpan({ name: 'authorization.ensureAuthorized', type: 'rules' }, () =>
context.authorization.ensureAuthorized({
ruleTypeId: data.alertTypeId,
consumer: data.consumer,
operation: WriteOperations.Create,
entity: AlertingAuthorizationEntity.Rule,
})
);
} catch (error) {
context.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.CREATE,
savedObject: { type: 'alert', id },
error,
})
);
throw error;
}
context.ruleTypeRegistry.ensureRuleTypeEnabled(data.alertTypeId);
// Throws an error if alert type isn't registered
const ruleType = context.ruleTypeRegistry.get(data.alertTypeId);
const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate.params);
const username = await context.getUserName();
let createdAPIKey = null;
let isAuthTypeApiKey = false;
try {
isAuthTypeApiKey = context.isAuthenticationTypeAPIKey();
const name = generateAPIKeyName(ruleType.id, data.name);
createdAPIKey = data.enabled
? isAuthTypeApiKey
? context.getAuthenticationAPIKey(`${name}-user-created`)
: await withSpan(
{
name: 'createAPIKey',
type: 'rules',
},
() => context.createAPIKey(name)
)
: null;
} catch (error) {
throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`);
}
await withSpan({ name: 'validateActions', type: 'rules' }, () =>
validateActions(context, ruleType, data, allowMissingConnectorSecrets)
);
// Throw error if schedule interval is less than the minimum and we are enforcing it
const intervalInMs = parseDuration(data.schedule.interval);
if (
intervalInMs < context.minimumScheduleIntervalInMs &&
context.minimumScheduleInterval.enforce
) {
throw Boom.badRequest(
`Error creating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}`
);
}
// Extract saved object references for this rule
const {
references,
params: updatedParams,
actions,
} = await withSpan({ name: 'extractReferences', type: 'rules' }, () =>
extractReferences(context, ruleType, data.actions, validatedAlertTypeParams)
);
const createTime = Date.now();
const lastRunTimestamp = new Date();
const legacyId = Semver.lt(context.kibanaVersion, '8.0.0') ? id : null;
const notifyWhen = getRuleNotifyWhenType(data.notifyWhen ?? null, data.throttle ?? null);
const throttle = data.throttle ?? null;
// Convert domain rule object to ES rule attributes
const ruleAttributes = transformRuleDomainToRuleAttributes(
{
...data,
...apiKeyAsAlertAttributes(createdAPIKey, username, isAuthTypeApiKey),
id,
createdBy: username,
updatedBy: username,
createdAt: new Date(createTime),
updatedAt: new Date(createTime),
snoozeSchedule: [],
muteAll: false,
mutedInstanceIds: [],
notifyWhen,
throttle,
executionStatus: getRuleExecutionStatusPending(lastRunTimestamp.toISOString()),
monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()) as Rule['monitoring'],
revision: 0,
running: false,
},
{
legacyId,
actionsWithRefs: actions as RuleAttributes['actions'],
paramsWithRefs: updatedParams,
}
);
const createdRuleSavedObject: SavedObject<RuleAttributes> = await withSpan(
{ name: 'createRuleSavedObject', type: 'rules' },
() =>
createRuleSavedObject(context, {
intervalInMs,
rawRule: ruleAttributes,
references,
ruleId: id,
options,
returnRuleAttributes: true,
})
);
// Convert ES RuleAttributes back to domain rule object
const ruleDomain: RuleDomain<Params> = transformRuleAttributesToRuleDomain<Params>(
createdRuleSavedObject.attributes,
{
id: createdRuleSavedObject.id,
logger: context.logger,
ruleType: context.ruleTypeRegistry.get(createdRuleSavedObject.attributes.alertTypeId),
references,
}
);
// Try to validate created rule, but don't throw.
try {
ruleDomainSchema.validate(ruleDomain);
} catch (e) {
context.logger.warn(`Error validating rule domain object for id: ${id}, ${e}`);
}
// Convert domain rule to rule (Remove certain properties)
const rule = transformRuleDomainToRule<Params>(ruleDomain, { isPublic: true });
// TODO (http-versioning): Remove this cast, this enables us to move forward
// without fixing all of other solution types
return rule as SanitizedRule<Params>;
}

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export type { CreateRuleOptions, CreateRuleParams } from './create_rule';
export type { CreateRuleData } from './types';
export { createRule } from './create_rule';

View file

@ -0,0 +1,42 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { validateDuration } from '../../../../../common/routes/rule/validation';
import { notifyWhenSchema, actionAlertsFilterSchema } from '../../schemas';
export const createRuleDataSchema = schema.object({
name: schema.string(),
alertTypeId: schema.string(),
enabled: schema.boolean({ defaultValue: true }),
consumer: schema.string(),
tags: schema.arrayOf(schema.string(), { defaultValue: [] }),
throttle: schema.maybe(schema.nullable(schema.string({ validate: validateDuration }))),
params: schema.recordOf(schema.string(), schema.maybe(schema.any()), { defaultValue: {} }),
schedule: schema.object({
interval: schema.string({ validate: validateDuration }),
}),
actions: schema.arrayOf(
schema.object({
group: schema.string(),
id: schema.string(),
actionTypeId: schema.maybe(schema.string()),
params: schema.recordOf(schema.string(), schema.maybe(schema.any()), { defaultValue: {} }),
frequency: schema.maybe(
schema.object({
summary: schema.boolean(),
notifyWhen: notifyWhenSchema,
throttle: schema.nullable(schema.string({ validate: validateDuration })),
})
),
uuid: schema.maybe(schema.string()),
alertsFilter: schema.maybe(actionAlertsFilterSchema),
}),
{ defaultValue: [] }
),
notifyWhen: schema.maybe(schema.nullable(notifyWhenSchema)),
});

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { createRuleDataSchema } from './create_rule_data_schema';

View file

@ -0,0 +1,25 @@
/*
* 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 { TypeOf } from '@kbn/config-schema';
import { createRuleDataSchema } from '../schemas';
import { RuleParams } from '../../types';
type CreateRuleDataType = TypeOf<typeof createRuleDataSchema>;
export interface CreateRuleData<Params extends RuleParams = never> {
name: CreateRuleDataType['name'];
alertTypeId: CreateRuleDataType['alertTypeId'];
enabled: CreateRuleDataType['enabled'];
consumer: CreateRuleDataType['consumer'];
tags: CreateRuleDataType['tags'];
throttle?: CreateRuleDataType['throttle'];
params: Params;
schedule: CreateRuleDataType['schedule'];
actions: CreateRuleDataType['actions'];
notifyWhen?: CreateRuleDataType['notifyWhen'];
}

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export type { CreateRuleData } from './create_rule_data';

View file

@ -0,0 +1,92 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { notifyWhenSchema } from './notify_when_schema';
export const actionParamsSchema = schema.recordOf(schema.string(), schema.maybe(schema.any()));
const actionAlertsFilterQueryFiltersSchema = schema.arrayOf(
schema.object({
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
meta: schema.recordOf(schema.string(), schema.any()),
state$: schema.maybe(schema.object({ store: schema.string() })),
})
);
const actionDomainAlertsFilterQuerySchema = schema.object({
kql: schema.string(),
filters: actionAlertsFilterQueryFiltersSchema,
dsl: schema.maybe(schema.string()),
});
const actionAlertsFilterTimeFrameSchema = schema.object({
days: schema.arrayOf(
schema.oneOf([
schema.literal(1),
schema.literal(2),
schema.literal(3),
schema.literal(4),
schema.literal(5),
schema.literal(6),
schema.literal(7),
])
),
hours: schema.object({
start: schema.string(),
end: schema.string(),
}),
timezone: schema.string(),
});
const actionDomainAlertsFilterSchema = schema.object({
query: schema.maybe(actionDomainAlertsFilterQuerySchema),
timeframe: schema.maybe(actionAlertsFilterTimeFrameSchema),
});
const actionFrequencySchema = schema.object({
summary: schema.boolean(),
notifyWhen: notifyWhenSchema,
throttle: schema.nullable(schema.string()),
});
/**
* Unsanitized (domain) action schema, used by internal rules clients
*/
export const actionDomainSchema = schema.object({
uuid: schema.maybe(schema.string()),
group: schema.string(),
id: schema.string(),
actionTypeId: schema.string(),
params: actionParamsSchema,
frequency: schema.maybe(actionFrequencySchema),
alertsFilter: schema.maybe(actionDomainAlertsFilterSchema),
});
/**
* Sanitized (non-domain) action schema, returned by rules clients for other solutions
*/
const actionAlertsFilterQuerySchema = schema.object({
kql: schema.string(),
filters: actionAlertsFilterQueryFiltersSchema,
dsl: schema.maybe(schema.string()),
});
export const actionAlertsFilterSchema = schema.object({
query: schema.maybe(actionAlertsFilterQuerySchema),
timeframe: schema.maybe(actionAlertsFilterTimeFrameSchema),
});
export const actionSchema = schema.object({
uuid: schema.maybe(schema.string()),
group: schema.string(),
id: schema.string(),
actionTypeId: schema.string(),
params: actionParamsSchema,
frequency: schema.maybe(actionFrequencySchema),
alertsFilter: schema.maybe(actionAlertsFilterSchema),
});

View file

@ -0,0 +1,17 @@
/*
* 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 moment from 'moment';
import { schema } from '@kbn/config-schema';
const validateDate = (string: Date) => {
if (moment(string).isValid()) {
return;
}
return `string is not a valid date: ${string}`;
};
export const dateSchema = schema.any({ validate: validateDate });

View file

@ -0,0 +1,26 @@
/*
* 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.
*/
export {
ruleParamsSchema,
rRuleSchema,
snoozeScheduleSchema,
ruleExecutionStatusSchema,
ruleLastRunSchema,
monitoringSchema,
ruleSchema,
ruleDomainSchema,
} from './rule_schemas';
export {
actionParamsSchema,
actionDomainSchema,
actionSchema,
actionAlertsFilterSchema,
} from './action_schemas';
export { notifyWhenSchema } from './notify_when_schema';

View file

@ -0,0 +1,15 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { ruleNotifyWhen } from '../constants';
export const notifyWhenSchema = schema.oneOf([
schema.literal(ruleNotifyWhen.CHANGE),
schema.literal(ruleNotifyWhen.ACTIVE),
schema.literal(ruleNotifyWhen.THROTTLE),
]);

View file

@ -0,0 +1,244 @@
/*
* 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 { schema } from '@kbn/config-schema';
import {
ruleLastRunOutcomeValues,
ruleExecutionStatusValues,
ruleExecutionStatusErrorReason,
ruleExecutionStatusWarningReason,
} from '../constants';
import { dateSchema } from './date_schema';
import { notifyWhenSchema } from './notify_when_schema';
import { actionDomainSchema, actionSchema } from './action_schemas';
export const ruleParamsSchema = schema.recordOf(schema.string(), schema.maybe(schema.any()));
export const mappedParamsSchema = schema.recordOf(schema.string(), schema.maybe(schema.any()));
export const intervalScheduleSchema = schema.object({
interval: schema.string(),
});
export const ruleExecutionStatusSchema = schema.object({
status: schema.oneOf([
schema.literal(ruleExecutionStatusValues.OK),
schema.literal(ruleExecutionStatusValues.ACTIVE),
schema.literal(ruleExecutionStatusValues.ERROR),
schema.literal(ruleExecutionStatusValues.WARNING),
schema.literal(ruleExecutionStatusValues.PENDING),
schema.literal(ruleExecutionStatusValues.UNKNOWN),
]),
lastExecutionDate: dateSchema,
lastDuration: schema.maybe(schema.number()),
error: schema.maybe(
schema.object({
reason: schema.oneOf([
schema.literal(ruleExecutionStatusErrorReason.READ),
schema.literal(ruleExecutionStatusErrorReason.DECRYPT),
schema.literal(ruleExecutionStatusErrorReason.EXECUTE),
schema.literal(ruleExecutionStatusErrorReason.UNKNOWN),
schema.literal(ruleExecutionStatusErrorReason.LICENSE),
schema.literal(ruleExecutionStatusErrorReason.TIMEOUT),
schema.literal(ruleExecutionStatusErrorReason.DISABLED),
schema.literal(ruleExecutionStatusErrorReason.VALIDATE),
]),
message: schema.string(),
})
),
warning: schema.maybe(
schema.object({
reason: schema.oneOf([
schema.literal(ruleExecutionStatusWarningReason.MAX_EXECUTABLE_ACTIONS),
schema.literal(ruleExecutionStatusWarningReason.MAX_ALERTS),
]),
message: schema.string(),
})
),
});
export const ruleLastRunSchema = schema.object({
outcome: schema.oneOf([
schema.literal(ruleLastRunOutcomeValues.SUCCEEDED),
schema.literal(ruleLastRunOutcomeValues.WARNING),
schema.literal(ruleLastRunOutcomeValues.FAILED),
]),
outcomeOrder: schema.maybe(schema.number()),
warning: schema.maybe(
schema.nullable(
schema.oneOf([
schema.literal(ruleExecutionStatusErrorReason.READ),
schema.literal(ruleExecutionStatusErrorReason.DECRYPT),
schema.literal(ruleExecutionStatusErrorReason.EXECUTE),
schema.literal(ruleExecutionStatusErrorReason.UNKNOWN),
schema.literal(ruleExecutionStatusErrorReason.LICENSE),
schema.literal(ruleExecutionStatusErrorReason.TIMEOUT),
schema.literal(ruleExecutionStatusErrorReason.DISABLED),
schema.literal(ruleExecutionStatusErrorReason.VALIDATE),
schema.literal(ruleExecutionStatusWarningReason.MAX_EXECUTABLE_ACTIONS),
schema.literal(ruleExecutionStatusWarningReason.MAX_ALERTS),
])
)
),
outcomeMsg: schema.maybe(schema.nullable(schema.arrayOf(schema.string()))),
alertsCount: schema.object({
active: schema.maybe(schema.nullable(schema.number())),
new: schema.maybe(schema.nullable(schema.number())),
recovered: schema.maybe(schema.nullable(schema.number())),
ignored: schema.maybe(schema.nullable(schema.number())),
}),
});
export const monitoringSchema = schema.object({
run: schema.object({
history: schema.arrayOf(
schema.object({
success: schema.boolean(),
timestamp: schema.number(),
duration: schema.maybe(schema.number()),
outcome: schema.maybe(ruleLastRunSchema),
})
),
calculated_metrics: schema.object({
p50: schema.maybe(schema.number()),
p95: schema.maybe(schema.number()),
p99: schema.maybe(schema.number()),
success_ratio: schema.number(),
}),
last_run: schema.object({
timestamp: schema.string(),
metrics: schema.object({
duration: schema.maybe(schema.number()),
total_search_duration_ms: schema.maybe(schema.nullable(schema.number())),
total_indexing_duration_ms: schema.maybe(schema.nullable(schema.number())),
total_alerts_detected: schema.maybe(schema.nullable(schema.number())),
total_alerts_created: schema.maybe(schema.nullable(schema.number())),
gap_duration_s: schema.maybe(schema.nullable(schema.number())),
}),
}),
}),
});
export const rRuleSchema = schema.object({
dtstart: schema.string(),
tzid: schema.string(),
freq: schema.maybe(
schema.oneOf([
schema.literal(0),
schema.literal(1),
schema.literal(2),
schema.literal(3),
schema.literal(4),
schema.literal(5),
schema.literal(6),
])
),
until: schema.maybe(schema.string()),
count: schema.maybe(schema.number()),
interval: schema.maybe(schema.number()),
wkst: schema.maybe(
schema.oneOf([
schema.literal('MO'),
schema.literal('TU'),
schema.literal('WE'),
schema.literal('TH'),
schema.literal('FR'),
schema.literal('SA'),
schema.literal('SU'),
])
),
byweekday: schema.maybe(schema.arrayOf(schema.oneOf([schema.string(), schema.number()]))),
bymonth: schema.maybe(schema.arrayOf(schema.number())),
bysetpos: schema.maybe(schema.arrayOf(schema.number())),
bymonthday: schema.arrayOf(schema.number()),
byyearday: schema.arrayOf(schema.number()),
byweekno: schema.arrayOf(schema.number()),
byhour: schema.arrayOf(schema.number()),
byminute: schema.arrayOf(schema.number()),
bysecond: schema.arrayOf(schema.number()),
});
export const snoozeScheduleSchema = schema.object({
duration: schema.number(),
rRule: rRuleSchema,
id: schema.maybe(schema.string()),
skipRecurrences: schema.maybe(schema.arrayOf(schema.string())),
});
/**
* Unsanitized (domain) rule schema, used by internal rules clients
*/
export const ruleDomainSchema = schema.object({
id: schema.string(),
enabled: schema.boolean(),
name: schema.string(),
tags: schema.arrayOf(schema.string()),
alertTypeId: schema.string(),
consumer: schema.string(),
schedule: intervalScheduleSchema,
actions: schema.arrayOf(actionDomainSchema),
params: ruleParamsSchema,
mapped_params: schema.maybe(mappedParamsSchema),
scheduledTaskId: schema.maybe(schema.string()),
createdBy: schema.nullable(schema.string()),
updatedBy: schema.nullable(schema.string()),
createdAt: dateSchema,
updatedAt: dateSchema,
apiKey: schema.nullable(schema.string()),
apiKeyOwner: schema.nullable(schema.string()),
apiKeyCreatedByUser: schema.maybe(schema.nullable(schema.boolean())),
throttle: schema.maybe(schema.nullable(schema.string())),
muteAll: schema.boolean(),
notifyWhen: schema.maybe(schema.nullable(notifyWhenSchema)),
mutedInstanceIds: schema.arrayOf(schema.string()),
executionStatus: ruleExecutionStatusSchema,
monitoring: schema.maybe(monitoringSchema),
snoozeSchedule: schema.maybe(schema.arrayOf(snoozeScheduleSchema)),
activeSnoozes: schema.maybe(schema.arrayOf(schema.string())),
isSnoozedUntil: schema.maybe(schema.nullable(dateSchema)),
lastRun: schema.maybe(schema.nullable(ruleLastRunSchema)),
nextRun: schema.maybe(schema.nullable(dateSchema)),
revision: schema.number(),
running: schema.maybe(schema.nullable(schema.boolean())),
viewInAppRelativeUrl: schema.maybe(schema.nullable(schema.string())),
});
/**
* Sanitized (non-domain) rule schema, returned by rules clients for other solutions
*/
export const ruleSchema = schema.object({
id: schema.string(),
enabled: schema.boolean(),
name: schema.string(),
tags: schema.arrayOf(schema.string()),
alertTypeId: schema.string(),
consumer: schema.string(),
schedule: intervalScheduleSchema,
actions: schema.arrayOf(actionSchema),
params: ruleParamsSchema,
mapped_params: schema.maybe(mappedParamsSchema),
scheduledTaskId: schema.maybe(schema.string()),
createdBy: schema.nullable(schema.string()),
updatedBy: schema.nullable(schema.string()),
createdAt: dateSchema,
updatedAt: dateSchema,
apiKeyOwner: schema.nullable(schema.string()),
apiKeyCreatedByUser: schema.maybe(schema.nullable(schema.boolean())),
throttle: schema.maybe(schema.nullable(schema.string())),
muteAll: schema.boolean(),
notifyWhen: schema.maybe(schema.nullable(notifyWhenSchema)),
mutedInstanceIds: schema.arrayOf(schema.string()),
executionStatus: ruleExecutionStatusSchema,
monitoring: schema.maybe(monitoringSchema),
snoozeSchedule: schema.maybe(schema.arrayOf(snoozeScheduleSchema)),
activeSnoozes: schema.maybe(schema.arrayOf(schema.string())),
isSnoozedUntil: schema.maybe(schema.nullable(dateSchema)),
lastRun: schema.maybe(schema.nullable(ruleLastRunSchema)),
nextRun: schema.maybe(schema.nullable(dateSchema)),
revision: schema.number(),
running: schema.maybe(schema.nullable(schema.boolean())),
viewInAppRelativeUrl: schema.maybe(schema.nullable(schema.string())),
});

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export { transformRuleAttributesToRuleDomain } from './transform_rule_attributes_to_rule_domain';
export { transformRuleDomainToRuleAttributes } from './transform_rule_domain_to_rule_attributes';
export { transformRuleDomainToRule } from './transform_rule_domain_to_rule';

View file

@ -0,0 +1,238 @@
/*
* 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 { omit, isEmpty } from 'lodash';
import { Logger } from '@kbn/core/server';
import { SavedObjectReference } from '@kbn/core/server';
import { ruleExecutionStatusValues } from '../constants';
import { getRuleSnoozeEndTime } from '../../../lib';
import { RuleDomain, Monitoring, RuleParams } from '../types';
import { RuleAttributes } from '../../../data/rule/types';
import { RawRule, PartialRule } from '../../../types';
import { UntypedNormalizedRuleType } from '../../../rule_type_registry';
import {
injectReferencesIntoActions,
injectReferencesIntoParams,
} from '../../../rules_client/common';
import { getActiveScheduledSnoozes } from '../../../lib/is_rule_snoozed';
const INITIAL_LAST_RUN_METRICS = {
duration: 0,
total_search_duration_ms: null,
total_indexing_duration_ms: null,
total_alerts_detected: null,
total_alerts_created: null,
gap_duration_s: null,
};
const transformEsExecutionStatus = (
logger: Logger,
ruleId: string,
esRuleExecutionStatus: RuleAttributes['executionStatus']
): RuleDomain['executionStatus'] => {
const {
lastExecutionDate,
lastDuration,
status = ruleExecutionStatusValues.UNKNOWN,
error,
warning,
} = esRuleExecutionStatus;
let parsedDateMillis = lastExecutionDate ? Date.parse(lastExecutionDate) : Date.now();
if (isNaN(parsedDateMillis)) {
logger.debug(
`invalid ruleExecutionStatus lastExecutionDate "${lastExecutionDate}" in raw rule ${ruleId}`
);
parsedDateMillis = Date.now();
}
return {
status,
lastExecutionDate: new Date(parsedDateMillis),
...(lastDuration != null ? { lastDuration } : {}),
...(error ? { error } : {}),
...(warning ? { warning } : {}),
};
};
export const updateMonitoring = ({
monitoring,
timestamp,
duration,
}: {
monitoring: Monitoring;
timestamp: string;
duration?: number;
}): Monitoring => {
const { run, ...restMonitoring } = monitoring;
const { last_run: lastRun, ...restRun } = run;
const { metrics = INITIAL_LAST_RUN_METRICS } = lastRun;
return {
...restMonitoring,
run: {
...restRun,
last_run: {
timestamp,
metrics: {
...metrics,
duration,
},
},
},
};
};
const transformEsMonitoring = (
logger: Logger,
ruleId: string,
monitoring?: RuleAttributes['monitoring']
): Monitoring | undefined => {
if (!monitoring) {
return undefined;
}
const lastRunDate = monitoring.run.last_run.timestamp;
let parsedDateMillis = lastRunDate ? Date.parse(lastRunDate) : Date.now();
if (isNaN(parsedDateMillis)) {
logger.debug(`invalid monitoring last_run.timestamp "${lastRunDate}" in raw rule ${ruleId}`);
parsedDateMillis = Date.now();
}
return updateMonitoring({
monitoring,
timestamp: new Date(parsedDateMillis).toISOString(),
duration: monitoring.run.last_run.metrics.duration,
});
};
interface TransformEsToRuleParams {
id: RuleDomain['id'];
logger: Logger;
ruleType: UntypedNormalizedRuleType;
references?: SavedObjectReference[];
includeSnoozeData?: boolean;
omitGeneratedValues?: boolean;
}
export const transformRuleAttributesToRuleDomain = <Params extends RuleParams = never>(
esRule: RuleAttributes,
transformParams: TransformEsToRuleParams
): RuleDomain<Params> => {
const { scheduledTaskId, executionStatus, monitoring, snoozeSchedule, lastRun } = esRule;
const {
id,
logger,
ruleType,
references,
includeSnoozeData = false,
omitGeneratedValues = true,
} = transformParams;
const snoozeScheduleDates = snoozeSchedule?.map((s) => ({
...s,
rRule: {
...s.rRule,
dtstart: new Date(s.rRule.dtstart).toISOString(),
...(s.rRule.until ? { until: new Date(s.rRule.until).toISOString() } : {}),
},
}));
const includeSnoozeSchedule = snoozeSchedule !== undefined && !isEmpty(snoozeSchedule);
const isSnoozedUntil = includeSnoozeSchedule
? getRuleSnoozeEndTime({
muteAll: esRule.muteAll ?? false,
snoozeSchedule,
})?.toISOString()
: null;
let actions = esRule.actions
? injectReferencesIntoActions(id, esRule.actions as RawRule['actions'], references || [])
: [];
if (omitGeneratedValues) {
actions = actions.map((ruleAction) => omit(ruleAction, 'alertsFilter.query.dsl'));
}
const params = injectReferencesIntoParams<Params, RuleParams>(
id,
ruleType,
esRule.params,
references || []
);
const activeSnoozes = getActiveScheduledSnoozes({
snoozeSchedule,
muteAll: esRule.muteAll ?? false,
})?.map((s) => s.id);
const rule = {
id,
enabled: esRule.enabled,
name: esRule.name,
tags: esRule.tags,
alertTypeId: esRule.alertTypeId,
consumer: esRule.consumer,
schedule: esRule.schedule,
actions: actions as RuleDomain['actions'],
params,
mapped_params: esRule.mapped_params,
...(scheduledTaskId ? { scheduledTaskId } : {}),
createdBy: esRule.createdBy,
updatedBy: esRule.updatedBy,
createdAt: new Date(esRule.createdAt),
updatedAt: new Date(esRule.updatedAt),
apiKey: esRule.apiKey,
apiKeyOwner: esRule.apiKeyOwner,
apiKeyCreatedByUser: esRule.apiKeyCreatedByUser,
throttle: esRule.throttle,
muteAll: esRule.muteAll,
notifyWhen: esRule.notifyWhen,
mutedInstanceIds: esRule.mutedInstanceIds,
executionStatus: transformEsExecutionStatus(logger, id, executionStatus),
...(monitoring ? { monitoring: transformEsMonitoring(logger, id, monitoring) } : {}),
snoozeSchedule: snoozeScheduleDates ?? [],
...(includeSnoozeData
? {
activeSnoozes,
...(isSnoozedUntil !== undefined
? { isSnoozedUntil: isSnoozedUntil ? new Date(isSnoozedUntil) : null }
: {}),
}
: {}),
...(lastRun
? {
lastRun: {
...lastRun,
...(lastRun.outcomeMsg && !Array.isArray(lastRun.outcomeMsg)
? { outcomeMsg: lastRun.outcomeMsg ? [lastRun.outcomeMsg] : null }
: { outcomeMsg: lastRun.outcomeMsg }),
},
}
: {}),
...(esRule.nextRun ? { nextRun: new Date(esRule.nextRun) } : {}),
revision: esRule.revision,
running: esRule.running,
};
// Bad casts, but will fix once we fix all rule types
const viewInAppRelativeUrl =
ruleType.getViewInAppRelativeUrl &&
ruleType.getViewInAppRelativeUrl({ rule: rule as unknown as PartialRule<Params> });
if (viewInAppRelativeUrl) {
(rule as unknown as PartialRule<Params>).viewInAppRelativeUrl = viewInAppRelativeUrl;
}
// Remove all undefined keys to clean up the object
type RuleKeys = keyof Omit<RuleDomain, 'viewInAppRelativeUrl'>;
for (const key in rule) {
if (rule[key as RuleKeys] === undefined) {
delete rule[key as RuleKeys];
}
}
return rule;
};

View file

@ -0,0 +1,70 @@
/*
* 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 { RuleDomain, Rule, RuleParams } from '../types';
interface TransformRuleDomainToRuleOptions {
isPublic?: boolean;
}
export const transformRuleDomainToRule = <Params extends RuleParams = never>(
ruleDomain: RuleDomain<Params>,
options?: TransformRuleDomainToRuleOptions
): Rule<Params> => {
const { isPublic = false } = options || {};
const rule: Rule<Params> = {
id: ruleDomain.id,
enabled: ruleDomain.enabled,
name: ruleDomain.name,
tags: ruleDomain.tags,
alertTypeId: ruleDomain.alertTypeId,
consumer: ruleDomain.consumer,
schedule: ruleDomain.schedule,
actions: ruleDomain.actions,
params: ruleDomain.params,
mapped_params: ruleDomain.mapped_params,
scheduledTaskId: ruleDomain.scheduledTaskId,
createdBy: ruleDomain.createdBy,
updatedBy: ruleDomain.updatedBy,
createdAt: ruleDomain.createdAt,
updatedAt: ruleDomain.updatedAt,
apiKeyOwner: ruleDomain.apiKeyOwner,
apiKeyCreatedByUser: ruleDomain.apiKeyCreatedByUser,
throttle: ruleDomain.throttle,
muteAll: ruleDomain.muteAll,
notifyWhen: ruleDomain.notifyWhen,
mutedInstanceIds: ruleDomain.mutedInstanceIds,
executionStatus: ruleDomain.executionStatus,
monitoring: ruleDomain.monitoring,
snoozeSchedule: ruleDomain.snoozeSchedule,
activeSnoozes: ruleDomain.activeSnoozes,
isSnoozedUntil: ruleDomain.isSnoozedUntil,
lastRun: ruleDomain.lastRun,
nextRun: ruleDomain.nextRun,
revision: ruleDomain.revision,
running: ruleDomain.running,
viewInAppRelativeUrl: ruleDomain.viewInAppRelativeUrl,
};
if (isPublic) {
delete rule.snoozeSchedule;
delete rule.activeSnoozes;
delete rule.isSnoozedUntil;
delete rule.monitoring;
delete rule.viewInAppRelativeUrl;
}
// Remove all undefined keys to clean up the object
type RuleKeys = keyof Rule;
for (const key in rule) {
if (rule[key as RuleKeys] === undefined) {
delete rule[key as RuleKeys];
}
}
return rule;
};

View file

@ -0,0 +1,72 @@
/*
* 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 { RuleDomain } from '../types';
import { RuleAttributes } from '../../../data/rule/types';
import { getMappedParams } from '../../../rules_client/common';
interface TransformRuleToEsParams {
legacyId: RuleAttributes['legacyId'];
actionsWithRefs: RuleAttributes['actions'];
paramsWithRefs: RuleAttributes['params'];
meta?: RuleAttributes['meta'];
}
export const transformRuleDomainToRuleAttributes = (
rule: Omit<RuleDomain, 'actions' | 'params'>,
params: TransformRuleToEsParams
): RuleAttributes => {
const { legacyId, actionsWithRefs, paramsWithRefs, meta } = params;
const mappedParams = getMappedParams(paramsWithRefs);
return {
name: rule.name,
tags: rule.tags,
enabled: rule.enabled,
alertTypeId: rule.alertTypeId,
consumer: rule.consumer,
legacyId,
schedule: rule.schedule,
actions: actionsWithRefs,
params: paramsWithRefs,
...(Object.keys(mappedParams).length ? { mapped_params: mappedParams } : {}),
...(rule.scheduledTaskId !== undefined ? { scheduledTaskId: rule.scheduledTaskId } : {}),
createdBy: rule.createdBy,
updatedBy: rule.updatedBy,
createdAt: rule.createdAt.toISOString(),
updatedAt: rule.updatedAt.toISOString(),
apiKey: rule.apiKey,
apiKeyOwner: rule.apiKeyOwner,
...(rule.apiKeyCreatedByUser !== undefined
? { apiKeyCreatedByUser: rule.apiKeyCreatedByUser }
: {}),
...(rule.throttle !== undefined ? { throttle: rule.throttle } : {}),
...(rule.notifyWhen !== undefined ? { notifyWhen: rule.notifyWhen } : {}),
muteAll: rule.muteAll,
mutedInstanceIds: rule.mutedInstanceIds,
...(meta ? { meta } : {}),
executionStatus: {
status: rule.executionStatus.status,
lastExecutionDate: rule.executionStatus.lastExecutionDate.toISOString(),
...(rule.executionStatus.lastDuration
? { lastDuration: rule.executionStatus.lastDuration }
: {}),
...(rule.executionStatus.error !== undefined ? { error: rule.executionStatus.error } : {}),
...(rule.executionStatus.warning !== undefined
? { warning: rule.executionStatus.warning }
: {}),
},
...(rule.monitoring ? { monitoring: rule.monitoring } : {}),
...(rule.snoozeSchedule ? { snoozeSchedule: rule.snoozeSchedule } : {}),
...(rule.isSnoozedUntil !== undefined
? { isSnoozedUntil: rule.isSnoozedUntil?.toISOString() || null }
: {}),
...(rule.lastRun !== undefined ? { lastRun: rule.lastRun } : {}),
...(rule.nextRun !== undefined ? { nextRun: rule.nextRun?.toISOString() || null } : {}),
revision: rule.revision,
...(rule.running !== undefined ? { running: rule.running } : {}),
};
};

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export type { Rule, RuleDomain, RuleLastRun, Monitoring, RuleParams, RuleNotifyWhen } from './rule';

View file

@ -0,0 +1,123 @@
/*
* 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 { TypeOf } from '@kbn/config-schema';
import {
ruleNotifyWhen,
ruleLastRunOutcomeValues,
ruleExecutionStatusValues,
ruleExecutionStatusErrorReason,
ruleExecutionStatusWarningReason,
} from '../constants';
import {
ruleParamsSchema,
rRuleSchema,
snoozeScheduleSchema,
ruleExecutionStatusSchema,
ruleLastRunSchema,
monitoringSchema,
actionSchema,
ruleSchema,
ruleDomainSchema,
} from '../schemas';
export type RuleNotifyWhen = typeof ruleNotifyWhen[keyof typeof ruleNotifyWhen];
export type RuleLastRunOutcomeValues =
typeof ruleLastRunOutcomeValues[keyof typeof ruleLastRunOutcomeValues];
export type RuleExecutionStatusValues =
typeof ruleExecutionStatusValues[keyof typeof ruleExecutionStatusValues];
export type RuleExecutionStatusErrorReason =
typeof ruleExecutionStatusErrorReason[keyof typeof ruleExecutionStatusErrorReason];
export type RuleExecutionStatusWarningReason =
typeof ruleExecutionStatusWarningReason[keyof typeof ruleExecutionStatusWarningReason];
export type RuleParams = TypeOf<typeof ruleParamsSchema>;
export type RRule = TypeOf<typeof rRuleSchema>;
export type SnoozeSchedule = TypeOf<typeof snoozeScheduleSchema>;
export type RuleLastRun = TypeOf<typeof ruleLastRunSchema>;
export type Monitoring = TypeOf<typeof monitoringSchema>;
export type Action = TypeOf<typeof actionSchema>;
type RuleSchemaType = TypeOf<typeof ruleSchema>;
type RuleDomainSchemaType = TypeOf<typeof ruleDomainSchema>;
type RuleExecutionStatusWithDateString = TypeOf<typeof ruleExecutionStatusSchema>;
export interface RuleExecutionStatus {
status: RuleExecutionStatusWithDateString['status'];
lastExecutionDate: Date;
lastDuration?: RuleExecutionStatusWithDateString['lastDuration'];
error?: RuleExecutionStatusWithDateString['error'];
warning?: RuleExecutionStatusWithDateString['warning'];
}
export interface Rule<Params extends RuleParams = never> {
id: RuleSchemaType['id'];
enabled: RuleSchemaType['enabled'];
name: RuleSchemaType['name'];
tags: RuleSchemaType['tags'];
alertTypeId: RuleSchemaType['alertTypeId'];
consumer: RuleSchemaType['consumer'];
schedule: RuleSchemaType['schedule'];
actions: RuleSchemaType['actions'];
params: Params;
mapped_params?: RuleSchemaType['mapped_params'];
scheduledTaskId?: RuleSchemaType['scheduledTaskId'];
createdBy: RuleSchemaType['createdBy'];
updatedBy: RuleSchemaType['updatedBy'];
createdAt: Date;
updatedAt: Date;
apiKeyOwner: RuleSchemaType['apiKeyOwner'];
apiKeyCreatedByUser?: RuleSchemaType['apiKeyCreatedByUser'];
throttle?: RuleSchemaType['throttle'];
muteAll: RuleSchemaType['muteAll'];
notifyWhen?: RuleSchemaType['notifyWhen'];
mutedInstanceIds: RuleSchemaType['mutedInstanceIds'];
executionStatus: RuleExecutionStatus;
monitoring?: RuleSchemaType['monitoring'];
snoozeSchedule?: RuleSchemaType['snoozeSchedule'];
activeSnoozes?: RuleSchemaType['activeSnoozes'];
isSnoozedUntil?: Date | null;
lastRun?: RuleSchemaType['lastRun'];
nextRun?: Date | null;
revision: RuleSchemaType['revision'];
running?: RuleSchemaType['running'];
viewInAppRelativeUrl?: RuleSchemaType['viewInAppRelativeUrl'];
}
export interface RuleDomain<Params extends RuleParams = never> {
id: RuleDomainSchemaType['id'];
enabled: RuleDomainSchemaType['enabled'];
name: RuleDomainSchemaType['name'];
tags: RuleDomainSchemaType['tags'];
alertTypeId: RuleDomainSchemaType['alertTypeId'];
consumer: RuleDomainSchemaType['consumer'];
schedule: RuleDomainSchemaType['schedule'];
actions: RuleDomainSchemaType['actions'];
params: Params;
mapped_params?: RuleDomainSchemaType['mapped_params'];
scheduledTaskId?: RuleDomainSchemaType['scheduledTaskId'];
createdBy: RuleDomainSchemaType['createdBy'];
updatedBy: RuleDomainSchemaType['updatedBy'];
createdAt: Date;
updatedAt: Date;
apiKey: RuleDomainSchemaType['apiKey'];
apiKeyOwner: RuleDomainSchemaType['apiKeyOwner'];
apiKeyCreatedByUser?: RuleDomainSchemaType['apiKeyCreatedByUser'];
throttle?: RuleDomainSchemaType['throttle'];
muteAll: RuleDomainSchemaType['muteAll'];
notifyWhen?: RuleDomainSchemaType['notifyWhen'];
mutedInstanceIds: RuleDomainSchemaType['mutedInstanceIds'];
executionStatus: RuleExecutionStatus;
monitoring?: RuleDomainSchemaType['monitoring'];
snoozeSchedule?: RuleDomainSchemaType['snoozeSchedule'];
activeSnoozes?: RuleDomainSchemaType['activeSnoozes'];
isSnoozedUntil?: Date | null;
lastRun?: RuleDomainSchemaType['lastRun'];
nextRun?: Date | null;
revision: RuleDomainSchemaType['revision'];
running?: RuleDomainSchemaType['running'];
viewInAppRelativeUrl?: RuleDomainSchemaType['viewInAppRelativeUrl'];
}

View file

@ -0,0 +1,43 @@
/*
* 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.
*/
export const ruleNotifyWhenAttributes = {
CHANGE: 'onActionGroupChange',
ACTIVE: 'onActiveAlert',
THROTTLE: 'onThrottleInterval',
} as const;
export const ruleLastRunOutcomeValuesAttributes = {
SUCCEEDED: 'succeeded',
WARNING: 'warning',
FAILED: 'failed',
} as const;
export const ruleExecutionStatusValuesAttributes = {
OK: 'ok',
ACTIVE: 'active',
ERROR: 'error',
WARNING: 'warning',
PENDING: 'pending',
UNKNOWN: 'unknown',
} as const;
export const ruleExecutionStatusErrorReasonAttributes = {
READ: 'read',
DECRYPT: 'decrypt',
EXECUTE: 'execute',
UNKNOWN: 'unknown',
LICENSE: 'license',
TIMEOUT: 'timeout',
DISABLED: 'disabled',
VALIDATE: 'validate',
} as const;
export const ruleExecutionStatusWarningReasonAttributes = {
MAX_EXECUTABLE_ACTIONS: 'maxExecutableActions',
MAX_ALERTS: 'maxAlerts',
} as const;

View file

@ -0,0 +1,25 @@
/*
* 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 {
SavedObjectsClientContract,
SavedObjectsCreateOptions,
SavedObject,
} from '@kbn/core/server';
import { RuleAttributes } from './types';
export interface CreateRuleSoParams {
savedObjectClient: SavedObjectsClientContract;
ruleAttributes: RuleAttributes;
savedObjectCreateOptions?: SavedObjectsCreateOptions;
}
export const createRuleSo = (params: CreateRuleSoParams): Promise<SavedObject<RuleAttributes>> => {
const { savedObjectClient, ruleAttributes, savedObjectCreateOptions } = params;
return savedObjectClient.create('alert', ruleAttributes, savedObjectCreateOptions);
};

View file

@ -0,0 +1,20 @@
/*
* 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 { SavedObjectsClientContract, SavedObjectsDeleteOptions } from '@kbn/core/server';
export interface DeleteRuleSoParams {
savedObjectClient: SavedObjectsClientContract;
id: string;
savedObjectDeleteOptions?: SavedObjectsDeleteOptions;
}
export const deleteRuleSo = (params: DeleteRuleSoParams): Promise<{}> => {
const { savedObjectClient, id, savedObjectDeleteOptions } = params;
return savedObjectClient.delete('alert', id, savedObjectDeleteOptions);
};

View file

@ -0,0 +1,13 @@
/*
* 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.
*/
export { createRuleSo } from './create_rule_so';
export type { CreateRuleSoParams } from './create_rule_so';
export { updateRuleSo } from './update_rule_so';
export type { UpdateRuleSoParams } from './update_rule_so';
export { deleteRuleSo } from './delete_rule_so';
export type { DeleteRuleSoParams } from './delete_rule_so';

View file

@ -0,0 +1,24 @@
/*
* 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.
*/
export type {
RuleNotifyWhenAttributes,
RuleLastRunOutcomeValuesAttributes,
RuleExecutionStatusValuesAttributes,
RuleExecutionStatusErrorReasonAttributes,
RuleExecutionStatusWarningReasonAttributes,
RRuleAttributes,
RuleSnoozeScheduleAttributes,
RuleExecutionStatusAttributes,
RuleLastRunAttributes,
RuleMonitoringHistoryAttributes,
RuleMonitoringCalculatedMetricsAttributes,
RuleMonitoringLastRunMetricsAttributes,
RuleMonitoringLastRunAttributes,
RuleMonitoringAttributes,
RuleAttributes,
} from './rule_attributes';

View file

@ -0,0 +1,202 @@
/*
* 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 type { SavedObjectAttributes } from '@kbn/core/server';
import { Filter } from '@kbn/es-query';
import type { WeekdayStr } from '@kbn/rrule';
import { IsoWeekday } from '../../../../common';
import {
ruleNotifyWhenAttributes,
ruleLastRunOutcomeValuesAttributes,
ruleExecutionStatusValuesAttributes,
ruleExecutionStatusErrorReasonAttributes,
ruleExecutionStatusWarningReasonAttributes,
} from '../constants';
export type RuleNotifyWhenAttributes =
typeof ruleNotifyWhenAttributes[keyof typeof ruleNotifyWhenAttributes];
export type RuleLastRunOutcomeValuesAttributes =
typeof ruleLastRunOutcomeValuesAttributes[keyof typeof ruleLastRunOutcomeValuesAttributes];
export type RuleExecutionStatusValuesAttributes =
typeof ruleExecutionStatusValuesAttributes[keyof typeof ruleExecutionStatusValuesAttributes];
export type RuleExecutionStatusErrorReasonAttributes =
typeof ruleExecutionStatusErrorReasonAttributes[keyof typeof ruleExecutionStatusErrorReasonAttributes];
export type RuleExecutionStatusWarningReasonAttributes =
typeof ruleExecutionStatusWarningReasonAttributes[keyof typeof ruleExecutionStatusWarningReasonAttributes];
type RRuleFreq = 0 | 1 | 2 | 3 | 4 | 5 | 6;
export interface RRuleAttributes {
dtstart: string;
tzid: string;
freq?: RRuleFreq;
until?: string;
count?: number;
interval?: number;
wkst?: WeekdayStr;
byweekday?: Array<string | number>;
bymonth?: number[];
bysetpos?: number[];
bymonthday: number[];
byyearday: number[];
byweekno: number[];
byhour: number[];
byminute: number[];
bysecond: number[];
}
export interface RuleSnoozeScheduleAttributes {
duration: number;
rRule: RRuleAttributes;
id?: string;
skipRecurrences?: string[];
}
export interface RuleExecutionStatusAttributes {
status: RuleExecutionStatusValuesAttributes;
lastExecutionDate: string;
lastDuration?: number;
error?: {
reason: RuleExecutionStatusErrorReasonAttributes;
message: string;
} | null;
warning?: {
reason: RuleExecutionStatusWarningReasonAttributes;
message: string;
} | null;
}
export interface RuleLastRunAttributes {
outcome: RuleLastRunOutcomeValuesAttributes;
outcomeOrder?: number;
warning?:
| RuleExecutionStatusErrorReasonAttributes
| RuleExecutionStatusWarningReasonAttributes
| null;
outcomeMsg?: string[] | null;
alertsCount: {
active?: number | null;
new?: number | null;
recovered?: number | null;
ignored?: number | null;
};
}
export interface RuleMonitoringHistoryAttributes {
success: boolean;
timestamp: number;
duration?: number;
outcome?: RuleLastRunAttributes;
}
export interface RuleMonitoringCalculatedMetricsAttributes {
p50?: number;
p95?: number;
p99?: number;
success_ratio: number;
}
export interface RuleMonitoringLastRunMetricsAttributes {
duration?: number;
total_search_duration_ms?: number | null;
total_indexing_duration_ms?: number | null;
total_alerts_detected?: number | null;
total_alerts_created?: number | null;
gap_duration_s?: number | null;
}
export interface RuleMonitoringLastRunAttributes {
timestamp: string;
metrics: RuleMonitoringLastRunMetricsAttributes;
}
export interface RuleMonitoringAttributes {
run: {
history: RuleMonitoringHistoryAttributes[];
calculated_metrics: RuleMonitoringCalculatedMetricsAttributes;
last_run: RuleMonitoringLastRunAttributes;
};
}
interface IntervaleScheduleAttributes extends SavedObjectAttributes {
interval: string;
}
interface AlertsFilterTimeFrameAttributes {
days: IsoWeekday[];
timezone: string;
hours: {
start: string;
end: string;
};
}
interface AlertsFilterAttributes {
query?: {
kql: string;
filters: Filter[];
dsl: string;
};
timeframe?: AlertsFilterTimeFrameAttributes;
}
interface RuleActionAttributes {
uuid: string;
group: string;
actionRef: string;
actionTypeId: string;
params: SavedObjectAttributes;
frequency?: {
summary: boolean;
notifyWhen: RuleNotifyWhenAttributes;
throttle: string | null;
};
alertsFilter?: AlertsFilterAttributes;
}
type MappedParamsAttributes = SavedObjectAttributes & {
risk_score?: number;
severity?: string;
};
interface RuleMetaAttributes {
versionApiKeyLastmodified?: string;
}
export interface RuleAttributes {
name: string;
tags: string[];
enabled: boolean;
alertTypeId: string;
consumer: string;
legacyId: string | null;
schedule: IntervaleScheduleAttributes;
actions: RuleActionAttributes[];
params: SavedObjectAttributes;
mapped_params?: MappedParamsAttributes;
scheduledTaskId?: string | null;
createdBy: string | null;
updatedBy: string | null;
createdAt: string;
updatedAt: string;
apiKey: string | null;
apiKeyOwner: string | null;
apiKeyCreatedByUser?: boolean | null;
throttle?: string | null;
notifyWhen?: RuleNotifyWhenAttributes | null;
muteAll: boolean;
mutedInstanceIds: string[];
meta?: RuleMetaAttributes;
executionStatus: RuleExecutionStatusAttributes;
monitoring?: RuleMonitoringAttributes;
snoozeSchedule?: RuleSnoozeScheduleAttributes[];
isSnoozedUntil?: string | null;
lastRun?: RuleLastRunAttributes | null;
nextRun?: string | null;
revision: number;
running?: boolean | null;
}

View file

@ -0,0 +1,33 @@
/*
* 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 {
SavedObjectsClientContract,
SavedObjectsUpdateOptions,
SavedObjectsUpdateResponse,
} from '@kbn/core/server';
import { RuleAttributes } from './types';
export interface UpdateRuleSoParams {
savedObjectClient: SavedObjectsClientContract;
id: string;
updateRuleAttributes: Partial<RuleAttributes>;
savedObjectUpdateOptions?: SavedObjectsUpdateOptions<RuleAttributes>;
}
export const updateRuleSo = (
params: UpdateRuleSoParams
): Promise<SavedObjectsUpdateResponse<RuleAttributes>> => {
const { savedObjectClient, id, updateRuleAttributes, savedObjectUpdateOptions } = params;
return savedObjectClient.update<RuleAttributes>(
'alert',
id,
updateRuleAttributes,
savedObjectUpdateOptions
);
};

View file

@ -44,6 +44,7 @@ export type {
BulkEditOptionsFilter,
BulkEditOptionsIds,
} from './rules_client';
export type { Rule } from './application/rule/types';
export type { PublicAlert as Alert } from './alert';
export { parseDuration, isRuleSnoozed } from './lib';
export { getEsErrorMessage } from './lib/errors';

View file

@ -5,13 +5,14 @@
* 2.0.
*/
import { RuleNotifyWhenType } from '../types';
import { ruleNotifyWhen } from '../application/rule/constants';
import { RuleNotifyWhen } from '../application/rule/types';
export function getRuleNotifyWhenType(
notifyWhen: RuleNotifyWhenType | null,
notifyWhen: RuleNotifyWhen | null,
throttle: string | null
): RuleNotifyWhenType | null {
): RuleNotifyWhen | null {
// We allow notifyWhen to be null for backwards compatibility. If it is null, determine its
// value based on whether the throttle is set to a value or null
return notifyWhen ? notifyWhen! : throttle ? 'onThrottleInterval' : null;
return notifyWhen ? notifyWhen! : throttle ? ruleNotifyWhen.THROTTLE : null;
}

View file

@ -24,6 +24,8 @@ export {
executionStatusFromError,
ruleExecutionStatusToRaw,
ruleExecutionStatusFromRaw,
getRuleExecutionStatusPending,
getRuleExecutionStatusPendingAttributes,
} from './rule_execution_status';
export { lastRunFromState, lastRunFromError, lastRunToRaw } from './last_run_status';
export {

View file

@ -11,6 +11,8 @@ import {
RuleExecutionStatusValues,
RuleExecutionStatusWarningReasons,
RawRuleExecutionStatus,
RawRule,
Rule,
} from '../types';
import { getReasonFromError } from './error_with_reason';
import { getEsErrorMessage } from './errors';
@ -137,9 +139,18 @@ export function ruleExecutionStatusFromRaw(
return executionStatus;
}
export const getRuleExecutionStatusPending = (lastExecutionDate: string) => ({
status: 'pending' as RuleExecutionStatuses,
export const getRuleExecutionStatusPendingAttributes = (
lastExecutionDate: string
): RawRule['executionStatus'] => ({
status: 'pending',
lastExecutionDate,
error: null,
warning: null,
});
export const getRuleExecutionStatusPending = (
lastExecutionDate: string
): Rule['executionStatus'] => ({
status: 'pending',
lastExecutionDate: new Date(lastExecutionDate),
});

View file

@ -15,7 +15,6 @@ import { RuleTypeDisabledError } from '../lib/errors/rule_type_disabled';
import { cloneRuleRoute } from './clone_rule';
import { SanitizedRule } from '../types';
import { AsApiContract } from './lib';
import { CreateOptions } from '../rules_client';
const rulesClient = rulesClientMock.create();
jest.mock('../lib/license_api_access', () => ({
@ -68,7 +67,7 @@ describe('cloneRuleRoute', () => {
revision: 0,
};
const ruleToClone: AsApiContract<CreateOptions<{ bar: boolean }>['data']> = {
const ruleToClone = {
...pick(mockedRule, 'consumer', 'name', 'schedule', 'tags', 'params', 'throttle', 'enabled'),
rule_type_id: mockedRule.alertTypeId,
notify_when: mockedRule.notifyWhen,

View file

@ -1,155 +0,0 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { validateDurationSchema, RuleTypeDisabledError } from '../lib';
import { CreateOptions } from '../rules_client';
import {
RewriteRequestCase,
RewriteResponseCase,
rewriteActionsReq,
rewriteActionsRes,
handleDisabledApiKeysError,
verifyAccessAndContext,
countUsageOfPredefinedIds,
actionsSchema,
rewriteRuleLastRun,
} from './lib';
import {
SanitizedRule,
validateNotifyWhenType,
RuleTypeParams,
BASE_ALERTING_API_PATH,
} from '../types';
import { RouteOptions } from '.';
export const bodySchema = schema.object({
name: schema.string(),
rule_type_id: schema.string(),
enabled: schema.boolean({ defaultValue: true }),
consumer: schema.string(),
tags: schema.arrayOf(schema.string(), { defaultValue: [] }),
throttle: schema.maybe(schema.nullable(schema.string({ validate: validateDurationSchema }))),
params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
schedule: schema.object({
interval: schema.string({ validate: validateDurationSchema }),
}),
actions: actionsSchema,
notify_when: schema.maybe(
schema.nullable(
schema.oneOf(
[
schema.literal('onActionGroupChange'),
schema.literal('onActiveAlert'),
schema.literal('onThrottleInterval'),
],
{ validate: validateNotifyWhenType }
)
)
),
});
const rewriteBodyReq: RewriteRequestCase<CreateOptions<RuleTypeParams>['data']> = ({
rule_type_id: alertTypeId,
notify_when: notifyWhen,
actions,
...rest
}): CreateOptions<RuleTypeParams>['data'] => ({
...rest,
alertTypeId,
notifyWhen,
actions: rewriteActionsReq(actions),
});
const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
actions,
alertTypeId,
scheduledTaskId,
createdBy,
updatedBy,
createdAt,
updatedAt,
apiKeyOwner,
apiKeyCreatedByUser,
notifyWhen,
muteAll,
mutedInstanceIds,
snoozeSchedule,
lastRun,
nextRun,
executionStatus: { lastExecutionDate, lastDuration, ...executionStatus },
...rest
}) => ({
...rest,
rule_type_id: alertTypeId,
scheduled_task_id: scheduledTaskId,
snooze_schedule: snoozeSchedule,
created_by: createdBy,
updated_by: updatedBy,
created_at: createdAt,
updated_at: updatedAt,
api_key_owner: apiKeyOwner,
notify_when: notifyWhen,
mute_all: muteAll,
muted_alert_ids: mutedInstanceIds,
execution_status: {
...executionStatus,
last_execution_date: lastExecutionDate,
last_duration: lastDuration,
},
actions: rewriteActionsRes(actions),
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
...(nextRun ? { next_run: nextRun } : {}),
...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}),
});
export const createRuleRoute = ({ router, licenseState, usageCounter }: RouteOptions) => {
router.post(
{
path: `${BASE_ALERTING_API_PATH}/rule/{id?}`,
validate: {
params: schema.maybe(
schema.object({
id: schema.maybe(schema.string()),
})
),
body: bodySchema,
},
},
handleDisabledApiKeysError(
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesClient = (await context.alerting).getRulesClient();
const rule = req.body;
const params = req.params;
countUsageOfPredefinedIds({
predefinedId: params?.id,
spaceId: rulesClient.getSpaceId(),
usageCounter,
});
try {
const createdRule: SanitizedRule<RuleTypeParams> =
await rulesClient.create<RuleTypeParams>({
data: rewriteBodyReq(rule),
options: { id: params?.id },
});
return res.ok({
body: rewriteBodyRes(createdRule),
});
} catch (e) {
if (e instanceof RuleTypeDisabledError) {
return e.sendResponse(res);
}
throw e;
}
})
)
)
);
};

View file

@ -11,7 +11,7 @@ import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-p
import { ILicenseState } from '../lib';
import { defineLegacyRoutes } from './legacy';
import { AlertingRequestHandlerContext } from '../types';
import { createRuleRoute } from './create_rule';
import { createRuleRoute } from './rule/create';
import { getRuleRoute, getInternalRuleRoute } from './get_rule';
import { updateRuleRoute } from './update_rule';
import { deleteRuleRoute } from './delete_rule';

View file

@ -6,22 +6,22 @@
*/
import { pick } from 'lodash';
import { createRuleRoute } from './create_rule';
import { createRuleRoute } from './create_rule_route';
import { httpServiceMock } from '@kbn/core/server/mocks';
import { licenseStateMock } from '../lib/license_state.mock';
import { verifyApiAccess } from '../lib/license_api_access';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { CreateOptions } from '../rules_client';
import { rulesClientMock } from '../rules_client.mock';
import { RuleTypeDisabledError } from '../lib';
import { AsApiContract } from './lib';
import { SanitizedRule } from '../types';
import { licenseStateMock } from '../../../lib/license_state.mock';
import { verifyApiAccess } from '../../../lib/license_api_access';
import { mockHandlerArguments } from '../../_mock_handler_arguments';
import type { CreateRuleRequestBodyV1 } from '../../../../common/routes/rule/create';
import { rulesClientMock } from '../../../rules_client.mock';
import { RuleTypeDisabledError } from '../../../lib';
import { AsApiContract } from '../../lib';
import { SanitizedRule } from '../../../types';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { usageCountersServiceMock } from '@kbn/usage-collection-plugin/server/usage_counters/usage_counters_service.mock';
const rulesClient = rulesClientMock.create();
jest.mock('../lib/license_api_access', () => ({
jest.mock('../../../lib/license_api_access', () => ({
verifyApiAccess: jest.fn(),
}));
@ -83,7 +83,7 @@ describe('createRuleRoute', () => {
revision: 0,
};
const ruleToCreate: AsApiContract<CreateOptions<{ bar: boolean }>['data']> = {
const ruleToCreate: CreateRuleRequestBodyV1<{ bar: boolean }> = {
...pick(mockedAlert, 'consumer', 'name', 'schedule', 'tags', 'params', 'throttle', 'enabled'),
rule_type_id: mockedAlert.alertTypeId,
notify_when: mockedAlert.notifyWhen,
@ -156,7 +156,17 @@ describe('createRuleRoute', () => {
['ok']
);
expect(await handler(context, req, res)).toEqual({ body: createResult });
expect(await handler(context, req, res)).toEqual({
body: {
...createResult,
created_at: createResult.created_at.toISOString(),
updated_at: createResult.updated_at.toISOString(),
execution_status: {
...createResult.execution_status,
last_execution_date: createResult.execution_status.last_execution_date.toISOString(),
},
},
});
expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled();
expect(rulesClient.create).toHaveBeenCalledTimes(1);
@ -166,6 +176,7 @@ describe('createRuleRoute', () => {
"data": Object {
"actions": Array [
Object {
"actionTypeId": undefined,
"alertsFilter": Object {
"query": Object {
"filters": Array [],
@ -213,7 +224,15 @@ describe('createRuleRoute', () => {
`);
expect(res.ok).toHaveBeenCalledWith({
body: createResult,
body: {
...createResult,
created_at: createResult.created_at.toISOString(),
updated_at: createResult.updated_at.toISOString(),
execution_status: {
...createResult.execution_status,
last_execution_date: createResult.execution_status.last_execution_date.toISOString(),
},
},
});
});
@ -253,7 +272,17 @@ describe('createRuleRoute', () => {
['ok']
);
expect(await handler(context, req, res)).toEqual({ body: expectedResult });
expect(await handler(context, req, res)).toEqual({
body: {
...expectedResult,
created_at: expectedResult.created_at.toISOString(),
updated_at: expectedResult.updated_at.toISOString(),
execution_status: {
...expectedResult.execution_status,
last_execution_date: expectedResult.execution_status.last_execution_date.toISOString(),
},
},
});
expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(1);
expect(rulesClient.create).toHaveBeenCalledTimes(1);
@ -263,6 +292,7 @@ describe('createRuleRoute', () => {
"data": Object {
"actions": Array [
Object {
"actionTypeId": undefined,
"alertsFilter": Object {
"query": Object {
"filters": Array [],
@ -310,7 +340,15 @@ describe('createRuleRoute', () => {
`);
expect(res.ok).toHaveBeenCalledWith({
body: expectedResult,
body: {
...expectedResult,
created_at: expectedResult.created_at.toISOString(),
updated_at: expectedResult.updated_at.toISOString(),
execution_status: {
...expectedResult.execution_status,
last_execution_date: expectedResult.execution_status.last_execution_date.toISOString(),
},
},
});
});
@ -351,7 +389,17 @@ describe('createRuleRoute', () => {
['ok']
);
expect(await handler(context, req, res)).toEqual({ body: expectedResult });
expect(await handler(context, req, res)).toEqual({
body: {
...expectedResult,
created_at: expectedResult.created_at.toISOString(),
updated_at: expectedResult.updated_at.toISOString(),
execution_status: {
...expectedResult.execution_status,
last_execution_date: expectedResult.execution_status.last_execution_date.toISOString(),
},
},
});
expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(1);
expect(rulesClient.create).toHaveBeenCalledTimes(1);
@ -361,6 +409,7 @@ describe('createRuleRoute', () => {
"data": Object {
"actions": Array [
Object {
"actionTypeId": undefined,
"alertsFilter": Object {
"query": Object {
"filters": Array [],
@ -408,7 +457,15 @@ describe('createRuleRoute', () => {
`);
expect(res.ok).toHaveBeenCalledWith({
body: expectedResult,
body: {
...expectedResult,
created_at: expectedResult.created_at.toISOString(),
updated_at: expectedResult.updated_at.toISOString(),
execution_status: {
...expectedResult.execution_status,
last_execution_date: expectedResult.execution_status.last_execution_date.toISOString(),
},
},
});
});
@ -449,7 +506,17 @@ describe('createRuleRoute', () => {
['ok']
);
expect(await handler(context, req, res)).toEqual({ body: expectedResult });
expect(await handler(context, req, res)).toEqual({
body: {
...expectedResult,
created_at: expectedResult.created_at.toISOString(),
updated_at: expectedResult.updated_at.toISOString(),
execution_status: {
...expectedResult.execution_status,
last_execution_date: expectedResult.execution_status.last_execution_date.toISOString(),
},
},
});
expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(2);
expect(rulesClient.create).toHaveBeenCalledTimes(1);
@ -459,6 +526,7 @@ describe('createRuleRoute', () => {
"data": Object {
"actions": Array [
Object {
"actionTypeId": undefined,
"alertsFilter": Object {
"query": Object {
"filters": Array [],
@ -506,7 +574,15 @@ describe('createRuleRoute', () => {
`);
expect(res.ok).toHaveBeenCalledWith({
body: expectedResult,
body: {
...expectedResult,
created_at: expectedResult.created_at.toISOString(),
updated_at: expectedResult.updated_at.toISOString(),
execution_status: {
...expectedResult.execution_status,
last_execution_date: expectedResult.execution_status.last_execution_date.toISOString(),
},
},
});
});

View file

@ -0,0 +1,75 @@
/*
* 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 { RuleTypeDisabledError } from '../../../lib';
import {
handleDisabledApiKeysError,
verifyAccessAndContext,
countUsageOfPredefinedIds,
} from '../../lib';
import { BASE_ALERTING_API_PATH } from '../../../types';
import { RouteOptions } from '../..';
import type {
CreateRuleRequestBodyV1,
CreateRuleRequestParamsV1,
CreateRuleResponseV1,
} from '../../../../common/routes/rule/create';
import { createBodySchemaV1, createParamsSchemaV1 } from '../../../../common/routes/rule/create';
import type { RuleParamsV1 } from '../../../../common/routes/rule/rule_response';
import { Rule } from '../../../application/rule/types';
import { transformCreateBodyV1 } from './transforms';
import { transformRuleToRuleResponseV1 } from '../transforms';
export const createRuleRoute = ({ router, licenseState, usageCounter }: RouteOptions) => {
router.post(
{
path: `${BASE_ALERTING_API_PATH}/rule/{id?}`,
validate: {
body: createBodySchemaV1,
params: createParamsSchemaV1,
},
},
handleDisabledApiKeysError(
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesClient = (await context.alerting).getRulesClient();
// Assert versioned inputs
const createRuleData: CreateRuleRequestBodyV1<RuleParamsV1> = req.body;
const params: CreateRuleRequestParamsV1 = req.params;
countUsageOfPredefinedIds({
predefinedId: params?.id,
spaceId: rulesClient.getSpaceId(),
usageCounter,
});
try {
// TODO (http-versioning): Remove this cast, this enables us to move forward
// without fixing all of other solution types
const createdRule: Rule<RuleParamsV1> = (await rulesClient.create<RuleParamsV1>({
data: transformCreateBodyV1<RuleParamsV1>(createRuleData),
options: { id: params?.id },
})) as Rule<RuleParamsV1>;
// Assert versioned response type
const response: CreateRuleResponseV1<RuleParamsV1> = {
body: transformRuleToRuleResponseV1<RuleParamsV1>(createdRule),
};
return res.ok(response);
} catch (e) {
if (e instanceof RuleTypeDisabledError) {
return e.sendResponse(res);
}
throw e;
}
})
)
)
);
};

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { createRuleRoute } from './create_rule_route';

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export { transformCreateBody } from './transform_create_body/latest';
export { transformCreateBody as transformCreateBodyV1 } from './transform_create_body/v1';

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,54 @@
/*
* 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 type {
CreateRuleActionV1,
CreateRuleRequestBodyV1,
} from '../../../../../../common/routes/rule/create';
import type { CreateRuleData } from '../../../../../application/rule/create';
import type { RuleParams } from '../../../../../application/rule/types';
const transformCreateBodyActions = (actions: CreateRuleActionV1[]): CreateRuleData['actions'] => {
if (!actions) return [];
return actions.map(({ frequency, alerts_filter: alertsFilter, ...action }) => {
return {
group: action.group,
id: action.id,
params: action.params,
actionTypeId: action.actionTypeId,
...(action.uuid ? { uuid: action.uuid } : {}),
...(frequency
? {
frequency: {
summary: frequency.summary,
throttle: frequency.throttle,
notifyWhen: frequency.notify_when,
},
}
: {}),
...(alertsFilter ? { alertsFilter } : {}),
};
});
};
export const transformCreateBody = <Params extends RuleParams = never>(
createBody: CreateRuleRequestBodyV1<Params>
): CreateRuleData<Params> => {
return {
name: createBody.name,
alertTypeId: createBody.rule_type_id,
enabled: createBody.enabled,
consumer: createBody.consumer,
tags: createBody.tags,
...(createBody.throttle ? { throttle: createBody.throttle } : {}),
params: createBody.params,
schedule: createBody.schedule,
actions: transformCreateBodyActions(createBody.actions),
...(createBody.notify_when ? { notifyWhen: createBody.notify_when } : {}),
};
};

View file

@ -0,0 +1,9 @@
/*
* 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.
*/
export { transformRuleToRuleResponse } from './transform_rule_to_rule_response/latest';
export { transformRuleToRuleResponse as transformRuleToRuleResponseV1 } from './transform_rule_to_rule_response/v1';

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { transformRuleToRuleResponse } from './v1';

View file

@ -0,0 +1,82 @@
/*
* 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 { RuleResponseV1, RuleParamsV1 } from '../../../../../common/routes/rule/rule_response';
import { Rule, RuleLastRun, RuleParams } from '../../../../application/rule/types';
const transformRuleLastRun = (lastRun: RuleLastRun): RuleResponseV1['last_run'] => {
return {
outcome: lastRun.outcome,
outcome_order: lastRun.outcomeOrder,
...(lastRun.warning ? { warning: lastRun.warning } : {}),
alerts_count: lastRun.alertsCount,
outcome_msg: lastRun.outcomeMsg,
};
};
export const transformRuleToRuleResponse = <Params extends RuleParams = never>(
rule: Rule<Params>
): RuleResponseV1<RuleParamsV1> => ({
id: rule.id,
enabled: rule.enabled,
name: rule.name,
tags: rule.tags,
rule_type_id: rule.alertTypeId,
consumer: rule.consumer,
schedule: rule.schedule,
actions: rule.actions.map(
({ group, id, actionTypeId, params, frequency, uuid, alertsFilter }) => ({
group,
id,
params,
connector_type_id: actionTypeId,
...(frequency
? {
frequency: {
summary: frequency.summary,
notify_when: frequency.notifyWhen,
throttle: frequency.throttle,
},
}
: {}),
...(uuid && { uuid }),
...(alertsFilter && { alerts_filter: alertsFilter }),
})
),
params: rule.params,
created_by: rule.createdBy,
updated_by: rule.updatedBy,
created_at: rule.createdAt.toISOString(),
updated_at: rule.updatedAt.toISOString(),
api_key_owner: rule.apiKeyOwner,
...(rule.apiKeyCreatedByUser !== undefined
? { api_key_created_by_user: rule.apiKeyCreatedByUser }
: {}),
...(rule.throttle !== undefined ? { throttle: rule.throttle } : {}),
...(rule.notifyWhen !== undefined ? { notify_when: rule.notifyWhen } : {}),
mute_all: rule.muteAll,
muted_alert_ids: rule.mutedInstanceIds,
...(rule.scheduledTaskId !== undefined ? { scheduled_task_id: rule.scheduledTaskId } : {}),
...(rule.isSnoozedUntil !== undefined
? { is_snoozed_until: rule.isSnoozedUntil?.toISOString() || null }
: {}),
execution_status: {
status: rule.executionStatus.status,
...(rule.executionStatus.error ? { error: rule.executionStatus.error } : {}),
...(rule.executionStatus.warning ? { warning: rule.executionStatus.warning } : {}),
last_execution_date: rule.executionStatus.lastExecutionDate?.toISOString(),
...(rule.executionStatus.lastDuration !== undefined
? { last_duration: rule.executionStatus.lastDuration }
: {}),
},
...(rule.lastRun !== undefined
? { last_run: rule.lastRun ? transformRuleLastRun(rule.lastRun) : null }
: {}),
...(rule.nextRun !== undefined ? { next_run: rule.nextRun?.toISOString() || null } : {}),
revision: rule.revision,
...(rule.running !== undefined ? { running: rule.running } : {}),
});

View file

@ -7,7 +7,8 @@
import { SavedObjectReference, SavedObject } from '@kbn/core/server';
import { withSpan } from '@kbn/apm-utils';
import { RawRule, RuleTypeParams } from '../../types';
import { Rule, RuleWithLegacyId, RawRule, RuleTypeParams } from '../../types';
import { RuleAttributes } from '../../data/rule/types';
import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
import { SavedObjectOptions } from '../types';
@ -15,23 +16,42 @@ import { RulesClientContext } from '../types';
import { updateMeta } from './update_meta';
import { scheduleTask } from './schedule_task';
import { getAlertFromRaw } from './get_alert_from_raw';
import { createRuleSo, deleteRuleSo, updateRuleSo } from '../../data/rule';
interface CreateRuleSavedObjectParams {
intervalInMs: number;
rawRule: RawRule;
references: SavedObjectReference[];
ruleId: string;
options?: SavedObjectOptions;
returnRuleAttributes?: false;
}
interface CreateRuleSavedObjectAttributeParams {
intervalInMs: number;
rawRule: RuleAttributes;
references: SavedObjectReference[];
ruleId: string;
options?: SavedObjectOptions;
returnRuleAttributes: true;
}
// TODO (http-versioning): Remove this overload when we convert all types,
// this exists for easy interop until then.
export async function createRuleSavedObject<Params extends RuleTypeParams = never>(
context: RulesClientContext,
{
intervalInMs,
rawRule,
references,
ruleId,
options,
}: {
intervalInMs: number;
rawRule: RawRule;
references: SavedObjectReference[];
ruleId: string;
options?: SavedObjectOptions;
}
) {
params: CreateRuleSavedObjectParams
): Promise<Rule<Params> | RuleWithLegacyId<Params>>;
export async function createRuleSavedObject<Params extends RuleTypeParams = never>(
context: RulesClientContext,
params: CreateRuleSavedObjectAttributeParams
): Promise<SavedObject<RuleAttributes>>;
export async function createRuleSavedObject<Params extends RuleTypeParams = never>(
context: RulesClientContext,
params: CreateRuleSavedObjectParams | CreateRuleSavedObjectAttributeParams
): Promise<Rule<Params> | RuleWithLegacyId<Params> | SavedObject<RuleAttributes>> {
const { intervalInMs, rawRule, references, ruleId, options, returnRuleAttributes } = params;
context.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.CREATE,
@ -40,17 +60,22 @@ export async function createRuleSavedObject<Params extends RuleTypeParams = neve
})
);
// TODO (http-versioning): Remove casts
let createdAlert: SavedObject<RawRule>;
try {
createdAlert = await withSpan(
createdAlert = (await withSpan(
{ name: 'unsecuredSavedObjectsClient.create', type: 'rules' },
() =>
context.unsecuredSavedObjectsClient.create('alert', updateMeta(context, rawRule), {
...options,
references,
id: ruleId,
createRuleSo({
ruleAttributes: updateMeta(context, rawRule as RawRule) as RuleAttributes,
savedObjectClient: context.unsecuredSavedObjectsClient,
savedObjectCreateOptions: {
...options,
references,
id: ruleId,
},
})
);
)) as SavedObject<RawRule>;
} catch (e) {
// Avoid unused API key
await bulkMarkApiKeysForInvalidation(
@ -75,7 +100,10 @@ export async function createRuleSavedObject<Params extends RuleTypeParams = neve
} catch (e) {
// Cleanup data, something went wrong scheduling the task
try {
await context.unsecuredSavedObjectsClient.delete('alert', createdAlert.id);
await deleteRuleSo({
savedObjectClient: context.unsecuredSavedObjectsClient,
id: createdAlert.id,
});
} catch (err) {
// Skip the cleanup error and throw the task manager error to avoid confusion
context.logger.error(
@ -86,8 +114,12 @@ export async function createRuleSavedObject<Params extends RuleTypeParams = neve
}
await withSpan({ name: 'unsecuredSavedObjectsClient.update', type: 'rules' }, () =>
context.unsecuredSavedObjectsClient.update<RawRule>('alert', createdAlert.id, {
scheduledTaskId,
updateRuleSo({
savedObjectClient: context.unsecuredSavedObjectsClient,
id: createdAlert.id,
updateRuleAttributes: {
scheduledTaskId,
},
})
);
createdAlert.attributes.scheduledTaskId = scheduledTaskId;
@ -103,6 +135,11 @@ export async function createRuleSavedObject<Params extends RuleTypeParams = neve
);
}
// TODO (http-versioning): Remove casts
if (returnRuleAttributes) {
return createdAlert as SavedObject<RuleAttributes>;
}
return getAlertFromRaw<Params>(
context,
createdAlert.id,

View file

@ -15,7 +15,7 @@ import { getDefaultMonitoring } from '../../lib';
import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
import { parseDuration } from '../../../common/parse_duration';
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
import { getRuleExecutionStatusPending } from '../../lib/rule_execution_status';
import { getRuleExecutionStatusPendingAttributes } from '../../lib/rule_execution_status';
import { isDetectionEngineAADRuleType } from '../../saved_objects/migrations/utils';
import { createNewAPIKeySet, createRuleSavedObject } from '../lib';
import { RulesClientContext } from '../types';
@ -114,7 +114,7 @@ export async function clone<Params extends RuleTypeParams = never>(
snoozeSchedule: [],
muteAll: false,
mutedInstanceIds: [],
executionStatus: getRuleExecutionStatusPending(lastRunTimestamp.toISOString()),
executionStatus: getRuleExecutionStatusPendingAttributes(lastRunTimestamp.toISOString()),
monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()),
revision: 0,
scheduledTaskId: null,

View file

@ -1,178 +0,0 @@
/*
* 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 Semver from 'semver';
import Boom from '@hapi/boom';
import { SavedObjectsUtils } from '@kbn/core/server';
import { withSpan } from '@kbn/apm-utils';
import { parseDuration } from '../../../common/parse_duration';
import { RawRule, SanitizedRule, RuleTypeParams, Rule } from '../../types';
import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
import { validateRuleTypeParams, getRuleNotifyWhenType, getDefaultMonitoring } from '../../lib';
import { getRuleExecutionStatusPending } from '../../lib/rule_execution_status';
import {
createRuleSavedObject,
extractReferences,
validateActions,
addGeneratedActionValues,
} from '../lib';
import { generateAPIKeyName, getMappedParams, apiKeyAsAlertAttributes } from '../common';
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
import { NormalizedAlertAction, RulesClientContext } from '../types';
interface SavedObjectOptions {
id?: string;
migrationVersion?: Record<string, string>;
}
export interface CreateOptions<Params extends RuleTypeParams> {
data: Omit<
Rule<Params>,
| 'id'
| 'createdBy'
| 'updatedBy'
| 'createdAt'
| 'updatedAt'
| 'apiKey'
| 'apiKeyOwner'
| 'apiKeyCreatedByUser'
| 'muteAll'
| 'mutedInstanceIds'
| 'actions'
| 'executionStatus'
| 'snoozeSchedule'
| 'isSnoozedUntil'
| 'lastRun'
| 'nextRun'
| 'revision'
> & { actions: NormalizedAlertAction[] };
options?: SavedObjectOptions;
allowMissingConnectorSecrets?: boolean;
}
export async function create<Params extends RuleTypeParams = never>(
context: RulesClientContext,
{ data: initialData, options, allowMissingConnectorSecrets }: CreateOptions<Params>
): Promise<SanitizedRule<Params>> {
const data = { ...initialData, actions: addGeneratedActionValues(initialData.actions) };
const id = options?.id || SavedObjectsUtils.generateId();
try {
await withSpan({ name: 'authorization.ensureAuthorized', type: 'rules' }, () =>
context.authorization.ensureAuthorized({
ruleTypeId: data.alertTypeId,
consumer: data.consumer,
operation: WriteOperations.Create,
entity: AlertingAuthorizationEntity.Rule,
})
);
} catch (error) {
context.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.CREATE,
savedObject: { type: 'alert', id },
error,
})
);
throw error;
}
context.ruleTypeRegistry.ensureRuleTypeEnabled(data.alertTypeId);
// Throws an error if alert type isn't registered
const ruleType = context.ruleTypeRegistry.get(data.alertTypeId);
const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate.params);
const username = await context.getUserName();
let createdAPIKey = null;
let isAuthTypeApiKey = false;
try {
isAuthTypeApiKey = context.isAuthenticationTypeAPIKey();
const name = generateAPIKeyName(ruleType.id, data.name);
createdAPIKey = data.enabled
? isAuthTypeApiKey
? context.getAuthenticationAPIKey(`${name}-user-created`)
: await withSpan(
{
name: 'createAPIKey',
type: 'rules',
},
() => context.createAPIKey(name)
)
: null;
} catch (error) {
throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`);
}
await withSpan({ name: 'validateActions', type: 'rules' }, () =>
validateActions(context, ruleType, data, allowMissingConnectorSecrets)
);
// Throw error if schedule interval is less than the minimum and we are enforcing it
const intervalInMs = parseDuration(data.schedule.interval);
if (
intervalInMs < context.minimumScheduleIntervalInMs &&
context.minimumScheduleInterval.enforce
) {
throw Boom.badRequest(
`Error creating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}`
);
}
// Extract saved object references for this rule
const {
references,
params: updatedParams,
actions,
} = await withSpan({ name: 'extractReferences', type: 'rules' }, () =>
extractReferences(context, ruleType, data.actions, validatedAlertTypeParams)
);
const createTime = Date.now();
const lastRunTimestamp = new Date();
const legacyId = Semver.lt(context.kibanaVersion, '8.0.0') ? id : null;
const notifyWhen = getRuleNotifyWhenType(data.notifyWhen ?? null, data.throttle ?? null);
const throttle = data.throttle ?? null;
const rawRule: RawRule = {
...data,
...apiKeyAsAlertAttributes(createdAPIKey, username, isAuthTypeApiKey),
legacyId,
actions,
createdBy: username,
updatedBy: username,
createdAt: new Date(createTime).toISOString(),
updatedAt: new Date(createTime).toISOString(),
snoozeSchedule: [],
params: updatedParams as RawRule['params'],
muteAll: false,
mutedInstanceIds: [],
notifyWhen,
throttle,
executionStatus: getRuleExecutionStatusPending(lastRunTimestamp.toISOString()),
monitoring: getDefaultMonitoring(lastRunTimestamp.toISOString()),
revision: 0,
running: false,
};
const mappedParams = getMappedParams(updatedParams);
if (Object.keys(mappedParams).length) {
rawRule.mapped_params = mappedParams;
}
return await withSpan({ name: 'createRuleSavedObject', type: 'rules' }, () =>
createRuleSavedObject(context, {
intervalInMs,
rawRule,
references,
ruleId: id,
options,
})
);
}

View file

@ -10,7 +10,7 @@ import { parseDuration } from '../../common/parse_duration';
import { RulesClientContext, BulkOptions, MuteOptions } from './types';
import { clone, CloneArguments } from './methods/clone';
import { create, CreateOptions } from './methods/create';
import { createRule, CreateRuleParams } from '../application/rule/create';
import { get, GetParams } from './methods/get';
import { resolve, ResolveParams } from './methods/resolve';
import { getAlertState, GetAlertStateParams } from './methods/get_alert_state';
@ -107,8 +107,8 @@ export class RulesClient {
aggregate<T>(this.context, params);
public clone = <Params extends RuleTypeParams = never>(...args: CloneArguments) =>
clone<Params>(this.context, ...args);
public create = <Params extends RuleTypeParams = never>(params: CreateOptions<Params>) =>
create<Params>(this.context, params);
public create = <Params extends RuleTypeParams = never>(params: CreateRuleParams<Params>) =>
createRule<Params>(this.context, params);
public delete = (params: { id: string }) => deleteRule(this.context, params);
public find = <Params extends RuleTypeParams = never>(params?: FindParams) =>
find<Params>(this.context, params);

View file

@ -35,7 +35,6 @@ export type {
BulkEditOptionsFilter,
BulkEditOptionsIds,
} from './methods/bulk_edit';
export type { CreateOptions } from './methods/create';
export type { FindOptions, FindResult } from './methods/find';
export type { UpdateOptions } from './methods/update';
export type { GetAlertSummaryParams } from './methods/get_alert_summary';

View file

@ -7,7 +7,7 @@
import { transformRulesForExport } from './transform_rule_for_export';
jest.mock('../lib/rule_execution_status', () => ({
getRuleExecutionStatusPending: () => ({
getRuleExecutionStatusPendingAttributes: () => ({
status: 'pending',
lastExecutionDate: '2020-08-20T19:23:38Z',
error: null,

View file

@ -6,7 +6,7 @@
*/
import { SavedObject } from '@kbn/core/server';
import { getRuleExecutionStatusPending } from '../lib/rule_execution_status';
import { getRuleExecutionStatusPendingAttributes } from '../lib/rule_execution_status';
import { RawRule } from '../types';
export function transformRulesForExport(rules: SavedObject[]): Array<SavedObject<RawRule>> {
@ -28,7 +28,7 @@ function transformRuleForExport(
apiKeyOwner: null,
apiKeyCreatedByUser: null,
scheduledTaskId: null,
executionStatus: getRuleExecutionStatusPending(exportDate),
executionStatus: getRuleExecutionStatusPendingAttributes(exportDate),
},
};
}