mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
# Backport This will backport the following commits from `main` to `8.8`: - [Fix config stacking order (#158827)](https://github.com/elastic/kibana/pull/158827) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Alex Szabo","email":"alex.szabo@elastic.co"},"sourceCommit":{"committedDate":"2023-06-05T13:15:07Z","message":"Fix config stacking order (#158827)\n\n## Summary\r\nFixes: #155154 (introduced in #149878), builds on #155436 .\r\n\r\n- Adds tests to ensure the configuration merging order, check those for\r\nreference.\r\n- Updates the README to explain the intention\r\n \r\nFor the tests, I needed to output something to the logs. I hope it's not\r\na big issue to log it. If needed, I might hide that behind a verbose- or\r\nfeature flag.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"c57589ec57e5e8265a66cd9c8c2102005736f6d8","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Operations","release_note:fix","backport:prev-minor","v8.9.0"],"number":158827,"url":"https://github.com/elastic/kibana/pull/158827","mergeCommit":{"message":"Fix config stacking order (#158827)\n\n## Summary\r\nFixes: #155154 (introduced in #149878), builds on #155436 .\r\n\r\n- Adds tests to ensure the configuration merging order, check those for\r\nreference.\r\n- Updates the README to explain the intention\r\n \r\nFor the tests, I needed to output something to the logs. I hope it's not\r\na big issue to log it. If needed, I might hide that behind a verbose- or\r\nfeature flag.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"c57589ec57e5e8265a66cd9c8c2102005736f6d8"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/158827","number":158827,"mergeCommit":{"message":"Fix config stacking order (#158827)\n\n## Summary\r\nFixes: #155154 (introduced in #149878), builds on #155436 .\r\n\r\n- Adds tests to ensure the configuration merging order, check those for\r\nreference.\r\n- Updates the README to explain the intention\r\n \r\nFor the tests, I needed to output something to the logs. I hope it's not\r\na big issue to log it. If needed, I might hide that behind a verbose- or\r\nfeature flag.\r\n\r\n### Checklist\r\n\r\n- [x] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>","sha":"c57589ec57e5e8265a66cd9c8c2102005736f6d8"}}]}] BACKPORT--> --------- Co-authored-by: Alex Szabo <alex.szabo@elastic.co>
This commit is contained in:
parent
9b343b4a78
commit
419cd20b4e
5 changed files with 261 additions and 5 deletions
|
@ -5,9 +5,12 @@ this configuration, pass `--serverless={mode}` or run `yarn serverless-{mode}`
|
|||
valid modes are currently: `es`, `oblt`, and `security`
|
||||
|
||||
configuration is applied in the following order, later values override
|
||||
1. kibana.yml
|
||||
2. serverless.yml
|
||||
3. serverless.{mode}.yml
|
||||
1. serverless.yml (serverless configs go first)
|
||||
2. serverless.{mode}.yml (serverless configs go first)
|
||||
3. base config, in this preference order:
|
||||
- my-config.yml(s) (set by --config)
|
||||
- env-config.yml (described by `env.KBN_CONFIG_PATHS`)
|
||||
- kibana.yml (default @ `env.KBN_PATH_CONF`/kibana.yml)
|
||||
4. kibana.dev.yml
|
||||
5. serverless.dev.yml
|
||||
6. serverless.{mode}.dev.yml
|
||||
|
|
|
@ -21,5 +21,6 @@ jest.doMock('@kbn/config', () => ({
|
|||
jest.doMock('./root', () => ({
|
||||
Root: jest.fn(() => ({
|
||||
shutdown: jest.fn(),
|
||||
logger: { get: () => ({ info: jest.fn(), debug: jest.fn() }) },
|
||||
})),
|
||||
}));
|
||||
|
|
|
@ -78,6 +78,9 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot
|
|||
}
|
||||
|
||||
const root = new Root(rawConfigService, env, onRootShutdown);
|
||||
const cliLogger = root.logger.get('cli');
|
||||
|
||||
cliLogger.debug('Kibana configurations evaluated in this order: ' + env.configs.join(', '));
|
||||
|
||||
process.on('SIGHUP', () => reloadConfiguration());
|
||||
|
||||
|
@ -93,7 +96,6 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot
|
|||
});
|
||||
|
||||
function reloadConfiguration(reason = 'SIGHUP signal received') {
|
||||
const cliLogger = root.logger.get('cli');
|
||||
cliLogger.info(`Reloading Kibana configuration (reason: ${reason}).`, { tags: ['config'] });
|
||||
|
||||
try {
|
||||
|
|
243
src/cli/serve/integration_tests/config_ordering.test.ts
Normal file
243
src/cli/serve/integration_tests/config_ordering.test.ts
Normal file
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* 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',
|
||||
'serverless.recent.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));
|
||||
}
|
|
@ -20,6 +20,8 @@ import { readKeystore } from '../keystore/read_keystore';
|
|||
/** @type {ServerlessProjectMode[]} */
|
||||
const VALID_SERVERLESS_PROJECT_MODE = ['es', 'oblt', 'security'];
|
||||
|
||||
const isNotEmpty = _.negate(_.isEmpty);
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} opts
|
||||
* @returns {ServerlessProjectMode | true | null}
|
||||
|
@ -311,8 +313,13 @@ export default function (program) {
|
|||
}
|
||||
|
||||
command.action(async function (opts) {
|
||||
const cliConfigs = opts.config || [];
|
||||
const envConfigs = getEnvConfigs();
|
||||
const defaultConfig = getConfigPath();
|
||||
|
||||
const configs = [cliConfigs, envConfigs, [defaultConfig]].find(isNotEmpty);
|
||||
|
||||
const unknownOptions = this.getUnknownOptions();
|
||||
const configs = [getConfigPath(), ...getEnvConfigs(), ...(opts.config || [])];
|
||||
const serverlessMode = getServerlessProjectMode(opts);
|
||||
|
||||
if (serverlessMode) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue