Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Public URL support for Elastic Cloud #21

Merged
merged 4 commits into from
Jul 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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' },
});
}
);
}