mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Migrations] Add migrator
node role (#151978)
## Summary Adds the `migrator` special role to the node roles config: ```yml # 👇🏻 new node.roles: ['migrator'] # or node.roles: ['background_tasks', 'ui'] # or node.roles: ['*'] # this one is slightly weird now because it actually excludes 'migrator' so it is not truly "all roles", but "all combinable roles"... ``` ## How to test Start Kibana locally and add `node.roles: ['migrator']` to the `kibana.dev.yml`. Kibana should start normally and log: ``` [2023-02-23T12:08:54.123+01:00][INFO ][node] Kibana process configured with roles: [migrator] ``` Note: this role currently does not do anything. This PR just adds the ability to configure it. Partially addresses https://github.com/elastic/kibana/issues/150295 ## Slight improvement to error messages When specifying known, accepted values but combining with either `migrator` or `*` you will get a message like: ``` [config validation of [node].roles]: wildcard ("*") cannot be used with other roles or specified more than once ``` --------- Co-authored-by: Luke Elmers <lukeelmers@gmail.com>
This commit is contained in:
parent
bbbf8d155b
commit
5c15399ff4
14 changed files with 137 additions and 24 deletions
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { rolesConfig } from './node_config';
|
||||
|
||||
describe('rolesConfig', () => {
|
||||
test('default', () => {
|
||||
expect(rolesConfig.validate(undefined)).toEqual(['*']);
|
||||
});
|
||||
test('empty', () => {
|
||||
expect(() => rolesConfig.validate([])).toThrow();
|
||||
});
|
||||
test('"ui" and "background_tasks" roles are allowed and can be combined', () => {
|
||||
expect(() => rolesConfig.validate(['ui', 'background_tasks'])).not.toThrow();
|
||||
expect(() => rolesConfig.validate(['ui'])).not.toThrow();
|
||||
expect(() => rolesConfig.validate(['background_tasks'])).not.toThrow();
|
||||
});
|
||||
test('exlcusive "*"', () => {
|
||||
const wildcardError = `wildcard ("*") cannot be used with other roles or specified more than once`;
|
||||
expect(() => rolesConfig.validate(['*'])).not.toThrow();
|
||||
|
||||
expect(() => rolesConfig.validate(['*', 'ui'])).toThrow(wildcardError);
|
||||
expect(() => rolesConfig.validate(['*', '*'])).toThrow(wildcardError);
|
||||
|
||||
expect(() => rolesConfig.validate(['*', 'unknown'])).toThrow();
|
||||
});
|
||||
test('exlcusive "migrator"', () => {
|
||||
const migratorError = `"migrator" cannot be used with other roles or specified more than once`;
|
||||
expect(() => rolesConfig.validate(['migrator'])).not.toThrow();
|
||||
|
||||
expect(() => rolesConfig.validate(['migrator', 'ui'])).toThrow(migratorError);
|
||||
expect(() => rolesConfig.validate(['migrator', 'migrator'])).toThrow(migratorError);
|
||||
|
||||
expect(() => rolesConfig.validate(['migrator', 'unknown'])).toThrow();
|
||||
});
|
||||
});
|
|
@ -6,33 +6,68 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import type { ServiceConfigDescriptor } from '@kbn/core-base-server-internal';
|
||||
|
||||
/** @internal */
|
||||
export const NODE_CONFIG_PATH = 'node' as const;
|
||||
/**
|
||||
* Wildchar is a special config option that implies all {@link NODE_DEFAULT_ROLES} roles.
|
||||
* @internal
|
||||
*/
|
||||
export const NODE_WILDCARD_CHAR = '*' as const;
|
||||
/** @internal */
|
||||
export const NODE_WILDCARD_CHAR = '*';
|
||||
export const NODE_BACKGROUND_TASKS_ROLE = 'background_tasks' as const;
|
||||
/** @internal */
|
||||
export const NODE_ACCEPTED_ROLES = ['background_tasks', 'ui'];
|
||||
export const NODE_UI_ROLE = 'ui' as const;
|
||||
/** @internal */
|
||||
export const NODE_MIGRATOR_ROLE = 'migrator' as const;
|
||||
/** @internal */
|
||||
export const NODE_DEFAULT_ROLES = [NODE_BACKGROUND_TASKS_ROLE, NODE_UI_ROLE] as const;
|
||||
/** @internal */
|
||||
export const NODE_ALL_ROLES = [
|
||||
NODE_UI_ROLE,
|
||||
NODE_MIGRATOR_ROLE,
|
||||
NODE_BACKGROUND_TASKS_ROLE,
|
||||
] as const;
|
||||
|
||||
/** @internal */
|
||||
export const rolesConfig = schema.arrayOf(
|
||||
schema.oneOf([
|
||||
schema.literal(NODE_BACKGROUND_TASKS_ROLE),
|
||||
schema.literal(NODE_MIGRATOR_ROLE),
|
||||
schema.literal(NODE_WILDCARD_CHAR),
|
||||
schema.literal(NODE_UI_ROLE),
|
||||
]),
|
||||
{
|
||||
defaultValue: [NODE_WILDCARD_CHAR],
|
||||
validate: (value) => {
|
||||
if (value.length > 1) {
|
||||
if (value.includes(NODE_WILDCARD_CHAR)) {
|
||||
return `wildcard ("*") cannot be used with other roles or specified more than once`;
|
||||
}
|
||||
if (value.includes(NODE_MIGRATOR_ROLE)) {
|
||||
return `"migrator" cannot be used with other roles or specified more than once`;
|
||||
}
|
||||
}
|
||||
},
|
||||
minSize: 1,
|
||||
}
|
||||
);
|
||||
|
||||
/** @internal */
|
||||
export type NodeRolesConfig = TypeOf<typeof rolesConfig>;
|
||||
|
||||
/** @internal */
|
||||
export interface NodeConfigType {
|
||||
roles: string[];
|
||||
roles: NodeRolesConfig;
|
||||
}
|
||||
|
||||
const configSchema = schema.object({
|
||||
roles: schema.oneOf(
|
||||
[
|
||||
schema.arrayOf(schema.oneOf([schema.literal('background_tasks'), schema.literal('ui')])),
|
||||
schema.arrayOf(schema.literal(NODE_WILDCARD_CHAR), { minSize: 1, maxSize: 1 }),
|
||||
],
|
||||
{
|
||||
defaultValue: [NODE_WILDCARD_CHAR],
|
||||
}
|
||||
),
|
||||
roles: rolesConfig,
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
export const nodeConfig: ServiceConfigDescriptor<NodeConfigType> = {
|
||||
path: NODE_CONFIG_PATH,
|
||||
schema: configSchema,
|
||||
|
|
|
@ -11,12 +11,13 @@ import { BehaviorSubject } from 'rxjs';
|
|||
import type { CoreContext } from '@kbn/core-base-server-internal';
|
||||
|
||||
import { NodeService } from './node_service';
|
||||
import type { NodeRolesConfig } from './node_config';
|
||||
|
||||
import { configServiceMock } from '@kbn/config-mocks';
|
||||
import { mockCoreContext } from '@kbn/core-base-server-mocks';
|
||||
import { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
|
||||
const getMockedConfigService = (nodeConfig: unknown) => {
|
||||
const getMockedConfigService = (nodeConfig: { roles: NodeRolesConfig }) => {
|
||||
const configService = configServiceMock.create();
|
||||
configService.atPath.mockImplementation((path) => {
|
||||
if (path === 'node') {
|
||||
|
@ -51,6 +52,7 @@ describe('NodeService', () => {
|
|||
|
||||
expect(roles.backgroundTasks).toBe(true);
|
||||
expect(roles.ui).toBe(true);
|
||||
expect(roles.migrator).toBe(false);
|
||||
});
|
||||
|
||||
it('returns correct roles when node is configured to `background_tasks`', async () => {
|
||||
|
@ -62,6 +64,7 @@ describe('NodeService', () => {
|
|||
|
||||
expect(roles.backgroundTasks).toBe(true);
|
||||
expect(roles.ui).toBe(false);
|
||||
expect(roles.migrator).toBe(false);
|
||||
});
|
||||
|
||||
it('returns correct roles when node is configured to `ui`', async () => {
|
||||
|
@ -73,6 +76,7 @@ describe('NodeService', () => {
|
|||
|
||||
expect(roles.backgroundTasks).toBe(false);
|
||||
expect(roles.ui).toBe(true);
|
||||
expect(roles.migrator).toBe(false);
|
||||
});
|
||||
|
||||
it('returns correct roles when node is configured to both `background_tasks` and `ui`', async () => {
|
||||
|
@ -84,6 +88,19 @@ describe('NodeService', () => {
|
|||
|
||||
expect(roles.backgroundTasks).toBe(true);
|
||||
expect(roles.ui).toBe(true);
|
||||
expect(roles.migrator).toBe(false);
|
||||
});
|
||||
|
||||
it('returns correct roles when node is configured to `migrator`', async () => {
|
||||
configService = getMockedConfigService({ roles: ['migrator'] });
|
||||
coreContext = mockCoreContext.create({ logger, configService });
|
||||
|
||||
service = new NodeService(coreContext);
|
||||
const { roles } = await service.preboot({ loggingSystem: logger });
|
||||
|
||||
expect(roles.backgroundTasks).toBe(false);
|
||||
expect(roles.ui).toBe(false);
|
||||
expect(roles.migrator).toBe(true);
|
||||
});
|
||||
|
||||
it('logs the node roles', async () => {
|
||||
|
|
|
@ -14,13 +14,15 @@ import type { ILoggingSystem } from '@kbn/core-logging-server-internal';
|
|||
import type { NodeRoles } from '@kbn/core-node-server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import {
|
||||
NodeConfigType,
|
||||
NODE_WILDCARD_CHAR,
|
||||
NODE_ACCEPTED_ROLES,
|
||||
type NodeConfigType,
|
||||
type NodeRolesConfig,
|
||||
NODE_ALL_ROLES,
|
||||
NODE_CONFIG_PATH,
|
||||
NODE_WILDCARD_CHAR,
|
||||
NODE_DEFAULT_ROLES,
|
||||
} from './node_config';
|
||||
|
||||
const DEFAULT_ROLES = NODE_ACCEPTED_ROLES;
|
||||
const DEFAULT_ROLES = [...NODE_DEFAULT_ROLES];
|
||||
const containsWildcard = (roles: string[]) => roles.includes(NODE_WILDCARD_CHAR);
|
||||
|
||||
/**
|
||||
|
@ -66,8 +68,9 @@ export class NodeService {
|
|||
loggingSystem.setGlobalContext({ service: { node: { roles } } });
|
||||
this.log.info(`Kibana process configured with roles: [${roles.join(', ')}]`);
|
||||
|
||||
this.roles = NODE_ACCEPTED_ROLES.reduce((acc, curr) => {
|
||||
return { ...acc, [camelCase(curr)]: roles.includes(curr) };
|
||||
// We assume the combination of node roles has been validated and avoid doing additional checks here.
|
||||
this.roles = NODE_ALL_ROLES.reduce((acc, curr) => {
|
||||
return { ...acc, [camelCase(curr)]: (roles as string[]).includes(curr) };
|
||||
}, {} as NodeRoles);
|
||||
|
||||
return {
|
||||
|
@ -86,7 +89,7 @@ export class NodeService {
|
|||
// nothing to do here yet
|
||||
}
|
||||
|
||||
private async getNodeRoles(): Promise<string[]> {
|
||||
private async getNodeRoles(): Promise<NodeRolesConfig> {
|
||||
const { roles } = await firstValueFrom(
|
||||
this.configService.atPath<NodeConfigType>(NODE_CONFIG_PATH)
|
||||
);
|
||||
|
|
|
@ -18,6 +18,7 @@ const createInternalPrebootContractMock = () => {
|
|||
roles: {
|
||||
backgroundTasks: true,
|
||||
ui: true,
|
||||
migrator: false,
|
||||
},
|
||||
};
|
||||
return prebootContract;
|
||||
|
@ -27,15 +28,18 @@ const createInternalStartContractMock = (
|
|||
{
|
||||
ui,
|
||||
backgroundTasks,
|
||||
migrator,
|
||||
}: {
|
||||
ui: boolean;
|
||||
backgroundTasks: boolean;
|
||||
} = { ui: true, backgroundTasks: true }
|
||||
migrator: boolean;
|
||||
} = { ui: true, backgroundTasks: true, migrator: false }
|
||||
) => {
|
||||
const startContract: jest.Mocked<InternalNodeServiceStart> = {
|
||||
roles: {
|
||||
backgroundTasks,
|
||||
ui,
|
||||
migrator,
|
||||
},
|
||||
};
|
||||
return startContract;
|
||||
|
|
|
@ -37,4 +37,9 @@ export interface NodeRoles {
|
|||
* to handle http traffic from the browser.
|
||||
*/
|
||||
ui: boolean;
|
||||
/**
|
||||
* Start Kibana with the specific purpose of completing the migrations phase then shutting down.
|
||||
* @remark This role is special as it precludes the use of other roles.
|
||||
*/
|
||||
migrator: boolean;
|
||||
}
|
||||
|
|
|
@ -176,6 +176,7 @@ describe('plugins discovery system', () => {
|
|||
roles: {
|
||||
backgroundTasks: true,
|
||||
ui: true,
|
||||
migrator: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -195,7 +195,7 @@ describe('createPluginInitializerContext', () => {
|
|||
opaqueId,
|
||||
manifest: createPluginManifest(),
|
||||
instanceInfo,
|
||||
nodeInfo: { roles: { backgroundTasks: false, ui: true } },
|
||||
nodeInfo: { roles: { backgroundTasks: false, ui: true, migrator: false } },
|
||||
});
|
||||
expect(pluginInitializerContext.node.roles.backgroundTasks).toBe(false);
|
||||
expect(pluginInitializerContext.node.roles.ui).toBe(true);
|
||||
|
|
|
@ -78,6 +78,7 @@ export function createPluginInitializerContext({
|
|||
roles: {
|
||||
backgroundTasks: nodeInfo.roles.backgroundTasks,
|
||||
ui: nodeInfo.roles.ui,
|
||||
migrator: nodeInfo.roles.migrator,
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -728,7 +728,7 @@ describe('PluginsService', () => {
|
|||
},
|
||||
coreContext: { coreId, env, logger, configService },
|
||||
instanceInfo: { uuid: 'uuid' },
|
||||
nodeInfo: { roles: { backgroundTasks: true, ui: true } },
|
||||
nodeInfo: { roles: { backgroundTasks: true, ui: true, migrator: false } },
|
||||
});
|
||||
|
||||
const logs = loggingSystemMock.collect(logger);
|
||||
|
|
|
@ -419,6 +419,7 @@ describe('SavedObjectsService', () => {
|
|||
startDeps.node = nodeServiceMock.createInternalStartContract({
|
||||
ui: true,
|
||||
backgroundTasks: true,
|
||||
migrator: false,
|
||||
});
|
||||
await soService.start(startDeps);
|
||||
|
||||
|
@ -444,6 +445,7 @@ describe('SavedObjectsService', () => {
|
|||
startDeps.node = nodeServiceMock.createInternalStartContract({
|
||||
ui: true,
|
||||
backgroundTasks: false,
|
||||
migrator: false,
|
||||
});
|
||||
await soService.start(startDeps);
|
||||
|
||||
|
@ -469,6 +471,7 @@ describe('SavedObjectsService', () => {
|
|||
startDeps.node = nodeServiceMock.createInternalStartContract({
|
||||
ui: false,
|
||||
backgroundTasks: true,
|
||||
migrator: false,
|
||||
});
|
||||
await soService.start(startDeps);
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
|
|||
it('passes node roles to server PluginInitializerContext', async () => {
|
||||
await supertest.get('/core_plugin_initializer_context/node/roles').expect(200, {
|
||||
backgroundTasks: true,
|
||||
migrator: false,
|
||||
ui: true,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
|
|||
it('passes node roles to server PluginInitializerContext', async () => {
|
||||
await supertest.get('/core_plugin_initializer_context/node/roles').expect(200, {
|
||||
backgroundTasks: true,
|
||||
migrator: false,
|
||||
ui: true,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
|
|||
it('passes node roles to server PluginInitializerContext', async () => {
|
||||
await supertest.get('/core_plugin_initializer_context/node/roles').expect(200, {
|
||||
backgroundTasks: false,
|
||||
migrator: false,
|
||||
ui: true,
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue