Skip to content

Commit

Permalink
PoC for the preboot stage.
Browse files Browse the repository at this point in the history
  • Loading branch information
azasypkin committed Jul 1, 2021
1 parent eb23cad commit d13a75c
Show file tree
Hide file tree
Showing 65 changed files with 1,901 additions and 728 deletions.
27 changes: 22 additions & 5 deletions src/core/server/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,17 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot
reloadConfiguration();
});

function reloadConfiguration() {
function reloadConfiguration(reason = 'SIGHUP') {
const cliLogger = root.logger.get('cli');
cliLogger.info('Reloading Kibana configuration due to SIGHUP.', { tags: ['config'] });
cliLogger.info(`Reloading Kibana configuration due to ${reason}.`, { tags: ['config'] });

try {
rawConfigService.reloadConfig();
} catch (err) {
return shutdown(err);
}

cliLogger.info('Reloaded Kibana configuration due to SIGHUP.', { tags: ['config'] });
cliLogger.info(`Reloaded Kibana configuration due to ${reason}.`, { tags: ['config'] });
}

process.on('SIGINT', () => shutdown());
Expand All @@ -81,11 +81,28 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot
}

try {
const { preboot } = await root.preboot();

// If setup is on hold then preboot server is supposed to serve user requests and we can let
// dev parent process know that we are ready for dev mode.
const isSetupOnHold = preboot.isSetupOnHold();
if (process.send && isSetupOnHold) {
process.send(['SERVER_LISTENING']);
}

if (isSetupOnHold) {
root.logger.get().info('Holding setup until preboot phase is completed.');
const { shouldReloadConfig } = await preboot.waitUntilCanSetup();
if (shouldReloadConfig) {
await reloadConfiguration('preboot request');
}
}

await root.setup();
await root.start();

// notify parent process know when we are ready for dev mode.
if (process.send) {
// Notify parent process if we haven't that yet during preboot phase.
if (process.send && !isSetupOnHold) {
process.send(['SERVER_LISTENING']);
}
} catch (err) {
Expand Down
22 changes: 15 additions & 7 deletions src/core/server/capabilities/capabilities_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { Capabilities, CapabilitiesProvider, CapabilitiesSwitcher } from './types';
import { CoreContext } from '../core_context';
import { Logger } from '../logging';
import { InternalHttpServiceSetup, KibanaRequest } from '../http';
import { InternalHttpServicePreboot, InternalHttpServiceSetup, KibanaRequest } from '../http';
import { mergeCapabilities } from './merge_capabilities';
import { getCapabilitiesResolver, CapabilitiesResolver } from './resolve_capabilities';
import { registerRoutes } from './routes';
Expand Down Expand Up @@ -120,6 +120,10 @@ export interface CapabilitiesStart {
): Promise<Capabilities>;
}

interface PrebootSetupDeps {
http: InternalHttpServicePreboot;
}

interface SetupDeps {
http: InternalHttpServiceSetup;
}
Expand Down Expand Up @@ -149,17 +153,21 @@ export class CapabilitiesService {
);
}

public preboot(prebootDeps: PrebootSetupDeps) {
this.logger.debug('Prebooting capabilities service');

// The preboot server has no need for real capabilities.
// Returning the un-augmented defaults is sufficient.
prebootDeps.http.registerRoutes('', (router) => {
registerRoutes(router, async () => defaultCapabilities);
});
}

