From b1bf08a779731512b661fa90a90d628b37c4cf94 Mon Sep 17 00:00:00 2001 From: Constance Date: Mon, 6 Jul 2020 12:22:17 -0700 Subject: [PATCH] Public URL support for Elastic Cloud (#21) * Add server-side public URL route - Per feedback from Kibana platform team, it's not possible to pass info from server/ to public/ without a HTTP call :[ * Update MockRouter for routes without any payload/params * Add client-side helper for calling the new public URL API + API seems to return a URL a trailing slash, which we need to omit * Update public/plugin.ts to check and set a public URL - relies on this.hasCheckedPublicUrl to only make the call once per page load instead of on every page nav --- .../get_enterprise_search_url.test.ts | 30 +++++++++++ .../get_enterprise_search_url.ts | 27 ++++++++++ .../shared/enterprise_search_url/index.ts | 7 +++ .../enterprise_search/public/plugin.ts | 16 +++++- .../enterprise_search/server/plugin.ts | 2 + .../server/routes/__mocks__/router.mock.ts | 6 ++- .../enterprise_search/public_url.test.ts | 54 +++++++++++++++++++ .../routes/enterprise_search/public_url.ts | 26 +++++++++ 8 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts new file mode 100644 index 0000000000000..42f308c554268 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getPublicUrl } from './'; + +describe('Enterprise Search URL helper', () => { + const httpMock = { get: jest.fn() } as any; + + it('calls and returns the public URL API endpoint', async () => { + httpMock.get.mockImplementationOnce(() => ({ publicUrl: 'http://some.vanity.url' })); + + expect(await getPublicUrl(httpMock)).toEqual('http://some.vanity.url'); + }); + + it('strips trailing slashes', async () => { + httpMock.get.mockImplementationOnce(() => ({ publicUrl: 'http://trailing.slash/' })); + + expect(await getPublicUrl(httpMock)).toEqual('http://trailing.slash'); + }); + + // For the most part, error logging/handling is done on the server side. + // On the front-end, we should simply gracefully fall back to config.host + // if we can't fetch a public URL + it('falls back to an empty string', async () => { + expect(await getPublicUrl(httpMock)).toEqual(''); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts new file mode 100644 index 0000000000000..419c187a0048a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpSetup } from 'src/core/public'; + +/** + * On Elastic Cloud, the host URL set in kibana.yml is not necessarily the same + * URL we want to send users to in the front-end (e.g. if a vanity URL is set). + * + * This helper checks a Kibana API endpoint (which has checks an Enterprise + * Search internal API endpoint) for the correct public-facing URL to use. + */ +export const getPublicUrl = async (http: HttpSetup): Promise => { + try { + const { publicUrl } = await http.get('/api/enterprise_search/public_url'); + return stripTrailingSlash(publicUrl); + } catch { + return ''; + } +}; + +const stripTrailingSlash = (url: string): string => { + return url.endsWith('/') ? url.slice(0, -1) : url; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts new file mode 100644 index 0000000000000..bbbb688b8ea7b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getPublicUrl } from './get_enterprise_search_url'; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 1ebfdd779a791..23df7044031ad 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -10,6 +10,7 @@ import { CoreSetup, CoreStart, AppMountParameters, + HttpSetup, } from 'src/core/public'; import { @@ -19,6 +20,7 @@ import { import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; import { LicensingPluginSetup } from '../../licensing/public'; +import { getPublicUrl } from './applications/shared/enterprise_search_url'; import AppSearchLogo from './applications/app_search/assets/logo.svg'; export interface ClientConfigType { @@ -31,13 +33,14 @@ export interface PluginsSetup { export class EnterpriseSearchPlugin implements Plugin { private config: ClientConfigType; + private hasCheckedPublicUrl: boolean = false; constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); } public setup(core: CoreSetup, plugins: PluginsSetup) { - const config = this.config; + const config = { host: this.config.host }; core.application.register({ id: 'app_search', @@ -47,6 +50,8 @@ export class EnterpriseSearchPlugin implements Plugin { mount: async (params: AppMountParameters) => { const [coreStart] = await core.getStartServices(); + await this.setPublicUrl(config, coreStart.http); + const { renderApp } = await import('./applications'); const { AppSearch } = await import('./applications/app_search'); @@ -71,4 +76,13 @@ export class EnterpriseSearchPlugin implements Plugin { public start(core: CoreStart) {} public stop() {} + + private async setPublicUrl(config: ClientConfigType, http: HttpSetup) { + if (!config.host) return; // No API to check + if (this.hasCheckedPublicUrl) return; // We've already performed the check + + const publicUrl = await getPublicUrl(http); + if (publicUrl) config.host = publicUrl; + this.hasCheckedPublicUrl = true; + } } diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 62c448bc83760..17da3460820e7 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -21,6 +21,7 @@ import { SecurityPluginSetup } from '../../security/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { checkAccess } from './lib/check_access'; +import { registerPublicUrlRoute } from './routes/enterprise_search/public_url'; import { registerEnginesRoute } from './routes/app_search/engines'; import { registerTelemetryRoute } from './routes/app_search/telemetry'; import { registerTelemetryUsageCollector } from './collectors/app_search/telemetry'; @@ -113,6 +114,7 @@ export class EnterpriseSearchPlugin implements Plugin { const router = http.createRouter(); const dependencies = { router, config, log: this.logger }; + registerPublicUrlRoute(dependencies); registerEnginesRoute(dependencies); /** diff --git a/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts b/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts index 332d1ad1062f2..1ca7755979f99 100644 --- a/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts +++ b/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts @@ -21,7 +21,7 @@ type payloadType = 'params' | 'query' | 'body'; interface IMockRouterProps { method: methodType; - payload: payloadType; + payload?: payloadType; } interface IMockRouterRequest { body?: object; @@ -33,7 +33,7 @@ type TMockRouterRequest = KibanaRequest | IMockRouterRequest; export class MockRouter { public router!: jest.Mocked; public method: methodType; - public payload: payloadType; + public payload?: payloadType; public response = httpServerMock.createResponseFactory(); constructor({ method, payload }: IMockRouterProps) { @@ -58,6 +58,8 @@ export class MockRouter { */ public validateRoute = (request: TMockRouterRequest) => { + if (!this.payload) throw new Error('Cannot validate wihout a payload type specified.'); + const [config] = this.router[this.method].mock.calls[0]; const validate = config.validate as RouteValidatorConfig<{}, {}, {}>; diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts new file mode 100644 index 0000000000000..6fbe4d99e9d46 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MockRouter } from '../__mocks__/router.mock'; + +jest.mock('../../lib/enterprise_search_config_api', () => ({ + callEnterpriseSearchConfigAPI: jest.fn(), +})); +import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api'; + +import { registerPublicUrlRoute } from './public_url'; + +describe('Enterprise Search Public URL API', () => { + const mockRouter = new MockRouter({ method: 'get' }); + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter.createRouter(); + + registerPublicUrlRoute({ + router: mockRouter.router, + config: {}, + log: {}, + } as any); + }); + + describe('GET /api/enterprise_search/public_url', () => { + it('returns a publicUrl', async () => { + (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => { + return Promise.resolve({ publicUrl: 'http://some.vanity.url' }); + }); + + await mockRouter.callRoute({}); + + expect(mockRouter.response.ok).toHaveBeenCalledWith({ + body: { publicUrl: 'http://some.vanity.url' }, + headers: { 'content-type': 'application/json' }, + }); + }); + + // For the most part, all error logging is handled by callEnterpriseSearchConfigAPI. + // This endpoint should mostly just fall back gracefully to an empty string + it('falls back to an empty string', async () => { + await mockRouter.callRoute({}); + expect(mockRouter.response.ok).toHaveBeenCalledWith({ + body: { publicUrl: '' }, + headers: { 'content-type': 'application/json' }, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts new file mode 100644 index 0000000000000..a9edd4eb10da0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouteDependencies } from '../../plugin'; +import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api'; + +export function registerPublicUrlRoute({ router, config, log }: IRouteDependencies) { + router.get( + { + path: '/api/enterprise_search/public_url', + validate: false, + }, + async (context, request, response) => { + const { publicUrl = '' } = + (await callEnterpriseSearchConfigAPI({ request, config, log })) || {}; + + return response.ok({ + body: { publicUrl }, + headers: { 'content-type': 'application/json' }, + }); + } + ); +}