-
Notifications
You must be signed in to change notification settings - Fork 8.4k
[RAM] 142183 create bulk delete on rules #142466
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
acbedf5
ff542b1
8211403
25a2471
e6ded33
6c5d4e4
09e4559
fa6aea6
577a0f3
8e29403
bcd74a2
a5a51db
daeceec
660c3b9
64c0621
561f63b
b8a0868
eed0b98
9d18083
1099901
957d99d
c84d0f4
bc5971a
593133d
d7a326f
2db12bf
597b652
61aff32
487cfdc
e9d946a
1b07f41
a365269
fad270a
be09680
f85cd12
635384e
5cebdc6
40e7fd1
076b3fa
867d99f
17f0eba
0e7beab
d08f2f6
f84fdde
4652756
7ac339f
e340ee2
6cf45fa
0c7110e
8668810
c482fc3
f558ab2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
/* | ||
* 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 { httpServiceMock } from '@kbn/core/server/mocks'; | ||
|
||
import { bulkDeleteRulesRoute } from './bulk_delete_rules'; | ||
import { licenseStateMock } from '../lib/license_state.mock'; | ||
import { mockHandlerArguments } from './_mock_handler_arguments'; | ||
import { rulesClientMock } from '../rules_client.mock'; | ||
import { RuleTypeDisabledError } from '../lib/errors/rule_type_disabled'; | ||
import { verifyApiAccess } from '../lib/license_api_access'; | ||
|
||
const rulesClient = rulesClientMock.create(); | ||
|
||
jest.mock('../lib/license_api_access', () => ({ | ||
verifyApiAccess: jest.fn(), | ||
})); | ||
|
||
beforeEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
describe('bulkDeleteRulesRoute', () => { | ||
const bulkDeleteRequest = { filter: '' }; | ||
const bulkDeleteResult = { errors: [], total: 1, taskIdsFailedToBeDeleted: [] }; | ||
|
||
it('should delete rules with proper parameters', async () => { | ||
const licenseState = licenseStateMock.create(); | ||
const router = httpServiceMock.createRouter(); | ||
|
||
bulkDeleteRulesRoute({ router, licenseState }); | ||
|
||
const [config, handler] = router.patch.mock.calls[0]; | ||
|
||
expect(config.path).toBe('/internal/alerting/rules/_bulk_delete'); | ||
|
||
rulesClient.bulkDeleteRules.mockResolvedValueOnce(bulkDeleteResult); | ||
|
||
const [context, req, res] = mockHandlerArguments( | ||
{ rulesClient }, | ||
{ | ||
body: bulkDeleteRequest, | ||
}, | ||
['ok'] | ||
); | ||
|
||
expect(await handler(context, req, res)).toEqual({ | ||
body: bulkDeleteResult, | ||
}); | ||
|
||
expect(rulesClient.bulkDeleteRules).toHaveBeenCalledTimes(1); | ||
expect(rulesClient.bulkDeleteRules.mock.calls[0]).toEqual([bulkDeleteRequest]); | ||
|
||
expect(res.ok).toHaveBeenCalled(); | ||
}); | ||
|
||
it('ensures the license allows bulk deleting rules', async () => { | ||
const licenseState = licenseStateMock.create(); | ||
const router = httpServiceMock.createRouter(); | ||
|
||
rulesClient.bulkDeleteRules.mockResolvedValueOnce(bulkDeleteResult); | ||
|
||
bulkDeleteRulesRoute({ router, licenseState }); | ||
|
||
const [, handler] = router.patch.mock.calls[0]; | ||
|
||
const [context, req, res] = mockHandlerArguments( | ||
{ rulesClient }, | ||
{ | ||
body: bulkDeleteRequest, | ||
} | ||
); | ||
|
||
await handler(context, req, res); | ||
|
||
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); | ||
}); | ||
|
||
it('ensures the license check prevents bulk deleting rules', async () => { | ||
const licenseState = licenseStateMock.create(); | ||
const router = httpServiceMock.createRouter(); | ||
|
||
(verifyApiAccess as jest.Mock).mockImplementation(() => { | ||
throw new Error('Failure'); | ||
}); | ||
|
||
bulkDeleteRulesRoute({ router, licenseState }); | ||
|
||
const [, handler] = router.patch.mock.calls[0]; | ||
|
||
const [context, req, res] = mockHandlerArguments( | ||
{ rulesClient }, | ||
{ | ||
body: bulkDeleteRequest, | ||
} | ||
); | ||
|
||
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: Failure]`); | ||
}); | ||
|
||
it('ensures the rule type gets validated for the license', async () => { | ||
const licenseState = licenseStateMock.create(); | ||
const router = httpServiceMock.createRouter(); | ||
|
||
bulkDeleteRulesRoute({ router, licenseState }); | ||
|
||
const [, handler] = router.patch.mock.calls[0]; | ||
|
||
rulesClient.bulkDeleteRules.mockRejectedValue( | ||
new RuleTypeDisabledError('Fail', 'license_invalid') | ||
); | ||
|
||
const [context, req, res] = mockHandlerArguments({ rulesClient }, { params: {}, body: {} }, [ | ||
'ok', | ||
'forbidden', | ||
]); | ||
|
||
await handler(context, req, res); | ||
|
||
expect(res.forbidden).toHaveBeenCalledWith({ body: { message: 'Fail' } }); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/* | ||
* 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 { IRouter } from '@kbn/core/server'; | ||
import { verifyAccessAndContext, handleDisabledApiKeysError } from './lib'; | ||
import { ILicenseState, RuleTypeDisabledError } from '../lib'; | ||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types'; | ||
|
||
export const bulkDeleteRulesRoute = ({ | ||
router, | ||
licenseState, | ||
}: { | ||
router: IRouter<AlertingRequestHandlerContext>; | ||
licenseState: ILicenseState; | ||
}) => { | ||
router.patch( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi, @pmuellr! |
||
{ | ||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_bulk_delete`, | ||
validate: { | ||
body: schema.object({ | ||
filter: schema.maybe(schema.string()), | ||
ids: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1, maxSize: 1000 })), | ||
}), | ||
}, | ||
}, | ||
handleDisabledApiKeysError( | ||
router.handleLegacyErrors( | ||
verifyAccessAndContext(licenseState, async (context, req, res) => { | ||
const rulesClient = (await context.alerting).getRulesClient(); | ||
const { filter, ids } = req.body; | ||
|
||
try { | ||
const result = await rulesClient.bulkDeleteRules({ filter, ids }); | ||
return res.ok({ body: result }); | ||
} catch (e) { | ||
if (e instanceof RuleTypeDisabledError) { | ||
return e.sendResponse(res); | ||
} | ||
throw e; | ||
} | ||
}) | ||
) | ||
) | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
/* | ||
* 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 { KueryNode } from '@kbn/es-query'; | ||
import { loggingSystemMock } from '@kbn/core/server/mocks'; | ||
|
||
import { retryIfBulkDeleteConflicts } from './retry_if_bulk_delete_conflicts'; | ||
import { RETRY_IF_CONFLICTS_ATTEMPTS } from './wait_before_next_retry'; | ||
|
||
const mockFilter: KueryNode = { | ||
type: 'function', | ||
value: 'mock', | ||
}; | ||
|
||
const mockLogger = loggingSystemMock.create().get(); | ||
|
||
const mockSuccessfulResult = { | ||
apiKeysToInvalidate: ['apiKey1'], | ||
errors: [], | ||
taskIdsToDelete: ['taskId1'], | ||
}; | ||
|
||
const error409 = { | ||
message: 'some fake message', | ||
status: 409, | ||
rule: { | ||
id: 'fake_rule_id', | ||
name: 'fake rule name', | ||
}, | ||
}; | ||
|
||
const getOperationConflictsTimes = (times: number) => { | ||
return async () => { | ||
conflictOperationMock(); | ||
times--; | ||
if (times >= 0) { | ||
return { | ||
apiKeysToInvalidate: [], | ||
taskIdsToDelete: [], | ||
errors: [error409], | ||
}; | ||
} | ||
return mockSuccessfulResult; | ||
}; | ||
}; | ||
|
||
const OperationSuccessful = async () => mockSuccessfulResult; | ||
const conflictOperationMock = jest.fn(); | ||
|
||
describe('retryIfBulkDeleteConflicts', () => { | ||
beforeEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
|
||
test('should work when operation is successful', async () => { | ||
const result = await retryIfBulkDeleteConflicts(mockLogger, OperationSuccessful, mockFilter); | ||
|
||
expect(result).toEqual(mockSuccessfulResult); | ||
}); | ||
|
||
test('should throw error when operation fails', async () => { | ||
await expect( | ||
retryIfBulkDeleteConflicts( | ||
mockLogger, | ||
async () => { | ||
throw Error('Test failure'); | ||
}, | ||
mockFilter | ||
) | ||
).rejects.toThrowError('Test failure'); | ||
}); | ||
|
||
test(`should return conflict errors when number of retries exceeds ${RETRY_IF_CONFLICTS_ATTEMPTS}`, async () => { | ||
const result = await retryIfBulkDeleteConflicts( | ||
mockLogger, | ||
getOperationConflictsTimes(RETRY_IF_CONFLICTS_ATTEMPTS + 1), | ||
mockFilter | ||
); | ||
|
||
expect(result.errors).toEqual([error409]); | ||
expect(mockLogger.warn).toBeCalledWith('Bulk delete rules conflicts, exceeded retries'); | ||
}); | ||
ymao1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
for (let i = 1; i <= RETRY_IF_CONFLICTS_ATTEMPTS; i++) { | ||
test(`should work when operation conflicts ${i} times`, async () => { | ||
const result = await retryIfBulkDeleteConflicts( | ||
mockLogger, | ||
getOperationConflictsTimes(i), | ||
mockFilter | ||
); | ||
|
||
expect(conflictOperationMock.mock.calls.length).toBe(i + 1); | ||
expect(result).toStrictEqual(mockSuccessfulResult); | ||
}); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to test for special handling for
RuleTypeDisabledError
s but there is no special handling in the route. I see there is special handling in the bulk edit route for this type of error. Should we add special handling for that error or remove this test?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wrote the new endpoint like this because the old delete endpoint does not have it. But now I see that most of endpoints catch
RuleTypeDisabledError
, so I added it: 0e7beabThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the old delete endpoint doesn't have it because the rules client does not check for
this.ruleTypeRegistry.ensureRuleTypeEnabled
during deletion, which I actually think is ok. Most likely it's in the bulk delete function because it was copied over from bulk edit? I would probably remove the check from the rules client bulk delete and remove that error handling from the route and remove this test. If for some reason, the rule type becomes disabled for license reasons, we shouldn't prevent users from deleting those rules.