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:
Alex Szabo 2023-06-08 09:35:10 +02:00 committed by GitHub
parent 74102e592f
commit f51f5f42e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 326 additions and 504 deletions

View file

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

View file

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

View file

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

View 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(', ')}`
);
}

View 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);
}

View file

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

View 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`);
}
);

View file

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

View file

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