diff --git a/packages/core/root/core-root-server-internal/src/bootstrap.test.mocks.ts b/packages/core/root/core-root-server-internal/src/bootstrap.test.mocks.ts deleted file mode 100644 index be54edf7f2124..0000000000000 --- a/packages/core/root/core-root-server-internal/src/bootstrap.test.mocks.ts +++ /dev/null @@ -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() }) }, - })), -})); diff --git a/packages/core/root/core-root-server-internal/src/bootstrap.test.ts b/packages/core/root/core-root-server-internal/src/bootstrap.test.ts deleted file mode 100644 index 1bd413314aa98..0000000000000 --- a/packages/core/root/core-root-server-internal/src/bootstrap.test.ts +++ /dev/null @@ -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 - ); - }); - }); -}); diff --git a/packages/core/root/core-root-server-internal/src/bootstrap.ts b/packages/core/root/core-root-server-internal/src/bootstrap.ts index bb0e3ddc8c701..90e603fb20d48 100644 --- a/packages/core/root/core-root-server-internal/src/bootstrap.ts +++ b/packages/core/root/core-root-server-internal/src/bootstrap.ts @@ -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; - } -} diff --git a/src/cli/serve/compile_config_stack.js b/src/cli/serve/compile_config_stack.js new file mode 100644 index 0000000000000..2058571c43c2b --- /dev/null +++ b/src/cli/serve/compile_config_stack.js @@ -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(', ')}` + ); +} diff --git a/src/cli/serve/compile_config_stack.test.js b/src/cli/serve/compile_config_stack.test.js new file mode 100644 index 0000000000000..097f56b52231a --- /dev/null +++ b/src/cli/serve/compile_config_stack.test.js @@ -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); +} diff --git a/src/cli/serve/integration_tests/config_ordering.test.ts b/src/cli/serve/integration_tests/config_ordering.test.ts deleted file mode 100644 index 7245083aa5141..0000000000000 --- a/src/cli/serve/integration_tests/config_ordering.test.ts +++ /dev/null @@ -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 { - 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 { - 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)); -} diff --git a/src/cli/serve/integration_tests/serverless_config_flag.test.ts b/src/cli/serve/integration_tests/serverless_config_flag.test.ts index 46c94752f40da..9d20722ecbfc5 100644 --- a/src/cli/serve/integration_tests/serverless_config_flag.test.ts +++ b/src/cli/serve/integration_tests/serverless_config_flag.test.ts @@ -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`); } ); diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 3d4b51be36fa5..a4ef584ec7413 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -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} 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 diff --git a/x-pack/plugins/serverless/server/plugin.ts b/x-pack/plugins/serverless/server/plugin.ts index 3320b6fda5e40..a2b1121d0c562 100644 --- a/x-pack/plugins/serverless/server/plugin.ts +++ b/x-pack/plugins/serverless/server/plugin.ts @@ -63,7 +63,7 @@ export class ServerlessPlugin implements Plugin