[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:
Jean-Louis Leysens 2023-02-27 14:49:31 +01:00 committed by GitHub
parent bbbf8d155b
commit 5c15399ff4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 137 additions and 24 deletions

View file

@ -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();
});
});

View file

@ -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,

View file

@ -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 () => {

View file

@ -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)
);

View file

@ -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;

View file

@ -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;
}

View file

@ -176,6 +176,7 @@ describe('plugins discovery system', () => {
roles: {
backgroundTasks: true,
ui: true,
migrator: false,
},
};

View file

@ -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);

View file

@ -78,6 +78,7 @@ export function createPluginInitializerContext({
roles: {
backgroundTasks: nodeInfo.roles.backgroundTasks,
ui: nodeInfo.roles.ui,
migrator: nodeInfo.roles.migrator,
},
},

View file

@ -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);

View file

@ -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);

View file

@ -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,
});
});

View file

@ -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,
});
});

View file

@ -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,
});
});