public setup(setupDeps: SetupDeps): CapabilitiesSetup {
this.logger.debug('Setting up capabilities service');

registerRoutes(setupDeps.http.createRouter(''), this.resolveCapabilities);

// The not ready server has no need for real capabilities.
// Returning the un-augmented defaults is sufficient.
setupDeps.http.notReadyServer?.registerRoutes('', (notReadyRouter) => {
registerRoutes(notReadyRouter, async () => defaultCapabilities);
});

return {
registerProvider: (provider: CapabilitiesProvider) => {
this.capabilitiesProviders.push(provider);
Expand Down
167 changes: 96 additions & 71 deletions src/core/server/core_app/core_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { Env } from '@kbn/config';
import { schema } from '@kbn/config-schema';
import { fromRoot } from '@kbn/utils';

import { InternalCoreSetup } from '../internal_types';
import { IRouter, KibanaRequest, RequestHandlerContext } from 'kibana/server';
import { InternalCorePrebootSetup, InternalCoreSetup } from '../internal_types';
import { CoreContext } from '../core_context';
import { Logger } from '../logging';
import { registerBundleRoutes } from './bundle_routes';
Expand All @@ -28,65 +29,95 @@ export class CoreApp {
this.env = core.env;
}

setup(coreSetup: InternalCoreSetup, uiPlugins: UiPlugins, notReadyServerUiPlugins: UiPlugins) {
this.logger.debug('Setting up core app.');
this.registerDefaultRoutes(coreSetup, uiPlugins, notReadyServerUiPlugins);
this.registerStaticDirs(coreSetup);
preboot(corePreboot: InternalCorePrebootSetup, uiPlugins: UiPlugins) {
this.logger.debug('Prebooting core app.');

corePreboot.http.registerRoutes('', (router) => {
this.registerPrebootDefaultRoutes(router, corePreboot, uiPlugins);
this.registerStaticDirs(corePreboot);
});
}

private registerDefaultRoutes(
coreSetup: InternalCoreSetup,
uiPlugins: UiPlugins,
notReadyServerUiPlugins: UiPlugins
) {
const httpSetup = coreSetup.http;
const router = httpSetup.createRouter('');
const resources = coreSetup.httpResources.createRegistrar(router);
setup(coreSetup: InternalCoreSetup, uiPlugins: UiPlugins) {
this.logger.debug('Setting up core app.');

router.get({ path: '/', validate: false }, async (context, req, res) => {
const defaultLocation = async (context: RequestHandlerContext, req: KibanaRequest) => {
const defaultRoute = await context.core.uiSettings.client.get<string>('defaultRoute');
const basePath = httpSetup.basePath.get(req);
const url = `${basePath}${defaultRoute}`;
const basePath = coreSetup.http.basePath.get(req);
return `${basePath}${defaultRoute}`;
};

return res.redirected({
headers: {
location: url,
const router = coreSetup.http.createRouter('');
this.registerDefaultRoutes(router, coreSetup, uiPlugins, defaultLocation);
this.registerStaticDirs(coreSetup);
}

private registerPrebootDefaultRoutes(
router: IRouter,
core: InternalCorePrebootSetup,
uiPlugins: UiPlugins
) {
// remove trailing slash catch-all
const resources = core.httpResources.createRegistrar(router);
resources.register(
{
path: '/{path*}',
validate: {
params: schema.object({
path: schema.maybe(schema.string()),
}),
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
},
});
});
},
async (context, req, res) => {
const { query, params } = req;
const { path } = params;
if (!path || !path.endsWith('/') || path.startsWith('/')) {
return res.renderAnonymousCoreApp();
}

httpSetup.notReadyServer?.registerRoutes('', (notReadyRouter) => {
const notReadyResources = coreSetup.httpResources.createRegistrar(notReadyRouter);
const basePath = core.http.basePath.get(req);
let rewrittenPath = path.slice(0, -1);
if (`/${path}`.startsWith(basePath)) {
rewrittenPath = rewrittenPath.substring(basePath.length);
}

// TODO: need better mechanism for this, -OR- make setup mode render at the root instead.
notReadyRouter.get({ path: '/', validate: false }, async (context, req, res) => {
const defaultRoute = '/app/setup';
const basePath = httpSetup.basePath.get(req);
const url = `${basePath}${defaultRoute}`;
const querystring = query ? stringify(query) : undefined;
const url = `${basePath}/${rewrittenPath}${querystring ? `?${querystring}` : ''}`;

return res.redirected({
headers: {
location: url,
},
});
});
}
);

registerBundleRoutes({
router: notReadyRouter,
uiPlugins: notReadyServerUiPlugins,
packageInfo: this.env.packageInfo,
serverBasePath: coreSetup.http.basePath.serverBasePath,
});
router.get({ path: '/core', validate: false }, async (context, req, res) =>
res.ok({ body: { version: '0.0.1' } })
);

notReadyResources.register(
{
path: '/app/{id}/{any*}',
validate: false,
registerBundleRoutes({
router,
uiPlugins,
packageInfo: this.env.packageInfo,
serverBasePath: core.http.basePath.serverBasePath,
});
}

private registerDefaultRoutes(
router: IRouter,
core: InternalCoreSetup | InternalCorePrebootSetup,
uiPlugins: UiPlugins,
defaultLocation: (context: RequestHandlerContext, req: KibanaRequest) => Promise<string>
) {
const resources = core.httpResources.createRegistrar(router);
router.get({ path: '/', validate: false }, async (context, req, res) => {
return res.redirected({
headers: {
location: await defaultLocation(context, req),
},
async (context, request, response) => {
return response.renderAnonymousCoreApp({ renderTarget: 'notReady' });
}
);
});
});

// remove trailing slash catch-all
Expand All @@ -107,7 +138,7 @@ export class CoreApp {
return res.notFound();
}

const basePath = httpSetup.basePath.get(req);
const basePath = core.http.basePath.get(req);
let rewrittenPath = path.slice(0, -1);
if (`/${path}`.startsWith(basePath)) {
rewrittenPath = rewrittenPath.substring(basePath.length);
Expand All @@ -132,7 +163,7 @@ export class CoreApp {
router,
uiPlugins,
packageInfo: this.env.packageInfo,
serverBasePath: coreSetup.http.basePath.serverBasePath,
serverBasePath: core.http.basePath.serverBasePath,
});

resources.register(
Expand All @@ -148,37 +179,31 @@ export class CoreApp {
}
);

const anonymousStatusPage = coreSetup.status.isStatusPageAnonymous();
resources.register(
{
path: '/status',
validate: false,
options: {
authRequired: !anonymousStatusPage,
if ('status' in core) {
const anonymousStatusPage = core.status.isStatusPageAnonymous();
resources.register(
{
path: '/status',
validate: false,
options: {
authRequired: !anonymousStatusPage,
},
},
},
async (context, request, response) => {
if (anonymousStatusPage) {
return response.renderAnonymousCoreApp();
} else {
return response.renderCoreApp();
async (context, request, response) => {
if (anonymousStatusPage) {
return response.renderAnonymousCoreApp();
} else {
return response.renderCoreApp();
}
}
}
);
);
}
}

private registerStaticDirs(coreSetup: InternalCoreSetup) {
coreSetup.http.notReadyServer?.registerStaticDir(
'/ui/{path*}',
Path.resolve(__dirname, './assets')
);
coreSetup.http.registerStaticDir('/ui/{path*}', Path.resolve(__dirname, './assets'));
private registerStaticDirs(core: InternalCoreSetup | InternalCorePrebootSetup) {
core.http.registerStaticDir('/ui/{path*}', Path.resolve(__dirname, './assets'));

coreSetup.http.notReadyServer?.registerStaticDir(
'/node_modules/@kbn/ui-framework/dist/{path*}',
fromRoot('node_modules/@kbn/ui-framework/dist')
);
coreSetup.http.registerStaticDir(
core.http.registerStaticDir(
'/node_modules/@kbn/ui-framework/dist/{path*}',
fromRoot('node_modules/@kbn/ui-framework/dist')
);
Expand Down
33 changes: 32 additions & 1 deletion src/core/server/elasticsearch/elasticsearch_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,26 @@ import { ElasticsearchClientConfig } from './client';
import { legacyClientMock } from './legacy/mocks';
import { ElasticsearchConfig } from './elasticsearch_config';
import { ElasticsearchService } from './elasticsearch_service';
import { InternalElasticsearchServiceSetup, ElasticsearchStatusMeta } from './types';
import {
InternalElasticsearchServiceSetup,
ElasticsearchStatusMeta,
InternalElasticsearchServicePreboot,
ElasticsearchServicePreboot,
} from './types';
import { NodesVersionCompatibility } from './version_check/ensure_es_version';
import { ServiceStatus, ServiceStatusLevels } from '../status';

type MockedElasticSearchServicePreboot = jest.Mocked<ElasticsearchServicePreboot>;
const createPrebootContractMock = () => {
const prebootSetupContract: MockedElasticSearchServicePreboot = {
createClient: jest.fn(),
};
prebootSetupContract.createClient.mockImplementation(() =>
elasticsearchClientMock.createCustomClusterClient()
);
return prebootSetupContract;
};

export interface MockedElasticSearchServiceSetup {
legacy: {
config$: BehaviorSubject<ElasticsearchConfig>;
Expand Down Expand Up @@ -75,6 +91,17 @@ const createStartContractMock = () => {

const createInternalStartContractMock = createStartContractMock;

type MockedInternalElasticSearchServicePreboot = jest.Mocked<InternalElasticsearchServicePreboot>;
const createInternalPrebootContractMock = () => {
const prebootContract: MockedInternalElasticSearchServicePreboot = {
createClient: jest.fn(),
};
prebootContract.createClient.mockImplementation(() =>
elasticsearchClientMock.createCustomClusterClient()
);
return prebootContract;
};

type MockedInternalElasticSearchServiceSetup = jest.Mocked<
InternalElasticsearchServiceSetup & {
legacy: { client: jest.Mocked<ILegacyClusterClient> };
Expand Down Expand Up @@ -105,10 +132,12 @@ const createInternalSetupContractMock = () => {
type ElasticsearchServiceContract = PublicMethodsOf<ElasticsearchService>;
const createMock = () => {
const mocked: jest.Mocked<ElasticsearchServiceContract> = {
preboot: jest.fn(),
setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
mocked.preboot.mockResolvedValue(createInternalPrebootContractMock());
mocked.setup.mockResolvedValue(createInternalSetupContractMock());
mocked.start.mockResolvedValueOnce(createInternalStartContractMock());
mocked.stop.mockResolvedValue();
Expand All @@ -117,6 +146,8 @@ const createMock = () => {

export const elasticsearchServiceMock = {
create: createMock,
createInternalPreboot: createInternalPrebootContractMock,
createPreboot: createPrebootContractMock,
createInternalSetup: createInternalSetupContractMock,
createSetup: createSetupContractMock,
createInternalStart: createInternalStartContractMock,
Expand Down
Loading

0 comments on commit d13a75c

Please sign in to comment.