Skip to content

Commit

Permalink
[RAM] Adds Bulk Edit API to rulesClient (#126904)
Browse files Browse the repository at this point in the history
Addresses
- #124715

## Summary

- adds bulkEdit method to rulesClient
- adds multi_terms bucket aggregations to savedObjectClient
- adds createPointInTimeFinderAsInternalUser to encryptedSavedObjectClient
- adds alerting API for bulkEdit
```bash
curl --location --request POST 'http://localhost:5601/kbn/internal/alerting/rules/_bulk_edit' \
--header 'kbn-xsrf: reporting' \
--header 'Authorization: Basic ZWxhc3RpYzpjaGFuZ2VtZQ==' \
--header 'Content-Type: application/json' \
--data-raw '{
    "ids": ["4cb80374-b5c7-11ec-8f1e-adaa7d7d57e5"],
    "operations":  [{
        "operation": "add",
        "field": "tags",
        "value": ["foo"]
    }]
}'
```
### Checklist

Delete any items that are not applicable to this PR.

- [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


## Release note

Adds new `bulkEdit` method to alerting rulesClient and internal _bulk_edit API, that allow bulk editing of rules.
  • Loading branch information
vitaliidm committed May 11, 2022
1 parent 1dc6216 commit e3c47ec
Show file tree
Hide file tree
Showing 50 changed files with 3,887 additions and 462 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { sortOrderSchema } from './common_schemas';
* - nested
* - reverse_nested
* - terms
* - multi_terms
*
* Not fully supported:
* - filter
Expand All @@ -37,7 +38,6 @@ import { sortOrderSchema } from './common_schemas';
* - global
* - ip_range
* - missing
* - multi_terms
* - parent
* - range
* - rare_terms
Expand All @@ -63,6 +63,36 @@ const boolSchema = s.object({
}),
});

const orderSchema = s.oneOf([
sortOrderSchema,
s.recordOf(s.string(), sortOrderSchema),
s.arrayOf(s.recordOf(s.string(), sortOrderSchema)),
]);

const termsSchema = s.object({
field: s.maybe(s.string()),
collect_mode: s.maybe(s.string()),
exclude: s.maybe(s.oneOf([s.string(), s.arrayOf(s.string())])),
include: s.maybe(s.oneOf([s.string(), s.arrayOf(s.string())])),
execution_hint: s.maybe(s.string()),
missing: s.maybe(s.number()),
min_doc_count: s.maybe(s.number({ min: 1 })),
size: s.maybe(s.number()),
show_term_doc_count_error: s.maybe(s.boolean()),
order: s.maybe(orderSchema),
});

const multiTermsSchema = s.object({
terms: s.arrayOf(termsSchema),
size: s.maybe(s.number()),
shard_size: s.maybe(s.number()),
show_term_doc_count_error: s.maybe(s.boolean()),
min_doc_count: s.maybe(s.number()),
shard_min_doc_count: s.maybe(s.number()),
collect_mode: s.maybe(s.oneOf([s.literal('depth_first'), s.literal('breadth_first')])),
order: s.maybe(s.recordOf(s.string(), orderSchema)),
});

