mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Serve.js refactors (#158750)
Closes #155137, with some extra reorganisation, modularisation and unit tests. ### Refactors to `maybeAddConfig` ### Refactoring serve.js <-> bootstrap.ts ### Unit tests for `compileConfigStack` --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
74102e592f
commit
f51f5f42e6
9 changed files with 326 additions and 504 deletions
|
@ -1,26 +0,0 @@
|
|||
/*
|
||||
* 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 { Env } from '@kbn/config';
|
||||
import { rawConfigServiceMock, configServiceMock } from '@kbn/config-mocks';
|
||||
|
||||
export const mockConfigService = configServiceMock.create();
|
||||
export const mockRawConfigService = rawConfigServiceMock.create();
|
||||
export const mockRawConfigServiceConstructor = jest.fn(() => mockRawConfigService);
|
||||
jest.doMock('@kbn/config', () => ({
|
||||
ConfigService: jest.fn(() => mockConfigService),
|
||||
Env,
|
||||
RawConfigService: jest.fn(mockRawConfigServiceConstructor),
|
||||
}));
|
||||
|
||||
jest.doMock('./root', () => ({
|
||||
Root: jest.fn(() => ({
|
||||
shutdown: jest.fn(),
|
||||
logger: { get: () => ({ info: jest.fn(), debug: jest.fn() }) },
|
||||
})),
|
||||
}));
|
|
@ -1,61 +0,0 @@
|
|||
/*
|
||||
* 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 { of } from 'rxjs';
|
||||
import type { CliArgs } from '@kbn/config';
|
||||
|
||||
import { mockRawConfigService, mockRawConfigServiceConstructor } from './bootstrap.test.mocks';
|
||||
|
||||
jest.mock('@kbn/core-logging-server-internal');
|
||||
|
||||
import { bootstrap } from './bootstrap';
|
||||
|
||||
const bootstrapCfg = {
|
||||
configs: ['config/kibana.yml'],
|
||||
cliArgs: {} as unknown as CliArgs,
|
||||
applyConfigOverrides: () => ({}),
|
||||
};
|
||||
|
||||
describe('bootstrap', () => {
|
||||
describe('serverless', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should load additional serverless files for a valid project', async () => {
|
||||
mockRawConfigService.getConfig$.mockReturnValue(of({ serverless: 'es' }));
|
||||
await bootstrap(bootstrapCfg);
|
||||
expect(mockRawConfigServiceConstructor).toHaveBeenCalledTimes(2);
|
||||
expect(mockRawConfigServiceConstructor).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
bootstrapCfg.configs,
|
||||
bootstrapCfg.applyConfigOverrides
|
||||
);
|
||||
expect(mockRawConfigServiceConstructor).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[
|
||||
expect.stringContaining('config/serverless.yml'),
|
||||
expect.stringContaining('config/serverless.es.yml'),
|
||||
...bootstrapCfg.configs,
|
||||
],
|
||||
bootstrapCfg.applyConfigOverrides
|
||||
);
|
||||
});
|
||||
|
||||
test('should skip loading the serverless files for an invalid project', async () => {
|
||||
mockRawConfigService.getConfig$.mockReturnValue(of({ serverless: 'not-valid' }));
|
||||
await bootstrap(bootstrapCfg);
|
||||
expect(mockRawConfigServiceConstructor).toHaveBeenCalledTimes(1);
|
||||
expect(mockRawConfigServiceConstructor).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
bootstrapCfg.configs,
|
||||
bootstrapCfg.applyConfigOverrides
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,14 +7,9 @@
|
|||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { getPackages } from '@kbn/repo-packages';
|
||||
import { CliArgs, Env, RawConfigService } from '@kbn/config';
|
||||
import { CriticalError } from '@kbn/core-base-server-internal';
|
||||
import { resolve } from 'path';
|
||||
import { getConfigDirectory } from '@kbn/utils';
|
||||
import { statSync } from 'fs';
|
||||
import { VALID_SERVERLESS_PROJECT_TYPES } from './root/serverless_config';
|
||||
import { Root } from './root';
|
||||
import { MIGRATION_EXCEPTION_CODE } from './constants';
|
||||
|
||||
|
@ -43,40 +38,15 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot
|
|||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { REPO_ROOT } = require('@kbn/repo-info');
|
||||
|
||||
let env = Env.createDefault(REPO_ROOT, {
|
||||
const env = Env.createDefault(REPO_ROOT, {
|
||||
configs,
|
||||
cliArgs,
|
||||
repoPackages: getPackages(REPO_ROOT),
|
||||
});
|
||||
|
||||
let rawConfigService = new RawConfigService(env.configs, applyConfigOverrides);
|
||||
const rawConfigService = new RawConfigService(env.configs, applyConfigOverrides);
|
||||
rawConfigService.loadConfig();
|
||||
|
||||
// Hack to load the extra serverless config files if `serverless: {projectType}` is found in it.
|
||||
const rawConfig = await firstValueFrom(rawConfigService.getConfig$());
|
||||
const serverlessProjectType = rawConfig?.serverless;
|
||||
if (
|
||||
typeof serverlessProjectType === 'string' &&
|
||||
VALID_SERVERLESS_PROJECT_TYPES.includes(serverlessProjectType)
|
||||
) {
|
||||
const extendedConfigs = [
|
||||
...['serverless.yml', `serverless.${serverlessProjectType}.yml`]
|
||||
.map((name) => resolve(getConfigDirectory(), name))
|
||||
.filter(configFileExists),
|
||||
...configs,
|
||||
];
|
||||
|
||||
env = Env.createDefault(REPO_ROOT, {
|
||||
configs: extendedConfigs,
|
||||
cliArgs: { ...cliArgs, serverless: true },
|
||||
repoPackages: getPackages(REPO_ROOT),
|
||||
});
|
||||
|
||||
rawConfigService.stop();
|
||||
rawConfigService = new RawConfigService(env.configs, applyConfigOverrides);
|
||||
rawConfigService.loadConfig();
|
||||
}
|
||||
|
||||
const root = new Root(rawConfigService, env, onRootShutdown);
|
||||
const cliLogger = root.logger.get('cli');
|
||||
|
||||
|
@ -160,15 +130,3 @@ function onRootShutdown(reason?: any) {
|
|||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function configFileExists(path: string) {
|
||||
try {
|
||||
return statSync(path).isFile();
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
|
156
src/cli/serve/compile_config_stack.js
Normal file
156
src/cli/serve/compile_config_stack.js
Normal file
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* 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 _ from 'lodash';
|
||||
|
||||
import { readFileSync, writeFileSync, statSync, existsSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { getConfigPath, getConfigDirectory } from '@kbn/utils';
|
||||
import { getConfigFromFiles } from '@kbn/config';
|
||||
|
||||
const isNotEmpty = _.negate(_.isEmpty);
|
||||
const isNotNull = _.negate(_.isNull);
|
||||
|
||||
/** @typedef {'es' | 'oblt' | 'security'} ServerlessProjectMode */
|
||||
/** @type {ServerlessProjectMode[]} */
|
||||
const VALID_SERVERLESS_PROJECT_MODE = ['es', 'oblt', 'security'];
|
||||
|
||||
/**
|
||||
* Collects paths to configurations to be included in the final configuration stack.
|
||||
* @param {{configOverrides?: string[], devConfig?: boolean, dev?: boolean, serverless?: string | true}} options Options impacting the outgoing config list
|
||||
* @returns List of paths to configurations to be merged, from left to right.
|
||||
*/
|
||||
export function compileConfigStack({ configOverrides, devConfig, dev, serverless }) {
|
||||
const cliConfigs = configOverrides || [];
|
||||
const envConfigs = getEnvConfigs();
|
||||
const defaultConfig = getConfigPath();
|
||||
|
||||
let configs = [cliConfigs, envConfigs, [defaultConfig]].find(isNotEmpty);
|
||||
|
||||
if (dev && devConfig !== false) {
|
||||
configs.push(resolveConfig('kibana.dev.yml'));
|
||||
}
|
||||
|
||||
if (dev && serverless) {
|
||||
writeProjectSwitcherConfig('serverless.recent.dev.yml', serverless);
|
||||
configs.push(resolveConfig('serverless.recent.dev.yml'));
|
||||
}
|
||||
|
||||
// Filter out all config paths that didn't exist
|
||||
configs = configs.filter(isNotNull);
|
||||
|
||||
const serverlessMode = validateServerlessMode(serverless) || getServerlessModeFromCfg(configs);
|
||||
if (serverlessMode) {
|
||||
configs.unshift(resolveConfig(`serverless.${serverlessMode}.yml`));
|
||||
configs.unshift(resolveConfig('serverless.yml'));
|
||||
|
||||
if (dev && devConfig !== false) {
|
||||
configs.push(resolveConfig('serverless.dev.yml'));
|
||||
configs.push(resolveConfig(`serverless.${serverlessMode}.dev.yml`));
|
||||
}
|
||||
}
|
||||
|
||||
return configs.filter(isNotNull);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} configs List of configuration file paths
|
||||
* @returns {ServerlessProjectMode|undefined} The serverless mode in the summed configs
|
||||
*/
|
||||
function getServerlessModeFromCfg(configs) {
|
||||
const config = getConfigFromFiles(configs);
|
||||
|
||||
return config.serverless;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fileName Name of the config within the config directory
|
||||
* @returns {string | null} The resolved path to the config, if it exists, null otherwise
|
||||
*/
|
||||
function resolveConfig(fileName) {
|
||||
const filePath = resolve(getConfigDirectory(), fileName);
|
||||
if (fileExists(filePath)) {
|
||||
return filePath;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fileName
|
||||
* @param {object} opts
|
||||
*/
|
||||
function writeProjectSwitcherConfig(fileName, serverlessOption) {
|
||||
const path = resolve(getConfigDirectory(), fileName);
|
||||
const configAlreadyExists = existsSync(path);
|
||||
|
||||
const preserveExistingConfig = serverlessOption === true;
|
||||
const serverlessMode = validateServerlessMode(serverlessOption) || 'es';
|
||||
|
||||
if (configAlreadyExists && preserveExistingConfig) {
|
||||
return;
|
||||
} else {
|
||||
const content = `xpack.serverless.plugin.developer.projectSwitcher.enabled: true\nserverless: ${serverlessMode}\n`;
|
||||
if (!configAlreadyExists || readFileSync(path).toString() !== content) {
|
||||
writeFileSync(path, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} filePath Path to the config file
|
||||
* @returns {boolean} Whether the file exists
|
||||
*/
|
||||
function fileExists(filePath) {
|
||||
try {
|
||||
return statSync(filePath).isFile();
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function getEnvConfigs() {
|
||||
const val = process.env.KBN_CONFIG_PATHS;
|
||||
if (typeof val === 'string') {
|
||||
return val
|
||||
.split(',')
|
||||
.filter((v) => !!v)
|
||||
.map((p) => resolve(p.trim()));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | true} serverlessMode
|
||||
* @returns {ServerlessProjectMode | null}
|
||||
*/
|
||||
function validateServerlessMode(serverlessMode) {
|
||||
if (!serverlessMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (serverlessMode === true) {
|
||||
// Defaulting to read the project-switcher's settings in `serverless.recent.dev.yml`
|
||||
return null;
|
||||
}
|
||||
|
||||
if (VALID_SERVERLESS_PROJECT_MODE.includes(serverlessMode)) {
|
||||
return serverlessMode;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`invalid --serverless value, must be one of ${VALID_SERVERLESS_PROJECT_MODE.join(', ')}`
|
||||
);
|
||||
}
|
149
src/cli/serve/compile_config_stack.test.js
Normal file
149
src/cli/serve/compile_config_stack.test.js
Normal file
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* 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 Path from 'path';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('@kbn/repo-info', () => ({
|
||||
REPO_ROOT: '/some/imaginary/path',
|
||||
}));
|
||||
jest.mock('@kbn/config');
|
||||
|
||||
import { statSync, existsSync, writeFileSync } from 'fs';
|
||||
import { getConfigFromFiles } from '@kbn/config';
|
||||
|
||||
import { compileConfigStack } from './compile_config_stack';
|
||||
|
||||
describe('compileConfigStack', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
statSync.mockImplementation(() => {
|
||||
return {
|
||||
isFile: () => true,
|
||||
};
|
||||
});
|
||||
|
||||
getConfigFromFiles.mockImplementation(() => {
|
||||
return {};
|
||||
});
|
||||
});
|
||||
|
||||
it('loads default config set without any options', () => {
|
||||
const configList = compileConfigStack({}).map(toFileNames);
|
||||
|
||||
expect(configList).toEqual(['kibana.yml']);
|
||||
});
|
||||
|
||||
it('loads serverless configs when --serverless is set', async () => {
|
||||
const configList = compileConfigStack({
|
||||
serverless: 'oblt',
|
||||
}).map(toFileNames);
|
||||
|
||||
expect(configList).toEqual(['serverless.yml', 'serverless.oblt.yml', 'kibana.yml']);
|
||||
});
|
||||
|
||||
it('prefers --config options over default', async () => {
|
||||
const configList = compileConfigStack({
|
||||
configOverrides: ['my-config.yml'],
|
||||
serverless: 'oblt',
|
||||
}).map(toFileNames);
|
||||
|
||||
expect(configList).toEqual(['serverless.yml', 'serverless.oblt.yml', 'my-config.yml']);
|
||||
});
|
||||
|
||||
it('adds dev configs to the stack', async () => {
|
||||
const configList = compileConfigStack({
|
||||
serverless: 'security',
|
||||
dev: true,
|
||||
}).map(toFileNames);
|
||||
|
||||
expect(configList).toEqual([
|
||||
'serverless.yml',
|
||||
'serverless.security.yml',
|
||||
'kibana.yml',
|
||||
'kibana.dev.yml',
|
||||
'serverless.recent.dev.yml',
|
||||
'serverless.dev.yml',
|
||||
'serverless.security.dev.yml',
|
||||
]);
|
||||
});
|
||||
|
||||
it('defaults to "es" if --serverless and --dev are there', async () => {
|
||||
existsSync.mockImplementationOnce((filename) => {
|
||||
if (Path.basename(filename) === 'serverless.recent.dev.yml') {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
getConfigFromFiles.mockImplementationOnce(() => {
|
||||
return {
|
||||
serverless: 'es',
|
||||
};
|
||||
});
|
||||
|
||||
const configList = compileConfigStack({
|
||||
dev: true,
|
||||
serverless: true,
|
||||
}).map(toFileNames);
|
||||
|
||||
expect(existsSync).toHaveBeenCalledWith(
|
||||
'/some/imaginary/path/config/serverless.recent.dev.yml'
|
||||
);
|
||||
expect(writeFileSync).toHaveBeenCalledWith(
|
||||
'/some/imaginary/path/config/serverless.recent.dev.yml',
|
||||
expect.stringContaining('serverless: es')
|
||||
);
|
||||
expect(configList).toEqual([
|
||||
'serverless.yml',
|
||||
'serverless.es.yml',
|
||||
'kibana.yml',
|
||||
'kibana.dev.yml',
|
||||
'serverless.recent.dev.yml',
|
||||
'serverless.dev.yml',
|
||||
'serverless.es.dev.yml',
|
||||
]);
|
||||
});
|
||||
|
||||
it('respects persisted project-switcher decision when --serverless && --dev true', async () => {
|
||||
existsSync.mockImplementationOnce((filename) => {
|
||||
if (Path.basename(filename) === 'serverless.recent.dev.yml') {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
getConfigFromFiles.mockImplementationOnce(() => {
|
||||
return {
|
||||
serverless: 'oblt',
|
||||
};
|
||||
});
|
||||
|
||||
const configList = compileConfigStack({
|
||||
dev: true,
|
||||
serverless: true,
|
||||
}).map(toFileNames);
|
||||
|
||||
expect(existsSync).toHaveBeenCalledWith(
|
||||
'/some/imaginary/path/config/serverless.recent.dev.yml'
|
||||
);
|
||||
expect(writeFileSync).not.toHaveBeenCalled();
|
||||
expect(configList).toEqual([
|
||||
'serverless.yml',
|
||||
'serverless.oblt.yml',
|
||||
'kibana.yml',
|
||||
'kibana.dev.yml',
|
||||
'serverless.recent.dev.yml',
|
||||
'serverless.dev.yml',
|
||||
'serverless.oblt.dev.yml',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
function toFileNames(path) {
|
||||
return Path.basename(path);
|
||||
}
|
|
@ -1,242 +0,0 @@
|
|||
/*
|
||||
* 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 * as Fs from 'fs';
|
||||
import * as Path from 'path';
|
||||
import * as Os from 'os';
|
||||
import * as Child from 'child_process';
|
||||
import Del from 'del';
|
||||
import * as Rx from 'rxjs';
|
||||
import { filter, map, take, timeout } from 'rxjs/operators';
|
||||
|
||||
const tempDir = Path.join(Os.tmpdir(), 'kbn-config-test');
|
||||
|
||||
const kibanaPath = follow('../../../../scripts/kibana.js');
|
||||
|
||||
const TIMEOUT_MS = 20000;
|
||||
|
||||
const envForTempDir = {
|
||||
env: { KBN_PATH_CONF: tempDir },
|
||||
};
|
||||
|
||||
const TestFiles = {
|
||||
fileList: [] as string[],
|
||||
|
||||
createEmptyConfigFiles(fileNames: string[], root: string = tempDir): string[] {
|
||||
const configFiles = [];
|
||||
for (const fileName of fileNames) {
|
||||
const filePath = Path.resolve(root, fileName);
|
||||
|
||||
if (!Fs.existsSync(filePath)) {
|
||||
Fs.writeFileSync(filePath, 'dummy');
|
||||
|
||||
TestFiles.fileList.push(filePath);
|
||||
}
|
||||
|
||||
configFiles.push(filePath);
|
||||
}
|
||||
|
||||
return configFiles;
|
||||
},
|
||||
cleanUpEmptyConfigFiles() {
|
||||
for (const filePath of TestFiles.fileList) {
|
||||
Del.sync(filePath);
|
||||
}
|
||||
TestFiles.fileList.length = 0;
|
||||
},
|
||||
};
|
||||
|
||||
describe('Server configuration ordering', () => {
|
||||
let kibanaProcess: Child.ChildProcessWithoutNullStreams;
|
||||
|
||||
beforeEach(() => {
|
||||
Fs.mkdirSync(tempDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (kibanaProcess !== undefined) {
|
||||
const exitPromise = new Promise((resolve) => kibanaProcess?.once('exit', resolve));
|
||||
kibanaProcess.kill('SIGKILL');
|
||||
await exitPromise;
|
||||
}
|
||||
|
||||
Del.sync(tempDir, { force: true });
|
||||
TestFiles.cleanUpEmptyConfigFiles();
|
||||
});
|
||||
|
||||
it('loads default config set without any options', async function () {
|
||||
TestFiles.createEmptyConfigFiles(['kibana.yml']);
|
||||
|
||||
kibanaProcess = Child.spawn(process.execPath, [kibanaPath, '--verbose'], envForTempDir);
|
||||
const configList = await extractConfigurationOrder(kibanaProcess);
|
||||
|
||||
expect(configList).toEqual(['kibana.yml']);
|
||||
});
|
||||
|
||||
it('loads serverless configs when --serverless is set', async () => {
|
||||
TestFiles.createEmptyConfigFiles([
|
||||
'serverless.yml',
|
||||
'serverless.oblt.yml',
|
||||
'kibana.yml',
|
||||
'serverless.recent.yml',
|
||||
]);
|
||||
|
||||
kibanaProcess = Child.spawn(
|
||||
process.execPath,
|
||||
[kibanaPath, '--verbose', '--serverless', 'oblt'],
|
||||
envForTempDir
|
||||
);
|
||||
const configList = await extractConfigurationOrder(kibanaProcess);
|
||||
|
||||
expect(configList).toEqual([
|
||||
'serverless.yml',
|
||||
'serverless.oblt.yml',
|
||||
'kibana.yml',
|
||||
'serverless.recent.yml',
|
||||
]);
|
||||
});
|
||||
|
||||
it('prefers --config options over default', async () => {
|
||||
const [configPath] = TestFiles.createEmptyConfigFiles([
|
||||
'potato.yml',
|
||||
'serverless.yml',
|
||||
'serverless.oblt.yml',
|
||||
'kibana.yml',
|
||||
'serverless.recent.yml',
|
||||
]);
|
||||
|
||||
kibanaProcess = Child.spawn(
|
||||
process.execPath,
|
||||
[kibanaPath, '--verbose', '--serverless', 'oblt', '--config', configPath],
|
||||
envForTempDir
|
||||
);
|
||||
const configList = await extractConfigurationOrder(kibanaProcess);
|
||||
|
||||
expect(configList).toEqual([
|
||||
'serverless.yml',
|
||||
'serverless.oblt.yml',
|
||||
'potato.yml',
|
||||
'serverless.recent.yml',
|
||||
]);
|
||||
});
|
||||
|
||||
it('defaults to "es" if --serverless and --dev are there', async () => {
|
||||
TestFiles.createEmptyConfigFiles([
|
||||
'serverless.yml',
|
||||
'serverless.es.yml',
|
||||
'kibana.yml',
|
||||
'kibana.dev.yml',
|
||||
'serverless.dev.yml',
|
||||
]);
|
||||
|
||||
kibanaProcess = Child.spawn(
|
||||
process.execPath,
|
||||
[kibanaPath, '--verbose', '--serverless', '--dev'],
|
||||
envForTempDir
|
||||
);
|
||||
const configList = await extractConfigurationOrder(kibanaProcess);
|
||||
|
||||
expect(configList).toEqual([
|
||||
'serverless.yml',
|
||||
'serverless.es.yml',
|
||||
'kibana.yml',
|
||||
'serverless.recent.yml',
|
||||
'kibana.dev.yml',
|
||||
'serverless.dev.yml',
|
||||
]);
|
||||
});
|
||||
|
||||
it('adds dev configs to the stack', async () => {
|
||||
TestFiles.createEmptyConfigFiles([
|
||||
'serverless.yml',
|
||||
'serverless.security.yml',
|
||||
'kibana.yml',
|
||||
'kibana.dev.yml',
|
||||
'serverless.dev.yml',
|
||||
]);
|
||||
|
||||
kibanaProcess = Child.spawn(
|
||||
process.execPath,
|
||||
[kibanaPath, '--verbose', '--serverless', 'security', '--dev'],
|
||||
envForTempDir
|
||||
);
|
||||
|
||||
const configList = await extractConfigurationOrder(kibanaProcess);
|
||||
|
||||
expect(configList).toEqual([
|
||||
'serverless.yml',
|
||||
'serverless.security.yml',
|
||||
'kibana.yml',
|
||||
'serverless.recent.yml',
|
||||
'kibana.dev.yml',
|
||||
'serverless.dev.yml',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
async function extractConfigurationOrder(
|
||||
proc: Child.ChildProcessWithoutNullStreams
|
||||
): Promise<string[] | undefined> {
|
||||
const configMessage = await waitForMessage(proc, /[Cc]onfig.*order:/, TIMEOUT_MS);
|
||||
|
||||
const configList = configMessage
|
||||
.match(/order: (.*)$/)
|
||||
?.at(1)
|
||||
?.split(', ')
|
||||
?.map((path) => Path.basename(path));
|
||||
|
||||
return configList;
|
||||
}
|
||||
|
||||
async function waitForMessage(
|
||||
proc: Child.ChildProcessWithoutNullStreams,
|
||||
expression: string | RegExp,
|
||||
timeoutMs: number
|
||||
): Promise<string> {
|
||||
const message$ = Rx.fromEvent(proc.stdout!, 'data').pipe(
|
||||
map((messages) => String(messages).split('\n').filter(Boolean))
|
||||
);
|
||||
|
||||
const trackedExpression$ = message$.pipe(
|
||||
// We know the sighup handler will be registered before this message logged
|
||||
filter((messages: string[]) => messages.some((m) => m.match(expression))),
|
||||
take(1)
|
||||
);
|
||||
|
||||
const error$ = message$.pipe(
|
||||
filter((messages: string[]) => messages.some((line) => line.match(/fatal/i))),
|
||||
take(1),
|
||||
map((line) => new Error(line.join('\n')))
|
||||
);
|
||||
|
||||
const value = await Rx.firstValueFrom(
|
||||
Rx.race(trackedExpression$, error$).pipe(
|
||||
timeout({
|
||||
first: timeoutMs,
|
||||
with: () =>
|
||||
Rx.throwError(
|
||||
() => new Error(`Config options didn't appear in logs for ${timeoutMs / 1000}s...`)
|
||||
),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (value instanceof Error) {
|
||||
throw value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value[0];
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function follow(file: string) {
|
||||
return Path.relative(process.cwd(), Path.resolve(__dirname, file));
|
||||
}
|
|
@ -60,10 +60,10 @@ describe('cli serverless project type', () => {
|
|||
);
|
||||
|
||||
it.each(['es', 'oblt', 'security'])(
|
||||
'writes the serverless project type %s in config/serverless.recent.yml',
|
||||
'writes the serverless project type %s in config/serverless.recent.dev.yml',
|
||||
async (mode) => {
|
||||
// Making sure `--serverless` translates into the `serverless` config entry, and validates against the accepted values
|
||||
child = spawn(process.execPath, ['scripts/kibana', `--serverless=${mode}`], {
|
||||
child = spawn(process.execPath, ['scripts/kibana', '--dev', `--serverless=${mode}`], {
|
||||
cwd: REPO_ROOT,
|
||||
});
|
||||
|
||||
|
@ -72,7 +72,7 @@ describe('cli serverless project type', () => {
|
|||
expect(found).not.toContain('FATAL');
|
||||
|
||||
expect(
|
||||
readFileSync(resolve(getConfigDirectory(), 'serverless.recent.yml'), 'utf-8')
|
||||
readFileSync(resolve(getConfigDirectory(), 'serverless.recent.dev.yml'), 'utf-8')
|
||||
).toContain(`serverless: ${mode}\n`);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -8,37 +8,16 @@
|
|||
|
||||
import { set as lodashSet } from '@kbn/safer-lodash-set';
|
||||
import _ from 'lodash';
|
||||
import { statSync, existsSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import url from 'url';
|
||||
|
||||
import { getConfigPath, getConfigDirectory } from '@kbn/utils';
|
||||
import { isKibanaDistributable } from '@kbn/repo-info';
|
||||
import { readKeystore } from '../keystore/read_keystore';
|
||||
import { compileConfigStack } from './compile_config_stack';
|
||||
import { getConfigFromFiles } from '@kbn/config';
|
||||
|
||||
/** @typedef {'es' | 'oblt' | 'security'} ServerlessProjectMode */
|
||||
/** @type {ServerlessProjectMode[]} */
|
||||
const VALID_SERVERLESS_PROJECT_MODE = ['es', 'oblt', 'security'];
|
||||
|
||||
const isNotEmpty = _.negate(_.isEmpty);
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} opts
|
||||
* @returns {ServerlessProjectMode | true | null}
|
||||
*/
|
||||
function getServerlessProjectMode(opts) {
|
||||
if (!opts.serverless) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (VALID_SERVERLESS_PROJECT_MODE.includes(opts.serverless) || opts.serverless === true) {
|
||||
return opts.serverless;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`invalid --serverless value, must be one of ${VALID_SERVERLESS_PROJECT_MODE.join(', ')}`
|
||||
);
|
||||
}
|
||||
const DEV_MODE_PATH = '@kbn/cli-dev-mode';
|
||||
const DEV_MODE_SUPPORTED = canRequire(DEV_MODE_PATH);
|
||||
|
||||
function canRequire(path) {
|
||||
try {
|
||||
|
@ -53,9 +32,6 @@ function canRequire(path) {
|
|||
}
|
||||
}
|
||||
|
||||
const DEV_MODE_PATH = '@kbn/cli-dev-mode';
|
||||
const DEV_MODE_SUPPORTED = canRequire(DEV_MODE_PATH);
|
||||
|
||||
const getBootstrapScript = (isDev) => {
|
||||
if (DEV_MODE_SUPPORTED && isDev && process.env.isDevCliChild !== 'true') {
|
||||
// need dynamic require to exclude it from production build
|
||||
|
@ -68,95 +44,17 @@ const getBootstrapScript = (isDev) => {
|
|||
}
|
||||
};
|
||||
|
||||
const pathCollector = function () {
|
||||
function pathCollector() {
|
||||
const paths = [];
|
||||
return function (path) {
|
||||
paths.push(resolve(process.cwd(), path));
|
||||
return paths;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const configPathCollector = pathCollector();
|
||||
const pluginPathCollector = pathCollector();
|
||||
|
||||
/**
|
||||
* @param {string} name The config file name
|
||||
* @returns {boolean} Whether the file exists
|
||||
*/
|
||||
function configFileExists(name) {
|
||||
const path = resolve(getConfigDirectory(), name);
|
||||
try {
|
||||
return statSync(path).isFile();
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {string[]} configs
|
||||
* @param {'push' | 'unshift'} method
|
||||
*/
|
||||
function maybeAddConfig(name, configs, method) {
|
||||
if (configFileExists(name)) {
|
||||
configs[method](resolve(getConfigDirectory(), name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
* @param {'es' | 'security' | 'oblt' | true} projectType
|
||||
* @param {boolean} isDevMode
|
||||
* @param {string[]} configs
|
||||
* @param {'push' | 'unshift'} method
|
||||
*/
|
||||
function maybeSetRecentConfig(file, projectType, isDevMode, configs, method) {
|
||||
const path = resolve(getConfigDirectory(), file);
|
||||
|
||||
function writeMode(selectedProjectType) {
|
||||
writeFileSync(
|
||||
path,
|
||||
`${
|
||||
isDevMode ? 'xpack.serverless.plugin.developer.projectSwitcher.enabled: true\n' : ''
|
||||
}serverless: ${selectedProjectType}\n`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
if (!existsSync(path)) {
|
||||
writeMode(projectType === true ? 'es' : projectType);
|
||||
} else if (typeof projectType === 'string') {
|
||||
const data = readFileSync(path, 'utf-8');
|
||||
const match = data.match(/serverless: (\w+)\n/);
|
||||
if (!match || match[1] !== projectType) {
|
||||
writeMode(projectType);
|
||||
}
|
||||
}
|
||||
|
||||
configs[method](path);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function getEnvConfigs() {
|
||||
const val = process.env.KBN_CONFIG_PATHS;
|
||||
if (typeof val === 'string') {
|
||||
return val
|
||||
.split(',')
|
||||
.filter((v) => !!v)
|
||||
.map((p) => resolve(p.trim()));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function applyConfigOverrides(rawConfig, opts, extraCliOptions) {
|
||||
const set = _.partial(lodashSet, rawConfig);
|
||||
const get = _.partial(_.get, rawConfig);
|
||||
|
@ -307,28 +205,17 @@ export default function (program) {
|
|||
}
|
||||
|
||||
command.action(async function (opts) {
|
||||
const cliConfigs = opts.config || [];
|
||||
const envConfigs = getEnvConfigs();
|
||||
const defaultConfig = getConfigPath();
|
||||
const configs = compileConfigStack({
|
||||
configOverrides: opts.config,
|
||||
devConfig: opts.devConfig,
|
||||
dev: opts.dev,
|
||||
serverless: opts.serverless,
|
||||
});
|
||||
|
||||
const configs = [cliConfigs, envConfigs, [defaultConfig]].find(isNotEmpty);
|
||||
const configsEvaluted = getConfigFromFiles(configs);
|
||||
const isServerlessMode = !!(configsEvaluted.serverless || opts.serverless);
|
||||
|
||||
const unknownOptions = this.getUnknownOptions();
|
||||
const serverlessMode = getServerlessProjectMode(opts);
|
||||
|
||||
if (serverlessMode) {
|
||||
maybeSetRecentConfig('serverless.recent.yml', serverlessMode, opts.dev, configs, 'push');
|
||||
}
|
||||
|
||||
// .dev. configs are "pushed" so that they override all other config files
|
||||
if (opts.dev && opts.devConfig !== false) {
|
||||
maybeAddConfig('kibana.dev.yml', configs, 'push');
|
||||
if (serverlessMode) {
|
||||
maybeAddConfig(`serverless.dev.yml`, configs, 'push');
|
||||
maybeAddConfig('serverless.recent.dev.yml', configs, 'push');
|
||||
}
|
||||
}
|
||||
|
||||
const cliArgs = {
|
||||
dev: !!opts.dev,
|
||||
envName: unknownOptions.env ? unknownOptions.env.name : undefined,
|
||||
|
@ -347,6 +234,7 @@ export default function (program) {
|
|||
oss: !!opts.oss,
|
||||
cache: !!opts.cache,
|
||||
dist: !!opts.dist,
|
||||
serverless: isServerlessMode,
|
||||
};
|
||||
|
||||
// In development mode, the main process uses the @kbn/dev-cli-mode
|
||||
|
|
|
@ -63,7 +63,7 @@ export class ServerlessPlugin implements Plugin<ServerlessPluginSetup, Serverles
|
|||
// with a specific config. So in this case, to ensure the switcher remains enabled,
|
||||
// write the selected config to `recent` and tack on the setting to enable the switcher.
|
||||
writeFileSync(
|
||||
resolve(getConfigDirectory(), 'serverless.recent.yml'),
|
||||
resolve(getConfigDirectory(), 'serverless.recent.dev.yml'),
|
||||
`xpack.serverless.plugin.developer.projectSwitcher.enabled: true\nserverless: ${selectedProjectType}\n`
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue