Skip to content

Commit

Permalink
Public URL support for Elastic Cloud (elastic#21)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Constance authored and cee-chen committed Jul 7, 2020
1 parent e6fc554 commit be45426
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -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('');
});
});
Original file line number Diff line number Diff line change
@@ -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<string> => {
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;
};
Original file line number Diff line number Diff line change
@@ -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';
16 changes: 15 additions & 1 deletion x-pack/plugins/enterprise_search/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
CoreSetup,
CoreStart,
AppMountParameters,
HttpSetup,
} from 'src/core/public';

import {
Expand All @@ -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 {
Expand All @@ -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<ClientConfigType>();
}

public setup(core: CoreSetup, plugins: PluginsSetup) {
const config = this.config;
const config = { host: this.config.host };

core.application.register({
id: 'app_search',
Expand All @@ -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');

Expand All @@ -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;
}
}
2 changes: 2 additions & 0 deletions x-pack/plugins/enterprise_search/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -113,6 +114,7 @@ export class EnterpriseSearchPlugin implements Plugin {
const router = http.createRouter();
const dependencies = { router, config, log: this.logger };

registerPublicUrlRoute(dependencies);
registerEnginesRoute(dependencies);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type payloadType = 'params' | 'query' | 'body';

interface IMockRouterProps {
method: methodType;
payload: payloadType;
payload?: payloadType;
}
interface IMockRouterRequest {
body?: object;
Expand All @@ -33,7 +33,7 @@ type TMockRouterRequest = KibanaRequest | IMockRouterRequest;
export class MockRouter {
public router!: jest.Mocked<IRouter>;
public method: methodType;
public payload: payloadType;
public payload?: payloadType;
public response = httpServerMock.createResponseFactory();

constructor({ method, payload }: IMockRouterProps) {
Expand All @@ -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<{}, {}, {}>;

Expand Down
Original file line number Diff line number Diff line change
@@ -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' },
});
});
});
});
Original file line number Diff line number Diff line change
@@ -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' },
});
}
);
}

0 comments on commit be45426

Please sign in to comment.