export const bucketAggsSchemas: Record<string, ObjectType> = {
date_range: s.object({
field: s.string(),
Expand Down Expand Up @@ -104,22 +134,6 @@ export const bucketAggsSchemas: Record<string, ObjectType> = {
reverse_nested: s.object({
path: s.maybe(s.string()),
}),
terms: s.object({
field: s.maybe(s.string()),
collect_mode: s.maybe(s.string()),
exclude: s.maybe(s.oneOf([s.string(), s.arrayOf(s.string())])),
include: s.maybe(s.oneOf([s.string(), s.arrayOf(s.string())])),
execution_hint: s.maybe(s.string()),
missing: s.maybe(s.number()),
min_doc_count: s.maybe(s.number({ min: 1 })),
size: s.maybe(s.number()),
show_term_doc_count_error: s.maybe(s.boolean()),
order: s.maybe(
s.oneOf([
sortOrderSchema,
s.recordOf(s.string(), sortOrderSchema),
s.arrayOf(s.recordOf(s.string(), sortOrderSchema)),
])
),
}),
multi_terms: multiTermsSchema,
terms: termsSchema,
};
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,28 @@ describe('validateAndConvertAggregations', () => {
});
});

it('validates multi_terms aggregations', () => {
expect(
validateAndConvertAggregations(
['foo'],
{
aggName: {
multi_terms: {
terms: [{ field: 'foo.attributes.description' }, { field: 'foo.attributes.bytes' }],
},
},
},
mockMappings
)
).toEqual({
aggName: {
multi_terms: {
terms: [{ field: 'foo.description' }, { field: 'foo.bytes' }],
},
},
});
});

it('validates a nested field in simple aggregations', () => {
expect(
validateAndConvertAggregations(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ObjectType } from '@kbn/config-schema';
import { isPlainObject } from 'lodash';
import { isPlainObject, isArray } from 'lodash';

import { IndexMapping } from '../../../mappings';
import {
Expand Down Expand Up @@ -181,11 +181,17 @@ const recursiveRewrite = (

const nestedContext = childContext(context, key);
const newKey = rewriteKey ? validateAndRewriteAttributePath(key, nestedContext) : key;
const newValue = rewriteValue
? validateAndRewriteAttributePath(value, nestedContext)
: isPlainObject(value)
? recursiveRewrite(value, nestedContext, [...parents, key])
: value;

let newValue = value;
if (rewriteValue) {
newValue = validateAndRewriteAttributePath(value, nestedContext);
} else if (isArray(value)) {
newValue = value.map((v) =>
isPlainObject(v) ? recursiveRewrite(v, nestedContext, parents) : v
);
} else if (isPlainObject(value)) {
newValue = recursiveRewrite(value, nestedContext, [...parents, key]);
}

return {
...memo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export enum WriteOperations {
MuteAlert = 'muteAlert',
UnmuteAlert = 'unmuteAlert',
Snooze = 'snooze',
BulkEdit = 'bulkEdit',
Unsnooze = 'unsnooze',
}

Expand Down
9 changes: 8 additions & 1 deletion x-pack/plugins/alerting/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,14 @@ export type {
} from './types';
export { DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT } from './config';
export type { PluginSetupContract, PluginStartContract } from './plugin';
export type { FindResult } from './rules_client';
export type {
FindResult,
BulkEditOperation,
BulkEditError,
BulkEditOptions,
BulkEditOptionsFilter,
BulkEditOptionsIds,
} from './rules_client';
export type { PublicAlert as Alert } from './alert';
export { parseDuration } from './lib';
export { getEsErrorMessage } from './lib/errors';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 { loggingSystemMock, savedObjectsClientMock } from '@kbn/core/server/mocks';
import { bulkMarkApiKeysForInvalidation } from './bulk_mark_api_keys_for_invalidation';

describe('bulkMarkApiKeysForInvalidation', () => {
test('should call savedObjectsClient bulkCreate with the proper params', async () => {
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [] });

await bulkMarkApiKeysForInvalidation(
{ apiKeys: [Buffer.from('123').toString('base64'), Buffer.from('456').toString('base64')] },
loggingSystemMock.create().get(),
unsecuredSavedObjectsClient
);

const bulkCreateCallMock = unsecuredSavedObjectsClient.bulkCreate.mock.calls[0];
const savedObjects = bulkCreateCallMock[0];

expect(unsecuredSavedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1);
expect(bulkCreateCallMock).toHaveLength(1);

expect(savedObjects).toHaveLength(2);
expect(savedObjects[0]).toHaveProperty('type', 'api_key_pending_invalidation');
expect(savedObjects[0]).toHaveProperty('attributes.apiKeyId', '123');
expect(savedObjects[0]).toHaveProperty('attributes.createdAt', expect.any(String));
expect(savedObjects[1]).toHaveProperty('type', 'api_key_pending_invalidation');
expect(savedObjects[1]).toHaveProperty('attributes.apiKeyId', '456');
expect(savedObjects[1]).toHaveProperty('attributes.createdAt', expect.any(String));
});

test('should log the proper error when savedObjectsClient create failed', async () => {
const logger = loggingSystemMock.create().get();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
unsecuredSavedObjectsClient.bulkCreate.mockRejectedValueOnce(new Error('Fail'));
await bulkMarkApiKeysForInvalidation(
{ apiKeys: [Buffer.from('123').toString('base64'), Buffer.from('456').toString('base64')] },
logger,
unsecuredSavedObjectsClient
);
expect(logger.error).toHaveBeenCalledWith(
'Failed to bulk mark list of API keys ["MTIz", "NDU2"] for invalidation: Fail'
);
});

test('should not call savedObjectsClient bulkCreate if list of apiKeys empty', async () => {
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValueOnce({ saved_objects: [] });

await bulkMarkApiKeysForInvalidation(
{ apiKeys: [] },
loggingSystemMock.create().get(),
unsecuredSavedObjectsClient
);

expect(unsecuredSavedObjectsClient.bulkCreate).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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 { Logger, SavedObjectsClientContract } from '@kbn/core/server';

export const bulkMarkApiKeysForInvalidation = async (
{ apiKeys }: { apiKeys: string[] },
logger: Logger,
savedObjectsClient: SavedObjectsClientContract
): Promise<void> => {
if (apiKeys.length === 0) {
return;
}

try {
const apiKeyIds = apiKeys.map(
(apiKey) => Buffer.from(apiKey, 'base64').toString().split(':')[0]
);
await savedObjectsClient.bulkCreate(
apiKeyIds.map((apiKeyId) => ({
attributes: {
apiKeyId,
createdAt: new Date().toISOString(),
},
type: 'api_key_pending_invalidation',
}))
);
} catch (e) {
logger.error(
`Failed to bulk mark list of API keys [${apiKeys
.map((key) => `"${key}"`)
.join(', ')}] for invalidation: ${e.message}`
);
}
};

This file was deleted.

This file was deleted.

Loading

0 comments on commit e3c47ec

Please sign in to comment.