mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[ZDT] Kibana as migration runner (#152813)
## Summary This implements the behaviour for the `migrator` role introduced in https://github.com/elastic/kibana/pull/151978. The approach taken is kept simple: 1. Start Kibana as per usual, letting the `Root` and `Server` be instantiated by the bootstrap logic 2. Inside the `Server`, just after the SO migrations are completed we throw a special `CriticalError` (named `CriticalException` with code `MigrationOnlyNode`) 3. The `Root` then handles this exception as it would handle any other start up exception (like invalid config) ## To reviewers * We can make this more sophisticated. I suspect this can quickly become a tricky flow to implement/maintain since there are lots of moving parts during "normal" Kibana startup. Using the same flow means our "migrator" node will always behave the same way up until SOs have been migrated. * Are we missing anything? Specifically, we want this "migrator" node to exit with `0` since shutting down is expected. But is throwing during `start` phase going to cover all cases we can think of? Closes https://github.com/elastic/kibana/issues/150295 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
91ef7654fd
commit
5b9206fba2
9 changed files with 89 additions and 7 deletions
|
@ -41,6 +41,7 @@ export const rolesConfig = schema.arrayOf(
|
|||
]),
|
||||
{
|
||||
defaultValue: [NODE_WILDCARD_CHAR],
|
||||
minSize: 1,
|
||||
validate: (value) => {
|
||||
if (value.length > 1) {
|
||||
if (value.includes(NODE_WILDCARD_CHAR)) {
|
||||
|
@ -51,7 +52,6 @@ export const rolesConfig = schema.arrayOf(
|
|||
}
|
||||
}
|
||||
},
|
||||
minSize: 1,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { getPackages } from '@kbn/repo-packages';
|
|||
import { CliArgs, Env, RawConfigService } from '@kbn/config';
|
||||
import { CriticalError } from '@kbn/core-base-server-internal';
|
||||
import { Root } from './root';
|
||||
import { MIGRATION_EXCEPTION_CODE } from './constants';
|
||||
|
||||
interface BootstrapArgs {
|
||||
configs: string[];
|
||||
|
@ -114,11 +115,13 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot
|
|||
|
||||
function onRootShutdown(reason?: any) {
|
||||
if (reason !== undefined) {
|
||||
// There is a chance that logger wasn't configured properly and error that
|
||||
// that forced root to shut down could go unnoticed. To prevent this we always
|
||||
// mirror such fatal errors in standard output with `console.error`.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`\n${chalk.white.bgRed(' FATAL ')} ${reason}\n`);
|
||||
if (reason.code !== MIGRATION_EXCEPTION_CODE) {
|
||||
// There is a chance that logger wasn't configured properly and error that
|
||||
// that forced root to shut down could go unnoticed. To prevent this we always
|
||||
// mirror such fatal errors in standard output with `console.error`.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`\n${chalk.white.bgRed(' FATAL ')} ${reason}\n`);
|
||||
}
|
||||
|
||||
process.exit(reason instanceof CriticalError ? reason.processExitCode : 1);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const MIGRATION_EXCEPTION_CODE = 'MigrationOnlyNode';
|
|
@ -27,6 +27,7 @@ export const mockServer = {
|
|||
setupCoreConfig: jest.fn(),
|
||||
preboot: jest.fn(),
|
||||
setup: jest.fn(),
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
configService,
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import { rawConfigService, configService, logger, mockServer } from './index.tes
|
|||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { filter, first } from 'rxjs/operators';
|
||||
import { CriticalError } from '@kbn/core-base-server-internal';
|
||||
import { REPO_ROOT } from '@kbn/repo-info';
|
||||
import { Env } from '@kbn/config';
|
||||
import { getEnvOptions } from '@kbn/config-mocks';
|
||||
|
@ -239,3 +240,16 @@ test('stops services if consequent logger upgrade fails', async () => {
|
|||
|
||||
expect(mockConsoleError.mock.calls).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('handles migrator-only node exception', async () => {
|
||||
const mockOnShutdown = jest.fn();
|
||||
const root = new Root(rawConfigService, env, mockOnShutdown);
|
||||
mockServer.start.mockImplementation(() => {
|
||||
throw new CriticalError('Test', 'MigratioOnlyNode', 0);
|
||||
});
|
||||
await root.preboot();
|
||||
await root.setup();
|
||||
await expect(() => root.start()).rejects.toBeInstanceOf(CriticalError);
|
||||
expect(mockServer.stop).toHaveBeenCalledTimes(1);
|
||||
expect(mockOnShutdown).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
|
|
@ -22,6 +22,7 @@ import apm from 'elastic-apm-node';
|
|||
import { isEqual } from 'lodash';
|
||||
import type { ElasticConfigType } from './elastic_config';
|
||||
import { Server } from '../server';
|
||||
import { MIGRATION_EXCEPTION_CODE } from '../constants';
|
||||
|
||||
/**
|
||||
* Top-level entry point to kick off the app and start the Kibana server.
|
||||
|
@ -89,7 +90,9 @@ export class Root {
|
|||
);
|
||||
}
|
||||
|
||||
this.log.fatal(reason);
|
||||
if (reason.code !== MIGRATION_EXCEPTION_CODE) {
|
||||
this.log.fatal(reason);
|
||||
}
|
||||
}
|
||||
|
||||
await this.server.stop();
|
||||
|
|
|
@ -33,8 +33,11 @@ import { REPO_ROOT } from '@kbn/repo-info';
|
|||
import { Env } from '@kbn/config';
|
||||
import { rawConfigServiceMock, getEnvOptions } from '@kbn/config-mocks';
|
||||
import { Server } from './server';
|
||||
import { MIGRATION_EXCEPTION_CODE } from './constants';
|
||||
|
||||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import type { InternalNodeServicePreboot } from '@kbn/core-node-server-internal';
|
||||
import { CriticalError } from '@kbn/core-base-server-internal';
|
||||
|
||||
const env = Env.createDefault(REPO_ROOT, getEnvOptions());
|
||||
const logger = loggingSystemMock.create();
|
||||
|
@ -58,6 +61,7 @@ beforeEach(() => {
|
|||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockEnsureValidConfiguration.mockReset();
|
||||
});
|
||||
|
||||
test('preboot services on "preboot"', async () => {
|
||||
|
@ -252,3 +256,33 @@ test(`doesn't preboot core services if config validation fails`, async () => {
|
|||
expect(mockPluginsService.preboot).not.toHaveBeenCalled();
|
||||
expect(mockPrebootService.preboot).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('migrator-only node throws exception during start', async () => {
|
||||
rawConfigService.getConfig$.mockReturnValue(
|
||||
new BehaviorSubject({ node: { roles: ['migrator'] } })
|
||||
);
|
||||
const nodeServiceContract: InternalNodeServicePreboot = {
|
||||
roles: { migrator: true, ui: false, backgroundTasks: false },
|
||||
};
|
||||
mockNodeService.preboot.mockResolvedValue(nodeServiceContract);
|
||||
mockNodeService.start.mockReturnValue(nodeServiceContract);
|
||||
|
||||
const server = new Server(rawConfigService, env, logger);
|
||||
|
||||
await server.preboot();
|
||||
await server.setup();
|
||||
|
||||
let migrationException: undefined | CriticalError;
|
||||
expect(mockSavedObjectsService.start).not.toHaveBeenCalled();
|
||||
await server.start().catch((e) => (migrationException = e));
|
||||
|
||||
expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1);
|
||||
expect(mockSavedObjectsService.start).toHaveNthReturnedWith(1, expect.anything());
|
||||
|
||||
expect(migrationException).not.toBeUndefined();
|
||||
expect(migrationException).toBeInstanceOf(CriticalError);
|
||||
expect(migrationException!.message).toBe('Migrations completed, shutting down Kibana');
|
||||
expect(migrationException!.code).toBe(MIGRATION_EXCEPTION_CODE);
|
||||
expect(migrationException!.processExitCode).toBe(0);
|
||||
expect(migrationException!.cause).toBeUndefined();
|
||||
});
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
import apm from 'elastic-apm-node';
|
||||
import { reportPerformanceMetricEvent } from '@kbn/ebt-tools';
|
||||
import type { Logger, LoggerFactory } from '@kbn/logging';
|
||||
import type { NodeRoles } from '@kbn/core-node-server';
|
||||
import { CriticalError } from '@kbn/core-base-server-internal';
|
||||
import { ConfigService, Env, RawConfigurationProvider } from '@kbn/config';
|
||||
import { DocLinksService } from '@kbn/core-doc-links-server-internal';
|
||||
import { LoggingService, ILoggingSystem } from '@kbn/core-logging-server-internal';
|
||||
|
@ -51,6 +53,7 @@ import type {
|
|||
import { DiscoveredPlugins, PluginsService } from '@kbn/core-plugins-server-internal';
|
||||
import { CoreAppsService } from '@kbn/core-apps-server-internal';
|
||||
import { registerServiceConfig } from './register_service_config';
|
||||
import { MIGRATION_EXCEPTION_CODE } from './constants';
|
||||
|
||||
const coreId = Symbol('core');
|
||||
const KIBANA_STARTED_EVENT = 'kibana_started';
|
||||
|
@ -103,6 +106,7 @@ export class Server {
|
|||
private coreStart?: InternalCoreStart;
|
||||
private discoveredPlugins?: DiscoveredPlugins;
|
||||
private readonly logger: LoggerFactory;
|
||||
private nodeRoles?: NodeRoles;
|
||||
|
||||
private readonly uptimePerStep: Partial<UptimeSteps> = {};
|
||||
|
||||
|
@ -159,6 +163,8 @@ export class Server {
|
|||
const environmentPreboot = await this.environment.preboot({ analytics: analyticsPreboot });
|
||||
const nodePreboot = await this.node.preboot({ loggingSystem: this.loggingSystem });
|
||||
|
||||
this.nodeRoles = nodePreboot.roles;
|
||||
|
||||
// Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph.
|
||||
this.discoveredPlugins = await this.plugins.discover({
|
||||
environment: environmentPreboot,
|
||||
|
@ -364,6 +370,17 @@ export class Server {
|
|||
await this.resolveSavedObjectsStartPromise!(savedObjectsStart);
|
||||
|
||||
soStartSpan?.end();
|
||||
|
||||
if (this.nodeRoles?.migrator === true) {
|
||||
startTransaction?.end();
|
||||
this.log.info('Detected migrator node role; shutting down Kibana...');
|
||||
throw new CriticalError(
|
||||
'Migrations completed, shutting down Kibana',
|
||||
MIGRATION_EXCEPTION_CODE,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
const capabilitiesStart = this.capabilities.start();
|
||||
const uiSettingsStart = await this.uiSettings.start();
|
||||
const customBrandingStart = this.customBranding.start();
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
"@kbn/core-custom-branding-server-internal",
|
||||
"@kbn/core-custom-branding-server-mocks",
|
||||
"@kbn/repo-packages",
|
||||
"@kbn/core-node-server",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue