From 44a46358c2e5dc8cf8332ce0d9bedd9126782a94 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 31 Mar 2021 11:55:41 +0200 Subject: [PATCH] [Snapshot Restore] Migrate to new ES client (#95499) * wip, migrated routes and plugins * refactored all ES error handling to use handleEsError and new isEsError detection * - fixed Jest tests for new es client - updated routes in light of new responses * remove unused import * remove unecessary isEsError check in rest api route handlers * mute all incorrect types from client lib using @ts-expect-error * reordered and clean up imports, removed legacy client code * update legacy test runner * updated use of legacyES Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/client/elasticsearch_sr.ts | 101 --------- .../server/lib/get_managed_policy_names.ts | 17 +- .../server/lib/get_managed_repository_name.ts | 14 +- .../plugins/snapshot_restore/server/plugin.ts | 41 +--- .../snapshot_restore/server/routes/api/app.ts | 61 +++--- .../server/routes/api/policy.test.ts | 85 +++++--- .../server/routes/api/policy.ts | 183 ++++++---------- .../server/routes/api/repositories.test.ts | 119 ++++++----- .../server/routes/api/repositories.ts | 199 +++++++----------- .../server/routes/api/restore.test.ts | 20 +- .../server/routes/api/restore.ts | 45 ++-- .../server/routes/api/snapshots.test.ts | 81 ++++--- .../server/routes/api/snapshots.ts | 98 ++++----- .../server/services/license.ts | 14 +- .../snapshot_restore/server/shared_imports.ts | 2 +- .../server/test/helpers/route_dependencies.ts | 7 +- .../server/test/helpers/router_mock.ts | 34 ++- .../plugins/snapshot_restore/server/types.ts | 22 +- .../snapshot_restore/lib/elasticsearch.ts | 49 +++-- .../api_integration/services/legacy_es.js | 3 +- .../apps/snapshot_restore/home_page.ts | 2 +- 21 files changed, 477 insertions(+), 720 deletions(-) delete mode 100644 x-pack/plugins/snapshot_restore/server/client/elasticsearch_sr.ts diff --git a/x-pack/plugins/snapshot_restore/server/client/elasticsearch_sr.ts b/x-pack/plugins/snapshot_restore/server/client/elasticsearch_sr.ts deleted file mode 100644 index e9244937e48c..000000000000 --- a/x-pack/plugins/snapshot_restore/server/client/elasticsearch_sr.ts +++ /dev/null @@ -1,101 +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. - */ - -export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { - const ca = components.clientAction.factory; - - Client.prototype.sr = components.clientAction.namespaceFactory(); - const sr = Client.prototype.sr.prototype; - - sr.policies = ca({ - urls: [ - { - fmt: '/_slm/policy', - }, - ], - method: 'GET', - }); - - sr.policy = ca({ - urls: [ - { - fmt: '/_slm/policy/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'GET', - }); - - sr.deletePolicy = ca({ - urls: [ - { - fmt: '/_slm/policy/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'DELETE', - }); - - sr.executePolicy = ca({ - urls: [ - { - fmt: '/_slm/policy/<%=name%>/_execute', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'PUT', - }); - - sr.updatePolicy = ca({ - urls: [ - { - fmt: '/_slm/policy/<%=name%>', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'PUT', - }); - - sr.executeRetention = ca({ - urls: [ - { - fmt: '/_slm/_execute_retention', - }, - ], - method: 'POST', - }); - - sr.cleanupRepository = ca({ - urls: [ - { - fmt: '/_snapshot/<%=name%>/_cleanup', - req: { - name: { - type: 'string', - }, - }, - }, - ], - method: 'POST', - }); -}; diff --git a/x-pack/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts b/x-pack/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts index 5eb669b32e08..2c3ebf2e0176 100644 --- a/x-pack/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts +++ b/x-pack/plugins/snapshot_restore/server/lib/get_managed_policy_names.ts @@ -5,17 +5,24 @@ * 2.0. */ +import type { ElasticsearchClient } from 'src/core/server'; + // Cloud has its own system for managing SLM policies and we want to make // this clear when Snapshot and Restore is used in a Cloud deployment. // Retrieve the Cloud-managed policies so that UI can switch // logical paths based on this information. -export const getManagedPolicyNames = async (callWithInternalUser: any): Promise => { +export const getManagedPolicyNames = async ( + clusterClient: ElasticsearchClient +): Promise => { try { - const { persistent, transient, defaults } = await callWithInternalUser('cluster.getSettings', { - filterPath: '*.*managed_policies', - flatSettings: true, - includeDefaults: true, + const { + body: { persistent, transient, defaults }, + } = await clusterClient.cluster.getSettings({ + filter_path: '*.*managed_policies', + flat_settings: true, + include_defaults: true, }); + const { 'cluster.metadata.managed_policies': managedPolicyNames = [] } = { ...defaults, ...persistent, diff --git a/x-pack/plugins/snapshot_restore/server/lib/get_managed_repository_name.ts b/x-pack/plugins/snapshot_restore/server/lib/get_managed_repository_name.ts index 24960120fa3b..65dc4a750c57 100644 --- a/x-pack/plugins/snapshot_restore/server/lib/get_managed_repository_name.ts +++ b/x-pack/plugins/snapshot_restore/server/lib/get_managed_repository_name.ts @@ -5,18 +5,22 @@ * 2.0. */ +import type { ElasticsearchClient } from 'src/core/server'; + // Cloud has its own system for managing snapshots and we want to make // this clear when Snapshot and Restore is used in a Cloud deployment. // Retrieve the Cloud-managed repository name so that UI can switch // logical paths based on this information. export const getManagedRepositoryName = async ( - callWithInternalUser: any + client: ElasticsearchClient ): Promise => { try { - const { persistent, transient, defaults } = await callWithInternalUser('cluster.getSettings', { - filterPath: '*.*managed_repository', - flatSettings: true, - includeDefaults: true, + const { + body: { persistent, transient, defaults }, + } = await client.cluster.getSettings({ + filter_path: '*.*managed_repository', + flat_settings: true, + include_defaults: true, }); const { 'cluster.metadata.managed_repository': managedRepositoryName = undefined } = { ...defaults, diff --git a/x-pack/plugins/snapshot_restore/server/plugin.ts b/x-pack/plugins/snapshot_restore/server/plugin.ts index c93b5dbc4c36..4414e3735959 100644 --- a/x-pack/plugins/snapshot_restore/server/plugin.ts +++ b/x-pack/plugins/snapshot_restore/server/plugin.ts @@ -6,34 +6,20 @@ */ import { i18n } from '@kbn/i18n'; -import { - CoreSetup, - ILegacyCustomClusterClient, - Plugin, - Logger, - PluginInitializerContext, -} from 'kibana/server'; +import { CoreSetup, Plugin, Logger, PluginInitializerContext } from 'kibana/server'; import { PLUGIN, APP_REQUIRED_CLUSTER_PRIVILEGES } from '../common'; import { License } from './services'; import { ApiRoutes } from './routes'; import { wrapEsError } from './lib'; -import { isEsError } from './shared_imports'; -import { elasticsearchJsPlugin } from './client/elasticsearch_sr'; -import type { Dependencies, SnapshotRestoreRequestHandlerContext } from './types'; +import { handleEsError } from './shared_imports'; +import type { Dependencies } from './types'; import { SnapshotRestoreConfig } from './config'; -async function getCustomEsClient(getStartServices: CoreSetup['getStartServices']) { - const [core] = await getStartServices(); - const esClientConfig = { plugins: [elasticsearchJsPlugin] }; - return core.elasticsearch.legacy.createClient('snapshotRestore', esClientConfig); -} - export class SnapshotRestoreServerPlugin implements Plugin { private readonly logger: Logger; private readonly apiRoutes: ApiRoutes; private readonly license: License; - private snapshotRestoreESClient?: ILegacyCustomClusterClient; constructor(private context: PluginInitializerContext) { const { logger } = this.context; @@ -52,7 +38,7 @@ export class SnapshotRestoreServerPlugin implements Plugin return; } - const router = http.createRouter(); + const router = http.createRouter(); this.license.setup( { @@ -82,17 +68,6 @@ export class SnapshotRestoreServerPlugin implements Plugin ], }); - http.registerRouteHandlerContext( - 'snapshotRestore', - async (ctx, request) => { - this.snapshotRestoreESClient = - this.snapshotRestoreESClient ?? (await getCustomEsClient(getStartServices)); - return { - client: this.snapshotRestoreESClient.asScoped(request), - }; - } - ); - this.apiRoutes.setup({ router, license: this.license, @@ -102,17 +77,11 @@ export class SnapshotRestoreServerPlugin implements Plugin isSlmEnabled: pluginConfig.slm_ui.enabled, }, lib: { - isEsError, + handleEsError, wrapEsError, }, }); } public start() {} - - public stop() { - if (this.snapshotRestoreESClient) { - this.snapshotRestoreESClient.close(); - } - } } diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/app.ts b/x-pack/plugins/snapshot_restore/server/routes/api/app.ts index c9ee33c1d387..217bce9721f6 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/app.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/app.ts @@ -27,12 +27,12 @@ export function registerAppRoutes({ router, config: { isSecurityEnabled }, license, - lib: { isEsError }, + lib: { handleEsError }, }: RouteDependencies) { router.get( { path: addBasePath('privileges'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const privilegesResult: Privileges = { hasAllPrivileges: true, @@ -48,42 +48,36 @@ export function registerAppRoutes({ } try { - // Get cluster priviliges - const { has_all_requested: hasAllPrivileges, cluster } = await callAsCurrentUser( - 'transport.request', - { - path: '/_security/user/_has_privileges', - method: 'POST', - body: { - cluster: [...APP_REQUIRED_CLUSTER_PRIVILEGES, ...APP_SLM_CLUSTER_PRIVILEGES], - }, - } - ); + // Get cluster privileges + const { + body: { has_all_requested: hasAllPrivileges, cluster }, + } = await clusterClient.asCurrentUser.security.hasPrivileges({ + body: { + cluster: [...APP_REQUIRED_CLUSTER_PRIVILEGES, ...APP_SLM_CLUSTER_PRIVILEGES], + }, + }); // Find missing cluster privileges and set overall app privileges privilegesResult.missingPrivileges.cluster = extractMissingPrivileges(cluster); privilegesResult.hasAllPrivileges = hasAllPrivileges; // Get all index privileges the user has - const { indices } = await callAsCurrentUser('transport.request', { - path: '/_security/user/_privileges', - method: 'GET', - }); + const { + body: { indices }, + } = await clusterClient.asCurrentUser.security.getUserPrivileges(); // Check if they have all the required index privileges for at least one index - const oneIndexWithAllPrivileges = indices.find( - ({ privileges }: { privileges: string[] }) => { - if (privileges.includes('all')) { - return true; - } - - const indexHasAllPrivileges = APP_RESTORE_INDEX_PRIVILEGES.every((privilege) => - privileges.includes(privilege) - ); - - return indexHasAllPrivileges; + const oneIndexWithAllPrivileges = indices.find(({ privileges }) => { + if (privileges.includes('all')) { + return true; } - ); + + const indexHasAllPrivileges = APP_RESTORE_INDEX_PRIVILEGES.every((privilege) => + privileges.includes(privilege) + ); + + return indexHasAllPrivileges; + }); // If they don't, return list of required index privileges if (!oneIndexWithAllPrivileges) { @@ -92,14 +86,7 @@ export function registerAppRoutes({ return res.ok({ body: privilegesResult }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts index ff66e020d222..5ef5f2d01b96 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.test.ts @@ -44,12 +44,23 @@ describe('[Snapshot and Restore API Routes] Policy', () => { isManagedPolicy: false, }; - const router = new RouterMock('snapshotRestore.client'); + const router = new RouterMock(); + + /** + * ES APIs used by these endpoints + */ + const getClusterSettingsFn = router.getMockApiFn('cluster.getSettings'); + const putClusterSettingsFn = router.getMockApiFn('cluster.putSettings'); + const getLifecycleFn = router.getMockApiFn('slm.getLifecycle'); + const putLifecycleFn = router.getMockApiFn('slm.putLifecycle'); + const executeLifecycleFn = router.getMockApiFn('slm.executeLifecycle'); + const deleteLifecycleFn = router.getMockApiFn('slm.deleteLifecycle'); + const resolveIndicesFn = router.getMockApiFn('indices.resolveIndex'); beforeAll(() => { registerPolicyRoutes({ - router: router as any, ...routeDependencies, + router, }); }); @@ -64,7 +75,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { fooPolicy: mockEsPolicy, barPolicy: mockEsPolicy, }; - router.callAsCurrentUserResponses = [[], mockEsResponse]; + getClusterSettingsFn.mockResolvedValue({ body: {} }); + getLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { policies: [ { @@ -84,7 +96,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return empty array if no repositories returned from ES', async () => { const mockEsResponse = {}; - router.callAsCurrentUserResponses = [[], mockEsResponse]; + getClusterSettingsFn.mockResolvedValue({ body: {} }); + getLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { policies: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse, @@ -92,11 +105,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(new Error()), // Get managed policyNames will silently fail - jest.fn().mockRejectedValueOnce(new Error()), // Call to 'sr.policies' - ]; - + getClusterSettingsFn.mockRejectedValue(new Error()); // Get managed policyNames should silently fail + getLifecycleFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -116,7 +126,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { [name]: mockEsPolicy, }; - router.callAsCurrentUserResponses = [mockEsResponse, {}]; + getLifecycleFn.mockResolvedValue({ body: mockEsResponse }); + getClusterSettingsFn.mockResolvedValue({ body: {} }); const expectedResponse = { policy: { @@ -130,14 +141,20 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }); it('should return 404 error if not returned from ES', async () => { - router.callAsCurrentUserResponses = [{}, {}]; + getLifecycleFn.mockRejectedValue({ + name: 'ResponseError', + body: {}, + statusCode: 404, + }); + getClusterSettingsFn.mockResolvedValue({}); const response = await router.runRequest(mockRequest); - expect(response.status).toBe(404); + expect(response.statusCode).toBe(404); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + getLifecycleFn.mockRejectedValueOnce(new Error('something unexpected')); + getClusterSettingsFn.mockResolvedValueOnce({ body: {} }); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -158,7 +175,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { const mockEsResponse = { snapshot_name: 'foo-policy-snapshot', }; - router.callAsCurrentUserResponses = [mockEsResponse]; + executeLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { snapshotName: 'foo-policy-snapshot', @@ -170,7 +187,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + executeLifecycleFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -189,7 +206,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return successful ES responses', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse, mockEsResponse]; + deleteLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: names, errors: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ @@ -202,10 +219,7 @@ describe('[Snapshot and Restore API Routes] Policy', () => { mockEsError.response = '{}'; mockEsError.statusCode = 500; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - jest.fn().mockRejectedValueOnce(mockEsError), - ]; + deleteLifecycleFn.mockRejectedValue(mockEsError); const expectedResponse = { itemsDeleted: [], @@ -228,10 +242,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { mockEsError.statusCode = 500; const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - mockEsResponse, - ]; + deleteLifecycleFn.mockRejectedValueOnce(mockEsError); + deleteLifecycleFn.mockResolvedValueOnce({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: [names[1]], @@ -264,7 +276,9 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [{}, mockEsResponse]; + + getLifecycleFn.mockResolvedValue({ body: {} }); + putLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { ...mockEsResponse }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ @@ -274,14 +288,15 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return error if policy with the same name already exists', async () => { const mockEsResponse = { [name]: {} }; - router.callAsCurrentUserResponses = [mockEsResponse]; + getLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const response = await router.runRequest(mockRequest); expect(response.status).toBe(409); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [{}, jest.fn().mockRejectedValueOnce(new Error())]; + getLifecycleFn.mockResolvedValue({ body: {} }); + putLifecycleFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -302,14 +317,15 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [{ [name]: {} }, mockEsResponse]; + getLifecycleFn.mockResolvedValue({ body: { [name]: {} } }); + putLifecycleFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { ...mockEsResponse }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + getLifecycleFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -343,7 +359,8 @@ describe('[Snapshot and Restore API Routes] Policy', () => { }, ], }; - router.callAsCurrentUserResponses = [mockEsResponse]; + + resolveIndicesFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { indices: ['fooIndex'], @@ -358,14 +375,14 @@ describe('[Snapshot and Restore API Routes] Policy', () => { aliases: [], data_streams: [], }; - router.callAsCurrentUserResponses = [mockEsResponse]; + resolveIndicesFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { indices: [], dataStreams: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + resolveIndicesFn.mockRejectedValueOnce(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -383,14 +400,14 @@ describe('[Snapshot and Restore API Routes] Policy', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse]; + putClusterSettingsFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { ...mockEsResponse }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + putClusterSettingsFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts index fa127880fd80..77264c4bffc9 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/policy.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { PutSnapshotLifecycleRequest } from '@elastic/elasticsearch/api/types'; import { schema, TypeOf } from '@kbn/config-schema'; import { SlmPolicyEs, PolicyIndicesResponse } from '../../../common/types'; @@ -17,21 +17,19 @@ import { nameParameterSchema, policySchema } from './validate_schemas'; export function registerPolicyRoutes({ router, license, - lib: { isEsError, wrapEsError }, + lib: { wrapEsError, handleEsError }, }: RouteDependencies) { // GET all policies router.get( { path: addBasePath('policies'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; - const managedPolicies = await getManagedPolicyNames(callAsCurrentUser); + const managedPolicies = await getManagedPolicyNames(clusterClient.asCurrentUser); try { // Get policies - const policiesByName: { - [key: string]: SlmPolicyEs; - } = await callAsCurrentUser('sr.policies', { + const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle({ human: true, }); @@ -39,19 +37,14 @@ export function registerPolicyRoutes({ return res.ok({ body: { policies: Object.entries(policiesByName).map(([name, policy]) => { - return deserializePolicy(name, policy, managedPolicies); + // TODO: Figure out why our {@link SlmPolicyEs} is not compatible with: + // import type { SnapshotLifecyclePolicyMetadata } from '@elastic/elasticsearch/api/types'; + return deserializePolicy(name, policy as SlmPolicyEs, managedPolicies); }), }, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -60,39 +53,25 @@ export function registerPolicyRoutes({ router.get( { path: addBasePath('policy/{name}'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; try { - const policiesByName: { - [key: string]: SlmPolicyEs; - } = await callAsCurrentUser('sr.policy', { - name, + const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle({ + policy_id: name, human: true, }); - if (!policiesByName[name]) { - // If policy doesn't exist, ES will return 200 with an empty object, so manually throw 404 here - return res.notFound({ body: 'Policy not found' }); - } - - const managedPolicies = await getManagedPolicyNames(callAsCurrentUser); + const managedPolicies = await getManagedPolicyNames(clusterClient.asCurrentUser); // Deserialize policy return res.ok({ body: { - policy: deserializePolicy(name, policiesByName[name], managedPolicies), + policy: deserializePolicy(name, policiesByName[name] as SlmPolicyEs, managedPolicies), }, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -101,13 +80,17 @@ export function registerPolicyRoutes({ router.post( { path: addBasePath('policies'), validate: { body: policySchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; + const policy = req.body as TypeOf; const { name } = policy; try { // Check that policy with the same name doesn't already exist - const policyByName = await callAsCurrentUser('sr.policy', { name }); + const { body: policyByName } = await clusterClient.asCurrentUser.slm.getLifecycle({ + policy_id: name, + }); + if (policyByName[name]) { return res.conflict({ body: 'There is already a policy with that name.' }); } @@ -117,21 +100,15 @@ export function registerPolicyRoutes({ try { // Otherwise create new policy - const response = await callAsCurrentUser('sr.updatePolicy', { - name, - body: serializePolicy(policy), + const response = await clusterClient.asCurrentUser.slm.putLifecycle({ + policy_id: name, + // TODO: bring {@link SlmPolicyEs['policy']} in line with {@link PutSnapshotLifecycleRequest['body']} + body: (serializePolicy(policy) as unknown) as PutSnapshotLifecycleRequest['body'], }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -143,31 +120,25 @@ export function registerPolicyRoutes({ validate: { params: nameParameterSchema, body: policySchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; const policy = req.body as TypeOf; try { // Check that policy with the given name exists // If it doesn't exist, 404 will be thrown by ES and will be returned - await callAsCurrentUser('sr.policy', { name }); + await clusterClient.asCurrentUser.slm.getLifecycle({ policy_id: name }); // Otherwise update policy - const response = await callAsCurrentUser('sr.updatePolicy', { - name, - body: serializePolicy(policy), + const response = await clusterClient.asCurrentUser.slm.putLifecycle({ + policy_id: name, + // TODO: bring {@link SlmPolicyEs['policy']} in line with {@link PutSnapshotLifecycleRequest['body']} + body: (serializePolicy(policy) as unknown) as PutSnapshotLifecycleRequest['body'], }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -176,7 +147,7 @@ export function registerPolicyRoutes({ router.delete( { path: addBasePath('policies/{name}'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; const policyNames = name.split(','); @@ -187,7 +158,8 @@ export function registerPolicyRoutes({ await Promise.all( policyNames.map((policyName) => { - return callAsCurrentUser('sr.deletePolicy', { name: policyName }) + return clusterClient.asCurrentUser.slm + .deleteLifecycle({ policy_id: policyName }) .then(() => response.itemsDeleted.push(policyName)) .catch((e) => response.errors.push({ @@ -206,23 +178,18 @@ export function registerPolicyRoutes({ router.post( { path: addBasePath('policy/{name}/run'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; try { - const { snapshot_name: snapshotName } = await callAsCurrentUser('sr.executePolicy', { - name, + const { + body: { snapshot_name: snapshotName }, + } = await clusterClient.asCurrentUser.slm.executeLifecycle({ + policy_id: name, }); return res.ok({ body: { snapshotName } }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -231,19 +198,14 @@ export function registerPolicyRoutes({ router.get( { path: addBasePath('policies/indices'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; try { - const resolvedIndicesResponse: ResolveIndexResponseFromES = await callAsCurrentUser( - 'transport.request', - { - method: 'GET', - path: `/_resolve/index/*`, - query: { - expand_wildcards: 'all', - }, - } - ); + const response = await clusterClient.asCurrentUser.indices.resolveIndex({ + name: '*', + expand_wildcards: 'all', + }); + const resolvedIndicesResponse = response.body as ResolveIndexResponseFromES; const body: PolicyIndicesResponse = { dataStreams: resolvedIndicesResponse.data_streams.map(({ name }) => name).sort(), @@ -256,14 +218,7 @@ export function registerPolicyRoutes({ body, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -272,18 +227,21 @@ export function registerPolicyRoutes({ router.get( { path: addBasePath('policies/retention_settings'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; - const { persistent, transient, defaults } = await callAsCurrentUser('cluster.getSettings', { - filterPath: '**.slm.retention*', - includeDefaults: true, + const { client: clusterClient } = ctx.core.elasticsearch; + const { + body: { persistent, transient, defaults }, + } = await clusterClient.asCurrentUser.cluster.getSettings({ + filter_path: '**.slm.retention*', + include_defaults: true, }); - const { slm: retentionSettings = undefined } = { + const { slm: retentionSettings }: { slm?: { retention_schedule: string } } = { ...defaults, ...persistent, ...transient, }; - const { retention_schedule: retentionSchedule } = retentionSettings; + const retentionSchedule = + retentionSettings != null ? retentionSettings.retention_schedule : undefined; return res.ok({ body: { retentionSchedule }, @@ -300,11 +258,11 @@ export function registerPolicyRoutes({ validate: { body: retentionSettingsSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { retentionSchedule } = req.body as TypeOf; try { - const response = await callAsCurrentUser('cluster.putSettings', { + const response = await clusterClient.asCurrentUser.cluster.putSettings({ body: { persistent: { slm: { @@ -314,16 +272,9 @@ export function registerPolicyRoutes({ }, }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -332,9 +283,9 @@ export function registerPolicyRoutes({ router.post( { path: addBasePath('policies/retention'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; - const response = await callAsCurrentUser('sr.executeRetention'); - return res.ok({ body: response }); + const { client: clusterClient } = ctx.core.elasticsearch; + const response = await clusterClient.asCurrentUser.slm.executeRetention(); + return res.ok({ body: response.body }); }) ); } diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts index 35dce2c5d558..7d14d62bfe1a 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.test.ts @@ -19,12 +19,25 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }, }; - const router = new RouterMock('snapshotRestore.client'); + const router = new RouterMock(); + + /** + * ES APIs used by these endpoints + */ + const clusterSettingsFn = router.getMockApiFn('cluster.getSettings'); + const createRepoFn = router.getMockApiFn('snapshot.createRepository'); + const getRepoFn = router.getMockApiFn('snapshot.getRepository'); + const deleteRepoFn = router.getMockApiFn('snapshot.deleteRepository'); + const getLifecycleFn = router.getMockApiFn('slm.getLifecycle'); + const getClusterSettingsFn = router.getMockApiFn('cluster.getSettings'); + const getSnapshotFn = router.getMockApiFn('snapshot.get'); + const verifyRepoFn = router.getMockApiFn('snapshot.verifyRepository'); + const catPluginsFn = router.getMockApiFn('cat.plugins'); beforeAll(() => { registerRepositoriesRoutes({ - router: router as any, ...routeDependencies, + router, }); }); @@ -48,11 +61,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }, }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockRepositoryEsResponse, - mockPolicyEsResponse, - ]; + clusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockRepositoryEsResponse }); + getLifecycleFn.mockResolvedValue({ body: mockPolicyEsResponse }); const expectedResponse = { repositories: [ @@ -85,11 +96,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }, }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockRepositoryEsResponse, - mockPolicyEsResponse, - ]; + clusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockRepositoryEsResponse }); + getLifecycleFn.mockResolvedValue({ body: mockPolicyEsResponse }); const expectedResponse = { repositories: [], @@ -103,10 +112,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - jest.fn().mockRejectedValueOnce(new Error()), - ]; + clusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -128,11 +135,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { [name]: { type: '', settings: {} }, }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockEsResponse, - {}, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockEsResponse }); + getSnapshotFn.mockResolvedValue({ body: {} }); const expectedResponse = { repository: { name, ...mockEsResponse[name] }, @@ -144,7 +149,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should return empty repository object if not returned from ES', async () => { - router.callAsCurrentUserResponses = [mockSnapshotGetManagedRepositoryEsResponse, {}, {}]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: {} }); + getSnapshotFn.mockResolvedValue({ body: {} }); const expectedResponse = { repository: {}, @@ -167,11 +174,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { ], }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockEsResponse, - mockEsSnapshotResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockEsResponse }); + getSnapshotFn.mockResolvedValue({ body: mockEsSnapshotResponse }); const expectedResponse = { repository: { name, ...mockEsResponse[name] }, @@ -190,11 +195,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }; const mockEsSnapshotError = jest.fn().mockRejectedValueOnce(new Error('snapshot error')); - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockEsResponse, - mockEsSnapshotError, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getRepoFn.mockResolvedValue({ body: mockEsResponse }); + getSnapshotFn.mockResolvedValue({ body: mockEsSnapshotError }); const expectedResponse = { repository: { name, ...mockEsResponse[name] }, @@ -208,10 +211,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - jest.fn().mockRejectedValueOnce(new Error()), - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + + getRepoFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -230,7 +232,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return repository verification response if returned from ES', async () => { const mockEsResponse = { nodes: {} }; - router.callAsCurrentUserResponses = [mockEsResponse]; + verifyRepoFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { verification: { valid: true, response: mockEsResponse }, @@ -241,7 +243,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return repository verification error if returned from ES', async () => { const mockEsResponse = { error: {}, status: 500 }; - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(mockEsResponse)]; + verifyRepoFn.mockRejectedValueOnce(mockEsResponse); const expectedResponse = { verification: { valid: false, error: mockEsResponse }, @@ -258,7 +260,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }; it('should return default types if no repository plugins returned from ES', async () => { - router.callAsCurrentUserResponses = [{}]; + catPluginsFn.mockResolvedValue({ body: {} }); const expectedResponse = [...DEFAULT_REPOSITORY_TYPES]; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); @@ -269,7 +271,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { const pluginTypes = Object.entries(REPOSITORY_PLUGINS_MAP).map(([key, value]) => value); const mockEsResponse = [...pluginNames.map((key) => ({ component: key }))]; - router.callAsCurrentUserResponses = [mockEsResponse]; + catPluginsFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = [...DEFAULT_REPOSITORY_TYPES, ...pluginTypes]; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); @@ -278,7 +280,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should not return non-repository plugins returned from ES', async () => { const pluginNames = ['foo-plugin', 'bar-plugin']; const mockEsResponse = [...pluginNames.map((key) => ({ component: key }))]; - router.callAsCurrentUserResponses = [mockEsResponse]; + catPluginsFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = [...DEFAULT_REPOSITORY_TYPES]; @@ -286,11 +288,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(new Error('Error getting pluggins')), - ]; + catPluginsFn.mockRejectedValueOnce(new Error('Error getting plugins')); - await expect(router.runRequest(mockRequest)).rejects.toThrowError('Error getting pluggins'); + await expect(router.runRequest(mockRequest)).rejects.toThrowError('Error getting plugins'); }); }); @@ -307,7 +307,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [{}, mockEsResponse]; + getRepoFn.mockResolvedValue({ body: {} }); + createRepoFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = { ...mockEsResponse }; @@ -315,15 +316,15 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should return error if repository with the same name already exists', async () => { - router.callAsCurrentUserResponses = [{ [name]: {} }]; - + getRepoFn.mockResolvedValue({ body: { [name]: {} } }); const response = await router.runRequest(mockRequest); expect(response.status).toBe(409); }); it('should throw if ES error', async () => { const error = new Error('Oh no!'); - router.callAsCurrentUserResponses = [{}, jest.fn().mockRejectedValueOnce(error)]; + getRepoFn.mockResolvedValue({ body: {} }); + createRepoFn.mockRejectedValue(error); await expect(router.runRequest(mockRequest)).rejects.toThrowError(error); }); @@ -344,7 +345,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return successful ES response', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [{ [name]: {} }, mockEsResponse]; + getRepoFn.mockResolvedValue({ body: { [name]: {} } }); + createRepoFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = mockEsResponse; @@ -352,7 +354,7 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + getRepoFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -369,7 +371,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { it('should return successful ES responses', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse, mockEsResponse]; + deleteRepoFn.mockResolvedValueOnce({ body: mockEsResponse }); + deleteRepoFn.mockResolvedValueOnce({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: names, errors: [] }; await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: expectedResponse }); @@ -380,10 +383,8 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { mockEsError.response = '{}'; mockEsError.statusCode = 500; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - jest.fn().mockRejectedValueOnce(mockEsError), - ]; + deleteRepoFn.mockRejectedValueOnce(mockEsError); + deleteRepoFn.mockRejectedValueOnce(mockEsError); const expectedResponse = { itemsDeleted: [], @@ -402,11 +403,9 @@ describe('[Snapshot and Restore API Routes] Repositories', () => { mockEsError.response = '{}'; mockEsError.statusCode = 500; const mockEsResponse = { acknowledged: true }; + const responses = [Promise.reject(mockEsError), Promise.resolve({ body: mockEsResponse })]; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - mockEsResponse, - ]; + deleteRepoFn.mockImplementation(() => responses.shift()); const expectedResponse = { itemsDeleted: [names[1]], diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts index c9945bb172e6..96099e3fbb1e 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/repositories.ts @@ -6,9 +6,10 @@ */ import { TypeOf } from '@kbn/config-schema'; +import type { SnapshotRepositorySettings } from '@elastic/elasticsearch/api/types'; import { DEFAULT_REPOSITORY_TYPES, REPOSITORY_PLUGINS_MAP } from '../../../common/constants'; -import { Repository, RepositoryType, SlmPolicyEs } from '../../../common/types'; +import { Repository, RepositoryType } from '../../../common/types'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../helpers'; import { nameParameterSchema, repositorySchema } from './validate_schemas'; @@ -28,21 +29,23 @@ export function registerRepositoriesRoutes({ router, license, config: { isCloudEnabled }, - lib: { isEsError, wrapEsError }, + lib: { wrapEsError, handleEsError }, }: RouteDependencies) { // GET all repositories router.get( { path: addBasePath('repositories'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; - const managedRepositoryName = await getManagedRepositoryName(callAsCurrentUser); + const { client: clusterClient } = ctx.core.elasticsearch; + const managedRepositoryName = await getManagedRepositoryName(clusterClient.asCurrentUser); let repositoryNames: string[] | undefined; let repositories: Repository[]; let managedRepository: ManagedRepository; try { - const repositoriesByName = await callAsCurrentUser('snapshot.getRepository', { + const { + body: repositoriesByName, + } = await clusterClient.asCurrentUser.snapshot.getRepository({ repository: '_all', }); repositoryNames = Object.keys(repositoriesByName); @@ -52,29 +55,20 @@ export function registerRepositoriesRoutes({ name, type, settings: deserializeRepositorySettings(settings), - }; + } as Repository; }); managedRepository = { name: managedRepositoryName, }; } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } // If a managed repository, we also need to check if a policy is associated to it if (managedRepositoryName) { try { - const policiesByName: { - [key: string]: SlmPolicyEs; - } = await callAsCurrentUser('sr.policies', { + const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle({ human: true, }); @@ -102,45 +96,28 @@ export function registerRepositoriesRoutes({ router.get( { path: addBasePath('repositories/{name}'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; - const managedRepository = await getManagedRepositoryName(callAsCurrentUser); + const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser); let repositoryByName: any; try { - repositoryByName = await callAsCurrentUser('snapshot.getRepository', { + ({ body: repositoryByName } = await clusterClient.asCurrentUser.snapshot.getRepository({ repository: name, - }); + })); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } - const { - responses: snapshotResponses, - }: { - responses: Array<{ - repository: string; - snapshots: any[]; - }>; - } = await callAsCurrentUser('snapshot.get', { + const response = await clusterClient.asCurrentUser.snapshot.get({ repository: name, snapshot: '_all', - }).catch((e) => ({ - responses: [ - { - snapshots: null, - }, - ], - })); + }); + + // @ts-expect-error @elastic/elasticsearch remove this "as unknown" workaround when the types for this endpoint are correct. Track progress at https://github.com/elastic/elastic-client-generator/issues/250. + const { responses: snapshotResponses } = response.body; if (repositoryByName[name]) { const { type = '', settings = {} } = repositoryByName[name]; @@ -176,18 +153,20 @@ export function registerRepositoriesRoutes({ router.get( { path: addBasePath('repository_types'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; // In ECE/ESS, do not enable the default types const types: RepositoryType[] = isCloudEnabled ? [] : [...DEFAULT_REPOSITORY_TYPES]; try { // Call with internal user so that the requesting user does not need `monitoring` cluster // privilege just to see list of available repository types - const plugins: any[] = await callAsCurrentUser('cat.plugins', { format: 'json' }); + const { body: plugins } = await clusterClient.asCurrentUser.cat.plugins({ format: 'json' }); // Filter list of plugins to repository-related ones if (plugins && plugins.length) { - const pluginNames: string[] = [...new Set(plugins.map((plugin) => plugin.component))]; + const pluginNames: string[] = [ + ...new Set(plugins.map((plugin) => plugin.component ?? '')), + ]; pluginNames.forEach((pluginName) => { if (REPOSITORY_PLUGINS_MAP[pluginName]) { types.push(REPOSITORY_PLUGINS_MAP[pluginName]); @@ -196,14 +175,7 @@ export function registerRepositoriesRoutes({ } return res.ok({ body: types }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -215,20 +187,24 @@ export function registerRepositoriesRoutes({ validate: { params: nameParameterSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; try { - const verificationResults = await callAsCurrentUser('snapshot.verifyRepository', { - repository: name, - }).catch((e) => ({ - valid: false, - error: e.response ? JSON.parse(e.response) : e, - })); + const { body: verificationResults } = await clusterClient.asCurrentUser.snapshot + .verifyRepository({ + repository: name, + }) + .catch((e) => ({ + body: { + valid: false, + error: e.response ? JSON.parse(e.response) : e, + }, + })); return res.ok({ body: { - verification: verificationResults.error + verification: (verificationResults as { error?: Error }).error ? verificationResults : { valid: true, @@ -237,14 +213,7 @@ export function registerRepositoriesRoutes({ }, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -256,20 +225,24 @@ export function registerRepositoriesRoutes({ validate: { params: nameParameterSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; try { - const cleanupResults = await callAsCurrentUser('sr.cleanupRepository', { - name, - }).catch((e) => ({ - cleaned: false, - error: e.response ? JSON.parse(e.response) : e, - })); + const { body: cleanupResults } = await clusterClient.asCurrentUser.snapshot + .cleanupRepository({ + repository: name, + }) + .catch((e) => ({ + body: { + cleaned: false, + error: e.response ? JSON.parse(e.response) : e, + }, + })); return res.ok({ body: { - cleanup: cleanupResults.error + cleanup: (cleanupResults as { error?: Error }).error ? cleanupResults : { cleaned: true, @@ -278,14 +251,7 @@ export function registerRepositoriesRoutes({ }, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -294,14 +260,16 @@ export function registerRepositoriesRoutes({ router.put( { path: addBasePath('repositories'), validate: { body: repositorySchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name = '', type = '', settings = {} } = req.body as TypeOf; // Check that repository with the same name doesn't already exist try { - const repositoryByName = await callAsCurrentUser('snapshot.getRepository', { - repository: name, - }); + const { body: repositoryByName } = await clusterClient.asCurrentUser.snapshot.getRepository( + { + repository: name, + } + ); if (repositoryByName[name]) { return res.conflict({ body: 'There is already a repository with that name.' }); } @@ -311,25 +279,19 @@ export function registerRepositoriesRoutes({ // Otherwise create new repository try { - const response = await callAsCurrentUser('snapshot.createRepository', { + const response = await clusterClient.asCurrentUser.snapshot.createRepository({ repository: name, body: { type, - settings: serializeRepositorySettings(settings), + // TODO: Bring {@link RepositorySettings} in line with {@link SnapshotRepositorySettings} + settings: serializeRepositorySettings(settings) as SnapshotRepositorySettings, }, verify: false, }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -341,37 +303,30 @@ export function registerRepositoriesRoutes({ validate: { body: repositorySchema, params: nameParameterSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; const { type = '', settings = {} } = req.body as TypeOf; try { // Check that repository with the given name exists // If it doesn't exist, 404 will be thrown by ES and will be returned - await callAsCurrentUser('snapshot.getRepository', { repository: name }); + await clusterClient.asCurrentUser.snapshot.getRepository({ repository: name }); // Otherwise update repository - const response = await callAsCurrentUser('snapshot.createRepository', { + const response = await clusterClient.asCurrentUser.snapshot.createRepository({ repository: name, body: { type, - settings: serializeRepositorySettings(settings), + settings: serializeRepositorySettings(settings) as SnapshotRepositorySettings, }, verify: false, }); return res.ok({ - body: response, + body: response.body, }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -380,7 +335,7 @@ export function registerRepositoriesRoutes({ router.delete( { path: addBasePath('repositories/{name}'), validate: { params: nameParameterSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { name } = req.params as TypeOf; const repositoryNames = name.split(','); @@ -392,7 +347,8 @@ export function registerRepositoriesRoutes({ try { await Promise.all( repositoryNames.map((repoName) => { - return callAsCurrentUser('snapshot.deleteRepository', { repository: repoName }) + return clusterClient.asCurrentUser.snapshot + .deleteRepository({ repository: repoName }) .then(() => response.itemsDeleted.push(repoName)) .catch((e) => response.errors.push({ @@ -405,14 +361,7 @@ export function registerRepositoriesRoutes({ return res.ok({ body: response }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts index fe33331522da..a6f6924aaae3 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/restore.test.ts @@ -17,15 +17,21 @@ describe('[Snapshot and Restore API Routes] Restore', () => { index: { size: {}, files: {} }, }; - const router = new RouterMock('snapshotRestore.client'); + const router = new RouterMock(); beforeAll(() => { registerRestoreRoutes({ - router: router as any, ...routeDependencies, + router, }); }); + /** + * ES APIs used by these endpoints + */ + const indicesRecoveryFn = router.getMockApiFn('indices.recovery'); + const restoreSnapshotFn = router.getMockApiFn('snapshot.restore'); + describe('Restore snapshot', () => { const mockRequest: RequestMock = { method: 'post', @@ -39,7 +45,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { it('should return successful response from ES', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse]; + restoreSnapshotFn.mockResolvedValue({ body: mockEsResponse }); await expect(router.runRequest(mockRequest)).resolves.toEqual({ body: mockEsResponse, @@ -47,7 +53,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + restoreSnapshotFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); @@ -76,7 +82,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { }, }; - router.callAsCurrentUserResponses = [mockEsResponse]; + indicesRecoveryFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse = [ { @@ -100,7 +106,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { it('should return empty array if no repositories returned from ES', async () => { const mockEsResponse = {}; - router.callAsCurrentUserResponses = [mockEsResponse]; + indicesRecoveryFn.mockResolvedValue({ body: mockEsResponse }); const expectedResponse: any[] = []; await expect(router.runRequest(mockRequest)).resolves.toEqual({ @@ -109,7 +115,7 @@ describe('[Snapshot and Restore API Routes] Restore', () => { }); it('should throw if ES error', async () => { - router.callAsCurrentUserResponses = [jest.fn().mockRejectedValueOnce(new Error())]; + indicesRecoveryFn.mockRejectedValue(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); }); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts b/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts index c4300bafc75f..b7281fee04c5 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/restore.ts @@ -6,6 +6,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { RestoreRequest } from '@elastic/elasticsearch/api/types'; import { SnapshotRestore, SnapshotRestoreShardEs } from '../../../common/types'; import { serializeRestoreSettings } from '../../../common/lib'; @@ -14,20 +15,20 @@ import { RouteDependencies } from '../../types'; import { addBasePath } from '../helpers'; import { restoreSettingsSchema } from './validate_schemas'; -export function registerRestoreRoutes({ router, license, lib: { isEsError } }: RouteDependencies) { +export function registerRestoreRoutes({ + router, + license, + lib: { handleEsError }, +}: RouteDependencies) { // GET all snapshot restores router.get( { path: addBasePath('restores'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; try { const snapshotRestores: SnapshotRestore[] = []; - const recoveryByIndexName: { - [key: string]: { - shards: SnapshotRestoreShardEs[]; - }; - } = await callAsCurrentUser('indices.recovery', { + const { body: recoveryByIndexName } = await clusterClient.asCurrentUser.indices.recovery({ human: true, }); @@ -40,7 +41,8 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R .filter((shard) => shard.type === 'SNAPSHOT') .sort((a, b) => a.id - b.id) .map((shard) => { - const deserializedShard = deserializeRestoreShard(shard); + // TODO: Bring {@link SnapshotRestoreShardEs} in line with {@link ShardRecovery} + const deserializedShard = deserializeRestoreShard(shard as SnapshotRestoreShardEs); const { startTimeInMillis, stopTimeInMillis } = deserializedShard; // Set overall latest activity time @@ -80,14 +82,7 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R return res.ok({ body: snapshotRestores }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -104,27 +99,21 @@ export function registerRestoreRoutes({ router, license, lib: { isEsError } }: R validate: { body: restoreSettingsSchema, params: restoreParamsSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { repository, snapshot } = req.params as TypeOf; const restoreSettings = req.body as TypeOf; try { - const response = await callAsCurrentUser('snapshot.restore', { + const response = await clusterClient.asCurrentUser.snapshot.restore({ repository, snapshot, - body: serializeRestoreSettings(restoreSettings), + // TODO: Bring {@link RestoreSettingsEs} in line with {@link RestoreRequest['body']} + body: serializeRestoreSettings(restoreSettings) as RestoreRequest['body'], }); - return res.ok({ body: response }); + return res.ok({ body: response.body }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts index 97eb34b4aaa7..bd7dffe987fe 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.test.ts @@ -29,12 +29,21 @@ const defaultSnapshot = { }; describe('[Snapshot and Restore API Routes] Snapshots', () => { - const router = new RouterMock('snapshotRestore.client'); + const router = new RouterMock(); + + /** + * ES APIs used by these endpoints + */ + const getClusterSettingsFn = router.getMockApiFn('cluster.getSettings'); + const getLifecycleFn = router.getMockApiFn('slm.getLifecycle'); + const getRepoFn = router.getMockApiFn('snapshot.getRepository'); + const getSnapshotFn = router.getMockApiFn('snapshot.get'); + const deleteSnapshotFn = router.getMockApiFn('snapshot.delete'); beforeAll(() => { registerSnapshotsRoutes({ - router: router as any, ...routeDependencies, + router, }); }); @@ -60,31 +69,29 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { barRepository: {}, }; - const mockGetSnapshotsFooResponse = Promise.resolve({ + const mockGetSnapshotsFooResponse = { responses: [ { repository: 'fooRepository', snapshots: [{ snapshot: 'snapshot1' }], }, ], - }); + }; - const mockGetSnapshotsBarResponse = Promise.resolve({ + const mockGetSnapshotsBarResponse = { responses: [ { repository: 'barRepository', snapshots: [{ snapshot: 'snapshot2' }], }, ], - }); + }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockSnapshotGetPolicyEsResponse, - mockSnapshotGetRepositoryEsResponse, - mockGetSnapshotsFooResponse, - mockGetSnapshotsBarResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getLifecycleFn.mockResolvedValue({ body: mockSnapshotGetPolicyEsResponse }); + getRepoFn.mockResolvedValue({ body: mockSnapshotGetRepositoryEsResponse }); + getSnapshotFn.mockResolvedValueOnce({ body: mockGetSnapshotsFooResponse }); + getSnapshotFn.mockResolvedValueOnce({ body: mockGetSnapshotsBarResponse }); const expectedResponse = { errors: {}, @@ -120,11 +127,9 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { const mockSnapshotGetPolicyEsResponse = {}; const mockSnapshotGetRepositoryEsResponse = {}; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockSnapshotGetPolicyEsResponse, - mockSnapshotGetRepositoryEsResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getLifecycleFn.mockResolvedValue({ body: mockSnapshotGetPolicyEsResponse }); + getRepoFn.mockResolvedValue({ body: mockSnapshotGetRepositoryEsResponse }); const expectedResponse = { errors: [], @@ -138,11 +143,9 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { }); test('throws if ES error', async () => { - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(new Error('Error getting managed repository')), - jest.fn().mockRejectedValueOnce(new Error('Error getting policies')), - jest.fn().mockRejectedValueOnce(new Error('Error getting repository')), - ]; + getClusterSettingsFn.mockRejectedValueOnce(new Error()); + getLifecycleFn.mockRejectedValueOnce(new Error()); + getRepoFn.mockRejectedValueOnce(new Error()); await expect(router.runRequest(mockRequest)).rejects.toThrowError(); }); @@ -177,10 +180,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { ], }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockSnapshotGetEsResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getSnapshotFn.mockResolvedValueOnce({ body: mockSnapshotGetEsResponse }); const expectedResponse = { ...defaultSnapshot, @@ -215,12 +216,13 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { ], }; - router.callAsCurrentUserResponses = [ - mockSnapshotGetManagedRepositoryEsResponse, - mockSnapshotGetEsResponse, - ]; + getClusterSettingsFn.mockResolvedValue({ body: mockSnapshotGetManagedRepositoryEsResponse }); + getSnapshotFn.mockResolvedValueOnce({ body: mockSnapshotGetEsResponse }); - await expect(router.runRequest(mockRequest)).rejects.toThrowError(); + await expect(router.runRequest(mockRequest)).resolves.toEqual({ + body: 'Snapshot not found', + status: 404, + }); }); }); @@ -243,7 +245,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { it('should return successful ES responses', async () => { const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [mockEsResponse, mockEsResponse]; + deleteSnapshotFn.mockResolvedValueOnce({ body: mockEsResponse }); + deleteSnapshotFn.mockResolvedValueOnce({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: [ @@ -261,10 +264,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { mockEsError.response = '{}'; mockEsError.statusCode = 500; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - jest.fn().mockRejectedValueOnce(mockEsError), - ]; + deleteSnapshotFn.mockRejectedValueOnce(mockEsError); + deleteSnapshotFn.mockRejectedValueOnce(mockEsError); const expectedResponse = { itemsDeleted: [], @@ -289,10 +290,8 @@ describe('[Snapshot and Restore API Routes] Snapshots', () => { mockEsError.statusCode = 500; const mockEsResponse = { acknowledged: true }; - router.callAsCurrentUserResponses = [ - jest.fn().mockRejectedValueOnce(mockEsError), - mockEsResponse, - ]; + deleteSnapshotFn.mockRejectedValueOnce(mockEsError); + deleteSnapshotFn.mockResolvedValueOnce({ body: mockEsResponse }); const expectedResponse = { itemsDeleted: [{ snapshot: 'snapshot-2', repository: 'barRepository' }], diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts index 03e3b4ecc088..8f6f44f63a55 100644 --- a/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts +++ b/x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts @@ -6,31 +6,31 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { RouteDependencies } from '../../types'; -import { addBasePath } from '../helpers'; -import { SnapshotDetails, SnapshotDetailsEs } from '../../../common/types'; +import type { SnapshotDetails, SnapshotDetailsEs } from '../../../common/types'; import { deserializeSnapshotDetails } from '../../../common/lib'; +import type { RouteDependencies } from '../../types'; import { getManagedRepositoryName } from '../../lib'; +import { addBasePath } from '../helpers'; export function registerSnapshotsRoutes({ router, license, - lib: { isEsError, wrapEsError }, + lib: { wrapEsError, handleEsError }, }: RouteDependencies) { // GET all snapshots router.get( { path: addBasePath('snapshots'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; - const managedRepository = await getManagedRepositoryName(callAsCurrentUser); + const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser); let policies: string[] = []; // Attempt to retrieve policies // This could fail if user doesn't have access to read SLM policies try { - const policiesByName = await callAsCurrentUser('sr.policies'); + const { body: policiesByName } = await clusterClient.asCurrentUser.slm.getLifecycle(); policies = Object.keys(policiesByName); } catch (e) { // Silently swallow error as policy names aren't required in UI @@ -44,7 +44,9 @@ export function registerSnapshotsRoutes({ let repositoryNames: string[]; try { - const repositoriesByName = await callAsCurrentUser('snapshot.getRepository', { + const { + body: repositoriesByName, + } = await clusterClient.asCurrentUser.snapshot.getRepository({ repository: '_all', }); repositoryNames = Object.keys(repositoriesByName); @@ -55,13 +57,7 @@ export function registerSnapshotsRoutes({ }); } } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - throw e; + return handleEsError({ error: e, response: res }); } const snapshots: SnapshotDetails[] = []; @@ -71,23 +67,27 @@ export function registerSnapshotsRoutes({ const fetchSnapshotsForRepository = async (repository: string) => { try { // If any of these repositories 504 they will cost the request significant time. - const { - responses: fetchedResponses, - }: { - responses: Array<{ - repository: 'string'; - snapshots: SnapshotDetailsEs[]; - }>; - } = await callAsCurrentUser('snapshot.get', { + const response = await clusterClient.asCurrentUser.snapshot.get({ repository, snapshot: '_all', ignore_unavailable: true, // Allow request to succeed even if some snapshots are unavailable. }); + // @ts-expect-error @elastic/elasticsearch remove this "as unknown" workaround when the types for this endpoint are correct. Track progress at https://github.com/elastic/elastic-client-generator/issues/250. + const { responses: fetchedResponses } = response.body; + // Decorate each snapshot with the repository with which it's associated. + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client fetchedResponses.forEach(({ snapshots: fetchedSnapshots }) => { + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client fetchedSnapshots.forEach((snapshot) => { - snapshots.push(deserializeSnapshotDetails(repository, snapshot, managedRepository)); + snapshots.push( + deserializeSnapshotDetails( + repository, + snapshot as SnapshotDetailsEs, + managedRepository + ) + ); }); }); @@ -124,28 +124,27 @@ export function registerSnapshotsRoutes({ validate: { params: getOneParamsSchema }, }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const { repository, snapshot } = req.params as TypeOf; - const managedRepository = await getManagedRepositoryName(callAsCurrentUser); + const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser); try { - const { - responses: snapshotsResponse, - }: { - responses: Array<{ - repository: string; - snapshots: SnapshotDetailsEs[]; - error?: any; - }>; - } = await callAsCurrentUser('snapshot.get', { + const response = await clusterClient.asCurrentUser.snapshot.get({ repository, snapshot: '_all', ignore_unavailable: true, }); + // @ts-expect-error @elastic/elasticsearch remove this "as unknown" workaround when the types for this endpoint are correct. Track progress at https://github.com/elastic/elastic-client-generator/issues/250. + const { responses: snapshotsResponse } = response.body; + const snapshotsList = snapshotsResponse && snapshotsResponse[0] && snapshotsResponse[0].snapshots; + if (!snapshotsList || snapshotsList.length === 0) { + return res.notFound({ body: 'Snapshot not found' }); + } const selectedSnapshot = snapshotsList.find( + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client ({ snapshot: snapshotName }) => snapshot === snapshotName ) as SnapshotDetailsEs; @@ -155,10 +154,12 @@ export function registerSnapshotsRoutes({ } const successfulSnapshots = snapshotsList + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client .filter(({ state }) => state === 'SUCCESS') + // @ts-expect-error @elastic/elasticsearch related to above incorrect type from client .sort((a, b) => { - return +new Date(b.end_time) - +new Date(a.end_time); - }); + return +new Date(b.end_time!) - +new Date(a.end_time!); + }) as SnapshotDetailsEs[]; return res.ok({ body: deserializeSnapshotDetails( @@ -169,14 +170,7 @@ export function registerSnapshotsRoutes({ ), }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); @@ -192,7 +186,7 @@ export function registerSnapshotsRoutes({ router.post( { path: addBasePath('snapshots/bulk_delete'), validate: { body: deleteSchema } }, license.guardApiRoute(async (ctx, req, res) => { - const { callAsCurrentUser } = ctx.snapshotRestore!.client; + const { client: clusterClient } = ctx.core.elasticsearch; const response: { itemsDeleted: Array<{ snapshot: string; repository: string }>; @@ -210,7 +204,8 @@ export function registerSnapshotsRoutes({ for (let i = 0; i < snapshots.length; i++) { const { snapshot, repository } = snapshots[i]; - await callAsCurrentUser('snapshot.delete', { snapshot, repository }) + await clusterClient.asCurrentUser.snapshot + .delete({ snapshot, repository }) .then(() => response.itemsDeleted.push({ snapshot, repository })) .catch((e) => response.errors.push({ @@ -222,14 +217,7 @@ export function registerSnapshotsRoutes({ return res.ok({ body: response }); } catch (e) { - if (isEsError(e)) { - return res.customError({ - statusCode: e.statusCode, - body: e, - }); - } - // Case: default - throw e; + return handleEsError({ error: e, response: res }); } }) ); diff --git a/x-pack/plugins/snapshot_restore/server/services/license.ts b/x-pack/plugins/snapshot_restore/server/services/license.ts index 93cf86eae535..e209edcd899b 100644 --- a/x-pack/plugins/snapshot_restore/server/services/license.ts +++ b/x-pack/plugins/snapshot_restore/server/services/license.ts @@ -6,11 +6,15 @@ */ import { Logger } from 'src/core/server'; -import type { KibanaRequest, KibanaResponseFactory, RequestHandler } from 'kibana/server'; +import type { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'kibana/server'; import { LicensingPluginSetup } from '../../../licensing/server'; import { LicenseType } from '../../../licensing/common/types'; -import type { SnapshotRestoreRequestHandlerContext } from '../types'; export interface LicenseStatus { isValid: boolean; @@ -51,13 +55,11 @@ export class License { }); } - guardApiRoute( - handler: RequestHandler - ) { + guardApiRoute(handler: RequestHandler) { const license = this; return function licenseCheck( - ctx: Context, + ctx: RequestHandlerContext, request: KibanaRequest, response: KibanaResponseFactory ) { diff --git a/x-pack/plugins/snapshot_restore/server/shared_imports.ts b/x-pack/plugins/snapshot_restore/server/shared_imports.ts index df9b3dd53cc1..7f55d189457c 100644 --- a/x-pack/plugins/snapshot_restore/server/shared_imports.ts +++ b/x-pack/plugins/snapshot_restore/server/shared_imports.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { isEsError } from '../../../../src/plugins/es_ui_shared/server'; +export { handleEsError } from '../../../../src/plugins/es_ui_shared/server'; diff --git a/x-pack/plugins/snapshot_restore/server/test/helpers/route_dependencies.ts b/x-pack/plugins/snapshot_restore/server/test/helpers/route_dependencies.ts index 6bd7c10497b2..77c8ab4759b5 100644 --- a/x-pack/plugins/snapshot_restore/server/test/helpers/route_dependencies.ts +++ b/x-pack/plugins/snapshot_restore/server/test/helpers/route_dependencies.ts @@ -6,13 +6,14 @@ */ import { License } from '../../services'; +import { handleEsError } from '../../shared_imports'; import { wrapEsError } from '../../lib'; -import { isEsError } from '../../shared_imports'; +import type { RouteDependencies } from '../../types'; const license = new License(); license.getStatus = jest.fn().mockReturnValue({ isValid: true }); -export const routeDependencies = { +export const routeDependencies: Omit = { license, config: { isSecurityEnabled: jest.fn().mockReturnValue(true), @@ -20,7 +21,7 @@ export const routeDependencies = { isSlmEnabled: true, }, lib: { - isEsError, wrapEsError, + handleEsError, }, }; diff --git a/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts b/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts index 656301abc535..efd0ebd0fd1c 100644 --- a/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts +++ b/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { set } from '@elastic/safer-lodash-set'; +import type { IRouter } from 'src/core/server'; +import { get } from 'lodash'; + +import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; type RequestHandler = (...params: any[]) => any; @@ -48,7 +51,7 @@ export interface RequestMock { [key: string]: any; } -export class RouterMock { +export class RouterMock implements IRouter { /** * Cache to keep a reference to all the request handler defined on the router for each HTTP method and path */ @@ -60,15 +63,13 @@ export class RouterMock { patch: {}, }; - private _callAsCurrentUserCallCount = 0; - private _callAsCurrentUserResponses: any[] = []; - private contextMock = {}; + public contextMock = { + core: { elasticsearch: { client: elasticsearchServiceMock.createScopedClusterClient() } }, + }; - constructor(pathToESclient = 'core.elasticsearch.dataClient') { - set(this.contextMock, pathToESclient, { - callAsCurrentUser: this.callAsCurrentUser.bind(this), - }); - } + getRoutes = jest.fn(); + handleLegacyErrors = jest.fn(); + routerPath = ''; get({ path }: { path: string }, handler: RequestHandler) { this.cacheHandlers.get[path] = handler; @@ -90,17 +91,8 @@ export class RouterMock { this.cacheHandlers.patch[path] = handler; } - private callAsCurrentUser() { - const index = this._callAsCurrentUserCallCount; - this._callAsCurrentUserCallCount += 1; - const response = this._callAsCurrentUserResponses[index]; - - return typeof response === 'function' ? Promise.resolve(response()) : Promise.resolve(response); - } - - public set callAsCurrentUserResponses(responses: any[]) { - this._callAsCurrentUserCallCount = 0; - this._callAsCurrentUserResponses = responses; + getMockApiFn(path: string): jest.Mock { + return get(this.contextMock.core.elasticsearch.client.asCurrentUser, path); } runRequest({ method, path, ...mockRequest }: RequestMock) { diff --git a/x-pack/plugins/snapshot_restore/server/types.ts b/x-pack/plugins/snapshot_restore/server/types.ts index c92de645aa2d..8c2ad74865e4 100644 --- a/x-pack/plugins/snapshot_restore/server/types.ts +++ b/x-pack/plugins/snapshot_restore/server/types.ts @@ -5,19 +5,14 @@ * 2.0. */ -import type { - LegacyScopedClusterClient, - ILegacyScopedClusterClient, - IRouter, - RequestHandlerContext, -} from 'src/core/server'; +import type { IRouter, RequestHandlerContext, IScopedClusterClient } from 'src/core/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; import { CloudSetup } from '../../cloud/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { License } from './services'; import { wrapEsError } from './lib'; -import { isEsError } from './shared_imports'; +import { handleEsError } from './shared_imports'; export interface Dependencies { licensing: LicensingPluginSetup; @@ -27,7 +22,7 @@ export interface Dependencies { } export interface RouteDependencies { - router: SnapshotRestoreRouter; + router: IRouter; license: License; config: { isSlmEnabled: boolean; @@ -35,8 +30,8 @@ export interface RouteDependencies { isCloudEnabled: boolean; }; lib: { - isEsError: typeof isEsError; wrapEsError: typeof wrapEsError; + handleEsError: typeof handleEsError; }; } @@ -56,13 +51,13 @@ export interface ResolveIndexResponseFromES { data_streams: Array<{ name: string; backing_indices: string[]; timestamp_field: string }>; } -export type CallAsCurrentUser = LegacyScopedClusterClient['callAsCurrentUser']; +export type CallAsCurrentUser = IScopedClusterClient['asCurrentUser']; /** * @internal */ export interface SnapshotRestoreContext { - client: ILegacyScopedClusterClient; + client: IScopedClusterClient; } /** @@ -71,8 +66,3 @@ export interface SnapshotRestoreContext { export interface SnapshotRestoreRequestHandlerContext extends RequestHandlerContext { snapshotRestore: SnapshotRestoreContext; } - -/** - * @internal - */ -export type SnapshotRestoreRouter = IRouter; diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts index 06ea5dc800e4..9b4d39a3b10b 100644 --- a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts +++ b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts @@ -14,7 +14,7 @@ interface SlmPolicy { repository: string; isManagedPolicy: boolean; config?: { - indices?: string | string[]; + indices: string | string[]; ignoreUnavailable?: boolean; includeGlobalState?: boolean; partial?: boolean; @@ -36,19 +36,21 @@ interface SlmPolicy { export const registerEsHelpers = (getService: FtrProviderContext['getService']) => { let policiesCreated: string[] = []; - const es = getService('legacyEs'); + const es = getService('es'); const createRepository = (repoName: string) => { - return es.snapshot.createRepository({ - repository: repoName, - body: { - type: 'fs', - settings: { - location: '/tmp/', + return es.snapshot + .createRepository({ + repository: repoName, + body: { + type: 'fs', + settings: { + location: '/tmp/', + }, }, - }, - verify: false, - }); + verify: false, + }) + .then(({ body }) => body); }; const createPolicy = (policy: SlmPolicy, cachePolicy?: boolean) => { @@ -56,20 +58,27 @@ export const registerEsHelpers = (getService: FtrProviderContext['getService']) policiesCreated.push(policy.name); } - return es.sr.updatePolicy({ - name: policy.name, - body: policy, - }); + return es.slm + .putLifecycle({ + policy_id: policy.name, + // TODO: bring {@link SlmPolicy} in line with {@link PutSnapshotLifecycleRequest['body']} + // @ts-expect-error + body: policy, + }) + .then(({ body }) => body); }; const getPolicy = (policyName: string) => { - return es.sr.policy({ - name: policyName, - human: true, - }); + return es.slm + .getLifecycle({ + policy_id: policyName, + human: true, + }) + .then(({ body }) => body); }; - const deletePolicy = (policyName: string) => es.sr.deletePolicy({ name: policyName }); + const deletePolicy = (policyName: string) => + es.slm.deleteLifecycle({ policy_id: policyName }).then(({ body }) => body); const cleanupPolicies = () => Promise.all(policiesCreated.map(deletePolicy)) diff --git a/x-pack/test/api_integration/services/legacy_es.js b/x-pack/test/api_integration/services/legacy_es.js index 49482292bfb2..0b02d394b107 100644 --- a/x-pack/test/api_integration/services/legacy_es.js +++ b/x-pack/test/api_integration/services/legacy_es.js @@ -10,7 +10,6 @@ import { format as formatUrl } from 'url'; import * as legacyElasticsearch from 'elasticsearch'; import { elasticsearchJsPlugin as indexManagementEsClientPlugin } from '../../../plugins/index_management/server/client/elasticsearch'; -import { elasticsearchJsPlugin as snapshotRestoreEsClientPlugin } from '../../../plugins/snapshot_restore/server/client/elasticsearch_sr'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config'; @@ -21,6 +20,6 @@ export function LegacyEsProvider({ getService }) { apiVersion: DEFAULT_API_VERSION, host: formatUrl(config.get('servers.elasticsearch')), requestTimeout: config.get('timeouts.esRequestTimeout'), - plugins: [indexManagementEsClientPlugin, snapshotRestoreEsClientPlugin], + plugins: [indexManagementEsClientPlugin], }); } diff --git a/x-pack/test/functional/apps/snapshot_restore/home_page.ts b/x-pack/test/functional/apps/snapshot_restore/home_page.ts index 955618774bdf..b72656a96980 100644 --- a/x-pack/test/functional/apps/snapshot_restore/home_page.ts +++ b/x-pack/test/functional/apps/snapshot_restore/home_page.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'snapshotRestore']); const log = getService('log'); - const es = getService('legacyEs'); + const es = getService('es'); describe('Home page', function () { before(async () => {